Aquí hay un escenario común que siempre es frustrante para mí enfrentar.
Tengo un modelo de objeto con un objeto padre. El padre contiene algunos objetos secundarios. Algo como esto.
public class Zoo
{
public List<Animal> Animals { get; set; }
public bool IsDirty { get; set; }
}
Cada objeto secundario tiene varios datos y métodos.
public class Animal
{
public string Name { get; set; }
public int Age { get; set; }
public void MakeMess()
{
...
}
}
Cuando el elemento secundario cambia, en este caso cuando se llama al método MakeMess, es necesario actualizar algún valor en el elemento primario. Digamos que cuando un cierto umbral de Animal ha hecho un desastre, entonces se debe establecer la bandera IsDirty del zoológico.
Hay algunas maneras de manejar este escenario (que yo sepa).
1) Cada animal puede tener una referencia primaria del zoológico para comunicar los cambios.
public class Animal
{
public Zoo Parent { get; set; }
...
public void MakeMess()
{
Parent.OnAnimalMadeMess();
}
}
Esta se siente como la peor opción, ya que combina Animal con su objeto padre. ¿Qué pasa si quiero un animal que vive en una casa?
2) Otra opción, si está utilizando un lenguaje que admite eventos (como C #) es hacer que el padre se suscriba para cambiar los eventos.
public class Animal
{
public event OnMakeMessDelegate OnMakeMess;
public void MakeMess()
{
OnMakeMess();
}
}
public class Zoo
{
...
public void SubscribeToChanges()
{
foreach (var animal in Animals)
{
animal.OnMakeMess += new OnMakeMessDelegate(OnMakeMessHandler);
}
}
public void OnMakeMessHandler(object sender, EventArgs e)
{
...
}
}
Esto parece funcionar, pero por experiencia se hace difícil de mantener. Si los animales alguna vez cambian los zoológicos, debe cancelar la suscripción a eventos en el antiguo zoológico y volver a suscribirse en el nuevo zoológico. Esto solo empeora a medida que el árbol de composición se vuelve más profundo.
3) La otra opción es mover la lógica al padre.
public class Zoo
{
public void AnimalMakesMess(Animal animal)
{
...
}
}
Esto parece muy poco natural y provoca la duplicación de la lógica. Por ejemplo, si tuviera un objeto House que no comparte ningún padre de herencia común con Zoo ...
public class House
{
// Now I have to duplicate this logic
public void AnimalMakesMess(Animal animal)
{
...
}
}
Todavía no he encontrado una buena estrategia para hacer frente a estas situaciones. ¿Qué más hay disponible? ¿Cómo se puede simplificar esto?
fuente
Respuestas:
Tuve que lidiar con esto un par de veces. La primera vez que usé la opción 2 (eventos) y como dijiste, se volvió realmente complicado. Si sigue esa ruta, le sugiero que necesite pruebas unitarias muy exhaustivas para asegurarse de que los eventos se realicen correctamente y que no deje referencias pendientes, de lo contrario, es un gran problema depurar.
La segunda vez, acabo de implementar la propiedad principal en función de los hijos, así que mantenga una
Dirty
propiedad en cada animal y deje queAnimal.IsDirty
regresethis.Animals.Any(x => x.IsDirty)
. Eso estaba en el modelo. Encima del modelo había un controlador, y el trabajo del controlador era saber que después de cambiar el modelo (todas las acciones en el modelo se pasaron a través del controlador para que supiera que algo había cambiado), entonces sabía que tenía que llamar a cierto -Funciones de evaluación, como activar elZooMaintenance
departamento para verificar siZoo
estaba sucio nuevamente. Alternativamente, podríaZooMaintenance
retrasar las comprobaciones hasta un tiempo posterior programado (cada 100 ms, 1 segundo, 2 minutos, 24 horas, lo que sea necesario).Encontré que esto último ha sido mucho más simple de mantener, y mis temores a los problemas de rendimiento nunca se materializaron.
Editar
Otra forma de lidiar con esto es un patrón de bus de mensajes . En lugar de usar un me
Controller
gusta en mi ejemplo, inyecta cada objeto con unIMessageBus
servicio. LaAnimal
clase puede publicar un mensaje, como "Mess Made" y suZoo
clase puede suscribirse al mensaje "Mess Made". El servicio de bus de mensajes se encargará de notificarZoo
cuando un animal publique uno de esos mensajes, y puede reevaluar suIsDirty
propiedad.Esto tiene la ventaja de que
Animals
ya no necesita unaZoo
referencia, yZoo
no tiene que preocuparse por suscribirse y darse de baja de eventos de cada unoAnimal
. La pena es que todas lasZoo
clases que se suscriban a ese mensaje tendrán que reevaluar sus propiedades, incluso si no fue uno de sus animales. Eso puede o no ser un gran problema. Si solo hay una o dosZoo
instancias, probablemente esté bien.Editar 2
No descarte la simplicidad de la opción 1. Cualquiera que revise el código no tendrá muchos problemas para entenderlo. Será obvio para alguien que esté mirando la
Animal
clase que cuandoMakeMess
se llama que propaga el mensaje hastaZoo
laZoo
clase y será obvio para la clase de donde obtiene sus mensajes. Recuerde que en la programación orientada a objetos, una llamada al método solía llamarse un "mensaje". De hecho, el único momento en el que tiene mucho sentido romper con la opción 1 es si hay algo más queZoo
notificar siAnimal
hace un desastre. Si hubiera más objetos que debían notificarse, probablemente me mudaría a un bus de mensajes o un controlador.fuente
He hecho un diagrama de clase simple que describe tu dominio:
Cada uno
Animal
tiene unHabitat
desastre.El
Habitat
no le importa qué o cuántos animales tiene (a menos que sea fundamentalmente parte de su diseño que en este caso usted describe no lo es).Pero
Animal
sí le importa, porque se comportará de manera diferente en cada unoHabitat
.Este diagrama es similar al Diagrama UML del patrón de diseño de la estrategia , pero lo usaremos de manera diferente.
Aquí hay algunos ejemplos de código en Java (no quiero cometer errores específicos de C #).
Por supuesto, puede hacer su propio ajuste a este diseño, lenguaje y requisitos.
Esta es la interfaz de estrategia:
Un ejemplo de concreto
Habitat
. Por supuesto, cadaHabitat
subclase puede implementar estos métodos de manera diferente.Por supuesto, puede tener múltiples subclases de animales, donde cada una lo desordena de manera diferente:
Esta es la clase de cliente, esto básicamente explica cómo puede usar este diseño.
Por supuesto, en su aplicación real puede informarle
Habitat
y administrarloAnimal
si lo necesita.fuente
He tenido bastante éxito con arquitecturas como su opción 2 en el pasado. Es la opción más general y permitirá la mayor flexibilidad. Pero, si tiene control sobre sus oyentes y no administra muchos tipos de suscripción, puede suscribirse a los eventos más fácilmente creando una interfaz.
La opción de interfaz tiene la ventaja de ser casi tan simple como su opción 1, pero también le permite alojar animales sin esfuerzo en un
House
oFairlyLand
.fuente
Dwelling
y proporcione unMakeMess
método. Eso rompe la dependencia circular. Luego, cuando el animal hace un desastre, también llamadwelling.MakeMess()
.En el espíritu de lex parsimoniae , voy a ir con este, aunque probablemente usaría la solución de cadena a continuación, conociéndome. (Este es el mismo modelo que sugiere @Benjamin Albert).
Tenga en cuenta que si estuviera modelando tablas de bases de datos relacionales, la relación iría de otra manera: Animal tendría una referencia a Zoo y la colección de Animales para un Zoo sería el resultado de una consulta.
Messable
y, en cada elemento desordenado, incluya una referencia anext
. Después de crear un desastre, llameMakeMess
al siguiente elemento.Entonces Zoo aquí está involucrado en hacer un desastre, porque también se vuelve desordenado. Tener:
Entonces, ahora tiene una cadena de cosas que reciben el mensaje de que se ha creado un desastre.
Opción 2, un modelo de publicación / suscripción podría funcionar aquí, pero se siente realmente pesado. El objeto y el contenedor tienen una relación conocida, por lo que parece un poco difícil usar algo más general que eso.
Opción 3: en este caso particular, llamar
Zoo.MakeMess(animal)
oHouse.MakeMess(animal)
no es realmente una mala opción, porque una casa puede tener una semántica diferente para ensuciarse que un zoológico.Incluso si no va por la ruta de la cadena, parece que hay dos problemas aquí: 1) el problema se trata de propagar un cambio de un objeto a su contenedor, 2) Parece que desea desconectar una interfaz para El contenedor para abstraer dónde pueden vivir los animales.
...
Si tiene funciones de primera clase, puede pasar una función (o delegar) a Animal para que la llame después de que haga un desastre. Eso es un poco como la idea de la cadena, excepto con una función en lugar de una interfaz.
Cuando el animal se mueve, solo establece un nuevo delegado.
fuente
Iría con 1, pero haría la relación padre-hijo junto con la lógica de notificación en un contenedor separado. Esto elimina la dependencia de Animal en Zoo y permite la gestión automática de las relaciones padre-hijo. Pero esto requiere rehacer los objetos en la jerarquía en interfaces / clases abstractas primero y escribir un contenedor específico para cada interfaz. Pero eso podría eliminarse mediante la generación de código.
Algo como :
Así es como algunos ORM hacen su seguimiento de cambios en las entidades. Crean envoltorios alrededor de las entidades y te hacen trabajar con ellos. Esos envoltorios generalmente se hacen usando reflexión y generación dinámica de código.
fuente
Dos opciones que uso a menudo. Puede usar el segundo enfoque y poner la lógica para conectar el evento en la colección en el padre.
Un enfoque alternativo (que en realidad se puede usar con cualquiera de las tres opciones) es utilizar la contención. Haz un AnimalContainer (o incluso hazlo una colección) que pueda vivir en la casa o en el zoológico o cualquier otra cosa. Proporciona la funcionalidad de seguimiento asociada con los animales, pero evita problemas de herencia ya que puede incluirse en cualquier objeto que lo necesite.
fuente
Comienzas con una falla básica: los objetos secundarios no deberían saber sobre sus padres.
¿Las cadenas saben que están en una lista? No. ¿Las fechas saben que existen en un calendario? No.
La mejor opción es cambiar su diseño para que este tipo de escenario no exista.
Después de eso, considere la inversión del control. En lugar de
MakeMess
sobreAnimal
con un efecto secundario o evento, pasarZoo
en el método. La opción 1 está bien si necesita proteger al invariante queAnimal
siempre necesita vivir en algún lugar. No es un padre, sino una asociación de pares entonces.Ocasionalmente, 2 y 3 irán bien, pero el principio arquitectónico clave a seguir es que los niños no saben acerca de sus padres.
fuente