Lose Kopplung in objektorientiertem Design

16

Ich versuche, GRASP zu lernen und ich fand dies erklärt ( hier auf Seite 3 ) über Low Coupling und ich war sehr überrascht, als ich dies fand:

Betrachten Sie die Methode addTrackfür eine AlbumKlasse, zwei mögliche Methoden sind:

addTrack( Track t )

und

addTrack( int no, String title, double duration )

Welche Methode reduziert die Kopplung? Das zweite ist der Fall, da die Klasse, die die Album-Klasse verwendet, keine Track-Klasse kennen muss. Im Allgemeinen sollten Parameter für Methoden Basistypen (int, char ...) und Klassen aus den Java. * -Paketen verwenden.

Ich neige dazu, damit einverstanden zu sein; Ich halte addTrack(Track t)es addTrack(int no, String title, double duration)aus verschiedenen Gründen für besser als :

  1. Es ist immer besser, wenn eine Methode so wenig Parameter wie möglich enthält (laut Uncle Bobs Clean Code keiner oder einer, vorzugsweise 2 in einigen Fällen und 3 in besonderen Fällen; mehr als 3 müssen überarbeitet werden - dies sind natürlich Empfehlungen, keine holly rules). .

  2. Wenn addTrackes sich um eine Methode einer Schnittstelle handelt und die Anforderungen erfordern, dass a Trackmehr Informationen (z. B. Jahr oder Genre) enthält, muss die Schnittstelle geändert werden, damit die Methode einen anderen Parameter unterstützt.

  3. Die Kapselung ist kaputt. wenn addTrackes sich in einer schnittstelle befindet, dann sollte es die interna der nicht kennen Track.

  4. Es ist tatsächlich mehr auf die zweite Art und Weise gekoppelt, mit vielen Parametern. Angenommen, der noParameter muss von nach geändert werden int, longda mehr als MAX_INTSpuren vorhanden sind (oder aus welchem ​​Grund auch immer). dann müssen sowohl die Trackals auch die Methode geändert werden, während wenn die Methode addTrack(Track track)nur die Trackgeändert werden würde.

Alle 4 Argumente sind tatsächlich miteinander verbunden, und einige von ihnen sind Konsequenzen aus anderen.

Welcher Ansatz ist besser?

m3th0dman
quelle
2
Handelt es sich um ein Dokument, das von einem Professor oder Trainer zusammengestellt wurde? Anhand der URL des von Ihnen angegebenen Links sieht es so aus, als ob es sich um eine Klasse handelte, obwohl das Dokument keine Informationen darüber enthält, wer den Link erstellt hat. Wenn dies Teil einer Klasse wäre, würde ich vorschlagen, dass Sie diese Fragen an die Person richten, die das Dokument bereitgestellt hat. Übrigens stimme ich Ihrer Argumentation zu - es scheint mir offensichtlich, dass eine Album-Klasse von Natur aus etwas über eine Track-Klasse wissen möchte.
Derek
Ehrlich gesagt, wenn ich von "Best Practices" lese, nehme ich sie mit einem Körnchen Salz!
AraK,
@Derek Ich habe das Dokument gefunden, indem ich in Google nach "Beispiel für Griffmuster" gesucht habe. Ich weiß nicht, wer es geschrieben hat, aber da es von einer Universität stammt, glaube ich, dass es zuverlässig ist. Ich suche ein Beispiel, das auf den gegebenen Informationen basiert und die Quelle ignoriert.
m3th0dman
4
@ m3th0dman "aber da es von einer Universität war, glaube ich, dass es zuverlässig ist." Für mich, weil es von einer Universität ist, halte ich es für unzuverlässig. Ich traue niemandem, der nicht an mehrjährigen Projekten über Best Practices in der Softwareentwicklung gearbeitet hat.
AraK
1
@AraK Zuverlässig bedeutet nicht, dass es fraglos ist. und das ist es, was ich hier mache und es hinterfrage.
m3th0dman

Antworten:

15

Nun, Ihre ersten drei Punkte beziehen sich eigentlich auf andere Prinzipien als die Kopplung. Sie müssen immer ein Gleichgewicht zwischen oft widersprüchlichen Designprinzipien finden.

Ihr vierte Punkt ist über Kopplung, und ich stimme mit Ihnen. Die Kupplung ist über den Fluss von Daten zwischen den Modulen. Der Typ des Containers, in dem Daten fließen, ist weitgehend unerheblich. Wenn Sie eine Duration als Double statt als Field of A übergeben, Trackmüssen Sie sie nicht weitergeben. Die Module müssen weiterhin dieselbe Datenmenge und dieselbe Kopplungsmenge gemeinsam nutzen.

