¿Existe una estrategia de diseño específica que pueda aplicarse para resolver la mayoría de los problemas de huevo y gallina mientras se usan objetos inmutables?

17

Viniendo de un fondo OOP (Java), estoy aprendiendo Scala por mi cuenta. Si bien puedo ver fácilmente las ventajas de usar objetos inmutables individualmente, me cuesta ver cómo se puede diseñar una aplicación completa como esa. Daré un ejemplo:

Digamos que tengo objetos que representan "materiales" y sus propiedades (estoy diseñando un juego, así que realmente tengo ese problema), como el agua y el hielo. Tendría un "gerente" que posee todas esas instancias de materiales. Una propiedad sería el punto de congelación y fusión, y a qué se congela o derrite el material.

[EDITAR] Todas las instancias materiales son "singleton", algo así como una enumeración de Java.

Quiero "agua" para decir que se congela a "hielo" a 0 ° C, y "hielo" para decir que se derrite a "agua" a 1 ° C. Pero si el agua y el hielo son inmutables, no pueden obtener una referencia el uno al otro como parámetros del constructor, porque uno de ellos debe crearse primero, y ese no podría obtener una referencia al otro que aún no existe como parámetro del constructor. Podría resolver esto dándoles a ambos una referencia al gerente para que puedan consultarlo para encontrar la otra instancia material que necesitan cada vez que se les solicite sus propiedades de congelación / fusión, pero luego obtengo el mismo problema entre el gerente y los materiales, que necesitan una referencia entre sí, pero solo se puede proporcionar en el constructor de uno de ellos, por lo que el administrador o el material no pueden ser inmutables.

¿Simplemente no hay forma de evitar este problema, o necesito usar técnicas de programación "funcionales", o algún otro patrón para resolverlo?

Sebastien Diot
fuente
2
para mí, como tú dices, no hay agua ni hielo. Solo hay h2omaterial
mosquito
1
Sé que esto tendría más "sentido científico", pero en un juego es más fácil modelarlo como dos materiales diferentes con propiedades "fijas", en lugar de un material con propiedades "variables" dependiendo del contexto.
Sebastien Diot el
Singleton es una idea tonta.
DeadMG
@DeadMG Bueno, está bien. No son Java Singletons reales. Solo quiero decir que no tiene sentido crear más de una instancia de cada una, ya que son inmutables y serían iguales entre sí. De hecho, no tendré ninguna instancia estática real. Mis objetos "raíz" son servicios OSGi.
Sebastien Diot
La respuesta a esta otra pregunta parece confirmar mi sospecha de que las cosas se complican muy rápidamente con los inmutables: programmers.stackexchange.com/questions/68058/…
Sebastien Diot

Respuestas:

2

La solución es hacer trampa un poco. Específicamente:

  • Cree A, pero deje su referencia a B sin inicializar (ya que B aún no existe).

  • Crea B y haz que apunte a A.

  • Actualice A para que apunte a B. No actualice A ni B después de esto.

Esto se puede hacer explícitamente (ejemplo en C ++):

struct List {
    int n;
    List *next;

    List(int n, List *next)
        : n(n), next(next);
};

// Return a list containing [0,1,0,1,...].
List *binary(void)
{
    List *a = new List(0, NULL);
    List *b = new List(1, a);
    a->next = b; // Evil, but necessary.
    return a;
}

o implícitamente (ejemplo en Haskell):

binary :: [Int]
binary = a where
    a = 0 : b
    b = 1 : a

El ejemplo de Haskell utiliza una evaluación perezosa para lograr la ilusión de valores inmutables mutuamente dependientes. Los valores comienzan como:

a = 0 : <thunk>
b = 1 : a

ay bson formas válidas de cabeza normal independientemente. Cada contra puede construirse sin necesitar el valor final de la otra variable. Cuando se evalúa el thunk, apuntará a los mismos bpuntos de datos .

Por lo tanto, si desea que dos valores inmutables se apunten entre sí, debe actualizar el primero después de construir el segundo o utilizar un mecanismo de nivel superior para hacer lo mismo.


En su ejemplo particular, podría expresarlo en Haskell como:

data Material = Water {temperature :: Double}
              | Ice   {temperature :: Double}

setTemperature :: Double -> Material -> Material
setTemperature newTemp (Water _) | newTemp <= 0.0 = Ice newTemp
                                 | otherwise      = Water newTemp
setTemperature newTemp (Ice _)   | newTemp >= 1.0 = Water newTemp
                                 | otherwise      = Ice newTemp

Sin embargo, estoy esquivando el problema. Me imagino que en un enfoque orientado a objetos, donde setTemperaturese adjunta un método al resultado de cada Materialconstructor, tendría que hacer que los constructores se apuntaran entre sí. Si los constructores se tratan como valores inmutables, puede utilizar el enfoque descrito anteriormente.

