4 Weitere Prinzipien
Analyse GRASP: Geringe Kopplung
Jeweils eine bis jetzt noch nicht behandelte Klasse als positives und negatives Beispiel geringer Kopplung; jeweils UML Diagramm mit zusammenspielenden Klassen, Aufgabenbeschreibung und BegrĂŒndung fĂŒr die Umsetzung der geringen Kopplung bzw. Beschreibung, wie die Kopplung aufgelöst werden kann
GRASP ist ein Akronym fĂŒr âGeneral Responsibility Assignment Software Patternsâ und umfasst Entwurfsprinzipien, die sich auf ZustĂ€ndigkeiten von Objekten beziehen, d.h. sie beschreiben, welche Objekte und Klassen wofĂŒr zustĂ€ndig sein sollen. Eins dieser Prinzipien ist die geringe Kopplung (Low Coupling), wobei Kopplung den Grad der AbhĂ€ngigkeit/VerknĂŒpfung zwischen verschiedenen Klassen, Softwaremodulen oder gar verschiedenen Systemen bezeichnet. Durch geringe Kopplung sind die einzelnen Komponenten leichter anpassbar (Ănderungen an einer Klasse haben nur lokale Auswirkungen), lassen sich besser alleine testen sowie besser warten, da weniger Kontext benötigt wird, um die Klasse zu verstehen (weniger Cognitive Load beim Lesen des Codes). Zudem wird die Wiederverwendbarkeit verbessert.
Positiv-Beispiel ⶠGeringe Kopplung
Die Klasse PlayerMove
bekommt in ihrem Konstruktor ein Objekt vom Typ Stock
ĂŒbergeben. Ein Stock
stellt den Kartenvorrat dar, von dem alle Karten im Spiel stammen. Wenn der Spieler einen Zug durchfĂŒhrt (siehe Klasse PlayerMove
), muss eine Referenz auf einen Stock
vorhanden sein, damit beispielsweise in der Kaufphase (Methode void doBuyPhase()
) eine Karte vom Vorrat genommen und dann auf den Ablagestapel des Spieler hinzugefĂŒgt werden kann. Auch bringen wir vom Stock
durch die Methode List<Card> getAvailableCardsWithMaxCosts(int maxCosts)
in Erfahrung, welche Karten der Spieler in der Kaufphase ĂŒberhaupt mit seinem Geld kaufen kann.
Positiv im Sinne der Kopplung ist hier hervorzuheben, dass der PlayerMove
nur eine schwache AbhÀngigkeit vom Kartenstapel hat. Denn PlayerMove
hÀngt nur von Stock
, nicht jedoch von dem konkreten GameStock
ab. Dadurch mĂŒssen dem PlayerMove
die Interna von GameStock
nicht bekannt sein. In Zukunft könnte deshalb ein komplett anderer Stock
an PlayerMove
ĂŒbergeben werden, ohne dass in PlayerMove
selbst etwas angepasst werden mĂŒsste. Ob dieser Fall jedoch realistisch gesehen benötigt wird, ist fraglich, da der Vorrat in Dominion bislang in allen Editionen auf die gleiche Art und Weise funktioniert hat. Trotzdem ist dies ein gutes Beispiel fĂŒr eine geringe Kopplung, indem nur schwache AbhĂ€ngigkeiten mittels Interfaces umgesetzt werden.
Negativ-Beispiel ⶠHohe Kopplung
Die abstrakte Klasse Player
liefert ein Beispiel fĂŒr hohe Kopplung. In diesem Fall besitzt der Player
eine Member-Variable zu der Klasse PlayerInteraction
, die wiederum Objekte von Typ PlayerDecision
und PlayerInformation
aggregiert. Diese beiden Objekte werden nach auĂen durch die Methoden decision()
und information()
zur VerfĂŒgung gestellt. Problematisch ist nun, dass der Player
diese Objekte ebenso nach auĂen reicht. Damit ist fĂŒr PlayerMove
, die in der konkreten Implementierung von Player
mit jedem Zug neu instantiiert wird, folgender Aufruf möglich:
player.decide().performMethodOnPlayerDecisionObject()
Die Klasse PlayerMove
bekommt also ĂŒber den Player
die Objekte durchgereicht, die eigentlich zu PlayerInteraction
gehören. Damit sind Player
und PlayerMove
stark miteinander gekoppelt, denn PlayerMove
ist direkt von Player
abhĂ€ngig und bekommt diesen sogar als Parameter ĂŒbergeben. Positiv ist hier jedoch anzufĂŒhren, dass bewusst nicht ein Aufruf der folgenden Art erlaubt wurde:
player.playerInteraction.decision().performMethodOnPlayerDecisionObject()
Dadurch ist es zumindest nicht notwendig, dass PlayerMove
ĂŒber den inneren Aufbau von Player
Bescheid weiĂ. Es ist jedoch weiterhin davon abhĂ€ngig, dass Player
die Methoden decide()
und inform()
bereitstellt, um auf die Objekte vom Typ PlayerDecision
bzw. PlayerInformation
zuzugreifen. Als mögliche Lösung könnte der Klasse PlayerMove
direkt im Konstruktor das PlayerInteraction
-Objekt von Player
mit ĂŒbergeben werden (Dependency Injection). Hier war jedoch die Schreibweise player.decide().<method>
zu âverlockendâ, da sie sich nah an der englischen Sprache orientiert und deshalb einfach zu verstehen ist.
Analyse GRASP: Hohe KohÀsion
Eine Klasse als positives Beispiel hoher KohĂ€sion; UML Diagramm und BegrĂŒndung, warum die KohĂ€sion hoch ist
Mit hoher KohĂ€sion ist gemeint, dass eine Klasse eine Sache âkannâ und dies sehr gut. Damit ist das Konzept eng verwandt mit dem Single Responsibility Prinzip. Bei KohĂ€sion geht es um die Frage, wie stark die Elemente (z.B. Methoden) eines Ganzen (z.B. einer Klasse) inhaltlich/logisch zusammengehören und funktional eine Aufgabe erledigen. Stark verwandte Dinge sollten zusammengehalten werden, dann ist die KohĂ€sion hoch. Eine gute ErklĂ€rung bietet diese diese Antwort von StackOverflow.
Ein positives Beispiel fĂŒr hohe KohĂ€sion ist das Interface PlayerDecision
bzw. dessen konkrete Implementierung PlayerDecisionCLI
.
Alle Methoden von PlayerDecision
beziehen sich auf die klare Aufgabe, der Benutzerin eine Entscheidung zu ĂŒberlassen. Dementsprechend tragen auch alle Methoden das Wort choose in ihrem Name. Die Methoden sind also eng miteinander verwandt und gehören inhaltlich zusammen. Ausnahme ist die Methode getPlayerNames()
im PlayerDecisionCLI
, die aus Bequemlichkeit zu dieser Klasse hinzugefĂŒgt wurde. Dennoch ist die KohĂ€sion insgesamt sehr hoch und gleichzeitig auch die Kopplung extrem gering, z.B. hat das PlayerDecisionCLI
keinen einzigen privaten Member.
Donât Repeat Yourself (DRY)
Einen Commit angeben, bei dem duplizierter Code/duplizierte Logik aufgelöst wurde; Code-Beispiele (vorher/nachher); begrĂŒnden und Auswirkung beschreiben
Durch kopierten Code wird das Refactoring enorm erschwert, denn ein kopierter Code kann auch kopierte Fehler bedeuten. Fehler und evtl. auch Sicherheitsschwachstellen mĂŒssen dann an mehreren Stellen gleichzeitig behoben werden, was die Wartung eine schwierige Aufgabe macht. Aber nicht nur bei Fehlern, auch bei normalen Anpassungen darf der duplizierte Code nicht vergessen werden, ansonsten drohen Inkonsistenzen im Programm. Code-Duplikationen sind daher unerwĂŒnscht, oftmals können sie durch geeignete Abstraktion oder auch ein einfaches Refactoring wie âExtract Methodâ behoben werden.
Im Commit 0140d
wurde duplizierter Code fĂŒr das Ausgeben einer Spielkarte auf der Konsole in eine eigene Methode ausgelagert. Allerdings wird dieser Commit bereits im Refactoring-Kapitel betrachtet.
Deshalb dient hier der Commit 3ba15
als Beispiel. Der Commit bezieht sich auf den CardFormatter
, der fĂŒr das Formatieren der Spielkarten als String unter Verwendung von Unicode-Zeichen zustĂ€ndig ist. Um Text mittig zu platzieren mussten an zwei Stellen jeweils berechnet werden, wie viel Platz noch nach links und nach rechts zur VerfĂŒgung steht. Diese Berechnung wurde nun in eine eigene Methode ausgelagert. Vorher sah der CardFormatter
so aus:
public final class CardFormatter {
...
private static String getHeader(Card card, int index) {
...
// Calculate spaces
int spacesLeft = (int) Math.floor(spaceLeft / 2.0);
int spacesRight = (int) Math.ceil(spaceLeft / 2.0);
...
}
private static String getFooter(Card card) {
...
// Calculate spaces
int spacesToTheLeft = (int) Math.floor(spacesBothSides / 2.0);
int spacesToTheRight = (int) Math.ceil(spacesBothSides / 2.0);
...
}
}
Der Code wurde nun durch die EinfĂŒhrung der neuen Methode Spaces getRemainingSpaces(int spacesRemainingOnBothSides)
vereinfacht:
public final class CardFormatter {
...
private static String getHeader(Card card, int index) {
...
Spaces spaces = getRemainingSpaces(spacesBothSides);
// und spÀter dann spaces.spacesToTheLeft sowie spaces.spacesToTheRight
...
}
private static String getFooter(Card card) {
...
Spaces spaces = getRemainingSpaces(spacesBothSides);
// und spÀter dann spaces.spacesToTheLeft sowie spaces.spacesToTheRight
...
}
private static record Spaces(int spacesToTheLeft, int spacesToTheRight) {};
private static Spaces getRemainingSpaces(int spacesRemainingOnBothSides) {
int spacesToTheLeft = (int) Math.floor(spacesRemainingOnBothSides / 2.0);
int spacesToTheRight = (int) Math.ceil(spacesRemainingOnBothSides / 2.0);
return new Spaces(spacesToTheLeft, spacesToTheRight);
}
}
WĂ€hrend dieses Refactorings wurden auch die Variabelnamen entsprechend angepasst. Die Variable spacesLeft
war zuvor sehr ungĂŒnstig gewĂ€hlt, weil hier mit âleftâ nicht links, sondern âĂŒbrigâ gemeint war. Gleichzeitig wurde derselbe Namen spacesLeft
verwendet, um die FreirĂ€ume anzuzeigen, die nach âlinksâ noch verfĂŒgbar sind. Im neuen Code ist nun die Rede von spacesRemainingOnBothSides
sowie von spacesToTheLeft
(jeweils anstelle von spacesLeft
).
Durch das Auslagern in die Methode könnte nun die Berechnung an diesem einen Orten angepasst werden, z.B. wenn bei ungerader Breite der Karte der Text eher weiter nach links oder nach rechts geschoben werden soll. Vorher hĂ€tte dies an zwei Stellen geĂ€ndert werden mĂŒssen. Sicherlich wĂ€ren gerade in dieser Klasse CardFormatter
noch weitere Verbesserungen mit ein wenig Aufwand möglich, gerade weil die Methoden getHeader(...)
und getFooter(...)
sehr Ă€hnlich bezĂŒglich ihres Aufbaus sind. ZugegebenermaĂen ist der Code in der Plugin-Schicht auch insgesamt der am wenigsten schönste geworden. Die inneren Schichten sind deutlich ausgefeilter, besser wartbar und entsprechen eher dem Prinzip von Clean Code. Nichtsdestotrotz: auch die gezeigte, kleine Anpassung hat Duplicate Code vermieden.