Modelli per propagare le modifiche su un modello a oggetti ..?

20

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

Ho un modello a oggetti con un oggetto genitore. Il genitore contiene alcuni oggetti figli. Qualcosa di simile.

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 bambino 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 il flag IsDirty dello Zoo deve essere impostato.

Ci sono alcuni modi per gestire questo scenario (che io sappia).

1) Ogni animale può avere un riferimento Zoo per poter comunicare le modifiche.

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

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

Questa sembra l'opzione peggiore dal momento che associa Animal al suo oggetto genitore. Cosa succede se voglio un animale che vive in una casa?

2) Un'altra opzione, se stai utilizzando una lingua che supporta eventi (come C #), è che il genitore si iscriva 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 dall'esperienza diventa difficile da mantenere. Se gli animali cambiano mai Zoo, devi annullare l'iscrizione agli eventi nel vecchio zoo e iscriversi nuovamente al nuovo zoo. Questo peggiora solo quando l'albero di composizione diventa più profondo.

3) L'altra opzione è spostare la logica su padre.

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?

    
posta ConditionRacer 22.03.2014 - 00:45
fonte

7 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 caldamente di aver bisogno di test unitari molto accurati per assicurarti che gli eventi siano fatti correttamente e che non ti vengano lasciati riferimenti ciondolanti, altrimenti è molto difficile eseguire il debug.

La seconda volta, ho appena implementato la proprietà genitore come funzione dei bambini, quindi mantieni una proprietà Dirty su ogni animale e lascia che Animal.IsDirty restituisca 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 sono state passate attraverso il controller in modo che sapesse che qualcosa era cambiato), quindi sapeva che doveva chiamare alcune funzioni di rivalutazione, come l'attivazione del dipartimento ZooMaintenance per verificare se il Zoo era di nuovo sporco. In alternativa, potrei semplicemente spingere il ZooMaintenance a spuntare fino a un orario programmato più tardi (ogni 100 ms, 1 secondo, 2 minuti, 24 ore, qualunque cosa fosse necessaria).

Ho scoperto che il secondo è stato molto più semplice da mantenere, e le mie paure sui problemi di performance non si sono mai materializzate.

Modifica

Un altro modo per gestire questo è un bus dei messaggi . Piuttosto che utilizzare un Controller come nel mio esempio, si inietta ogni oggetto con un servizio IMessageBus . La classe Animal può quindi pubblicare un messaggio, come "Mess Made" e la tua classe Zoo può iscriversi al messaggio "Mess Made". Il servizio di bus messaggi si prenderà cura di notificare Zoo quando un animale pubblica uno di questi messaggi e può rivalutare la sua proprietà IsDirty .

Questo ha il vantaggio che Animals non ha più bisogno di un riferimento Zoo , e Zoo non ha bisogno di preoccuparsi di sottoscrizione e annullamento dell'iscrizione dagli eventi da ogni singolo Animal . La penalità è che tutte le classi Zoo che si iscrivono a quel messaggio dovranno rivalutare le loro proprietà, anche se non era uno dei suoi animali. Potrebbe essere o non essere un grosso problema. Se sono presenti solo uno o due% di istanze diZoo, probabilmente è corretto.

Modifica 2

Non sottovalutare la semplicità dell'opzione 1. Chiunque riveda il codice non avrà molti problemi a comprenderlo. Sarà ovvio per qualcuno che guarda alla classe Animal quando viene chiamato MakeMess che propaga il messaggio fino a Zoo e sarà ovvio alla classe Zoo da cui riceve i messaggi da . Ricorda che nella programmazione orientata agli oggetti, una chiamata al metodo veniva chiamata "messaggio". In effetti, l'unica volta che ha molto senso interrompere l'opzione 1 è se deve essere notificato più di Zoo se il Animal fa un casino. Se ci fossero più oggetti che dovevano essere notificati, probabilmente mi trasferirei su un bus dei messaggi o su un controller.

    
risposta data 22.03.2014 - 01:15
fonte
5

Ho creato un semplice diagramma di classe che descrive il tuo dominio:

OgniAnimalhaunHabitatcheincasina.

IlHabitatnonsipreoccupadicosaoquantianimaliha(amenochenonsiafondamentalmentepartedeltuodisegnocheinquestocasohaidescrittononloè).

MailAnimalsipreoccupa,perchésicomporteràinmododiversoinogniHabitat.

QuestodiagrammaèsimilealdiagrammaUMLdel modello di progettazione della strategia , ma lo useremo in modo diverso.

