Entschuldigung, wenn "Composition Hierarchy" keine Sache ist, aber ich werde erklären, was ich damit in der Frage meine.
Es gibt keinen OO-Programmierer, der nicht auf eine Variante von "Vererbungshierarchien flach halten" oder "Komposition der Vererbung vorziehen" usw. gestoßen ist. Tiefe Kompositionshierarchien scheinen jedoch ebenfalls problematisch zu sein.
Angenommen, wir benötigen eine Sammlung von Berichten, in denen die Ergebnisse eines Experiments aufgeführt sind:
class Model {
// ... interface
Array<Result> m_results;
}
Jedes Ergebnis hat bestimmte Eigenschaften. Dazu gehören der Zeitpunkt des Experiments sowie einige Metadaten aus jeder Phase des Experiments:
enum Stage {
Pre = 1,
Post
};
class Result {
// ... interface
Epoch m_epoch;
Map<Stage, ExperimentModules> m_modules;
}
Ok, großartig. Jetzt enthält jedes Experimentmodul eine Zeichenfolge, die das Ergebnis des Experiments beschreibt, sowie eine Sammlung von Verweisen auf experimentelle Probensätze:
class ExperimentalModules {
// ... interface
String m_reportText;
Array<Sample> m_entities;
}
Und dann hat jede Probe ... nun, Sie bekommen das Bild.
Das Problem ist, dass das Modellieren von Objekten aus meiner Anwendungsdomäne sehr natürlich erscheint, aber letztendlich Result
ist a nur ein blöder Datencontainer! Es scheint nicht lohnenswert, eine große Gruppe von Klassen dafür zu erstellen.
Unter der Annahme, dass die oben gezeigten Datenstrukturen und Klassen die Beziehungen in der Anwendungsdomäne korrekt modellieren, gibt es eine bessere Möglichkeit, ein solches "Ergebnis" zu modellieren, ohne auf eine tiefe Kompositionshierarchie zurückzugreifen? Gibt es einen externen Kontext, anhand dessen Sie feststellen können, ob ein solches Design gut ist oder nicht?
quelle
Antworten:
Darum geht es im Gesetz von Demeter / Prinzip des geringsten Wissens . Ein Benutzer der
Model
sollte nicht unbedingt über die Benutzeroberfläche Bescheid wissenExperimentalModules
müssen, um seine Arbeit zu erledigen.Das allgemeine Problem ist, dass, wenn eine Klasse von einem anderen Typ erbt oder ein öffentliches Feld mit einem Typ hat oder wenn eine Methode einen Typ als Parameter annimmt oder einen Typ zurückgibt, die Schnittstellen aller dieser Typen Teil der effektiven Schnittstelle meiner Klasse werden. Die Frage lautet: Von diesen Typen, von denen ich abhänge, ist die Verwendung dieser Typen der springende Punkt meiner Klasse? Oder handelt es sich nur um Implementierungsdetails, die ich versehentlich enthüllt habe? Denn wenn es sich um Implementierungsdetails handelt, habe ich sie nur offengelegt und den Benutzern meiner Klasse ermöglicht, sich auf sie zu verlassen. Meine Kunden sind jetzt eng mit meinen Implementierungsdetails verknüpft.
Wenn ich also den Zusammenhalt verbessern möchte, kann ich diese Kopplung einschränken, indem ich mehr Methoden schreibe, die nützliche Dinge tun, anstatt dass Benutzer in meine privaten Strukturen greifen müssen. Hier bieten wir möglicherweise Methoden zum Suchen oder Filtern von Ergebnissen an. Das erleichtert die Refaktorisierung meiner Klasse, da ich jetzt die interne Darstellung ändern kann, ohne die Benutzeroberfläche zu ändern.
Dies ist jedoch nicht kostenlos - die Schnittstelle meiner exponierten Klasse ist jetzt voller Operationen, die nicht immer benötigt werden. Dies verstößt gegen das Prinzip der Schnittstellentrennung: Ich sollte Benutzer nicht zwingen, sich auf mehr Operationen zu verlassen, als sie tatsächlich verwenden. Und hier ist Design wichtig: Beim Design geht es darum, zu entscheiden, welches Prinzip in Ihrem Kontext wichtiger ist, und einen geeigneten Kompromiss zu finden.
Wenn ich eine Bibliothek entwerfe, die die Quellkompatibilität über mehrere Versionen hinweg gewährleisten muss, neige ich eher dazu, das Prinzip des geringsten Wissens genauer zu befolgen. Wenn meine Bibliothek intern eine andere Bibliothek verwendet, würde ich meinen Benutzern niemals etwas zugänglich machen, das von dieser Bibliothek definiert wird. Wenn ich eine Schnittstelle aus der internen Bibliothek verfügbar machen möchte, würde ich ein einfaches Wrapper-Objekt erstellen. Design Patterns wie das Bridge Pattern oder das Facade Pattern können hier Abhilfe schaffen.
Aber wenn meine Schnittstellen nur intern verwendet werden oder wenn die Schnittstellen, die ich verfügbar mache, sehr grundlegend sind (Teil einer Standardbibliothek oder einer Bibliothek, die so grundlegend ist, dass es niemals Sinn macht, sie zu ändern), kann es sinnlos sein, sie zu verbergen diese Schnittstellen. Wie üblich gibt es keine einheitlichen Antworten.
quelle
Na sicher. Die Regel sollte eigentlich lauten: "Halte alle Hierarchien so flach wie möglich."
Tiefe Kompositionshierarchien sind jedoch weniger anfällig als tiefe Vererbungshierarchien und können flexibler geändert werden. Intuitiv sollte dies nicht überraschen; Familienhierarchien sind der gleiche Weg. Ihre Beziehung zu Ihren Blutsverwandten ist genetisch festgelegt. Ihre Beziehungen zu Ihren Freunden und Ihrem Ehepartner sind nicht.
Ihr Beispiel erscheint mir nicht als "tiefe Hierarchie". Ein Formular erbt von einem Rechteck, das von einer Reihe von Punkten erbt, die von den x- und y-Koordinaten stammen, und ich habe noch nicht einmal einen vollen Dampf bekommen.
quelle
Einstein umschreiben:
Das heißt, wenn das Problem, das Sie modellieren, so ist, sollten Sie keine Bedenken haben, es so zu modellieren, wie es ist.
Wenn die App nur eine riesige Tabelle ist, müssen Sie die Kompositionshierarchie nicht so modellieren, wie sie ist. Ansonsten mach es. Ich glaube nicht, dass das Ding unendlich tief ist. Halten Sie die Getter, die Sammlungen zurückgeben, einfach faul, wenn das Auffüllen solcher Sammlungen teuer ist, und fordern Sie sie beispielsweise in einem Repository an, in dem sie aus einer Datenbank abgerufen werden. Mit faul meine ich, bevölkere sie nicht, bis sie gebraucht werden.
quelle
Ja, Sie können es wie relationale Tabellen modellieren
usw., was häufig zu einer einfacheren Programmierung und Zuordnung zu einer Datenbank führen kann. aber auf Kosten des Verlustes der festen Kodierung Ihrer Verhältnisse
quelle
Ich weiß nicht wirklich, wie ich das in Java lösen soll, weil Java auf der Idee basiert, dass alles ein Objekt ist. Sie können Zwischenklassen nicht unterdrücken, indem Sie sie durch ein Wörterbuch ersetzen, da die Typen in Ihren Datenblobs heterogen sind (z. B. Zeitstempel + Liste oder Zeichenfolge + Liste ...). Die Alternative, eine Containerklasse durch ihr Feld zu ersetzen (z. B. eine "m_epoch" -Variable durch ein "m_modules" zu übergeben), ist einfach schlecht, da ein implizites Objekt (ohne das andere kann man es nicht haben) ohne bestimmtes Objekt erstellt wird Vorteil.
In meiner bevorzugten Sprache (Python) würde meine Faustregel darin bestehen, kein Objekt zu erstellen, wenn keine bestimmte Logik (mit Ausnahme des grundlegenden Abrufens und Einstellens) dazu gehört. Aber angesichts Ihrer Einschränkungen denke ich, dass Ihre Kompositionshierarchie das bestmögliche Design ist.
quelle