Dependency Inversion Principle (DIP)

Dependency Inversion Principle (DIP) – Wissenshäppchen #7

Im siebten Wissenshäppchen geht es um das Dependency Inversion Principle.

Probeabo bei Audible (Affiliate)

Inhalt

Das DIP ist das letzte der fünf SOLID-Prinzipien.

High level modules should not depend upon low level modules. Both should depend upon abstractions.

Oder:

Abstractions should not depend upon details. Details should depend upon abstractions.

Welche Abhängigkeiten werden hier „umgedreht“? Komponenten, die andere Komponenten benötigen, erzeugen sich diese nicht selbst, sondern bekommen sie von außen hineingegeben. Braucht ein Service beispielsweise ein Repository um arbeiten zu können, erzeugt er es sich nicht selbst, z.B. mit this.repo = new Repository() in seinem Konstruktor, sondern bekommt es stattdessen schon fertig als Parameter übergeben, z.B. public Service(Repository repo). Das heißt, obwohl die Abhängigkeit vom Service zum Repository geht, wird sie „umgekehrt“ aufgelöst, indem das Repository dem Service übergeben wird.

Erklärung

  • Wenn Komponenten sich ihre Abhängigkeiten selbst erzeugen, sind diese später nur umständlich oder gar nicht mehr austauschbar.
  • Tests von Komponenten, die sich ihre Abhängigkeiten selbst erzeugen, sind schwierig/unmöglich/langsam/fehleranfällig, da die Abhängigkeiten nicht so einfach gegen Fake-Objekte ausgetauscht werden können.
  • Die Abhängigkeiten sind nicht explizit in der API sichtbar. Der obige Service „braucht“ ein Repository, aber das sieht man nur, wenn man sich den Quelltext von Service anschaut. Beim Erzeugen eines Services ist dies nicht offensichtlich, weil er sich das Repository „heimlich“ selbst erzeugt.

Beispiel

Um alle Employees zu bezahlen, erzeugt sich die Payroll ihr eigenes MySqlEmployeeRepository. Dadurch wird auch im Test die echte Datenbank verwendet und ein Austauschen gegen eine Oracle-Datenbank erfordert eine Anpassung der Klasse Payroll (vgl. Open Closed Principle).

class Payroll
    def initialize
        @repo = MySqlEmployeeRepository.new
    end

    def pay_employees
        @repo.find_all.each { |employee| employee.pay }
    end
end

class MySqlEmployeeRepository
    def find_all
        puts "Accessing the database and reading all employees"
        [Employee.new] * 10
    end
end

class Employee
    def pay
        puts "Thank you for paying me"
    end
end

Payroll.new.pay_employees

# Accessing the database and reading all employees 
# Thank you for paying me
# ...
# Thank you for paying me

Statt dieser „harten“ und versteckten Abhängigkeit sollte Payroll sich lieber die Implementierung eines Repositorys liefern lassen. Das erfordert nur eine kleine Umstrukturierung des Codes und ermöglicht deutlich einfachere Tests.

class Payroll
    def initialize(repo)
        @repo = repo
    end

    def pay_employees
        @repo.find_all.each { |employee| employee.pay }
    end
end

class MySqlEmployeeRepository
    def find_all
        puts "Accessing the database and reading all employees"
        [Employee.new] * 10
    end
end

class FakeEmployeeRepository
    def find_all
        [Employee.new] * 1
    end
end

class Employee
    def pay
        puts "Thank you for paying me"
    end
end

puts "Production:"
Payroll.new(MySqlEmployeeRepository.new).pay_employees

puts "Test:"
Payroll.new(FakeEmployeeRepository.new).pay_employees

# Production:
# Accessing the database and reading all employees
# Thank you for paying me
# ...
# Thank you for paying me
# Test:
# Thank you for paying me

Außerdem kann Payroll nun auch ohne Anpassungen (!) mit einer Oracle-Datenbank arbeiten.

class OracleEmployeeRepository
    def find_all
        puts "Reading employees from Oracle database"
        [Employee.new] * 100
    end
end

puts "Using Oracle:"
Payroll.new(OracleEmployeeRepository.new).pay_employees

# Reading employees from Oracle database
# Thank you for paying me
# ...
# Thank you for paying me

Vorteile

  • Abhängigkeiten sind explizit sichtbar und können bei geänderten Anforderungen oder in Tests einfach ausgetauscht werden.
  • Neue Funktionalitäten können dem Programm einfacher hinzugefügt werden, da durch die Abhängigkeit von Abstraktionen oftmals auch das OCP umgesetzt wird.

Nachteile

  • Ein Problem könnte wieder ein „Overengineering“ sein. Es ist wahrscheinlich nicht sinnvoll, jede Kleinigkeit hinter einer Abstraktion zu verbergen. Aber wie entscheidet man, welche Abstraktion sinnvoll ist?
  • Bei vielen Abhängigkeiten wird das Erstellen von Objekten (und damit auch deren Test) schwierig, weil viel Arbeit vorab erledigt werden muss. Hier könnte dann ein DI-Container zum Einsatz kommen.

Literaturempfehlungen

Wie sollte es anders sein: Auch für das letzte SOLID-Prinzip empfehle ich noch einmal Clean Code* von Uncle Bob.

Robert C. Martin - Clean Code: A Handbook of Agile Software Craftsmanship (Affiliate)*

Links

Probeabo bei Audible (Affiliate)

Navigation der Serie<< Interface Segregation Principle (ISP) – Wissenshäppchen #6

Schreibe einen Kommentar

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

To create code blocks or other preformatted text, indent by four spaces:

    This will be displayed in a monospaced font. The first four 
    spaces will be stripped off, but all other whitespace
    will be preserved.
    
    Markdown is turned off in code blocks:
     [This is not a link](http://example.com)

To create not a block, but an inline code span, use backticks:

Here is some inline `code`.

For more help see http://daringfireball.net/projects/markdown/syntax