
Das Law of Demeter ist ein Entwurfsprinzip aus der – vornehmlich objektorientierten – Programmierung. Die direkte deutsche Übersetzung „Gesetz von Demeter“ ist weniger geläufig. Gelegentlich wird auch vom Principle of Least Knowledge gesprochen. Das Law of Demeter wird auch gerne mit dem Spruch „Don’t talk to strangers“ (rede nicht mit Fremden) verbunden.
Das Law of Demeter wurde 1987 an der Northeastern University in Boston von Ian Holland und weiteren Kollegen von ihm vorgeschlagen. Es wurde nach dem Demeter-Projekt benannt, in dessen Kontext es entstanden ist.
The Demeter project was named after Demeter because we were working on a hardware description language Zeus and we were looking for a tool to simplify the implementation of Zeus. We were looking for a tool name related to Zeus and we chose a sister of Zeus: Demeter.
Ian Holland et al.
Definition
Das Law of Demeter besagt, dass ein Objekt nur mit Objekten in seiner unmittelbaren Nähe kommunizieren sollte. Formal definiert es, dass eine Methode foo
eines Objektes bar
nur auf folgende Elemente zugreifen darf:
- auf
bar
selbst - auf alle Felder von
bar
- auf die an
foo
übergebenen Argumente - auf Objekte, die innerhalb von
foo
instanziert werden
Im Gegensatz zu vielen anderen Prinzipien wie dem Single Responsibility Principle und dem Principle of Least Astonishment ist ein Verstoß gegen das Law of Demeter eindeutig messbar und damit objektiv.
Beispiel
In diesem Beispiel berechnet ein Bestellservice
den Bestellwert. Jede Bestellposition
innerhalb der Bestellung
hat einen Preis
, der wiederum den numerischen Wert kapselt. Der Bestellwert ergibt sich aus der Summe der Preise aller Bestellpositionen:
// Konstruktoren etc. weggelassen
class Preis {
BigDecimal value;
}
class Bestellposition {
String bezeichnung;
Preis preis;
}
class Bestellung {
List<Bestellposition> bestellpositionen;
}
class Bestellservice {
void bestellen(Bestellung bestellung) {
var bestellwert = bestellung.bestellpositionen
.stream()
.map(bestellposition ->
bestellposition.preis.value)
// ^^^^^^^^^^^
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
}
}
Dieser Code verstößt gegen das Law of Demeter, da der Bestellservice
nicht nur auf die an ihn übergebene Bestellung
zugreift, sondern auch auf die darin gekapselte Bestellposition
und den darin enthaltenen Preis
.
Dies hat zur Folge, dass bei einer Umstruktrierung der Interna der Bestellung auch der Bestellservice angepasst werden muss. Wenn weitere Komponenten den Preis oder andere Bestandteile der Bestellung verwenden, sind diese ebenfalls betroffen, etwa ein Gutschriftservice oder Lieferservice.
Um den Misstand gemäß dem Law of Demeter zu beheben, können wir die Preisberechnung in die Bestellung verlagern und die Bestellposition um eine Methode erweitern, die den numerischen Preiswert zurückgibt:
class Preis {
BigDecimal value;
}
class Bestellposition {
String bezeichnung;
Preis preis;
BigDecimal preisValue() {
return preis.value;
}
}
class Bestellung {
List<Bestellposition> bestellpositionen;
BigDecimal calcBestellwert() {
return bestellpositionen.stream()
.map(Bestellposition::preisValue)
.reduce(BigDecimal::add)
.orElse(BigDecimal.ZERO);
}
}
class Bestellservice {
void bestellen(Bestellung bestellung) {
BigDecimal bestellwert =
bestellung.calcBestellwert();
}
}
Nach diesem Refactoring wird das Law of Demeter befolgt:
- der
Bestellservice
spricht nur mit derBestellung
- die
Bestellung
spricht nur mit derBestellposition
- die
Bestellposition
spricht nur mit demPreis
Zugegebenermaßen ist das Beispiel etwas konstruiert. In der Realität würde ich dem Preis wohl eine Berechnungsfunktion hinzufügen und den rohen Preiswert in Form des BigDecimals nur an wenigen Stellen verwenden, aber ein Beispiel aus der realen Welt wäre auch weniger anschaulich und die Intention hinter dem Refactoring hier sollte deutlich sein.
Und was ist, wenn der Bestellservice ein anderes Merkmal einer einzigen Bestellposition abfragen möchte? Nun als erstes würde ich die berechtigte Frage stellen, warum die Bestellposition selbst dann nicht als Argument an die jeweilige Methode des Bestellservices übergeben wird, als zweites könnte man aber auch die Bestellposition selbst an eine Methode der Bestellung übergeben, um die jeweiligen Informationen zu extrahieren. Hier ein reduziertes Beispiel:
class Bestellposition {
boolean isFreeOfCharge;
boolean isCancelled;
}
class Bestellung {
List<Bestellposition> bestellpositionen;
boolean isIncludedInInvoice(Bestellposition pos) {
return !(pos.isFreeOfCharge || pos.isCancelled);
}
}
class Bestellservice {
void createInvoice(Bestellung bestellung) {
var chargeableBestellpositionen =
bestellung.bestellpositionen
.stream()
.filter(bestellung::isIncludedInInvoice);
}
}
Vorteile
Das Law of Demeter soll die Wartbarkeit, Erweiterbarkeit und Testbarkeit von Code verbessern. Es reduziert die Abhängigkeit zu internen Strukturen anderer Objekte und erhöht die Modularität. Ändern sich die Interna einer Klasse, so hat das Befolgen des Law of Demeter im Code insgesamt weniger Änderungen zur Folge, als wenn dies ignoriert werden würde. Ein regelmäßiges Verstoßen gegen das Law of Demeter erhöht die Kopplung verschiedener Klassen untereinander und kann langfristig in einem Big Ball of Mud enden. Denn je mehr Abhängigkeiten zwischen Klassen existieren, umso schwieriger wird es, die darunterliegenden Strukturen zu ändern.
Das Law of Demeter kann sich positiv auf die Software-Metrik Response For a Class (RFC) auswirken. Diese Metrik misst die Anzahl der möglichen aufgerufenen Methoden, die ein Aufruf einer Methode zur Folge haben kann. Diese wird potenziell dadurch reduziert, dass nur benachbarte Objekte miteinander „sprechen“.
Nachteile
Das Law of Demeter erfordert ein umsichtigeres Vorgehen, als wenn man gegen dieses verstößt. Zu meinem Beispiel der Bestellung weiter oben gibt es Alternativen, die ihre Vor- und Nachteile mit sich bringen. Macht man sich nicht ausreichend Gedanken über gute Lösungen, so kann ein zwanghaftes Befolgen des Law of Demeter schnell zu ungewollter Komplexität führen. Diese äußert sich z.B. in Wrapper- bzw. Proxy-Methoden wie im Beispiel oben, die Anfragen an ein benachbartes Objekt delegieren, damit nicht gegen das Law of Demeter verstoßen wird. Dadurch wächst die Menge an Code insgesamt.
Das Law of Demeter wirkt sich potenziell negativ auf die Software-Metrik Weighted Methods per Class (WMC) aus, die die zyklomatische Komplexität aller Methoden einer Klasse misst. Ursächlich hierfür ist der Overhead durch zusätzliche Wrapper-/Proxy-Methoden.
Meine Meinung
Mein erster Gedanke zum Law of Demeter ist, dass ich den Namen nicht so gelungen finde. Ein Gesetz im physikalischen Sinne ist eine unumstößliche Gegebenheit. Gegen ein physikalisches Gesetz kann ich nicht verstoßen. Der Verstoß gegen ein juristisches Gesetz kann gravierende Folgen haben. Der Verstoß gegen das Law of Demeter… ist relativ egal. Ein Prinzip oder eine Richtlinie hätte für mich besser gepasst, denn ich sehe das Law of Demeter eher als eine Empfehlung, von der ich auch gerne gut begründet abweiche, denn es hat eben auch Nachteile.
Das Law of Demeter wirkt sich einerseits positiv auf die Response for a Class aus, andererseits negativ auf die Weighted Methods per Class, damit baut es keine Komplexität auf oder ab, sondern verschiebt sie. Beide Metriken können zu einem gewissen Grad Rückschlüsse auf Testbarkeit und Wartbarkeit zulassen. Bei einer höheren RFC (Law of Demeter wird ignoriert) haben Klassen potenziell mehr Beziehungen zu anderen Klassen, im Fall einer einer höheren WMC (Law of Demeter wird befolgt) haben Klassen potenziell mehr Methoden.
Der Wert des Law of Demeter ist in erster Linie wie so vieles in der Software-Entwicklung kontextabhängig. Ein gutes Design enthält in der Regel weniger Verstöße gegen das Law of Demeter, als ein gewachsenes, schwer wartbares System. Aber nicht in jedem Fall sollte das Law of Demeter blind befolgt werden.
Seine Stärken spielt das Law of Demeter meiner Meinung nach am besten innerhalb der Domäne aus, also dem fachlichen, möglichst Technik-freien Teil einer Software, in dem ich den größten Wert auf Verständlichkeit und nachhaltige Wartbarkeit lege. Das Beispiel weiter oben könnte aus einer Domäne nach Domain-driven Design kommen. In dem Fall wären die Bestellung ein Aggregat, die Bestellposition eine Domain Entity innerhalb dieses Aggregats und der Preis ein Value Object. Gemäß Don’t Repeat Yourself möchten wir vermeiden, dass Geschäftslogik dupliziert wird. Insofern ergibt sich automatisch, dass die Preisberechnung an zentraler Stelle erfolgt und dies so nah wie möglich an den beteiligten Entitäten. Kann allein aus einer Bestellung heraus der Preis berechnet werden, so wird diese Berechnung in diesem Aggregat selbst untergebracht.
Ein Verstoß gegen das Law of Demeter in der Domäne ist für mich kein definitiver Fehler, aber auf jeden Fall ein Impuls, über die aktuelle Struktur nachzudenken.
Anders verhält es sich bei reinen Datenklassen. In den meisten Sprachen wird zwischen Klassen, die Geschäftslogik beinhalten, und Klassen, die lediglich Datenstrukturen abbilden, nicht oder kaum differenziert. In Java kann die Verwendung von einem record
seit kurzem ein Anhaltspunkt dafür sein, in Kotlin ist es die data class
.
Wendet man das Law of Demeter auf eine API an, so kann dies schnell zu einem hohen Overhead führen. Nehmen wir als Beispiel wieder eine Bestellung und gehen wir davon aus, dass uns bei einem Aufruf eines API-Endpunktes lediglich das Land existiert. Der Zugriff auf das Datum könnte so aussehen: bestellung.kunde.lieferadresse.land.isocode
Bei Einhaltung des Law of Demeter müsste die reine Datenklasse um mindestens vier Proxy-Methoden erweitert werden, um den ISO-Code des Landes der Kundenlieferadresse abzufragen. Unter dem Gesichtspunkt, dass sich APIs eher selten nicht-abwärtskompatibel ändern, in der Software sich wahrscheinlich nur ein Modul mit der Abfrage dieser API befasst und auch eine API-Änderung bei Einsatz von Proxy-Methoden entsprechende Refactorings zur Folge hätte, halte ich hier die Anwendung des Law of Demeter nicht für sinnvoll.
Ich würde auch zwischen untergeordneten Komponenten und Komposition von Komponenten auf gleicher Ebene differenzieren: Einen direkten Zugriff auf die Bestellposition einer Bestellung in der Domäne sehe ich weniger kritisch als einen Zugriff auf einen an der Bestellung hinterlegten Kunden. Eine Bestellung inklusive ihrer Bestellpositionen kann als Ganzes betrachtet werden, da eine Bestellposition nicht alleinstehend ist. Bei Verletzung des Law of Demeters würde ein Refactoring nur Module betreffen, die direkt mit Bestellungen arbeiten. Hängt an der Bestellung aber ein vollwertiger Kunde, der auch an Rechnungen, Abonnements und diversen anderen Aggregaten hinterlegt sein kann, ist die Folge einer Verletzung bei einer Änderung der Struktur sehr viel drastischer.
Fazit
Ich möchte Datenstrukturen und Beziehungen zwischen Klassen und Modulen ungeachtet des Law of Demeter durchdacht modellieren und auf unnötige Abhängigkeiten verzichten, statt aus Bequemlichkeit den Pfad des geringsten Widerstandes zu wählen. Kleine gut getestete Module mit wohlüberlegten öffentlichen Schnittstellen sind für mich entscheidender, als die konsequente Einhaltung des Laws of Demeter innerhalb dieser Module. Das Law of Demeter feiert bald seinen 40. Geburtstag. Es ist sicherlich zeitloser als viele konkrete Trends und Technologien, aber dennoch passt es nicht gleichermaßen in jeder Situation. Für mich haben Erfahrung und Kontext einen höheren Stellenwert als das Law of Demeter.
- Dokument zum Demeter-Projekt von Karl J. Liebherr