Pattern per la propagazione delle modifiche su un modello a oggetti ..?


22

Ecco uno scenario comune che è sempre frustrante per me affrontare.

Ho un modello a oggetti con un oggetto genitore. Il genitore contiene alcuni oggetti figlio. Qualcosa come questo.

public class Zoo
{
    public List<Animal> Animals { get; set; }
    public bool IsDirty { get; set; }
}

Ogni oggetto figlio ha vari dati e metodi

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

    public void MakeMess()
    {
        ...
    }
}

Quando il figlio cambia, in questo caso quando viene chiamato il metodo MakeMess, è necessario aggiornare un valore nel genitore. Diciamo che quando una certa soglia di Animal ha fatto un casino, allora deve essere impostata la bandiera IsDirty dello Zoo.

Esistono alcuni modi per gestire questo scenario (di cui sono a conoscenza).

1) Ogni animale può avere un riferimento Zoo genitore per comunicare i cambiamenti.

public class Animal
{
    public Zoo Parent { get; set; }
    ...

    public void MakeMess()
    {
        Parent.OnAnimalMadeMess();
    }
}

Questa sembra l'opzione peggiore in quanto accoppia Animal con l'oggetto principale. E se volessi un animale che vive in una casa?

2) Un'altra opzione, se stai usando una lingua che supporta gli eventi (come C #) è quella di abbonarsi al genitore per cambiare gli eventi.

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)
    {
        ...
    }
}

Questo sembra funzionare, ma per esperienza diventa difficile da mantenere. Se gli animali cambiano mai Zoo, devi annullare l'iscrizione agli eventi nel vecchio Zoo e iscriverti nuovamente al nuovo Zoo. Questo peggiora solo quando l'albero della composizione diventa più profondo.

3) L'altra opzione è quella di spostare la logica su al genitore.

