Hallo Spaß-Coder.
Weiter geht es mit unserer Reihe über Entwurfsmuster, heute mit einem weiteren Muster aus der Kategorie der Strukturmuster. Wir schauen uns in diesem Artikel den Kompositum (engl. Composite) an, der auch aus der Sammlung der Gang of Four stammt.
Kompo…was?
Kompositum (oder auch Teile-Ganzes genannt) ist ein einfaches Muster um Hierarchien von Objekten abzubilden. Ein simples Beispiel dazu ist eine Baumstruktur wie bei einem Menü, bei dem es einen Einstiegspunkt, darunter Untermenüs und darunter ggf. wiederum weitere Untermenüs gibt. Die Besonderheit bei diesem Muster ist, dass alle Bestandteile der Hierarchie – egal ob auf der untersten Ebene (Blatt genannt) oder mitten im Baum (im Baum Knoten genannt) – gleich behandelt werden können. Alle Klassen implementieren dabei dieselbe Schnittstelle.
Wie funktioniert das Kompositum?
Schauen wir uns einmal eine Implementierung mit Hilfe des Kompositum an und nehmen dabei allerdings nicht das bereits oben erwähnte klassische Beispiel des Menübaums. Machen wir es etwas interessanter … damit hat jetzt niemand gerechnet, oder?
Das nachfolgende Beispiel ist bezüglich des Kompositum vielleicht etwas ungewöhnlich. Dies soll jedoch aufzeigen, dass auch spezielle Arten von Baumstrukturen abgebildet werden können. Alles beginnt mit der einen Schnittstelle, die später von den Knoten und Blättern implementiert wird.
1 2 3 |
public interface Computable { double compute(); } |
In unserem Fall soll etwas berechnet und das Ergebnis vom Typ double zurückgegeben werden. Unser Blatt des Baums ist eine einfache Zahl. Nennen wir die Klasse Operand, sieht der Code dazu wie folgt aus.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class Operand implements Computable { private double value; public Operand(double value) { this.value = value; } @Override public double compute() { return this.value; } } |
Hier wird der per Konstruktor einmalig gesetzte Wert zurückgegeben. Soweit so einfach. Nun der Operator.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class Operator implements Computable { private ComputingStrategy computingStrategy; private Computable firstOperand; private Computable secondOperand; public Operator(Computable firstOperand, ComputingStrategy computingStrategy, Computable secondOperand) { this.computingStrategy = computingStrategy; this.firstOperand = firstOperand; this.secondOperand = secondOperand; } @Override public double compute() { return this.computingStrategy.compute(firstOperand, secondOperand); } } |
Hier schauen wir einmal genauer hin, auch wenn sich die Funktionsweise vermutlich fast von selbst erklärt. Ein Operator besteht aus einem Operanden, aus einer Rechenvorschrift und einem weiteren Operanden. Somit könnten wir z.B. die Rechnung 5 + 4 abbilden. Um eine größtmögliche Flexibilität bei der Rechenvorschrift zu haben nutzen wir hier das Strategie-Muster.
1 2 3 |
public interface ComputingStrategy { double compute(Computable firstOperand, Computable secondOperand); } |
Hier werden zwei Operanden miteinander verknüpft. Für die Addition haben wir damit folgende Implementierung.
1 2 3 4 5 6 |
public class Plus implements ComputingStrategy { @Override public double compute(Computable firstOperand, Computable secondOperand) { return firstOperand.compute() + secondOperand.compute(); } } |
Für die Verknüpfung der Operanden wird der jeweilige Wert über die Methode compute() ermittelt. Vielleicht ahnt der ein oder andere von euch an dieser Stelle schon so etwas wie Rekursion und liegt damit ganz richtig. Wir verwenden für die Verknüpfung innerhalb der Rechenvorschrift nur das Interface Computable. Demnach ist es egal, ob es sich dabei um ein Operand oder ein Operator handelt. Dies genau ist die Stärke des Kompositum: alle Bestandteile des Baum werden gleich behandelt!
Ein Test der Implementierung der Addition ist an dieser Stelle schon ziemlich grün 🙂
1 2 3 4 5 6 7 |
@Test public void testComputeCalcsCorrectWithoutDecimalValues() throws Exception { Operand five = new Operand(5d); Operand four = new Operand(4d); ComputingStrategy sut = new Plus(); Assertions.assertThat(sut.compute(five, four)).isEqualTo(9d, Offset.offset(0.0)); } |
Eine kleine Rechnung mit verschiedenen Rechenschritten lässt sich nun in dieser Form gestalten.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
public class CompositeExample { public static void main(String...args) { Operand five = new Operand(5d); Operand four = new Operand(4d); Operand two = new Operand(2d); Operator fivePlusFour = new Operator(five, new Plus(), four); Operator twoTimesNine = new Operator(two, new Multiplication(), fivePlusFour); Operator eighteenDividedByFour = new Operator(twoTimesNine, new Division(), four); System.out.print("(5 + 4) * 2 / 4 = "); System.out.println(eighteenDividedByFour.compute()); } } |
Wir „bauen“ die Rechnung nun als Baum auf und rufen einmalig die Methode compute() auf und erhalten das Gesamtergebnis. Wichtig dabei zu beachten ist, dass unsere Rechnung immer linksseitig ausgewertet wird! Dies liegt daran, dass in der Rechenvorschrift zuerst firstOperand.compute() aufgerufen wird. Grafisch dargestellt sieht unser Baum aus obigem Beispiel so aus:
Besonderheit des Beispiels
Zu unserem Beispiel ist noch zu sagen, dass hier gegenüber der üblichen Verwendung des Kompositums eine Besonderheit vorliegt. Unser Operand enthält exakt zwei Variablen vom Typ Computable. Häufig ist es jedoch so, dass ein Baumknoten eine Liste von Komponenten enthält, die bei der Verarbeitung iteriert wird. Die Grundidee bleibt jedoch dieselbe – Knoten und Blätter des Baums werden durch die Implementierung derselben Schnittstelle identisch verwendet. Demnach muss der Nutzer keine Kenntnis darüber haben, um welche Art von Komponente es sich handelt.
Zusammenfassung?
Wir haben uns in diesem Artikel ein Strukturmuster aus der Liste der Entwurfsmuster angeschaut, mit welchem eine Baumstruktur oder Teile-Ganzes-Beziehung abgebildet werden kann. Dies kann für Menüs, Unternehmenshierarchien oder Berechnungen genutzt werden. Mit der einmalig festgelegten Schnittstelle kann der Baum auch sehr einfach um weitere Implementierungen von Komponenten erweitert werden, welche dann nach belieben orchestriert werden können.
In welcher Situation habt ihr schon einmal einen Baum wachsen lassen?
Eure Spaß-Coder
Dieser Artikel basiert neben unseren Erfahrungen auf den Ausführungen aus:
Code-Beispiele auf Github:
- https://github.com/invidit/CodeQuality.git