Muster zur Weitergabe von Änderungen in einem Objektmodell ..?


22

Hier ist ein häufiges Szenario, das für mich immer frustrierend ist.

Ich habe ein Objektmodell mit einem übergeordneten Objekt. Das übergeordnete Objekt enthält einige untergeordnete Objekte. Etwas wie das.

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

Jedes untergeordnete Objekt verfügt über verschiedene Daten und Methoden

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

    public void MakeMess()
    {
        ...
    }
}

Wenn sich das untergeordnete Element ändert, muss in diesem Fall beim Aufrufen der MakeMess-Methode ein Wert im übergeordneten Element aktualisiert werden. Sagen wir, wenn eine bestimmte Schwelle von Animal's ein Chaos verursacht hat, muss das IsDirty-Flag des Zoos gesetzt werden.

Es gibt einige Möglichkeiten, mit diesem Szenario umzugehen (von denen ich weiß).

1) Jedes Tier kann eine übergeordnete Zoo-Referenz haben, um Änderungen mitzuteilen.

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

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

Dies scheint die schlechteste Option zu sein, da Animal an das übergeordnete Objekt gekoppelt wird. Was ist, wenn ich ein Tier will, das in einem Haus lebt?

2) Wenn Sie eine Sprache verwenden, die Ereignisse unterstützt (z. B. C #), können Sie auch festlegen, dass das übergeordnete Element Ereignisse ändert.

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

Dies scheint zu funktionieren, ist aber aus Erfahrung schwer zu pflegen. Wenn Tiere den Zoo wechseln, müssen Sie die Termine im alten Zoo abbestellen und sich erneut im neuen Zoo anmelden. Dies wird nur schlimmer, wenn der Kompositionsbaum tiefer wird.

3) Die andere Möglichkeit besteht darin, die Logik auf das übergeordnete Element zu verschieben.

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

Dies erscheint sehr unnatürlich und führt zu einer Verdoppelung der Logik. Wenn ich zum Beispiel ein House-Objekt hätte, das kein gemeinsames übergeordnetes Vererbungselement mit Zoo teilt.

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

Ich habe noch keine gute Strategie gefunden, um mit diesen Situationen umzugehen. Was gibt es noch? Wie kann das einfacher gemacht werden?


Sie haben Recht damit, dass Nummer 1 schlecht ist, und ich bin auch nicht scharf auf Nummer 2; Im Allgemeinen möchten Sie Nebenwirkungen vermeiden, und stattdessen erhöhen Sie sie. Warum können Sie AnimalMakeMess in Bezug auf Option 3 nicht in eine statische Methode zerlegen, die alle Klassen aufrufen können?
Doval

4
# 1 ist nicht unbedingt schlecht, wenn Sie über eine Schnittstelle (IAnimalObserver) statt über diese bestimmte Elternklasse kommunizieren.
Coredump

Antworten:


11

Ich musste mich ein paar Mal damit auseinandersetzen. Beim ersten Mal habe ich Option 2 (Ereignisse) verwendet und wie Sie sagten, wurde es sehr kompliziert. Wenn Sie diesen Weg gehen, empfehle ich dringend, dass Sie sehr gründliche Komponententests benötigen, um sicherzustellen, dass die Ereignisse korrekt durchgeführt werden und Sie keine baumelnden Referenzen hinterlassen. Andernfalls ist das Debuggen sehr schwierig.

Beim zweiten Mal habe ich die Eltern-Eigenschaft als Funktion der Kinder implementiert. Behalten Sie also eine DirtyEigenschaft für jedes Tier bei und lassen Sie es Animal.IsDirtyzurückkehren this.Animals.Any(x => x.IsDirty). Das war im Modell. Über dem Modell befand sich ein Controller, und der Controller musste wissen, dass nach dem Ändern des Modells (alle Aktionen am Modell wurden durch den Controller geleitet, sodass er wusste, dass sich etwas geändert hatte), dass er bestimmte Re aufrufen musste -Auswertungsfunktionen, wie das Auslösen der ZooMaintenanceAbteilung, um zu überprüfen, ob die Zoowieder verschmutzt war. Alternativ könnte ich die ZooMaintenancePrüfungen auch bis zu einem späteren Zeitpunkt verschieben (alle 100 ms, 1 Sekunde, 2 Minuten, 24 Stunden, je nachdem, was erforderlich ist).

Ich fand, dass letzteres viel einfacher zu warten war und meine Befürchtungen vor Leistungsproblemen nie aufkamen.

Bearbeiten

Eine andere Möglichkeit, damit umzugehen, ist ein Nachrichtenbusmuster . Anstatt Controllerwie in meinem Beispiel zu verwenden, injizieren Sie jedem Objekt einen IMessageBusDienst. Die AnimalKlasse kann dann eine Nachricht wie "Mess Made" veröffentlichen und Ihre ZooKlasse kann die Nachricht "Mess Made" abonnieren. Der Nachrichtenbusdienst wird sich darum kümmern, zu benachrichtigen, Zoowann ein Tier eine dieser Nachrichten veröffentlicht, und er kann sein IsDirtyEigentum neu bewerten .

