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 Person
con un miembro que hace father
referencia a otro Person
objeto. 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.
fuente
Respuestas:
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
size
método en unaIList
instancia, pero la llamada podría resolverse a una implementación en unaLinkedList
clase). 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.
fuente
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.
fuente
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
for
bucles (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.fuente
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:
Ninguna referencia al objeto se expondrá a nada que pueda cambiar el estado encapsulado en él.
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.
fuente
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
StringBuilder
clase en lugar de lastring
clase 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.
fuente
string
concatenació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.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)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.
fuente
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.
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.
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.
Pero no es posible mezclar el estilo de paso de mensajes y objetos inmutables.
fuente