Inmutabilidad completa y programación orientada a objetos.

43

En la mayoría de los lenguajes OOP, los objetos son generalmente mutables con un conjunto limitado de excepciones (como, por ejemplo, tuplas y cadenas en python). En la mayoría de los lenguajes funcionales, los datos son inmutables.

Tanto los objetos mutables como los inmutables traen una lista completa de ventajas y desventajas propias.

Hay lenguajes que intentan casar ambos conceptos como, por ejemplo, scala, donde tienes (explícitamente declarado) datos mutables e inmutables (corrígeme si estoy equivocado, mi conocimiento de scala es más que limitado).

Mi pregunta es: ¿la inmutabilidad completa (sic!), Es decir, ningún objeto puede mutar una vez que se ha creado, ¿tiene algún sentido en un contexto OOP?

¿Hay diseños o implementaciones de tal modelo?

Básicamente, ¿son la inmutabilidad (completa) y los opuestos de OOP u ortogonales?

Motivación: en OOP, normalmente opera con datos, cambiando (mutando) la información subyacente, manteniendo referencias entre esos objetos. Por ejemplo, un objeto de clase Personcon un miembro que hace fatherreferencia a otro Personobjeto. Si cambia el nombre del padre, esto es inmediatamente visible para el objeto hijo sin necesidad de actualización. Al ser inmutable, necesitaría construir nuevos objetos tanto para el padre como para el hijo. Pero tendría mucho menos kerfuffle con objetos compartidos, subprocesos múltiples, GIL, etc.

Hiperbóreo
fuente
2
La inmutabilidad se puede simular en un lenguaje OOP, exponiendo solo los puntos de acceso a objetos como métodos o propiedades de solo lectura que no mutan los datos. La inmutabilidad funciona de la misma manera en los lenguajes OOP que en cualquier lenguaje funcional, excepto que es posible que falten algunas características funcionales del lenguaje.
Robert Harvey
55
La mutabilidad no es una propiedad de lenguajes OOP como C # y Java, ni tampoco es inmutabilidad. Usted especifica mutabilidad o inmutabilidad por la forma en que escribe la clase.
Robert Harvey
99
Su presunción parece ser que la mutabilidad es una característica central de la orientación a objetos. No lo es La mutabilidad es simplemente una propiedad de objetos o valores. La orientación a objetos abarca una serie de conceptos intrínsecos (encapsulación, polimorfismo, herencia, etc.) que tienen poco o nada que ver con la mutación, y aún obtendría los beneficios de esas características. incluso si hiciste todo inmutable.
Robert Harvey
2
@MichaelT La pregunta no se trata de hacer que las cosas específicas sean mutables, se trata de hacer que todas las cosas sean inmutables.

Respuestas:

43

La POO y la inmutabilidad son casi completamente ortogonales entre sí. Sin embargo, la programación imperativa y la inmutabilidad no lo son.

OOP se puede resumir en dos características principales:

  • Encapsulación : no accederé directamente al contenido de los objetos, sino que me comunicaré a través de una interfaz específica ("métodos") con este objeto. Esta interfaz puede ocultarme datos internos. Técnicamente, esto es específico para la programación modular en lugar de OOP. Acceder a los datos a través de una interfaz definida es más o menos equivalente a un tipo de datos abstracto.

  • Despacho dinámico : cuando llamo a un método en un objeto, el método ejecutado se resolverá en tiempo de ejecución. (Por ejemplo, en la OOP basada en clases, podría llamar a un sizemétodo en una IListinstancia, pero la llamada podría resolverse a una implementación en una LinkedListclase). El despacho dinámico es una forma de permitir el comportamiento polimórfico.

La encapsulación tiene menos sentido sin mutabilidad (no existe un estado interno que pueda ser corrompido por la intromisión externa), pero aún tiende a facilitar las abstracciones incluso cuando todo es inmutable.

Un programa imperativo consiste en declaraciones que se ejecutan secuencialmente. Una declaración tiene efectos secundarios como cambiar el estado del programa. Con la inmutabilidad, el estado no se puede cambiar (por supuesto, se podría crear un nuevo estado). Por lo tanto, la programación imperativa es fundamentalmente incompatible con la inmutabilidad.

