¿Patrones para propagar cambios en un modelo de objeto ...?


22

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?


Tienes razón acerca de que el n. ° 1 es malo, y tampoco estoy interesado en el n. ° 2; generalmente desea evitar los efectos secundarios y, en cambio, los está aumentando. Con respecto a la opción # 3, ¿por qué no puede factorizar AnimalMakeMess en un método estático al que todas las clases pueden llamar?
Doval

44
# 1 no es necesariamente malo si se comunica a través de una interfaz (IAnimalObserver) en lugar de esa clase principal específica.
coredump

Respuestas:


11

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 Dirtypropiedad en cada animal y deje que Animal.IsDirtyregrese this.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 el ZooMaintenancedepartamento para verificar si Zooestaba sucio nuevamente. Alternativamente, podría ZooMaintenanceretrasar 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 Controllergusta en mi ejemplo, inyecta cada objeto con un IMessageBusservicio. La Animalclase puede publicar un mensaje, como "Mess Made" y su Zooclase puede suscribirse al mensaje "Mess Made". El servicio de bus de mensajes se encargará de notificar Zoocuando un animal publique uno de esos mensajes, y puede reevaluar su IsDirtypropiedad.

Esto tiene la ventaja de que Animalsya no necesita una Zooreferencia, y Zoono tiene que preocuparse por suscribirse y darse de baja de eventos de cada uno Animal. La pena es que todas las Zooclases 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 dos Zooinstancias, 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 Animalclase que cuando MakeMessse llama que propaga el mensaje hasta Zoola Zooclase 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 que Zoonotificar si Animalhace un desastre. Si hubiera más objetos que debían notificarse, probablemente me mudaría a un bus de mensajes o un controlador.


5

He hecho un diagrama de clase simple que describe tu dominio: ingrese la descripción de la imagen aquí

Cada uno Animal tiene un Habitat desastre.

El Habitatno 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 Animalsí le importa, porque se comportará de manera diferente en cada uno Habitat.

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:

public interface Habitat {
    public void messUp(float magnitude);

    public float getCleanliness();
}

Un ejemplo de concreto Habitat. Por supuesto, cada Habitatsubclase puede implementar estos métodos de manera diferente.

public class Zoo implements Habitat {
    public float cleanliness = 1;

    public float getCleanliness() {
        return cleanliness;
    }

    public void messUp(float magnitude) {
        cleanliness -= magnitude;
    }
}

Por supuesto, puede tener múltiples subclases de animales, donde cada una lo desordena de manera diferente:

public class Animel {
    private Habitat habitat;

    public void makeMess() {
        habitat.messUp(.05f);
    }

    public Animel addTo(Habitat habitat) {
        this.habitat = habitat;
        return this;
    }
}

Esta es la clase de cliente, esto básicamente explica cómo puede usar este diseño.

public class ZooKeeper {
    public Habitat zoo = new Zoo();

    public ZooKeeper() {
        new Animal()
            .addTo( zoo )
            .makeMess();

        if (zoo.getCleanliness() < 0.5f) {
            System.out.println("The zoo is really messy");
        } else {
            System.out.println("The zoo looks clean");
        }
    }
}

Por supuesto, en su aplicación real puede informarle Habitaty administrarlo Animalsi lo necesita.


3

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.

interface MessablePlace
{
  void OnMess(object sender, MessEvent e);
}

class MessEvent
{
  String DetailsOrWhatever;
}

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 Houseo FairlyLand.


3
  • La opción 1 es realmente bastante sencilla. Eso es solo una referencia. Pero generalícelo con la interfaz llamada Dwellingy proporcione un MakeMessmétodo. Eso rompe la dependencia circular. Luego, cuando el animal hace un desastre, también llama dwelling.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.

  • Llevando esa idea más lejos, podrías usar una arquitectura encadenada. Es decir, cree una interfaz Messabley, en cada elemento desordenado, incluya una referencia a next. Después de crear un desastre, llame MakeMessal siguiente elemento.

Entonces Zoo aquí está involucrado en hacer un desastre, porque también se vuelve desordenado. Tener:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

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)o House.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.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Cuando el animal se mueve, solo establece un nuevo delegado.

  • Llevado al extremo, puede usar la Programación Orientada a Aspectos (AOP) con consejos "posteriores" sobre MakeMess.

2

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 :

public interface IAnimal
{
    string Name { get; set; }
    int Age { get; set; }

    void MakeMess();
}

public class Animal : IAnimal
{
    public string Name { get; set; }
    public int Age { get; set; }

    public void MakeMess()
    {
        // makes mess
    }
}

public class ZooAnimals
{
    class AnimalInZoo : IAnimal
    {
        public IAnimal _animal;
        public ZooAnimals _zoo;

        public AnimalInZoo(IAnimal animal, ZooAnimals zoo)
        {
            _animal = animal;
            _zoo = zoo;
        }

        public string Name { get { return _animal.Name; } set { _animal.Name = value; } }
        public int Age { get { return _animal.Age; } set { _animal.Age = value; } }

        public void MakeMess()
        {
            _animal.MakeMess();
            _zoo.IsDirty = true;
        }
    }

    private Collection<AnimalInZoo> animals = new Collection<AnimalInZoo>();

    public IAnimal Add(IAnimal animal)
    {
        if (animal is AnimalInZoo)
        {
            var inZoo = (AnimalInZoo)animal;
            if (inZoo._zoo != this)
            {
                // animal is in a different zoo, what to do ?
                // either move animal to this zoo
                // or throw an exception so caller is forced to remove the animal from previous zoo first
            }
        }

        var anim = new AnimalInZoo(animal, this);
        animals.Add(anim);
        return anim;
    }

    public IAnimal Remove(IAnimal animal)
    {
        if (!(animal is AnimalInZoo))
        {
            // animal is not in zoo, throw an exception?
        }
        var inZoo = (AnimalInZoo)animal;
        if (inZoo._zoo != this)
        {
            // animal is in a different zoo, throw an exception?
        }

        animals.Remove(inZoo);
        return inZoo._animal;
    }

    public bool IsDirty { get; set; }
}

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.


1

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.


0

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 MakeMesssobre Animalcon un efecto secundario o evento, pasar Zooen el método. La opción 1 está bien si necesita proteger al invariante que Animalsiempre 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.


Sospecho que esto se parece más a un botón de envío en un formulario que a una cadena en una lista.
svidgen

1
@svidgen: luego pasa una devolución de llamada. Más infalible que un evento, más fácil de razonar y sin referencias traviesas a cosas que no necesita saber.
Telastyn
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.