Link Search Menu Expand Document

4 Weitere Prinzipien

Inhalt

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

Refactoring CardFormatter

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

Refactoring CardFormatter

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.

PlayerDecision Hohe KohÀsion

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.