Er betrachtet auch nicht alle Kopplungen im System als ein Aggregat. Die Einführung einer TrackKlasse fügt zwar eine weitere Abhängigkeit zwischen zwei einzelnen Modulen hinzu, kann jedoch die Kopplung des Systems erheblich verringern , was hier die wichtige Maßnahme ist.

Betrachten Sie beispielsweise eine Schaltfläche "Zur Wiedergabeliste hinzufügen" und ein PlaylistObjekt. Das Einführen eines TrackObjekts kann die Kopplung erhöhen, wenn Sie nur diese beiden Objekte berücksichtigen. Sie haben jetzt drei voneinander abhängige Klassen anstelle von zwei. Dies ist jedoch nicht die Gesamtheit Ihres Systems. Sie müssen auch die Spur importieren, die Spur abspielen, die Spur anzeigen usw. Das Hinzufügen einer weiteren Klasse zu dieser Mischung ist vernachlässigbar.

Überlegen Sie nun, ob die Wiedergabe von Titeln über das Netzwerk und nicht nur lokal unterstützt werden soll. Sie müssen nur ein NetworkTrackObjekt erstellen , das der gleichen Schnittstelle entspricht. Ohne das TrackObjekt müssten Sie überall Funktionen erstellen wie:

addNetworkTrack(int no, string title, double duration, URL location)

Das verdoppelt effektiv Ihre Kopplung und erfordert sogar Module, die sich nicht um die netzwerkspezifischen Dinge kümmern, um sie trotzdem zu verfolgen, um sie weitergeben zu können.

Ihr Welligkeitstest ist gut, um das wahre Ausmaß der Kopplung zu bestimmen. Es geht uns darum, die Stellen zu begrenzen, an denen sich eine Änderung auswirkt.

Karl Bielefeldt
quelle
1
+ Das Koppeln an Primitive funktioniert unabhängig vom Schnitt immer noch.
JustinC
+1 für die Erwähnung der URL-Option / des Ripple-Effekts.
user949300
4
+1 Interessant ist auch die Erörterung des Abhängigkeitsinversionsprinzips in DIP in freier Wildbahn, bei der die Verwendung primitiver Typen als primitiver Besessenheitsgeruch mit Value Object als Fehlerbehebung angesehen wird. Für mich hört sich das so an, als wäre es besser, ein Track-Objekt zu übergeben, das eine ganze Reihe von primitiven Typen umfasst ... Und wenn Sie die Abhängigkeit von / die Kopplung mit bestimmten Klassen vermeiden möchten, verwenden Sie Interfaces.
Marjan Venema
Akzeptierte Antwort wegen der schönen Erklärung des Unterschieds zwischen der Kopplung des Gesamtsystems und der Kopplung der Module.
m3th0dman
10

Meine Empfehlung lautet:

Verwenden

addTrack( ITrack t )

Aber stellen Sie sicher, dass dies ITrackeine Schnittstelle und keine konkrete Klasse ist.

Album kennt die Interna der ITrackImplementierer nicht. Es ist nur an den Vertrag gekoppelt, der von der ITrack.

Ich denke, dies ist die Lösung, die die geringste Menge an Kopplung erzeugt.

Tulains Córdova
quelle
1
Ich glaube, dass Track nur ein einfaches Bean- / Datenübertragungsobjekt ist, über dem sich nur Felder und Getter / Setter befinden. ist in diesem Fall eine Schnittstelle erforderlich?
m3th0dman
6
Erforderlich? Wahrscheinlich nicht. Andeutend, ja. Die konkrete Bedeutung einer Spur kann und wird sich weiterentwickeln, aber was die konsumierende Klasse von ihr verlangt, wird es wahrscheinlich nicht.
JustinC
2
@ m3th0dman Hängt immer von Abstraktionen ab, nicht von Konkretionen. Das gilt unabhängig davon Track, ob man dumm oder schlau ist. Trackist eine Konkretion. ITrackSchnittstelle ist eine Abstraktion. Auf diese Weise können Sie in Zukunft verschiedene Arten von Tracks verwenden, sofern diese den Anforderungen entsprechen ITrack.
Tulains Córdova
4
Ich stimme der Idee zu, verliere aber das Präfix "Ich". Aus "Clean Code" von Robert Martin, Seite 24: "Das vorangegangene" I ", wie es in den heutigen" Legacy Wads "üblich ist, ist bestenfalls eine Ablenkung und im schlimmsten Fall eine zu große Informationsmenge. Ich möchte nicht, dass meine Benutzer wissen, dass ich sie überreiche Schnittstelle."
Benjamin Brumfield
1
@BenjaminBrumfield Sie haben Recht. Ich mag das Präfix auch nicht, obwohl ich es der Klarheit halber in der Antwort belassen werde.
Tulains Córdova
4

