In dieser Artikelserie möchte ich Ihnen die Prinzipien des Softwaredesign anhand von Beispielen aus der realen Welt näher bringen. Eine Einleitung zum Thema können Sie hier nachlesen.
In der heutigen Folge geht es um das Gesetz von Demeter.
Bevor ich Sie mit Programmierrichtlinien und Quellcode quäle, möchte ich Ihnen das Designprinzip anhand eines Beispiels aus dem täglichen Leben näher bringen. Die beiden nachfolgenden Taxifahrten könnten sich so oder so ähnlich abgespielt haben.
Taxifahrt 1
Der Fahrgast steigt in das Taxi ein und sagt dem Taxifahrer wohin er möchte. Als der Taxifahrer das Ziel im Navigationsgerät eingeben möchte reisst es ihm der Fahrgast aus der Hand mit der Aussage: „Ich habe nicht viel Zeit. Fahren Sie also bitte los und lassen sie mich das Navigationsgerät einstellen“. Nach kurzer Fahrtzeit kommt das Taxi an eine Kreuzung. Die nette Stimme aus dem Navigationsgerät sagt dem Fahrer das er geradeaus fahren soll. Diesem Vorschlag folgt der Taxifahrer. Mitten auf der Kreuzung greift der Fahrgast aber plötzlich in das Lenkrad, reisst es nach rechts und sagt zum Taxifahrer: „Ich kenne da eine Abkürzung. Lassen Sie uns also lieber hier rechts abbiegen“. Der Fahrer hält daraufhin an und wirft den Fahrgast aus dem Taxi.
Taxifahrt 2
Der Fahrgast steigt in das Taxi sagt dem Taxifahrer wohin er möchte. Der Taxifahrer gibt die Zieladresse in das Navigationsgerät ein und fährt los. Vor einer Kreuzung bittet der Fahrgast den Fahrer an dieser Kreuzung nicht auf das Navigationsgerät zu hören und rechts statt geradeaus zu fahren. Er begründet dies damit dass die Kreuzung erst vor kurzer Zeit erneuert wurde und die meisten Navigationsgeräte die neue Route noch nicht kennen. Der Taxifahrer kommt dieser bitte nach und nach kurzer Fahrt erreicht er die Zieladresse.
Zusammenfassung des Beispiels
Erkennen Sie wodurch sich die beiden Beispiele ändern? Es gab einen grossen Unterschied in beiden Fällen, mal abgesehen von der unfreundlichen Art des ersten Fahrgast und abgesehen davon das ein Taxi das Ziel erreicht hat und das anderen nicht.
Richtig, die Interaktion der beteiligten Subjekte und Objekte unterscheidet sich grundlegend.
Folgende Komponenten (Subjekte und Objekte) sind beteiligt:
- Fahrgast
- Taxifahrer
- Taxi
- Navigationsgerät
Im ersten Beispiel interagiert der Fahrgast mit allen anderen Komponenten. Er gibt dem Taxifahrer Anweisungen und greift direkt auf das Navigationsgerät und sogar das Taxi zu.
Im zweiten Beispiel gibt es eine klare Kommunikationshierarchie. Der Fahrgast gibt dem Taxifahrer Anweisungen. Der Taxifahrer setzt diese Anweisungen um und nutzt dazu das Navigationsgerät und das Taxi.
Die nachfolgende Grafik zeigt diesen Zusammenhang. Dabei sind die Interaktionsverbindungen die sich in dem Praxistest als erfolgreich herausgestellt haben mit „OK“ gekennzeichnet und die weniger erfolgreichen Verbindungen mit „NOK“ beschriftet.
Programmierbeispiel
Versuchen wir nun die die Grundgedanken aus dem Praxisbeispiel in eine Softwareapplikation zu übertragen. In der Beispielanwendung soll das Grundgerüst einer Steuersoftware für eine Werkzeugmaschine realisiert werden. Dabei nutzen wir eine Konsolenanwendung als grafische Oberfläche. Die Werkzeugmaschine hat viele Einzelkomponenten für welche eigene Controller Klassen erstellt werden. Beispielhaft wird der Controller für die Tür des Maschinengehäuses implementiert. Diese Controller Klassen für die Einzelkomponenten werden von einer Klasse zur Steuerung der Maschine genutzt.
Der Beispielcode soll das Grundgerüst der Anwendung mit diesen drei Klassen enthalten (Tür, Maschine, GUI). Dabei soll in der GUI Klasse die Funktionalität zum Öffnen der Tür aufgerufen werden.
Lösung 1
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 ConsoleKeyInfo keyInfo;
6
7 _deviceController = new DeviceController();
8
9 //…show menu…
10
11 keyInfo = Console.ReadKey();
12
13 if (keyInfo.Key == ConsoleKey.F1)
14 {
15 _deviceController.Door.Open();
16 }
17 }
18
19 private static DeviceController _deviceController;
20 }
21
22 class DoorController
23 {
24 public void Open();
25 public void Close();
26 }
27
28 class DeviceController
29 {
30 public void OpenDoor()
31 {
32 _doorController.Open();
33 }
34
35 public DoorController Door
36 {
37 get
38 {
39 return _doorController;
40 }
41 }
42
43 private DoorController _doorController = new DoorController();
44 }
Die Klasse DoorController dient zur Ansteuerung der Tür des Gehäuses der Werkzeugmaschine. Die Klasse DeviceController enthält den Code zur Ansteuerung der Maschine. Im Moment enthält die Klasse beispielhaft nur die Funktion zum Öffnen der Tür. Die Klasse würde in einer realen Anwendung zusätzlich alle weiteren Funktionen zur Ansteuerung der Maschine enthalten. Als GUI dient eine Konsolenanwendung. In der Main Funktion würde man dazu zum Beispiel ein Menü anzeigen. In diesem Beispiel wird durch die Taste F1 das Öffnen der Tür veranlasst.
Das interessante an diesem Beispiel ist, welche Funktion zum Öffnen der Tür genutzt wird. Statt direkt die Funktion aus dem DeviceController aufzurufen wird indirekt vorgegangen. Dazu wird das Property Door genutzt um an die Instanz der Klasse DoorController zu gelangen. Anschliessend wird die Tür mittels der Open Funktion der DoorController Klasse geöffnet.
Dieses Vorgehen hat einen grossen Nachteil. Damit wird die eigentliche Kontrollklasse der Werkzeugmaschine umgangen. Erinnern wir uns zurück an das Beispiel mit dem Taxi. Der Fahrgast umgeht im ersten Beispiel ebenfalls die Kontrollinstanz in Form des Taxifahrers indem er in das Lenkrad greift und dieses nach rechts zieht um abzubiegen. Wie Sie sich vorstellen können ist solch ein Eingriff gefährlich und kann einen Unfall nach sich ziehen. Selbiges gilt selbstverständlich auch für die Werkzeugmaschine. Die Kontrollinstanz in Form der Maschinenklasse wird umgangen und die Gehäusetür wird direkt geöffnet. Ist die Maschine in diesem Moment noch aktiv dann hätte die Kontrollklasse das Öffnen der Tür verhindert. So aber wurde die Tür trotzdem geöffnet. Für Anwender die diese Applikation zur Ansteuerung der Maschine nutzen könnte somit die Umstellung vom 10 Finger Schreibsystem auf ein 9 Finger Schreibsystem bittere Realität werden.
Durch ein paar kleine Änderungen kann die Applikation aber verbessert werden. Der nachfolgende Quellcode zeigt eine optimierte Programmversion.
Lösung 2
1 class Program
2 {
3 static void Main(string[] args)
4 {
5 ConsoleKeyInfo keyInfo;
6
7 _deviceController = new DeviceController();
8
9 //…show menu…
10
11 keyInfo = Console.ReadKey();
12
13 if (keyInfo.Key == ConsoleKey.F1)
14 {
15 _deviceController.OpenDoor();
16 }
17 }
18
19 private static DeviceController _deviceController;
20 }
21
22 class DoorController
23 {
24 public void Open();
25 public void Close();
26 }
27
28 class DeviceController
29 {
30 public void OpenDoor()
31 {
32 _doorController.Open();
33 }
34
35 private DoorController _doorController = new DoorController();
36 }
Aus der Klasse DeviceController wurde das Property zum Zugriff auf die Tür Klasse entfernt. In der Konsolenanwendung wird jetzt die Funktion der Kontrollklasse genutzt um die Tür zu öffnen.
Law of Demeter
Anhand des Praxisbeispiels und des Programmierbeispiels lässt sich der Kerngedanke des Law of Demeter gut erkennen. Diese lautet: Objekte sollten nur mit Objekten in ihrer unmittelbaren Umgebung kommunizieren.
Eine Methode einer Klasse sollte daher ausschliesslich auf die folgenden Komponenten zugreifen:
- Andere Methoden der Klasse
- Properties der Klasse
- Methoden der in der Klasse verwendeten Objekten
- Methoden von Objekten welche in der Methode selbst erzeugt werden
Die nachfolgende Grafik zeigt diesen Zusammenhang. Die Klasse A nutzt die Methoden der Klasse B. Die Klasse B wiederum nutzt die Methoden der Klasse C. Die Klasse A hat somit nur die Klasse B in ihrer unmittelbaren Umgebung. Daher darf die Klasse A die Klasse C nicht nutzen. Die erlaubten Verbindungen sind mit „OK“ und die nicht erlaubte Verbindung ist mit „NOK“ gekennzeichnet.
Das Ziel des Designprinzip liegt in der Verringerung der Kopplung der Komponenten in einem Softwaresystem. Dies erhöht die Wartbarkeit, Anpassbarkeit und Wiederverwendbarkeit des Quellcodes. Durch Entkopplung wird zusätzlich die Testbarkeit wesentlich verbessert. Insgesamt führt dies zu einer Steigerung der Softwarequalität.
Zwei Punkte Regel
Verstösse gegen das Law of Demeter können Sie im Quellcode schnell erkennen, wenn Sie auf die Zwei Punkte Regel achten: Wenn in eine Anweisung zwei Punkte enthalten sind – also in der Form A.B.C – dann wird meist ein komponentenübergreifender Aufruf durchgeführt, welcher gegen das Law of Demeter verstösst.
Im ersten Programmierbeispiel ist dies gut zu sehen. Die Anweisung zum Öffnen der Tür lautet: _deviceController.Door.Open()
In der Anweisung sind zwei Punkte enthalten. Und tatsächlich wird hier gegen das Law of Demeter verstossen. Halten Sie daher in Ihren eigenen Anwendungen oder bei Code Reviews die Augen diesbezüglich offen und werden Sie skeptisch wenn Sie zwei Punkte in einer Anweisung entdecken.
Fazit
Bei Einhaltung des Law of Demeter ergeben sich klare Interaktionsstrukturen innerhalb des Softwaresystems. Dabei steht die Verringerung der Kopplung der Softwarekomponenten im Vordergrund. Die Anwendung dieses Designprinzips ist mit geringem Aufwand verbunden und führt zu einer grossen Steigerung der Softwarequalität. Meiner Meinung nach ist das Law of Demeter eines der wichtigsten Designprinzipien zur Steigerung der Softwarequalität und sollte in die SOLID Prinzipien eingegliedert werden. Ich plädiere daher für die Anwendung der SOLLID Prinzipien statt der SOLID Prinzipien.