Gesetz von Demeter

Gesetz von Demeter

vg

Das Gesetz von Demeter (LoD), auch als „Prinzip der geringsten Kenntnis“ bekannt, ist ein Konzept in der Softwareentwicklung, das besagt, dass ein Objekt nur mit seinen direkten Nachbarn kommunizieren sollte. Es soll verhindern, dass Objekte zu viel über die inneren Details anderer Objekte wissen. In der objektorientierten Programmierung fördert dieses Gesetz eine lose Kopplung und schützt vor unnötigen Abhängigkeiten zwischen den Komponenten.

Was ist das Gesetz von Demeter?

Das Gesetz von Demeter (auch als „Principle of Least Knowledge“ bekannt) ist ein wichtiges Konzept in der Softwareentwicklung, das ursprünglich im Zusammenhang mit objektorientierter Programmierung formuliert wurde. Das Gesetz von Demeter wurde 1987 von  Karl J. Lieberherr und Ian Holland formuliert. Ziel des Gesetzes ist es, die Kopplung zwischen verschiedenen Komponenten eines Systems zu minimieren, um eine höhere Modularität und Wartbarkeit des Codes zu erreichen. Um das Konzept tiefer zu verstehen, ist es hilfreich, sich sowohl mit den theoretischen Grundlagen als auch mit den praktischen Auswirkungen und möglichen Herausforderungen auseinanderzusetzen.

1. Theoretische Grundlagen

Das Gesetz von Demeter besagt, dass ein Objekt nur mit den Objekten kommunizieren sollte, die es direkt kennt und zu denen es eine klare, gut definierte Beziehung hat. Diese Objekte werden als „direkte Freunde“ bezeichnet. Das Gesetz kann in die folgende vier Kategorien unterteilt werden:

  1. Objekte, die es selbst erstellt hat: Ein Objekt kann mit Objekten kommunizieren, die es selbst instanziiert hat, da es diese Objekte direkt kontrolliert und ihre Struktur kennt.
  2. Objekte, die ihm als Argumente übergeben wurden: Ein Objekt kann mit den Objekten kommunizieren, die ihm als Parameter in einer Methode übergeben wurden, da diese Objekte in diesem Kontext bekannt sind und eine explizite Beziehung besteht.
  3. Objekte, die es direkt besitzt: Ein Objekt kann auf Objekte zugreifen, die es in einer Eigenschaft oder einem Attribut direkt besitzt. Diese Objekte sind Teil des internen Zustands des Objekts.
  4. Objekte, die in seiner eigenen Rückgabewerte enthalten sind: Ein Objekt kann mit Objekten kommunizieren, die es als Ergebnis einer Methode zurückgibt, da diese Objekte innerhalb der Methodenlogik erzeugt oder abgerufen werden.

Durch diese Einschränkungen wird die Anzahl der direkten Abhängigkeiten eines Objekts reduziert, was zu einer besseren Modularität und einer geringeren Gefahr von Fehlern bei späteren Änderungen führt.

2. Vermeidung von „Kettenaufrufen“

Ein oft genanntes Beispiel zur Veranschaulichung des Gesetzes ist das Vermeiden von sogenannten „Kettenaufrufen“. Das bedeutet, dass ein Objekt nicht die intern verschachtelten Methoden oder Eigenschaften eines anderen Objekts direkt ansprechen sollte. Ein typischer Code, der gegen das Gesetz von Demeter verstößt, könnte folgendermaßen aussehen:

#include <iostream>
#include <string>

class City {
public:
    std::string name;

    City(const std::string& name) : name(name) {}
};

class Address {
public:
    City* city;

    Address(City* city) : city(city) {}
};

class Person {
public:
    Address* address;

    Person(Address* address) : address(address) {}

    std::string getCityName() {
        return address->city->name;  // Verstoss gegen das Gesetz von Demeter
    }
};

int main() {
    City city("Berlin");
    Address address(&city);
    Person person(&address);
    
    std::cout << person.getCityName() << std::endl;  // Ausgabe: Berlin
    return 0;
}

In diesem Beispiel greift die Person-Klasse direkt auf die City-Klasse zu, indem sie die Adresse und dann die Stadt des Address-Objekts abruft. Dies verletzt das Gesetz von Demeter, weil Person nicht direkt wissen sollte, wie die Adresse intern strukturiert ist, noch sollte es Details der Stadt kennen.

Das korrekte Design wäre, dass die Person-Klasse lediglich mit der Address-Klasse kommuniziert und die Address-Klasse selbst die Verantwortung trägt, Informationen zur Stadt bereitzustellen:

#include <iostream>
#include <string>

class City {
public:
    std::string name;

    City(const std::string& name) : name(name) {}
};

class Address {
public:
    City* city;