public class Zoo
{
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Questo sembra molto innaturale e causa la duplicazione della logica. Ad esempio, se avessi un oggetto House che non condivide alcun genitore ereditario comune con Zoo ..

public class House
{
    // Now I have to duplicate this logic
    public void AnimalMakesMess(Animal animal)
    {
        ...
    }
}

Non ho ancora trovato una buona strategia per affrontare queste situazioni. Cos'altro è disponibile? Come può essere reso più semplice?


Hai ragione sul fatto che il n. 1 sia cattivo, e non mi piace neanche il n. 2; generalmente vuoi evitare gli effetti collaterali e invece li stai aumentando. Per quanto riguarda l'opzione n. 3, perché non puoi scomporre AnimalMakeMess in un metodo statico che tutte le classi possono chiamare?
Doval,

4
Il numero 1 non è necessariamente negativo se si comunica attraverso un'interfaccia (IAnimalObserver) anziché quella specifica classe Parent.
coredump,

Risposte:


11

Ho dovuto occuparmene un paio di volte. La prima volta che ho usato l'opzione 2 (eventi) e come hai detto è diventato davvero complicato. Se segui questa strada, ti consiglio vivamente di aver bisogno di test unitari molto approfonditi per assicurarti che gli eventi siano fatti correttamente e non stai lasciando riferimenti penzolanti, altrimenti è davvero un grande dolore eseguire il debug.

La seconda volta, ho appena implementato la proprietà genitore in funzione dei bambini, quindi mantieni una Dirtyproprietà su ciascun animale e lascia che Animal.IsDirtytorni this.Animals.Any(x => x.IsDirty). Quello era nel modello. Sopra il modello c'era un controller, e il compito del controller era sapere che dopo aver cambiato il modello (tutte le azioni sul modello venivano passate attraverso il controller in modo che sapesse che qualcosa era cambiato), quindi sapeva che doveva chiamare certi re -valutazione funzioni, come l'attivazione del ZooMaintenancedipartimento per verificare se Zooera di nuovo sporco. In alternativa, potrei semplicemente spingere i ZooMaintenancecontrolli fino a un po 'di tempo programmato in seguito (ogni 100 ms, 1 secondo, 2 minuti, 24 ore, qualunque cosa fosse necessaria).

Ho scoperto che quest'ultimo è stato molto più semplice da mantenere e le mie paure di problemi di prestazioni non si sono mai materializzate.

modificare

Un altro modo di gestire questo è un modello di Message Bus . Invece di usare un Controllerlike nel mio esempio, inietti ogni oggetto con un IMessageBusservizio. La Animalclasse può quindi pubblicare un messaggio, ad esempio "Mess Made" e la tua Zooclasse può iscriversi al messaggio "Mess Made". Il servizio di bus messaggi si occuperà di avvisare Zooquando un animale pubblica uno di questi messaggi e può rivalutare la sua IsDirtyproprietà.

Questo ha il vantaggio di Animalsnon avere più bisogno di un Zooriferimento e Zoonon deve preoccuparsi di iscriversi e annullare l'iscrizione agli eventi di ogni singolo Animal. La pena è che tutte le Zooclassi che si iscrivono a quel messaggio dovranno rivalutare le loro proprietà, anche se non era uno dei suoi animali. Questo può essere o meno un grosso problema. Se ci sono solo una o due Zooistanze, probabilmente va bene.

Modifica 2

Non scartare la semplicità dell'opzione 1. Chiunque riveda il codice non avrà molti problemi a capirlo. Sarà ovvio per qualcuno che guarda la Animalclasse che quando MakeMessviene chiamato propaga il messaggio fino alla classe Zooe sarà ovvio per la Zooclasse da cui riceve i suoi messaggi. Ricorda che nella programmazione orientata agli oggetti, una chiamata al metodo era chiamata "messaggio". In effetti, l'unica volta che ha molto senso uscire dall'opzione 1 è se più di un semplice Zoodeve essere avvisato se Animalfa un casino. Se ci fossero più oggetti che dovevano essere notificati, probabilmente mi sposterei su un bus messaggi o un controller.


5

Ho creato un semplice diagramma di classe che descrive il tuo dominio: inserisci qui la descrizione dell'immagine

Ognuno Animal ha un Habitat casino.

Il Habitatnon si preoccupa che cosa o quanti animali che ha (a meno che non è fondamentalmente parte del disegno che in questo caso da te descritto non lo è).

Ma Animalimporta, perché si comporterà diversamente in ogni Habitat.

Questo diagramma è simile al diagramma UML del modello di progettazione della strategia , ma lo useremo in modo diverso.

Ecco alcuni esempi di codice in Java (non voglio commettere errori specifici per C #).

Naturalmente puoi personalizzare il tuo design, la lingua e i requisiti.

Questa è l'interfaccia strategica:

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

    public float getCleanliness();
}

Un esempio di un concreto Habitat. ovviamente ogni Habitatsottoclasse può implementare questi metodi in modo diverso.

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

    public float getCleanliness() {
        return cleanliness;
    }

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

Ovviamente puoi avere più sottoclassi di animali, dove ognuna le confonde in modo diverso:

public class Animel {
    private Habitat habitat;

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

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

Questa è la classe client, questo spiega fondamentalmente come è possibile utilizzare questo design.

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");
        }
    }
}

Ovviamente nella tua vera applicazione puoi far Habitatconoscere e gestire Animalse ne hai bisogno.


3

Ho avuto un discreto successo con architetture come la tua opzione 2 in passato. È l'opzione più generale e consentirà la massima flessibilità. Ma se hai il controllo sui tuoi ascoltatori e non gestisci molti tipi di abbonamento, puoi abbonarti agli eventi più facilmente creando un'interfaccia.

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

class MessEvent
{
  String DetailsOrWhatever;
}

L'opzione di interfaccia ha il vantaggio di essere quasi semplice come la tua opzione 1, ma ti consente anche di ospitare animali abbastanza facilmente in un Houseo FairlyLand.


3
  • L'opzione 1 è in realtà piuttosto semplice. Questo è solo un riferimento indietro. Ma generalizzalo con l'interfaccia chiamata Dwellinge fornisci un MakeMessmetodo su di esso. Ciò rompe la dipendenza circolare. Quindi quando l'animale fa un casino, chiama dwelling.MakeMess()anche lui.

Nello spirito di lex parsimonia , vado con questo, anche se probabilmente userò la soluzione a catena di seguito, conoscendomi. (Questo è esattamente lo stesso modello suggerito da @Benjamin Albert.)

