¿Realmente podemos usar la inmutabilidad en OOP sin perder todas las características clave de OOP?

11

Veo los beneficios de hacer que los objetos en mi programa sean inmutables. Cuando realmente estoy pensando profundamente en un buen diseño para mi aplicación, a menudo llego naturalmente a que muchos de mis objetos son inmutables. A menudo llega al punto en que me gustaría tener todos mis objetos inmutables.

Esta pregunta aborda la misma idea, pero ninguna respuesta sugiere cuál es un buen enfoque para la inmutabilidad y cuándo usarla realmente. ¿Hay algunos buenos patrones de diseño inmutables? La idea general parece ser "hacer que los objetos sean inmutables a menos que sea absolutamente necesario que cambien", lo cual es inútil en la práctica.

Mi experiencia es que la inmutabilidad conduce mi código cada vez más al paradigma funcional y esta progresión siempre ocurre:

  1. Comienzo a necesitar estructuras de datos persistentes (en el sentido funcional) como listas, mapas, etc.
  2. Es extremadamente inconveniente trabajar con referencias cruzadas (por ejemplo, el nodo de árbol que hace referencia a sus hijos mientras que los niños hacen referencia a sus padres), lo que me hace no usar referencias cruzadas, lo que nuevamente hace que mis estructuras de datos y mi código sean más funcionales.
  3. La herencia se detiene para tener sentido y en su lugar empiezo a usar la composición.
  4. Todas las ideas básicas de OOP como la encapsulación comienzan a desmoronarse y mis objetos comienzan a parecerse a funciones.

En este punto, prácticamente ya no uso nada del paradigma OOP y puedo cambiar a un lenguaje puramente funcional. Por lo tanto, mi pregunta: ¿existe un enfoque coherente para un buen diseño OOP inmutable o es siempre el caso de que cuando llevas la idea inmutable a su máximo potencial, siempre terminas programando en un lenguaje funcional que ya no necesita nada del mundo OOP? ¿Hay alguna buena guía para decidir qué clases deberían ser inmutables y cuáles deberían permanecer mutables para garantizar que la POO no se desmorone?

Solo por conveniencia, daré un ejemplo. Tengamos ChessBoarduna colección inmutable de piezas de ajedrez inmutables (ampliando la clase abstractaPiece) Desde el punto de vista de OOP, una pieza es responsable de generar movimientos válidos desde su posición en el tablero. Pero para generar los movimientos, la pieza necesita una referencia a su tablero, mientras que el tablero debe tener referencia a sus piezas. Bueno, hay algunos trucos para crear estas referencias cruzadas inmutables dependiendo de su lenguaje OOP, pero son difíciles de manejar, mejor no tener una pieza para hacer referencia a su tablero. Pero entonces la pieza no puede generar sus movimientos ya que no conoce el estado del tablero. Luego, la pieza se convierte en una estructura de datos que contiene el tipo de pieza y su posición. Luego puede usar una función polimórfica para generar movimientos para todo tipo de piezas. Esto se puede lograr perfectamente en la programación funcional, pero es casi imposible en OOP sin verificaciones de tipo de tiempo de ejecución y otras malas prácticas de OOP ... Entonces,

lishaak
fuente
3
Me gusta la pregunta base, pero me resulta difícil con los detalles. Por ejemplo, ¿por qué la herencia deja de tener sentido cuando maximizas la inmutabilidad?
Martin Ba
1
¿Tus objetos no tienen métodos?
Deja de dañar a Monica el
44
"Desde el punto de vista de OOP, una pieza es responsable de generar movimientos válidos desde su posición en el tablero". Definitivamente no, diseñar una pieza como esa probablemente dañaría a SRP.
Doc Brown
1
@lishaak Usted "lucha por explicar el problema de la herencia en términos simples" porque no es un problema; El mal diseño es un problema. La herencia es la esencia misma de OOP, la condición sine qua non si se quiere. Muchos de los llamados "problemas de herencia" son en realidad problemas con idiomas que no tienen una sintaxis de anulación explícita, y son mucho menos problemáticos en lenguajes mejor diseñados. Sin métodos virtuales y polimorfismo, no tienes OOP; tienes programación procesal con sintaxis de objetos divertidos. ¡No es de extrañar que no esté viendo los beneficios de OO si lo está evitando!
Mason Wheeler

Respuestas:

24

¿Realmente podemos usar la inmutabilidad en OOP sin perder todas las características clave de OOP?

No veo por qué no. Lo he estado haciendo durante años antes de que Java 8 funcionara de todos modos. ¿Has oído hablar de cadenas? Agradable e inmutable desde el principio.

  1. Comienzo a necesitar estructuras de datos persistentes (en el sentido funcional) como listas, mapas, etc.