Ecco alcuni esempi di codice in Java (Non voglio fare errori specifici C #).

Naturalmente puoi apportare il tuo contributo a questo design, lingua e requisiti.

Questa è l'interfaccia della strategia:

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

    public float getCleanliness();
}

Un esempio di calcestruzzo Habitat . ovviamente ogni sottoclasse Habitat 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, in cui ognuno lo incassa 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 del cliente, questo spiega in sostanza come puoi usare 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 consentire a Habitat di conoscere e gestire Animal se necessario.

    
risposta data 22.03.2014 - 12:02
fonte
3

Ho avuto un discreto successo con architetture come la tua opzione 2 in passato. È l'opzione più generale e consentirà la massima flessibilità. Tuttavia, se hai il controllo sugli ascoltatori e non gestisci molti tipi di abbonamento, puoi sottoscrivere gli 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 l'opzione 1, ma ti permette anche di ospitare animali senza fatica in un House o FairlyLand .

    
risposta data 22.03.2014 - 03:05
fonte
3
  • L'opzione 1 è in realtà piuttosto semplice. Questo è solo un back-reference. Ma generalizzalo con un'interfaccia chiamata Dwelling e fornisci un metodo MakeMess su di esso. Questo rompe la dipendenza circolare. Poi quando l'animale fa un casino, chiama anche dwelling.MakeMess() .

Nello spirito di lex parsimoniae , ho intenzione di andare con questo, anche se probabilmente utilizzerei la seguente soluzione a catena, conoscendomi. (Questo è lo stesso modello suggerito da @Benjamin Albert.)

Si noti che se si modellano le tabelle del database relazionale, la relazione andrebbe nell'altro senso: Animal avrebbe un riferimento allo Zoo e la raccolta di Animals for a Zoo sarebbe un risultato di query.

  • Prendendo questa idea più lontano, potresti usare un'architettura concatenata. Cioè, crea un'interfaccia Messable , e in ogni elemento incasinato, includi un riferimento a next . Dopo aver creato un disastro, chiama MakeMess sull'elemento successivo.

Quindi Zoo qui è coinvolto nel fare un casino, perché anche questo diventa complicato. 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 danno 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.

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

Anche se non si scende lungo il percorso della catena, sembra che ci siano due problemi qui: 1) il problema riguarda la propagazione di una modifica da un oggetto al suo contenitore, 2) Sembra che si desideri eseguire lo spin off un'interfaccia per il contenitore per astrarre dove possono vivere gli animali.

...

Se hai funzioni di prima classe, puoi passare una funzione (o delegare) in Animal per chiamare dopo aver fatto un casino. È un po 'come l'idea della catena, tranne con una funzione invece di un'interfaccia.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Quando l'animale si muove, imposta un nuovo delegato.

  • Portato all'estremo, potresti usare Aspect Oriented Programming (AOP) con i consigli "after" su MakeMess.
risposta data 22.03.2014 - 01:29
fonte
2

Vorrei andare con 1, ma vorrei rendere la relazione genitore-figlio con la logica di notifica in un wrapper separato. Questo rimuove la dipendenza di Animal on Zoo e consente la gestione automatica delle relazioni genitore-figlio. Ma questo richiede di rifare gli oggetti nella gerarchia in interfacce / classi astratte prima e scrivere un wrapper specifico per ogni interfaccia. Ma potrebbe essere rimosso usando la generazione del codice.

Qualcosa come:

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 è il modo in cui alcuni ORM eseguono il rilevamento delle modifiche sulle entità. Crea wrapper attorno alle entità e ti fanno lavorare con quelli. Questi wrapper vengono solitamente creati usando la reflection e la generazione dinamica del codice.

    
risposta data 22.03.2014 - 08:48
fonte
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) è usare il contenimento. Crea un AnimalContainer (o anche rendilo una collezione) che può vivere in casa o allo zoo o qualsiasi altra cosa. Fornisce la funzionalità di tracciamento associata agli animali, ma evita i problemi di ereditarietà in quanto può essere incluso in qualsiasi oggetto che ne abbia bisogno.

    
risposta data 22.03.2014 - 01:29
fonte
0

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

Gli archi sanno che sono in una lista? No. Le date sanno che esistono in un calendario? No.

L'opzione migliore è modificare la progettazione in modo che questo tipo di scenario non esista.

Dopo, considerate l'inversione del controllo. Invece di MakeMess su Animal con un effetto collaterale o un evento, passa Zoo nel metodo. L'opzione 1 va bene se è necessario proteggere l'invariante che Animal ha sempre bisogno di vivere da qualche parte. Quindi non è un genitore, ma un'associazione di pari.

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

    
risposta data 22.03.2014 - 02:05
fonte

Leggi altre domande sui tag