Si noti che se si modellasse le tabelle del database relazionale, la relazione sarebbe andata diversamente: Animale avrebbe un riferimento a Zoo e la raccolta di Animali per uno Zoo sarebbe un risultato di query.

  • Prendendo ulteriormente questa idea, potresti usare un'architettura incatenata. Vale a dire, creare un'interfaccia Messablee in ogni elemento scrivibile, includere un riferimento a next. Dopo aver creato un pasticcio, chiama MakeMessl'elemento successivo.

Quindi Zoo qui è coinvolto nel fare casino, perché diventa anche disordinato. Avere:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Quindi ora hai una catena di cose che ricevono il messaggio che è stato creato un pasticcio.

  • Opzione 2, un modello di pubblicazione / sottoscrizione potrebbe funzionare qui, ma sembra davvero pesante. L'oggetto e il contenitore hanno una relazione nota, quindi sembra un po 'pesante usare qualcosa di più generale di così.

  • Opzione 3: in questo caso particolare, chiamare Zoo.MakeMess(animal)o in House.MakeMess(animal)realtà non è una cattiva opzione, perché una casa può avere una semantica diversa per diventare disordinata rispetto a uno zoo.

Anche se non percorri la catena, sembra che ci siano due problemi qui: 1) il problema riguarda la propagazione di un cambiamento da un oggetto al suo contenitore, 2) Sembra che tu voglia girare un'interfaccia per il contenitore per astrarre dove gli animali possono vivere.

...

Se si dispone di funzioni di prima classe, è possibile passare una funzione (o delegare) in Animal da chiamare dopo aver fatto un pasticcio. È un po 'come l'idea della catena, tranne con una funzione anziché un'interfaccia.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Quando l'animale si muove, basta impostare un nuovo delegato.

  • Presi all'estremo, è possibile utilizzare Aspect Oriented Programming (AOP) con consigli "after" su MakeMess.

2

Andrei con 1, ma trasformerei la relazione genitore-figlio insieme alla logica di notifica in un wrapper separato. Ciò rimuove la dipendenza di Animal on Zoo e consente la gestione automatica delle relazioni padre-figlio. Ma questo richiede di rifare gli oggetti nella gerarchia in interfacce / classi astratte prima e scrivere un wrapper specifico per ciascuna interfaccia. Ma ciò potrebbe essere rimosso usando la generazione del codice.

Qualcosa di simile a :

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; }
}

Questo è in realtà il modo in cui alcuni ORM eseguono il rilevamento delle modifiche sulle entità. Creano involucri attorno alle entità e ti fanno lavorare con quelli. Questi wrapper sono generalmente realizzati utilizzando la generazione di codice di riflessione e dinamica.


1

Due opzioni che uso spesso. È possibile utilizzare il secondo approccio e inserire la logica per il cablaggio dell'evento nella raccolta stessa sul genitore.

Un approccio alternativo (che può essere effettivamente utilizzato con una delle tre opzioni) è l'uso del contenimento. Crea un AnimalContainer (o addirittura trasformalo in una collezione) che possa vivere in casa, allo zoo o altro. Fornisce la funzionalità di tracciamento associata agli animali, ma evita i problemi di ereditarietà poiché può essere incluso in qualsiasi oggetto che ne abbia bisogno.


0

Inizi con un fallimento di base: gli oggetti figlio non dovrebbero conoscere i loro genitori.

Le stringhe sanno che sono in un elenco? No. Le date sanno che esistono in un calendario? No.

L'opzione migliore è cambiare il tuo design in modo che questo tipo di scenario non esista.

Successivamente, considera l'inversione del controllo. Anziché MakeMesssu Animalun effetto collaterale o eventi, passare Zooal metodo. L'opzione 1 va bene se devi proteggere l'invariante che Animaldeve sempre vivere da qualche parte. Quindi non è un genitore, ma un'associazione tra pari.

Occasionalmente 2 e 3 andranno bene, ma il principio architettonico chiave da seguire è che i bambini non conoscono i loro genitori.


Sospetto che sia più simile a un pulsante di invio in un modulo che a una stringa in un elenco.
svidgen,

1
@svidgen - Quindi passare un callback. Più infallibile di un evento, più facile da ragionare e nessun riferimento cattivo a cose che non è necessario conoscere.
Telastyn,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.