He estado necesitando esos todo el tiempo también. Invalidar mis iteradores porque mutaste la colección mientras lo leía es simplemente grosero.

  1. Es extremadamente inconveniente trabajar con referencias cruzadas (por ejemplo, el nodo de árbol que hace referencia a sus hijos mientras que los niños hacen referencia a sus padres), lo que me hace no usar referencias cruzadas, lo que nuevamente hace que mis estructuras de datos y mi código sean más funcionales.

Las referencias circulares son un tipo especial de infierno. La inmutabilidad no te salvará de eso.

  1. La herencia se detiene para tener sentido y en su lugar empiezo a usar la composición.

Bueno, aquí estoy contigo, pero no veo qué tiene que ver con la inmutabilidad. La razón por la que me gusta la composición no es porque me encanta el patrón de estrategia dinámica, sino porque me permite cambiar mi nivel de abstracción.

  1. Todas las ideas básicas de OOP como la encapsulación comienzan a desmoronarse y mis objetos comienzan a parecerse a funciones.

Me estremezco al pensar cuál es su idea de "OOP como encapsulación". Si se trata de captadores y establecedores, simplemente deje de llamar a esa encapsulación porque no lo es. Nunca fue Es una programación manual orientada a aspectos. Una oportunidad para validar y un lugar para poner un punto de interrupción es agradable, pero no es una encapsulación. La encapsulación está preservando mi derecho a no saber ni preocuparme por lo que sucede dentro.

Se supone que sus objetos parecen funciones. Son bolsas de funciones. Son bolsas de funciones que se mueven juntas y se redefinen juntas.

La programación funcional está de moda en este momento y la gente está arrojando algunas ideas falsas sobre la POO. No dejes que eso te confunda en creer que este es el final de la POO. Funcional y OOP pueden vivir juntos bastante bien.

  • La programación funcional es formal sobre las tareas.

  • OOP está siendo formal sobre los punteros de función.

Realmente eso. Dykstra nos dijo que gotoera dañino, así que nos formamos al respecto y creamos una programación estructurada. Solo así, estos dos paradigmas tratan de encontrar formas de hacer las cosas mientras se evitan las trampas que surgen de hacer estas cosas problemáticas de manera casual.

Déjame mostrarte algo:

f n (x)

Esa es una función. En realidad es un continuo de funciones:

f 1 (x)
f 2 (x)
...
f n (x)

¿Adivina cómo expresamos eso en los idiomas OOP?

n.f(x)

Ese poco nallí elige qué implementación fse usa Y decide cuáles son algunas de las constantes usadas en esa función (lo que francamente significa lo mismo). Por ejemplo:

f 1 (x) = x + 1
f 2 (x) = x + 2

Eso es lo mismo que proporcionan los cierres. Cuando los cierres se refieren a su alcance, los métodos de objeto se refieren a su estado de instancia. Los objetos pueden hacer cierres uno mejor. Un cierre es una sola función devuelta por otra función. Un constructor devuelve una referencia a una bolsa completa de funciones:

g 1 (x) = x 2 + 1
g 2 (x) = x 2 + 2

Sí lo adivinaste:

n.g(x)

f y g son funciones que cambian juntas y se mueven juntas. Entonces los metemos en la misma bolsa. Esto es realmente un objeto. Mantenerse nconstante (inmutable) solo significa que es más fácil predecir qué harán cuando los llames.

Ahora esa es solo la estructura. La forma en que pienso sobre OOP es un montón de pequeñas cosas que hablan con otras pequeñas cosas. Con suerte a un pequeño grupo selecto de pequeñas cosas. Cuando codifico me imagino como el objeto. Miro las cosas desde el punto de vista del objeto. Y trato de ser flojo para no sobrecargar el objeto. Tomo mensajes simples, trabajo un poco en ellos y envío mensajes simples solo a mis mejores amigos. Cuando termino con ese objeto, me meto en otro y miro las cosas desde su perspectiva.

Las Tarjetas de Responsabilidad de Clase fueron las primeras en enseñarme a pensar de esa manera. Hombre, estaba confundido acerca de ellos en ese entonces, pero maldita sea si todavía no son relevantes hoy.

Tengamos un tablero de ajedrez como una colección inmutable de piezas de ajedrez inmutables (extendiendo la pieza de clase abstracta). Desde el punto de vista de OOP, una pieza es responsable de generar movimientos válidos desde su posición en el tablero. Pero para generar los movimientos, la pieza necesita una referencia a su tablero, mientras que el tablero debe tener referencia a sus piezas.