Ahora sucede que OOP siempre se ha conectado históricamente con la programación imperativa (Simula se basa en Algol), y todos los lenguajes OOP principales tienen raíces imperativas (C ++, Java, C #, ... todos están enraizados en C). Esto no implica que la OOP en sí misma sea imperativa o mutable, esto solo significa que la implementación de la OOP por estos lenguajes permite la mutabilidad.

amon
fuente
2
Muchas gracias, especialmente por la definición de las dos características principales.
Hyperboreus
Dynamic Dispatch no es una característica central de OOP. Tampoco lo es realmente la encapsulación (como ya admitiste).
Deja de dañar a Monica el
55
@OrangeDog Sí, no existe una definición universalmente aceptada de OOP, pero necesitaba una definición para trabajar. Así que elegí algo lo más cercano a la verdad que pude obtener sin escribir una disertación completa al respecto. Sin embargo, considero que el despacho dinámico es la principal característica distintiva de OOP de otros paradigmas. Algo que parece OOP pero que en realidad tiene todas las llamadas resueltas estáticamente, en realidad es solo programación modular con polimorfismo ad-hoc. Los objetos son un par de métodos y datos, y son como tales equivalentes a los cierres.
amon
2
"Los objetos son una combinación de métodos y datos" sería suficiente. Todo lo que necesitas es algo que no tiene nada que ver con la inmutabilidad.
Deja de dañar a Monica el
3
@CodeYogi La ocultación de datos es el tipo de encapsulación más común. Sin embargo, la forma en que un objeto almacena internamente los datos no es el único detalle de implementación que debe ocultarse. Es igualmente importante ocultar cómo se implementa la interfaz pública, por ejemplo, si utilizo algún método auxiliar. Tales métodos auxiliares también deberían ser privados, en general. Para resumir: la encapsulación es un principio, mientras que la ocultación de datos es una técnica de encapsulación.
amon
25

Tenga en cuenta que existe una cultura entre los programadores orientados a objetos en la que las personas suponen que si está haciendo OOP, la mayoría de sus objetos serán mutables, pero ese es un problema separado de si OOP requiere mutabilidad. Además, esa cultura parece estar cambiando lentamente hacia una mayor inmutabilidad, debido a la exposición de las personas a la programación funcional.

Scala es una muy buena ilustración de que la mutabilidad no es necesaria para la orientación a objetos. Si bien Scala admite la mutabilidad, se desaconseja su uso. Idiomatic Scala está muy orientado a objetos y también es casi completamente inmutable. Principalmente permite la mutabilidad para compatibilidad con Java, y porque en ciertas circunstancias los objetos inmutables son ineficientes o complicados para trabajar.

Compare una lista Scala y una lista Java , por ejemplo. La lista inmutable de Scala contiene los mismos métodos de objeto que la lista mutable de Java. Más, de hecho, porque Java usa funciones estáticas para operaciones como sort , y Scala agrega métodos de estilo funcional como map. Todas las características distintivas de OOP (encapsulación, herencia y polimorfismo) están disponibles en una forma familiar para los programadores orientados a objetos y se usan de manera apropiada.

La única diferencia que verá es que cuando cambia la lista, obtiene un nuevo objeto como resultado. Eso a menudo requiere que uses patrones de diseño diferentes de los que usarías con objetos mutables, pero no requiere que abandones OOP por completo.

Karl Bielefeldt
fuente
17

La inmutabilidad se puede simular en un lenguaje OOP, exponiendo solo los puntos de acceso a objetos como métodos o propiedades de solo lectura que no mutan los datos. La inmutabilidad funciona de la misma manera en los lenguajes OOP que en cualquier lenguaje funcional, excepto que es posible que falten algunas características funcionales del lenguaje.

Su presunción parece ser que la mutabilidad es una característica central de la orientación a objetos. Pero la mutabilidad es simplemente una propiedad de objetos o valores. La orientación a objetos abarca una serie de conceptos intrínsecos (encapsulación, polimorfismo, herencia, etc.) que tienen poco o nada que ver con la mutación, y aún obtendría los beneficios de esas características, incluso si hiciera todo inmutable.

No todos los lenguajes funcionales requieren inmutabilidad tampoco. Clojure tiene una anotación específica que permite que los tipos sean mutables, y la mayoría de los lenguajes funcionales "prácticos" tienen una forma de especificar tipos mutables.

Una mejor pregunta podría ser "¿Tiene sentido la inmutabilidad completa en la programación imperativa ?" Yo diría que la respuesta obvia a esa pregunta es no. Para lograr una inmutabilidad completa en la programación imperativa, deberías renunciar a cosas como los forbucles (ya que tendrías que mutar una variable de bucle) a favor de la recursividad, y ahora estás programando de manera funcional de todos modos.

Robert Harvey
fuente
Gracias. ¿Podría por favor elaborar un poco su último párrafo ("obvio" podría ser un poco subjetivo).
Hyperboreus
Ya lo hice ....
Robert Harvey
1
@Hyperboreus Hay muchas formas de lograr el polimorfismo. Los subtipos con despacho dinámico, el polimorfismo estático ad-hoc (también conocido como sobrecarga de funciones) y el polimorfismo paramétrico (también conocidos como genéricos) son las formas más comunes de hacerlo, y todas las formas tienen sus fortalezas y debilidades. Los lenguajes OOP modernos combinan estas tres formas, mientras que Haskell se basa principalmente en el polimorfismo paramétrico y el polimorfismo ad-hoc.
amon
3
@RobertHarvey Usted dice que necesita mutabilidad porque necesita hacer un bucle (de lo contrario, debe usar la recursividad). Antes de comenzar a usar Haskell hace dos años, pensé que también necesitaba variables mutables. Solo digo que hay otras formas de "bucle" (mapa, plegado, filtro, etc.). Una vez que elimine el bucle de la tabla, ¿por qué más necesitaría variables mutables?
cimmanon
1
@RobertHarvey Pero este es exactamente el punto de los lenguajes de programación: lo que está expuesto a usted y no lo que está sucediendo bajo el capó. Esta última es responsabilidad del compilador o intérprete, no del desarrollador de la aplicación. De lo contrario, de vuelta al ensamblador.
Hyperboreus
5

A menudo es útil clasificar los objetos como valores o entidades encapsulantes, con la distinción de que si algo es un valor, el código que tiene una referencia a él nunca debería ver su cambio de estado de ninguna manera que el código en sí no inició. Por el contrario, el código que contiene una referencia a una entidad puede esperar que cambie de formas que escapan al control del titular de la referencia.

Si bien es posible utilizar el valor de encapsulado utilizando objetos de tipos mutables o inmutables, un objeto solo puede comportarse como un valor si se aplica al menos una de las siguientes condiciones:

  1. Ninguna referencia al objeto se expondrá a nada que pueda cambiar el estado encapsulado en él.

  2. El titular de al menos una de las referencias al objeto conoce todos los usos a los que se podría poner cualquier referencia existente.

Como todas las instancias de tipos inmutables satisfacen automáticamente el primer requisito, usarlas como valores es fácil. Por el contrario, garantizar que se cumpla cualquiera de los requisitos cuando se usan tipos mutables es mucho más difícil. Mientras que las referencias a tipos inmutables se pueden pasar libremente como un medio de encapsular el estado encapsulado en ellas, pasar el estado almacenado en tipos mutables requiere construir objetos envolventes inmutables o copiar el estado encapsulado por objetos privados en otros objetos que están suministrado o construido para el destinatario de los datos.

Los tipos inmutables funcionan muy bien para pasar valores, y a menudo son al menos algo utilizables para manipularlos. Sin embargo, no son tan buenos para manejar entidades. Lo más cercano que uno puede tener a una entidad en un sistema con tipos puramente inmutables es una función que, dado el estado del sistema, informará sobre los atributos de alguna parte del mismo, o producirá una nueva instancia de estado del sistema que es como un suministrado uno, excepto una parte particular del mismo que será diferente de alguna manera seleccionable. Además, si el propósito de una entidad es interconectar algún código con algo que existe en el mundo real, puede ser imposible para la entidad evitar exponer el estado mutable.

Por ejemplo, si uno recibe algunos datos a través de una conexión TCP, podría producir un nuevo objeto "estado del mundo" que incluye esos datos en su búfer sin afectar ninguna referencia al antiguo "estado del mundo", pero copias antiguas de El estado mundial que no incluye el último lote de datos será defectuoso y no debe utilizarse, ya que ya no coincidirá con el estado del socket TCP del mundo real.

Super gato
fuente
4

En c #, algunos tipos son inmutables como una cadena.

Esto parece sugerir, además, que la elección ha sido fuertemente considerada.

Por supuesto, es realmente un rendimiento exigente usar tipos inmutables si tiene que modificar ese tipo cientos de miles de veces. Esa es la razón por la que se sugiere usar la StringBuilderclase en lugar de la stringclase en estos casos.

Hice un experimento con un generador de perfiles y usar el tipo inmutable es realmente más exigente con la CPU y la RAM.

También es intuitivo si considera que para modificar solo una letra en una cadena de 4000 caracteres, debe copiar cada carácter en otra área de la RAM.

Revious
fuente
66
La modificación frecuente de datos inmutables no necesita ser catastróficamente lenta como con la stringconcatenación repetida . Para prácticamente todo tipo de datos / casos de uso, se puede inventar (a menudo ya se ha inventado) una estructura persistente eficiente. La mayoría de ellos tienen un rendimiento aproximadamente igual, incluso si los factores constantes a veces son peores.
@delnan También creo que el último párrafo de la respuesta es más sobre un detalle de implementación que sobre la (im) mutabilidad.
Hyperboreus
@Hyperboreus: ¿crees que debería eliminar esa parte? Pero, ¿cómo puede cambiar una cadena si es inmutable? Quiero decir ... en mi opinión, pero seguro que puedo estar equivocado, esa podría ser la razón principal por la cual los objetos no son inmutables.
Anterior
1
@Revious De ninguna manera. Déjalo, para que provoque discusiones y opiniones y puntos de vista más interesantes.
Hyperboreus
1
@Revious Sí, la lectura sería más lenta, aunque no tan lenta como cambiar a string(la representación tradicional). Una "cadena" (en la representación de la que estoy hablando) después de 1000 modificaciones sería como una cadena recién creada (contenido del módulo); ninguna estructura de datos persistente útil o ampliamente utilizada degrada la calidad después de las operaciones X. La fragmentación de la memoria no es un problema grave (tendría muchas asignaciones, sí, pero la fragmentación no es un problema en los recolectores de basura modernos)
0

La inmutabilidad completa de todo no tiene mucho sentido en OOP, o en la mayoría de los otros paradigmas, por una razón muy grande:

Todo programa útil tiene efectos secundarios.

Un programa que no hace que nada cambie, no tiene valor. Es posible que ni siquiera lo haya ejecutado, ya que el efecto será idéntico.

Incluso si cree que no está cambiando nada, y simplemente está resumiendo una lista de números que recibió de alguna manera, considere que debe hacer algo con el resultado, ya sea que lo imprima en una salida estándar, lo escriba en un archivo, o donde sea. Y eso implica mutar un búfer y cambiar el estado del sistema.

Puede tener mucho sentido restringir la mutabilidad a las partes que necesitan poder cambiar. Pero si absolutamente nada necesita cambiar, entonces no estás haciendo nada que valga la pena.

cHao
fuente
44
No veo cómo su respuesta se relaciona con la pregunta, ya que no abordé los lenguajes funcionales puros. Tomemos erlang por ejemplo: datos inmutables, sin asignación destructiva, sin dudas sobre los efectos secundarios. También tiene estado en un lenguaje funcional, solo que el estado "fluye" a través de las funciones, a diferencia de las funciones que operan en el estado. El estado cambia, pero no muta en su lugar, pero un estado futuro reemplaza al estado actual. La inmutabilidad no se trata de si un búfer de memoria se cambia o no, se trata de si estas mutaciones son visibles desde el exterior.
Hyperboreus
Y un estado futuro reemplaza al actual ¿cómo , exactamente? En un programa OO, ese estado es una propiedad de un objeto en algún lugar. Reemplazar el estado requiere cambiar un objeto (o reemplazarlo con otro, lo que requiere un cambio en los objetos que se refieren a él (o reemplazarlo con otro, que ... eh. Obtienes el punto)). Es posible que se te ocurra algún tipo de pirateo monádico, donde cada acción termina creando una aplicación completamente nueva ... pero incluso así, el estado actual del programa debe registrarse en algún lugar.
cHao
77
-1. Esto es incorrecto. Está confundiendo los efectos secundarios con la mutación, y aunque a menudo son tratados de la misma manera por lenguajes funcionales, son diferentes. Todo programa útil tiene efectos secundarios; No todos los programas útiles tienen mutación.
Michael Shaw
@Michael: Cuando se trata de OOP, la mutación y los efectos secundarios están tan entrelazados que no pueden separarse de manera realista. Si no tiene mutación, no puede tener efectos secundarios sin cantidades masivas de piratería.
cHao
-2

Creo que depende de si su definición de OOP es que usa un estilo de paso de mensajes.

Las funciones puras no tienen que mutar nada porque devuelven valores que puede almacenar en nuevas variables.

var brandNewVariable = pureFunction(foo);

Con el estilo de paso de mensajes, le dice a un objeto que almacene nuevos datos en lugar de preguntarle qué nuevos datos debe almacenar en una nueva variable.

sameOldObject.changeMe(foo);

Es posible tener objetos y no mutarlos, haciendo que sus métodos sean funciones puras que viven en el interior del objeto en lugar de en el exterior.

var brandNewVariable = nonMutatingObject.askMe(foo);

Pero no es posible mezclar el estilo de paso de mensajes y objetos inmutables.

presley
fuente