Ich würde argumentieren, dass die zweite Beispielmethode höchstwahrscheinlich die Kopplung erhöht , da sie höchstwahrscheinlich ein Track-Objekt instanziiert und es im aktuellen Album-Objekt speichert. (Wie in meinem obigen Kommentar vorgeschlagen, würde ich davon ausgehen, dass eine Album-Klasse irgendwo das Konzept einer Track-Klasse enthält.)

Bei der ersten Beispielmethode wird davon ausgegangen, dass ein Track außerhalb der Album-Klasse instanziiert wird. Daher können wir zumindest davon ausgehen, dass die Instanziierung der Track-Klasse nicht mit der Album-Klasse gekoppelt ist.

Wenn die Best Practices vorschlugen, dass wir niemals eine Klasse haben, die auf eine zweite Klasse verweist, würde die gesamte objektorientierte Programmierung aus dem Fenster geworfen.

Derek
quelle
Ich verstehe nicht, dass ein impliziter Verweis auf eine andere Klasse eine stärkere Kopplung bewirkt als ein expliziter Verweis. In beiden Fällen sind die beiden Klassen gekoppelt. Ich denke, es ist besser, die Kopplung explizit zu haben, aber ich denke nicht, dass sie in irgendeiner Weise "mehr" gekoppelt ist.
TMN
1
@TMN, die zusätzliche Kopplung besteht darin, wie ich impliziere, dass das zweite Beispiel wahrscheinlich intern ein neues Track-Objekt erstellen würde. Die Instanziierung des Objekts wird an eine Methode gekoppelt, die andernfalls nur ein Track-Objekt zu einer Art Liste im Album-Objekt hinzufügen sollte (Verstoß gegen das Prinzip der Einzelverantwortung). Sollte die Art und Weise, wie der Track erstellt wird, geändert werden müssen, muss auch die Methode addTrack () geändert werden. Dies ist im Fall des ersten Beispiels nicht der Fall.
Derek
3

Die Kopplung ist nur einer von vielen Aspekten, die Sie in Ihrem Code erhalten möchten. Indem Sie die Kopplung reduzieren, verbessern Sie nicht unbedingt Ihr Programm. Im Allgemeinen ist dies eine bewährte Methode, aber warum sollte dies in diesem speziellen Fall nicht Trackbekannt sein?

Durch die Verwendung einer TrackKlasse, an die übergeben Albumwerden soll, verbessern Sie die Lesbarkeit des Codes, und vor allem wandeln Sie, wie bereits erwähnt, eine statische Liste von Parametern in ein dynamisches Objekt um. Das macht Ihre Oberfläche letztendlich viel dynamischer.

Sie erwähnen, dass die Kapselung kaputt ist, aber nicht. AlbumDie Interna von müssen bekannt sein Track, und wenn Sie kein Objekt verwenden, Albummüssen Sie alle Informationen kennen , die an das Objekt weitergegeben werden, bevor es sie trotzdem verwenden kann . Der Aufrufer muss auch die Interna kennen Track, da er ein TrackObjekt erstellen muss, aber der Aufrufer muss diese Informationen trotzdem kennen, wenn sie direkt an die Methode übergeben werden. Mit anderen Worten, wenn der Vorteil der Verkapselung darin besteht, den Inhalt eines Objekts nicht zu kennen, kann er in diesem Fall möglicherweise nicht verwendet werden, da Albumdie TrackInformationen des Objekts trotzdem verwendet werden müssen .

Sie möchten nicht verwenden, Trackwenn Trackinterne Logik enthalten ist, auf die der Anrufer keinen Zugriff haben soll. Mit anderen Worten, wenn Albumes sich um eine Klasse handelt, die ein Programmierer, der Ihre Bibliothek verwendet, verwenden soll, möchten Sie nicht, dass er sie verwendet, Trackwenn Sie sagen: Rufen Sie eine Methode auf, um sie in der Datenbank beizubehalten. Das wahre Problem dabei liegt in der Tatsache, dass die Schnittstelle mit dem Modell verwickelt ist.

Um das Problem zu beheben, müssen Sie Trackin seine Schnittstellenkomponenten und seine Logikkomponenten trennen und zwei separate Klassen erstellen. Für den Anrufer Trackwird dies zu einer leichten Klasse, die Informationen enthalten und geringfügige Optimierungen (berechnete Daten und / oder Standardwerte) bieten soll. Im Inneren Albumwürden Sie eine Klasse namens verwenden TrackDAO, um das schwere Heben durchzuführen, das mit dem Speichern der Informationen Trackin der Datenbank verbunden ist.