Arg! De nuevo con las innecesarias referencias circulares.

Qué tal: A ChessBoardDataStructureconvierte los cables xy en referencias de piezas. Esas piezas tienen un método que toma x, y y un particular ChessBoardDataStructurey lo convierte en una colección de nuevos y sorprendentes ChessBoardDataStructures. Luego mete eso en algo que puede elegir el mejor movimiento. Ahora ChessBoardDataStructurepuede ser inmutable y también lo pueden ser las piezas. De esta manera, solo tienes un peón blanco en la memoria. Solo hay varias referencias a él en las ubicaciones xy correctas. Orientado a objetos, funcional e inmutable.

Espera, ¿ya no hablamos de ajedrez?

naranja confitada
fuente
66
Todos deberían leer esto. Y luego lea Sobre la comprensión de la abstracción de datos, revisada por William R. Cook . Y luego lea esto de nuevo. Y luego lea la propuesta de Cook para definiciones modernas y simplificadas de "objeto" y "orientado a objetos" . Agregue " La gran idea es 'mensajes' " de Alan Kay , su definición de OO
Jörg W Mittag
1
Y por último, pero no menos importante, el Tratado de Orlando .
Jörg W Mittag
1
@Euphoric: creo que es una formulación bastante estándar que coincide con las definiciones estándar de "programación funcional", "programación imperativa", "programación lógica", etc. De lo contrario, C es un lenguaje funcional porque puede codificar FP en él, un lenguaje lógico, porque puede codificar la programación lógica en él, un lenguaje dinámico, porque puede codificar la escritura dinámica, un lenguaje de actor, porque puede codificar un sistema de actor en él, y así sucesivamente. Y Haskell es el lenguaje imperativo más grande del mundo ya que en realidad puedes nombrar efectos secundarios, almacenarlos en variables, pasarlos como ...
Jörg W Mittag
1
Creo que podemos usar ideas similares a las del genial artículo de Matthias Felleisen sobre la expresividad del lenguaje. Mover un programa OO de, por ejemplo, Java a C♯ se puede hacer solo con transformaciones locales, pero moverlo a C requiere una reestructuración global (básicamente, debe introducir un mecanismo de envío de mensajes y redirigir todas las llamadas de funciones a través de él), por lo tanto Java y C♯ pueden expresar OO y C "solo" puede codificarlo.
Jörg W Mittag
1
@lishaak Porque lo estás especificando para diferentes cosas. Esto lo mantiene en un nivel de abstracción y evita la duplicación del almacenamiento de información posicional que de lo contrario podría volverse inconsistente. Si simplemente te molesta la escritura adicional, pégala en un método para que solo la escribas una vez ... Una pieza no tiene que recordar dónde está si le dices dónde está. Ahora las piezas solo almacenan información como color, movimientos e imagen / abreviatura, que siempre son verdaderas e inmutables.
candied_orange
2

Los conceptos más útiles introducidos en la corriente principal por OOP, en mi opinión, son:

  • Modularización adecuada.
  • Encapsulación de datos.
  • Separación clara entre interfaces privadas y públicas.
  • Mecanismos claros para la extensibilidad del código.

Todos estos beneficios también se pueden obtener sin los detalles de implementación tradicionales, como la herencia o incluso las clases. La idea original de Alan Kay de un "sistema orientado a objetos" utilizaba "mensajes" en lugar de "métodos", y estaba más cerca de Erlang que, por ejemplo, C ++. Mire Go, que elimina muchos detalles de implementación de OOP tradicionales, pero aún se siente razonablemente orientado a objetos.

Si usa objetos inmutables, aún puede usar la mayoría de los objetos tradicionales de OOP: interfaces, despacho dinámico, encapsulación. No necesita setters, y a menudo ni siquiera necesita getters para objetos más simples. También puede obtener los beneficios de la inmutabilidad: siempre está seguro de que un objeto no cambió mientras tanto, sin copia defensiva, sin carreras de datos, y es fácil hacer puros los métodos.

Observe cómo Scala intenta combinar la inmutabilidad y los enfoques de FP con OOP. Es cierto que no es el lenguaje más simple y elegante. Sin embargo, es razonablemente prácticamente exitoso. Además, eche un vistazo a Kotlin que proporciona muchas herramientas y enfoques para una mezcla similar.

El problema habitual al intentar un enfoque diferente al que los creadores del lenguaje tenían en mente hace N años es el "desajuste de impedancia" con la biblioteca estándar. OTOH, los ecosistemas Java y .NET actualmente tienen un soporte razonable de biblioteca estándar para estructuras de datos inmutables; por supuesto, también existen bibliotecas de terceros.

9000
fuente