Dies hat den Vorteil, dass Sie Animalskeinen ZooVerweis mehr benötigen und Zoosich nicht um das Abonnieren und Abbestellen von Ereignissen aus jedem einzelnen kümmern müssen Animal. Die Strafe ist, dass alle ZooKlassen, die diese Nachricht abonnieren, ihre Eigenschaften neu bewerten müssen, auch wenn es nicht eines ihrer Tiere war. Das kann eine große Sache sein oder auch nicht. Wenn es nur ein oder zwei ZooInstanzen gibt, ist das wahrscheinlich in Ordnung.

Bearbeiten 2

Lassen Sie die Einfachheit von Option 1 nicht außer Acht. Jeder, der den Code noch einmal besucht, wird keine großen Probleme damit haben, ihn zu verstehen. Für jemanden, der sich die AnimalKlasse ansieht , MakeMessist es offensichtlich, dass sie , wenn sie aufgerufen wird, die Nachricht an die Zooweitergibt, und für die ZooKlasse, von der sie ihre Nachrichten erhält , offensichtlich ist . Denken Sie daran, dass in der objektorientierten Programmierung ein Methodenaufruf als "Nachricht" bezeichnet wurde. Tatsächlich ist es nur dann sinnvoll, von Option 1 abzubrechen, wenn mehr als nur die ZooMeldung erforderlich ist, wenn das AnimalProblem auftritt. Wenn es mehr Objekte gäbe, die benachrichtigt werden müssten, würde ich wahrscheinlich auf einen Nachrichtenbus oder eine Steuerung umsteigen.


5

Ich habe ein einfaches Klassendiagramm erstellt, das Ihre Domain beschreibt: Bildbeschreibung hier eingeben

Jeder Animal hat ein Habitat Durcheinander.

Dem Habitatist es egal, welche oder wie viele Tiere es hat (es sei denn, es ist grundlegend Teil Ihres Designs, was Sie in diesem Fall nicht beschrieben haben).

Aber das Animalkümmert das, denn es wird sich bei jedem anders verhalten Habitat.

Dieses Diagramm ähnelt dem UML-Diagramm des Strategieentwurfsmusters , wird jedoch anders verwendet.

Hier sind einige Codebeispiele in Java (ich möchte keine C # -spezifischen Fehler machen).

Natürlich können Sie dieses Design, die Sprache und die Anforderungen selbst anpassen.

Dies ist die Strategie-Oberfläche:

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

    public float getCleanliness();
}

Ein Beispiel für einen Beton Habitat. Natürlich kann jede HabitatUnterklasse diese Methoden anders implementieren.

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

    public float getCleanliness() {
        return cleanliness;
    }

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

Natürlich können Sie mehrere Tierunterklassen haben, in denen jede es anders vermasselt:

public class Animel {
    private Habitat habitat;

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

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

Dies ist die Client-Klasse. Dies erklärt im Grunde, wie Sie dieses Design verwenden können.

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

Natürlich können Sie in Ihrer realen Anwendung das Habitatwissen lassen und verwalten, Animalwenn Sie es brauchen.


3

Mit Architekturen wie Ihrer Option 2 war ich in der Vergangenheit ziemlich erfolgreich. Dies ist die allgemeinste Option und ermöglicht die größte Flexibilität. Wenn Sie jedoch die Kontrolle über Ihre Listener haben und nicht viele Abonnementtypen verwalten, können Sie Ereignisse einfacher abonnieren, indem Sie eine Benutzeroberfläche erstellen.

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

class MessEvent
{
  String DetailsOrWhatever;
}

Die Schnittstellenoption hat den Vorteil, dass sie fast so einfach ist wie Ihre Option 1, Sie können jedoch auch Tiere in einem Houseoder mühelos unterbringen FairlyLand.


3
  • Option 1 ist eigentlich ziemlich einfach. Das ist nur eine Referenz. Aber verallgemeinern Sie es mit der aufgerufenen Schnittstelle Dwellingund geben Sie eine MakeMessMethode dafür an. Das unterbricht die zirkuläre Abhängigkeit. Wenn das Tier dann ein Chaos macht, ruft es dwelling.MakeMess()auch.

Im Geiste von lex parsimoniae werde ich mit dieser gehen, obwohl ich wahrscheinlich die Kettenlösung unten verwenden würde, um mich zu kennen. (Dies ist genau das gleiche Modell, das @Benjamin Albert vorschlägt.)

Beachten Sie, dass bei der Modellierung relationaler Datenbanktabellen die Beziehung in die andere Richtung verlaufen würde: Animal hätte einen Verweis auf Zoo, und die Auflistung von Animals für einen Zoo wäre ein Abfrageergebnis.

  • Wenn Sie diese Idee weiter verfolgen, könnten Sie eine verkettete Architektur verwenden. Erstellen Sie also eine Schnittstelle Messable, und fügen Sie in jedes messbare Element einen Verweis auf ein next. Rufen Sie MakeMessnach dem Erstellen eines Chaos das nächste Element an.

Also ist Zoo hier daran beteiligt, ein Chaos zu verursachen, weil es auch chaotisch wird. Haben:

Zoo implements Messable
House implements Messable
Animal implements Messable
   Messable next