    Address(City* city) : city(city) {}

    std::string getCityName() const {
        return city->name;
    }
};

class Person {
public:
    Address* address;

    Person(Address* address) : address(address) {}

    std::string getCityName() const {
        return address->getCityName();  // Einhaltung des Gesetzes von Demeter
    }
};

int main() {
    City city("Berlin");
    Address address(&city);
    Person person(&address);
    
    std::cout << person.getCityName() << std::endl;  // Ausgabe: Berlin
    return 0;
}

In diesem Beispiel sind die internen Details der City-Klasse von der Person-Klasse versteckt. Dies entspricht dem Gesetz von Demeter und fördert lose Kopplung und geringere Abhängigkeiten.

3. Vorteile des Gesetzes von Demeter

Die Anwendung des Gesetzes von Demeter bietet mehrere Vorteile:

  1. Reduzierte Kopplung: Ein System mit weniger direkten Abhängigkeiten zwischen Objekten ist robuster gegenüber Änderungen. Änderungen an einem Objekt wirken sich nicht direkt auf viele andere Objekte aus, was die Wartung und Erweiterung erleichtert.
  2. Erhöhte Wiederverwendbarkeit: Wenn ein Objekt nur mit wenigen anderen Objekten kommuniziert und deren interne Details nicht kennt, kann es leichter in anderen Kontexten oder Projekten wiederverwendet werden.
  3. Bessere Testbarkeit: Da Objekte weniger voneinander abhängen, können sie unabhängig voneinander getestet werden. Die Einheitlichkeit und das Verhalten eines Objekts können isoliert überprüft werden, ohne dass andere Komponenten des Systems einbezogen werden müssen.
  4. Verbesserte Lesbarkeit und Verständlichkeit des Codes: Wenn der Code klar trennt, welche Objekte miteinander kommunizieren, und unnötige Abhängigkeiten vermeidet, wird der Code verständlicher und leichter zu pflegen.

4. Herausforderungen und Einschränkungen

Obwohl das Gesetz von Demeter viele Vorteile bietet, gibt es auch Herausforderungen und Einschränkungen:

  1. Erhöhter Aufwand bei der Implementierung: Die Einhaltung des Gesetzes kann dazu führen, dass zusätzliche Methoden oder Hilfsklassen eingeführt werden müssen, um den Zugriff auf interne Details zu kapseln. Dies kann den Code komplexer machen und den initialen Entwicklungsaufwand erhöhen.
  2. Praktische Notwendigkeit der Balance: In einigen Fällen kann es sinnvoll sein, das Gesetz etwas flexibler anzuwenden, insbesondere wenn ein Objekt eine relativ einfache Struktur hat und seine Interaktionen mit anderen Objekten sehr transparent sind. In solchen Fällen könnte eine zu strikte Anwendung des Gesetzes den Code unnötig verkomplizieren.
  3. Fehlende Flexibilität bei tief verschachtelten Datenstrukturen: In Anwendungen, die mit komplexen und tief verschachtelten Datenstrukturen arbeiten (zum Beispiel in der Datenbankabstraktion oder bei komplexen Algorithmen), kann das strikte Befolgen des Gesetzes zu einer übermäßigen Aufspaltung der Logik führen. Hier muss sorgfältig abgewogen werden, ob die strikte Trennung von Verantwortlichkeiten wirklich sinnvoll ist.
  4. Verständnis und Akzeptanz im Team: Die konsequente Umsetzung des Gesetzes von Demeter erfordert ein gutes Verständnis der Prinzipien von lose gekoppelten Systemen und eine Kultur der Code-Qualität im Team. Ohne eine klare Verständigung über die Notwendigkeit und die Vorteile des Gesetzes kann es zu Widerstand kommen, insbesondere wenn Entwickler an weniger restriktive Paradigmen gewöhnt sind.

Weitere Verbesserung durch das Gesetz von Demeter

Es gibt zusätzliche Techniken, die helfen, das Gesetz von Demeter noch konsequenter anzuwenden. Eine Möglichkeit ist, zusätzliche Abstraktionen einzuführen, die den Zugriff auf die Objektstrukturen steuern. Hier könnte man zusätzliche Design Patterns wie das Facade Pattern verwenden, um den Zugang zu komplexen Objekten zu abstrahieren.

Beispiel – Anwendung des Facade Patterns:

class AddressFacade {
public:
    AddressFacade(Person& person) : person(person) {}
    std::string getStreet() { return person.getStreet(); }

private:
    Person& person;
};

void printStreet(Company& company) {
    AddressFacade facade(*company.getPerson());
    std::cout << facade.getStreet() << std::endl;  // Vereinfachter Zugang
}