Joey Adams
fuente
(suponiendo que entendí la sintaxis de Haskell) Creo que mi solución actual es en realidad muy similar, pero me preguntaba si era la "correcta" o si existe algo mejor. Primero creo un "identificador" (referencia) para cada objeto (aún no creado), luego creo todos los objetos, dándoles los identificadores que necesitan, y finalmente inicializa el identificador a los objetos. Los objetos son inmutables, pero no los tiradores.
Sebastien Diot
6

En su ejemplo, está aplicando una transformación a un objeto, por lo que usaría algo como un ApplyTransform()método que devuelve un en BlockBaselugar de intentar cambiar el objeto actual.

Por ejemplo, para cambiar un IceBlock a un WaterBlock aplicando algo de calor, llamaría algo así como

BlockBase currentBlock = new IceBlock();
currentBlock = currentBlock.ApplyTemperature(1); 
// currentBlock is now a WaterBlock 

y el IceBlock.ApplyTemperature()método se vería así:

public class IceBlock() : BlockBase
{
    public BlockBase ApplyTemperature(int temp)
    {
        return (temp > 0 ? new WaterBlock((BlockBase)this) : this);
    }
}
Rachel
fuente
Esta es una buena respuesta, pero desafortunadamente solo porque no mencioné que mis "materiales", de hecho mis "bloques", son todos únicos, por lo que el nuevo WaterBlock () simplemente no es una opción. Ese es el principal beneficio de inmutable, puede reutilizarlos infinitamente. En lugar de tener 500,000 bloques en ram, tengo 500,000 referencias a 100 bloques. ¡Mucho mas barato!
Sebastien Diot el
Entonces, ¿qué hay de regresar en BlockList.WaterBlocklugar de crear un nuevo bloque?
Rachel
Sí, esto es lo que hago, pero ¿cómo obtengo la lista de bloqueo? Obviamente, los bloques deben crearse antes de la lista de bloques y, por lo tanto, si el bloque es realmente inmutable, no puede recibir la lista de bloques como parámetro. Entonces, ¿de dónde saca la lista? Mi punto general es que, al hacer que el código sea más complicado, resuelve el problema del huevo y la gallina en un nivel, solo para obtenerlo nuevamente en el siguiente. Básicamente, no veo forma de crear una aplicación completa basada en la inmutabilidad. Parece solo aplicable a los "objetos pequeños", pero no a los contenedores.
Sebastien Diot
@Sebastien Estoy pensando que BlockListes solo una staticclase que es responsable de las instancias individuales de cada bloque, por lo que no es necesario crear una instancia de BlockList(Estoy acostumbrado a C #)
Rachel
@Sebastien: si usas Singeltons, pagas el precio.
DeadMG
6

Otra forma de romper el ciclo es separar las preocupaciones del material y la transmutación, en un lenguaje inventado:

water = new Block("water");
ice = new Block("ice");

transitions = new Transitions([
    new transitions.temperature.Below(0.0, water, ice),
    new transitions.temperature.Above(0.0, ice, water),
]);
SingleNegationElimination
fuente
Huh, fue difícil para mí leer esto inicialmente, pero creo que es esencialmente el mismo enfoque que defendí.
Aidan Cully
1

Si va a usar un lenguaje funcional, y quiere darse cuenta de los beneficios de la inmutabilidad, entonces debe abordar el problema con eso en mente. Está intentando definir un tipo de objeto "hielo" o "agua" que pueda soportar un rango de temperaturas; para soportar la inmutabilidad, entonces necesitaría crear un nuevo objeto cada vez que cambie la temperatura, lo cual es un desperdicio. Así que trate de hacer que los conceptos de tipo de bloque y temperatura sean más independientes. No sé Scala (está en mi lista de aprendizaje :-)), pero tomando prestado de Joey Adams Answer en Haskell , sugiero algo como:

data Material = Water | Ice

blockForTemperature :: Double -> Material
blockForTemperature x = 
  if x < 0 then Ice else Water

o tal vez:

transitionForTemperature :: Material -> Double -> Material
transitionForTemperature oldMaterial newTemp = 
  case (oldMaterial, newTemp) of
    (Ice, _) | newTemp > 0 -> Water
    (Water, _) | newTemp <= 0 -> Ice

(Nota: no he intentado ejecutar esto, y mi Haskell está un poco oxidado). Ahora, la lógica de transición está separada del tipo de material, por lo que no desperdicia tanta memoria y (en mi opinión) es bastante un poco más funcionalmente orientado.

Aidan Cully
fuente
En realidad no estoy tratando de usar "lenguaje funcional" porque simplemente no lo entiendo. Lo único que normalmente guardo de cualquier ejemplo de programación funcional no trivial es: "¡Maldita sea, yo era más inteligente!" Está más allá de mí cómo esto puede tener sentido para cualquiera. Desde los días de mis alumnos, recuerdo que Prolog (basado en la lógica), Occam (todo funciona en paralelo por defecto) e incluso el ensamblador tenía sentido, pero Lisp era simplemente una locura. Pero sí entiendo cómo mover el código que causa un "cambio de estado" fuera del "estado".
Sebastien Diot