Das Dependency Inversion Principle

Definition

Das Dependency Inversion Principle (Abhängigkeits-Umkehrungs-Prinzip, DIP) ist eines der SOLID-Prinzipien. Es sagt aus:

  1. „High level modules should not depend upon low level modules. Both should depend upon abstractions.“
  2. „Abstractions should not depend upon details. Details should depend upon abstractions.“
  1. „Höherstufige Module sollten nicht von niedrigstufigen Modulen abhängig sein. Beide sollten von Abstraktionen abhängen.“
  2. „Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.“

Beschreibung

Beim erstellen von Software Entwürfen sollte man stets die Auswirkungen auf die höherstufigen (highlevel) Module beachten, die von niedrigstufigen (lowlevel) Modulen abhängig sind. Die höherstufigen Module enthalten die konkreten Abläufe und die Geschäftslogik. Sie bilden die Identität einer Anwendung. Sind diese Module jedoch direkt von niedrigstufigen Modulen abhängig, kann es direkte Auswirkungen auf sie haben. Ändert sich etwas auf der unteren Ebene oder muss ein niedrigstufiges Modul ersetzt werden, so ist man gezwungen das höherstufiges Modul zu ändern. Je nach Komplexität des Codes kann eine Änderung auch zu unbeabsichtigten Fehlern führen.

Um solche Probleme zu vermeiden und den Entwurf flexibler zu gestallten, kann man zwischen den niedrig- und höherstufigen Modulen eine neue Abstraktionsebene einführen. Die höherstufigen Module werden durch die Verwendung der Abstraktionsebene von den niedrigstufigen Modulen entkoppelt, welche im Gegenzug durch die Implementierung der Abstraktion gleichzeitig eine Schnittstelle für den Zugriff auf die Module zur Verfügung stellen.

Beispiel

Als Beispiel schauen wir uns die Verwendung eines Schalters und einer Lampe an. Der Schalter soll die Lampe ein- und ausschalten können. Dabei spielt es keine Rolle, welcher Mechanismus hinter dem Schalter steckt. Es könnte ein Icon in einer GUI sein, ein physischer Schalter oder ein Bewegungsmelder. Der Schalter stell nur fest, ob er aktiviert oder deaktiviert wurde. Ebenso spielt die konkrete Ausprägung bei der Lampe keine Rolle. Sie soll nur beim Aufruf der TurnOn() Methode eingeschaltet, und beim Aufruf der TurnOff() Methode ausgeschaltet werden.

Eine Implementierung könnte wie folgt aussehen:

public class Lamp
{
     public void TrunOn() { ... }
     public void TrunOff() { ... }
}

public class Button
{
     private Lamp _lamp;

     public Button(Lamp lamp)
     {
          _lamp = lamp
     }

     public void Detect()
     {
          bool buttonOn = GetState();

          if (buttonOn == true)
               _lamp.TurnOn();
          else
               _lamp.TurnOff();
     }
}

Die Implementierung zeigt deutlich, dass die Button Klasse direkt von der Lamp Klasse abhängig ist. Diese Abhängigkeit bedeutet, dass die Button Klasse geändert werden muss, oder zumindest neu kompiliert, wenn sich die Lamp Klasse ändert. Darüber hinaus kann man die Button Klasse auch nicht wieder verwenden, um z.B. einen Motor zu starten.

In dieser Implementierung wird das Abhängigkeits-Umkehrungs-Prinzip eindeutig verletzt. Hier ist das höherstufige Modul nicht von dem niedrigstufigen Modul getrennt, so dass automatisch eine Abhängigkeit erzeugt wird.

Das Finden der zugrunde liegenden Abstraktion

Die Abstraktionen, die einer Anwendung zugrunde liegen, sind Abläufe die gleich bleiben, wenn sich die Details ändern. In dem Schalter/Lampe ist die zugrundeliegende Abstraktion das Feststellen des Ein/Aus Zustands des Schalters und das weiterleiten dieses Zustands an das Zielobjekt. Welcher Mechanismus den Zustand des Schalters feststellt und was das eigentliche Zielobjekt ist, ist hierbei unwichtig.

Um den Abhängigkeits-Umkehrungs-Prinzip zu entsprechen, müssen wir die Abstraktion von den Details trennen. Dann müssen wir die Abhängigkeiten so umleiten, dass die Details von den Abstraktionen abhängen. Die Implementierung sieht dann wie folgt aus:

public abstract class ButtonClient
{
     public abstract void TrunOn();
     public abstract void TrunOff();
}

public class Lamp : ButtonClient
{
     public override void TrunOn() { ... }
     public override void TrunOff() { ... }
}

public abstract class AbstractButton
{
     private ButtonClient _buttonClient;

     public AbstractButton(ButtonClient buttonClient)
     {
          _buttonClient = buttonClient
     }

     public void Detect()
     {
          bool buttonOn = GetState();

          if (buttonOn == true)
               _buttonClient.TurnOn();
          else
               _buttonClient.TurnOff();
     }

     public bool abstract GetState();
}

public class Button : AbstractButton
{
     public Button(ButtonClient buttonClient)
          : base(buttonClient)
     {
     }

     public bool override GetState() { ... }
}

Die Geschäftslogik ist jetzt in der AbstractButton Klasse gekapselt. Die AbstractButton Klasse weiß nichts von dem Mechanisums der den Ein/Aus Zustand ermittelt oder der Lampe. Diese Details sind in der Button und Lamp Klasse implementiert.

Somit ist die Geschäftslogik mit jeder Art von Schaltern wieder verwendbar, und kann jedes Gerät kontrollieren, dass es benötigt. Darüber hinaus wird es nicht von den Änderungen an den niedrigstufigen Modulen beeinflusst. Damit ist der Entwurf robust, flexibel und wiederverwendbar.

Fazit

Das Abhängigkeits-Umkehrungs-Prinzip ist die Quelle vieler Vorteile, die die objektorientierte Programmierung bietet. Es ist notwendig für die Erstellung von wiederverwendbaren Frameworks. Es ist von entscheidender Bedeutung für den Aufbau eines Codes, der flexibel geändert werden kann. Und da die Abstraktionen und die Details alle voneinander isoliert sind, ist der Code auch viel leichter zu pflegen. Dieses Prinzips sollte jedoch nicht blind für jedes Modul oder Klasse verwendet werden. Bei Funktionalitäten, bei denen eine künftige Änderung eher unwahrscheinlich ist, ist die Anwendung dieses Prinzips nicht nötig.

Quellen

Schreibe einen Kommentar

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.