In diesem Beispiel wird das Facade Pattern verwendet, um den Zugang zu Address zu abstrahieren. Dadurch wird der Code noch sauberer und das Gesetz von Demeter besser eingehalten.

Fazit

Das Gesetz von Demeter spielt eine zentrale Rolle beim Design wartbarer Software. Es fördert eine lose Kopplung und schützt vor übermäßigen Abhängigkeiten zwischen den verschiedenen Komponenten eines Systems. Diese lose Kopplung erleichtert es, einzelne Teile des Systems unabhängig voneinander zu ändern, ohne dass umfangreiche Anpassungen an anderen Teilen erforderlich sind. Dadurch wird die Wartbarkeit erheblich verbessert, da Änderungen an einem Modul keine weitreichenden Auswirkungen auf andere Module haben, was insbesondere in großen Softwareprojekten von entscheidender Bedeutung ist.

Ein weiterer Vorteil der Anwendung des Gesetzes von Demeter ist die Verbesserung der Testbarkeit des Codes. Wenn Objekte nur mit ihren direkten „Freunden“ kommunizieren, wird der Code modularer und einfacher zu isolieren, was bedeutet, dass jede Komponente unabhängig getestet werden kann. Dies trägt dazu bei, die Qualität der Software zu erhöhen, da Fehler schneller identifiziert und behoben werden können.

Jedoch ist die Anwendung des Gesetzes von Demeter nicht ohne Herausforderungen. Eine der Hauptnachteile ist, dass es oft zusätzliche Schnittstellen erfordert, um die Interaktionen zwischen den Objekten zu verwalten. Diese zusätzlichen Schnittstellen können den Code komplexer machen und dazu führen, dass mehr Klassen und Methoden benötigt werden, um die Kommunikation zu ermöglichen. Diese zusätzliche Komplexität kann insbesondere bei kleinen Projekten oder in einfacheren Szenarien als unnötig empfunden werden.

Ein weiterer potenzieller Nachteil ist der Performance-Overhead, der entstehen kann, wenn viele zusätzliche Methodenaufrufe oder Getter-Setter verwendet werden, um die Kapselung der Daten zu gewährleisten. In Performance-kritischen Anwendungen könnte dies zu einer geringfügigen Verlangsamung führen, da mehr Funktionalität über zusätzliche Ebenen von Abstraktionen und Schnittstellen zugänglich gemacht werden muss. Obwohl der Performance-Overhead in vielen modernen Anwendungen vernachlässigbar ist, kann er in hochoptimierten oder ressourcenintensiven Anwendungen problematisch sein.

Trotz dieser potenziellen Nachteile bleibt das Gesetz von Demeter ein wichtiger Baustein für sauberen, flexiblen und gut strukturierten Code. Es sorgt dafür, dass der Code langfristig leichter zu pflegen ist, da er auf Prinzipien der Kapselung und Modularität basiert, die auch in großen, komplexen Systemen gut skalierbar sind. Durch die Reduzierung der direkten Abhängigkeiten zwischen den Modulen wird die Gefahr von „spaghettifiziertem“ Code verringert, bei dem Änderungen in einem Teil des Systems unvorhergesehene Auswirkungen auf andere Teile haben.

In C++ ist die Anwendung des Gesetzes von Demeter besonders wichtig, da die Sprache eine sehr flexible und leistungsstarke Möglichkeit zur Manipulation von Daten und Objekten bietet. C++ erfordert jedoch auch ein gutes Verständnis der internen Speicherverwaltung und der Auswirkungen von Abstraktionen auf die Performance. Bei der Anwendung des Gesetzes in C++ ist es daher entscheidend, den Überblick zu behalten und sicherzustellen, dass der Code sowohl gut strukturiert als auch performant bleibt. Dies kann durch den gezielten Einsatz von Abstraktionen, die auf die jeweilige Problemstellung abgestimmt sind, sowie durch den bewussten Umgang mit Speicher- und Ressourcennutzung erreicht werden.

Insgesamt zeigt sich, dass das Gesetz von Demeter eine grundlegende Rolle beim Design von Software spielt, die nicht nur wartbar, sondern auch skalierbar und flexibel ist. Es fördert die Prinzipien der sauberen Architektur und der guten Softwarepraxis und hilft, Systeme zu entwickeln, die über längere Zeiträume hinweg problemlos gewartet und weiterentwickelt werden können. Der bewusste Einsatz dieses Prinzips in C++-Projekten erfordert zwar eine sorgfältige Planung, aber die langfristigen Vorteile überwiegen in den meisten Fällen die anfänglichen Herausforderungen.

Auch lesenswert: Design Pattern und Fünf Anforderungen von Bertrand Meyer

com

Newsletter Anmeldung

Bleiben Sie informiert! Wir informieren Sie über alle neuen Beiträge (max. 1 Mail pro Woche – versprochen)