Dies ist natürlich nur ein Beispiel. Ich bin mir sicher, dass dies überhaupt nicht Ihr Fall ist, und zögern Sie nicht, ohne TrackSchuldgefühle zu handeln. Denken Sie daran, Ihren Anrufer beim Erstellen von Klassen im Auge zu behalten und bei Bedarf Schnittstellen zu erstellen.

Neil
quelle
3

Beide sind richtig

addTrack( Track t ) 

ist besser (wie du schon argumentiert hast) während

addTrack( int no, String title, double duration ) 

ist weniger gekoppelt, da der verwendete Code addTracknicht wissen muss, dass es eine TrackKlasse gibt. Der Track kann beispielsweise umbenannt werden, ohne dass der Anrufcode aktualisiert werden muss.

Während Sie über besser lesbaren / wartbaren Code sprechen, geht es in dem Artikel um das Koppeln . Weniger gekoppelter Code ist nicht unbedingt einfacher zu implementieren und zu verstehen.

k3b
quelle
Siehe Argument 4; Ich sehe nicht, wie das der zweite weniger gekoppelt ist.
m3th0dman
3

Geringe Kopplung bedeutet nicht, dass keine Kopplung vorliegt. Irgendetwas muss irgendwo über Objekte in der Codebasis Bescheid wissen. Je geringer die Abhängigkeit von "benutzerdefinierten" Objekten ist, desto mehr Gründe geben Sie an, warum sich der Code ändert. Was der Autor, den Sie zitieren, mit der zweiten Funktion fördert, ist weniger gekoppelt, aber auch weniger objektorientiert, was der gesamten Idee von GRASP als objektorientierter Entwurfsmethodik widerspricht . Der springende Punkt ist, wie das System als Sammlung von Objekten und deren Interaktionen gestaltet werden kann. Sie zu meiden ist, als würde man einem das Autofahren beibringen, indem man sagt, man sollte stattdessen Fahrrad fahren.

Stattdessen besteht der richtige Weg darin, die Abhängigkeit von konkreten Objekten zu verringern , was die Theorie der "losen Kopplung" ist. Je weniger konkrete Typen eine Methode kennen muss, desto besser. Nur durch diese Aussage ist die erste Option tatsächlich weniger gekoppelt, da die zweite Methode, die die einfacheren Typen verwendet, über all diese einfacheren Typen Bescheid wissen muss. Sicher, sie sind integriert, und der Code in der Methode muss sich möglicherweise darum kümmern, aber die Signatur der Methode und die Aufrufer der Methode tun dies definitiv nicht . Wenn Sie einen dieser Parameter für eine konzeptionelle Audiospur ändern, sind weitere Änderungen erforderlich, wenn sie getrennt von denen in einem Spurobjekt (dem Punkt von Objekten; Kapselung) enthalten sind.

Wenn Sie einen Schritt weiter gehen und erwarten würden, dass Track durch etwas ersetzt wird, das die gleiche Aufgabe besser erfüllt, wäre möglicherweise eine Schnittstelle, die die erforderliche Funktionalität definiert, in Ordnung, ein ITrack. Dies könnte unterschiedliche Implementierungen wie "AnalogTrack", "CdTrack" und "Mp3Track" ermöglichen, die zusätzliche Informationen speziell für diese Formate liefern und gleichzeitig die grundlegende Datenexposition von ITrack bereitstellen, die konzeptionell eine "Spur" darstellt. ein endliches Teilstück von Audio. Track könnte ebenfalls eine abstrakte Basisklasse sein, aber dafür müssen Sie immer die in Track enthaltene Implementierung verwenden. Wenn Sie es als BetterTrack neu implementieren, müssen Sie die erwarteten Parameter ändern.

So lautet die goldene Regel; Programme und ihre Code-Komponenten haben immer Gründe, sich zu ändern. Sie können kein Programm schreiben, für das Sie niemals bereits geschriebenen Code bearbeiten müssen, um etwas Neues hinzuzufügen oder sein Verhalten zu ändern. In jeder Methode (GRASP, SOLID, jedes andere Akronym oder Modewort, das Sie sich vorstellen können) besteht Ihr Ziel einfach darin, die Dinge zu identifizieren, die sich im Laufe der Zeit ändern müssen, und das System so zu gestalten, dass diese Änderungen so einfach wie möglich sind (Übersetzt; Berühren Sie so wenig Codezeilen wie möglich und betreffen Sie so wenig andere Bereiche des Systems, die über den Rahmen Ihrer beabsichtigten Änderung hinausgehen.) In diesem Fall ist es am wahrscheinlichsten, dass ein Track mehr Datenelemente erhält, die addTrack () interessieren oder nicht Dieser Track wird durch BetterTrack ersetzt.

KeithS
quelle