   MakeMess()
       messy = true
       next.MakeMess

Nun haben Sie eine Reihe von Dingen, die die Nachricht erhalten, dass ein Durcheinander erzeugt wurde.

  • Option 2, ein Publish / Subscribe-Modell, könnte hier funktionieren, fühlt sich aber sehr schwer an. Das Objekt und der Container haben eine bekannte Beziehung, daher scheint es etwas unüberlegt zu sein, etwas Allgemeineres zu verwenden.

  • Option 3: In diesem speziellen Fall ist das Aufrufen Zoo.MakeMess(animal)oder House.MakeMess(animal)Nicht- Aufrufen eine schlechte Option, da ein Haus möglicherweise eine andere Semantik hat, um unordentlich zu werden als ein Zoo.

Selbst wenn Sie die Kettenroute nicht abfahren, scheint es hier zwei Probleme zu geben: 1) Das Problem besteht darin, eine Änderung von einem Objekt in seinen Container zu übertragen der behälter zu abstrahieren, wo die tiere leben können.

...

Wenn Sie erstklassige Funktionen haben, können Sie eine Funktion (oder einen Delegaten) an Animal übergeben, um sie aufzurufen, nachdem sie ein Durcheinander verursacht hat. Das ist ein bisschen wie mit der Kettenidee, außer mit einer Funktion anstelle einer Schnittstelle.

public Animal
    Function afterMess

    public MakeMess()
        messy = true
        afterMess()

Wenn sich das Tier bewegt, stellen Sie einfach einen neuen Delegierten ein.

  • Im Extremfall können Sie die aspektorientierte Programmierung (Aspect Oriented Programming, AOP) mit "After" -Hinweisen zu MakeMess verwenden.

2

Ich würde mit 1 gehen, aber ich würde die Eltern-Kind-Beziehung zusammen mit der Benachrichtigungslogik in einen separaten Wrapper umwandeln. Dadurch wird die Abhängigkeit von Animal von Zoo aufgehoben und die automatische Verwaltung von Eltern-Kind-Beziehungen ermöglicht. Dies erfordert jedoch, dass Sie die Objekte in der Hierarchie zuerst in Schnittstellen / abstrakte Klassen umwandeln und für jede Schnittstelle einen spezifischen Wrapper schreiben. Dies könnte jedoch durch Codegenerierung beseitigt werden.

So etwas wie :

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

So führen einige ORMs ihre Änderungsnachverfolgung für Entitäten durch. Sie erstellen Wrapper um die Entitäten und sorgen dafür, dass Sie mit diesen arbeiten. Diese Wrapper werden normalerweise mithilfe von Reflektion und dynamischer Codegenerierung erstellt.


1

Zwei Optionen, die ich oft benutze. Sie können den zweiten Ansatz verwenden und die Logik für die Verkabelung des Ereignisses in der Auflistung selbst auf dem übergeordneten Element ablegen.

Ein alternativer Ansatz (der tatsächlich mit einer der drei Optionen verwendet werden kann) ist die Verwendung von Containment. Machen Sie einen AnimalContainer (oder machen Sie sogar eine Sammlung daraus), der im Haus oder im Zoo oder irgendetwas anderem leben kann. Es bietet die den Tieren zugeordnete Verfolgungsfunktion, vermeidet jedoch Vererbungsprobleme, da es in jedem Objekt enthalten sein kann, das es benötigt.


0

Sie beginnen mit einem grundlegenden Fehler: Untergeordnete Objekte sollten nichts über ihre Eltern wissen.

Wissen Strings, dass sie in einer Liste enthalten sind? Wissen Daten, dass sie in einem Kalender vorhanden sind? Nein.

Die beste Option ist, Ihr Design so zu ändern, dass ein solches Szenario nicht existiert.

Betrachten Sie danach die Umkehrung der Kontrolle. Statt der MakeMessauf Animaleiner Nebenwirkung oder ein Ereignis, übergeben Zooin das Verfahren. Option 1 ist in Ordnung, wenn Sie die Invariante schützen müssen, die Animalimmer irgendwo leben muss. Es ist also kein Elternteil, sondern eine Gleichaltrige.

Gelegentlich gehen 2 und 3 in Ordnung, aber das wichtigste architektonische Prinzip ist, dass Kinder nichts über ihre Eltern wissen.


Ich vermute, dies ist eher eine Schaltfläche zum Senden in einem Formular als eine Zeichenfolge in einer Liste.
Svidgen

1
@svidgen - Dann gib einen Rückruf ein. Narrensicherer als ein Ereignis, einfacher zu überlegen und keine bösen Verweise auf Dinge, die es nicht wissen muss.
Telastyn
Durch die Nutzung unserer Website bestätigen Sie, dass Sie unsere Cookie-Richtlinie und Datenschutzrichtlinie gelesen und verstanden haben.
Licensed under cc by-sa 3.0 with attribution required.