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?
quelle
Antworten:
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
Dirty
Eigenschaft für jedes Tier bei und lassen Sie esAnimal.IsDirty
zurückkehrenthis.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 derZooMaintenance
Abteilung, um zu überprüfen, ob dieZoo
wieder verschmutzt war. Alternativ könnte ich dieZooMaintenance
Prü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
Controller
wie in meinem Beispiel zu verwenden, injizieren Sie jedem Objekt einenIMessageBus
Dienst. DieAnimal
Klasse kann dann eine Nachricht wie "Mess Made" veröffentlichen und IhreZoo
Klasse kann die Nachricht "Mess Made" abonnieren. Der Nachrichtenbusdienst wird sich darum kümmern, zu benachrichtigen,Zoo
wann ein Tier eine dieser Nachrichten veröffentlicht, und er kann seinIsDirty
Eigentum neu bewerten .Dies hat den Vorteil, dass Sie
Animals
keinenZoo
Verweis mehr benötigen undZoo
sich nicht um das Abonnieren und Abbestellen von Ereignissen aus jedem einzelnen kümmern müssenAnimal
. Die Strafe ist, dass alleZoo
Klassen, 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 zweiZoo
Instanzen 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
Animal
Klasse ansieht ,MakeMess
ist es offensichtlich, dass sie , wenn sie aufgerufen wird, die Nachricht an dieZoo
weitergibt, und für dieZoo
Klasse, 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 dieZoo
Meldung erforderlich ist, wenn dasAnimal
Problem auftritt. Wenn es mehr Objekte gäbe, die benachrichtigt werden müssten, würde ich wahrscheinlich auf einen Nachrichtenbus oder eine Steuerung umsteigen.quelle
Ich habe ein einfaches Klassendiagramm erstellt, das Ihre Domain beschreibt:
Jeder
Animal
hat einHabitat
Durcheinander.Dem
Habitat
ist 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
Animal
kümmert das, denn es wird sich bei jedem anders verhaltenHabitat
.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:
Ein Beispiel für einen Beton
Habitat
. Natürlich kann jedeHabitat
Unterklasse diese Methoden anders implementieren.Natürlich können Sie mehrere Tierunterklassen haben, in denen jede es anders vermasselt:
Dies ist die Client-Klasse. Dies erklärt im Grunde, wie Sie dieses Design verwenden können.
Natürlich können Sie in Ihrer realen Anwendung das
Habitat
wissen lassen und verwalten,Animal
wenn Sie es brauchen.quelle
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.
Die Schnittstellenoption hat den Vorteil, dass sie fast so einfach ist wie Ihre Option 1, Sie können jedoch auch Tiere in einem
House
oder mühelos unterbringenFairlyLand
.quelle
Dwelling
und geben Sie eineMakeMess
Methode dafür an. Das unterbricht die zirkuläre Abhängigkeit. Wenn das Tier dann ein Chaos macht, ruft esdwelling.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.
Messable
, und fügen Sie in jedes messbare Element einen Verweis auf einnext
. Rufen SieMakeMess
nach 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:
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)
oderHouse.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.
Wenn sich das Tier bewegt, stellen Sie einfach einen neuen Delegierten ein.
quelle
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 :
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.
quelle
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.
quelle
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
MakeMess
aufAnimal
einer Nebenwirkung oder ein Ereignis, übergebenZoo
in das Verfahren. Option 1 ist in Ordnung, wenn Sie die Invariante schützen müssen, dieAnimal
immer 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.
quelle