Was ist das Prinzip der Abhängigkeitsinversion und warum ist es wichtig?
Was ist das Prinzip der Abhängigkeitsinversion und warum ist es wichtig?
Antworten:
Schauen Sie sich dieses Dokument an: Das Prinzip der Abhängigkeitsinversion .
Grundsätzlich heißt es:
Kurz gesagt, warum dies wichtig ist: Änderungen sind riskant. Indem Sie von einem Konzept anstelle einer Implementierung abhängen, reduzieren Sie den Änderungsbedarf an Anrufstandorten.
Das DIP reduziert effektiv die Kopplung zwischen verschiedenen Codeteilen. Die Idee ist, dass, obwohl es viele Möglichkeiten gibt, beispielsweise eine Protokollierungsfunktion zu implementieren, die Art und Weise, wie Sie sie verwenden würden, zeitlich relativ stabil sein sollte. Wenn Sie eine Schnittstelle extrahieren können, die das Konzept der Protokollierung darstellt, sollte diese Schnittstelle zeitlich viel stabiler sein als ihre Implementierung, und Anrufstandorte sollten weniger von Änderungen betroffen sein, die Sie vornehmen können, während Sie diesen Protokollierungsmechanismus beibehalten oder erweitern.
Indem Sie die Implementierung auch von einer Schnittstelle abhängig machen, können Sie zur Laufzeit auswählen, welche Implementierung für Ihre spezielle Umgebung besser geeignet ist. Je nach Fall kann dies auch interessant sein.
Die Bücher Agile Softwareentwicklung, Prinzipien, Muster und Praktiken sowie Agile Prinzipien, Muster und Praktiken in C # sind die besten Ressourcen, um die ursprünglichen Ziele und Motivationen hinter dem Prinzip der Abhängigkeitsinversion vollständig zu verstehen. Der Artikel "The Dependency Inversion Principle" ist ebenfalls eine gute Ressource, aber aufgrund der Tatsache, dass es sich um eine komprimierte Version eines Entwurfs handelt, der schließlich in die zuvor erwähnten Bücher eingegangen ist, lässt er einige wichtige Diskussionen über das Konzept von a aus Paket- und Schnittstellenbesitz, die der Schlüssel zur Unterscheidung dieses Prinzips von den allgemeineren Empfehlungen zum "Programmieren auf eine Schnittstelle, nicht auf eine Implementierung" sind, die im Buch Design Patterns (Gamma, et al.) enthalten sind.
Um eine Zusammenfassung zu liefern, geht es beim Prinzip der Abhängigkeitsinversion in erster Linie darum , die herkömmliche Richtung von Abhängigkeiten von Komponenten "höherer Ebene" zu Komponenten "niedrigerer Ebene" umzukehren, sodass Komponenten "niedrigerer Ebene" von den Schnittstellen abhängen, deren Eigentümer die Komponenten "höherer Ebene" sind . (Hinweis: Die Komponente "höhere Ebene" bezieht sich hier auf die Komponente, die externe Abhängigkeiten / Dienste erfordert, nicht unbedingt auf ihre konzeptionelle Position innerhalb einer geschichteten Architektur.) Dabei wird die Kopplung nicht so stark reduziert , sondern von Komponenten verschoben , die theoretisch sind weniger wertvoll für Komponenten, die theoretisch wertvoller sind.
Dies wird erreicht, indem Komponenten entworfen werden, deren externe Abhängigkeiten in Form einer Schnittstelle ausgedrückt werden, für die der Verbraucher der Komponente eine Implementierung bereitstellen muss. Mit anderen Worten, die definierten Schnittstellen drücken aus, was von der Komponente benötigt wird, nicht wie Sie die Komponente verwenden (z. B. "INeedSomething", nicht "IDoSomething").
Worauf sich das Prinzip der Abhängigkeitsinversion nicht bezieht, ist die einfache Praxis, Abhängigkeiten mithilfe von Schnittstellen zu abstrahieren (z. B. MyService → [ILogger ⇐ Logger]). Dadurch wird eine Komponente vom spezifischen Implementierungsdetail der Abhängigkeit entkoppelt, die Beziehung zwischen dem Verbraucher und der Abhängigkeit wird jedoch nicht umgekehrt (z. B. [MyService → IMyServiceLogger] ⇐ Logger).
Die Bedeutung des Abhängigkeitsinversionsprinzips kann auf das singuläre Ziel reduziert werden, Softwarekomponenten, die für einen Teil ihrer Funktionalität auf externen Abhängigkeiten beruhen (Protokollierung, Validierung usw.), wiederverwenden zu können.
Innerhalb dieses allgemeinen Ziels der Wiederverwendung können wir zwei Untertypen der Wiederverwendung abgrenzen:
Verwenden einer Softwarekomponente in mehreren Anwendungen mit Subabhängigkeitsimplementierungen (z. B. Sie haben einen DI-Container entwickelt und möchten die Protokollierung bereitstellen, möchten Ihren Container jedoch nicht an einen bestimmten Logger koppeln, sodass jeder, der Ihren Container verwendet, dies auch tun muss Verwenden Sie die von Ihnen gewählte Protokollierungsbibliothek.
Verwenden von Softwarekomponenten in einem sich entwickelnden Kontext (z. B. Sie haben Geschäftslogikkomponenten entwickelt, die in mehreren Versionen einer Anwendung, in denen sich die Implementierungsdetails weiterentwickeln, gleich bleiben).
Beim ersten Fall der Wiederverwendung von Komponenten über mehrere Anwendungen hinweg, z. B. mit einer Infrastrukturbibliothek, besteht das Ziel darin, Ihren Verbrauchern einen zentralen Infrastrukturbedarf zu bieten, ohne Ihre Verbraucher an Unterabhängigkeiten Ihrer eigenen Bibliothek zu koppeln, da die Übernahme von Abhängigkeiten von solchen Abhängigkeiten Ihre Anforderungen erfordert Verbraucher müssen die gleichen Abhängigkeiten auch verlangen. Dies kann problematisch sein, wenn Benutzer Ihrer Bibliothek eine andere Bibliothek für dieselben Infrastrukturanforderungen verwenden (z. B. NLog vs. log4net) oder wenn sie eine spätere Version der erforderlichen Bibliothek verwenden, die nicht abwärtskompatibel mit der Version ist von Ihrer Bibliothek benötigt.
Beim zweiten Fall der Wiederverwendung von Business-Logic-Komponenten (dh "übergeordneten Komponenten") besteht das Ziel darin, die Kerndomänenimplementierung Ihrer Anwendung von den sich ändernden Anforderungen Ihrer Implementierungsdetails (dh Ändern / Aktualisieren von Persistenzbibliotheken, Messaging-Bibliotheken) zu isolieren , Verschlüsselungsstrategien usw.). Im Idealfall sollte das Ändern der Implementierungsdetails einer Anwendung die Komponenten, die die Geschäftslogik der Anwendung kapseln, nicht beschädigen.
Hinweis: Einige lehnen es möglicherweise ab, diesen zweiten Fall als tatsächliche Wiederverwendung zu beschreiben, da Komponenten wie Geschäftslogikkomponenten, die in einer einzelnen sich entwickelnden Anwendung verwendet werden, nur eine einzige Verwendung darstellen. Die Idee hier ist jedoch, dass jede Änderung an den Implementierungsdetails der Anwendung einen neuen Kontext und damit einen anderen Anwendungsfall ergibt, obwohl die endgültigen Ziele als Isolation vs. Portabilität unterschieden werden könnten.
Während das Befolgen des Abhängigkeitsinversionsprinzips in diesem zweiten Fall einige Vorteile bieten kann, sollte beachtet werden, dass sein Wert, der auf moderne Sprachen wie Java und C # angewendet wird, stark reduziert ist, möglicherweise bis zu dem Punkt, dass er irrelevant ist. Wie bereits erwähnt, werden beim DIP Implementierungsdetails vollständig in separate Pakete aufgeteilt. Im Fall einer sich entwickelnden Anwendung wird jedoch durch die einfache Verwendung von Schnittstellen, die in Bezug auf die Geschäftsdomäne definiert sind, verhindert, dass übergeordnete Komponenten aufgrund sich ändernder Anforderungen an Implementierungsdetailkomponenten geändert werden müssen, selbst wenn sich die Implementierungsdetails letztendlich im selben Paket befinden . Dieser Teil des Prinzips spiegelt Aspekte wider, die für die Sprache relevant waren, als das Prinzip kodifiziert wurde (dh C ++) und die für neuere Sprachen nicht relevant sind. Das gesagt,
Eine längere Diskussion dieses Prinzips in Bezug auf die einfache Verwendung von Schnittstellen, Abhängigkeitsinjektion und dem Muster der getrennten Schnittstelle finden Sie hier . Zusätzlich wird eine Diskussion darüber , wie bezieht sich das Prinzip dynamisch typisierten Sprachen wie JavaScript können gefunden werden hier .
Wenn wir Softwareanwendungen entwerfen, können wir die Klassen auf niedriger Ebene als Klassen betrachten, die grundlegende und primäre Operationen implementieren (Festplattenzugriff, Netzwerkprotokolle, ...), und Klassen auf hoher Ebene die Klassen, die komplexe Logik (Geschäftsabläufe, ...) kapseln.
Die letzten stützen sich auf die unteren Klassen. Ein natürlicher Weg, solche Strukturen zu implementieren, wäre das Schreiben von Klassen auf niedriger Ebene und sobald wir sie haben, die komplexen Klassen auf hoher Ebene zu schreiben. Da hochrangige Klassen in Bezug auf andere definiert sind, scheint dies der logische Weg zu sein. Dies ist jedoch kein flexibles Design. Was passiert, wenn wir eine niedrige Klasse ersetzen müssen?
Das Abhängigkeitsinversionsprinzip besagt, dass:
Dieses Prinzip versucht, die herkömmliche Vorstellung "umzukehren", dass High-Level-Module in Software von den Low-Level-Modulen abhängen sollten. Hier besitzen übergeordnete Module die Abstraktion (z. B. die Entscheidung über die Methoden der Schnittstelle), die von untergeordneten Modulen implementiert werden. Dadurch werden Module niedrigerer Ebene von Modulen höherer Ebene abhängig.
Die gut angewendete Abhängigkeitsinversion bietet Flexibilität und Stabilität auf der Ebene der gesamten Architektur Ihrer Anwendung. Dadurch kann sich Ihre Anwendung sicherer und stabiler entwickeln.
Traditionell hing eine Benutzeroberfläche mit geschichteter Architektur von der Geschäftsschicht ab, und dies hing wiederum von der Datenzugriffsschicht ab.
Sie müssen die Ebene, das Paket oder die Bibliothek verstehen. Mal sehen, wie der Code wäre.
Wir hätten eine Bibliothek oder ein Paket für die Datenzugriffsschicht.
// DataAccessLayer.dll
public class ProductDAO {
}
Und eine andere Geschäftslogik der Bibliotheks- oder Paketschicht, die von der Datenzugriffsschicht abhängt.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private ProductDAO productDAO;
}
Die Abhängigkeitsinversion zeigt Folgendes an:
High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
Abstraktionen sollten nicht von Details abhängen. Details sollten von Abstraktionen abhängen.
Was sind die High-Level-Module und Low-Level-Module? Bei Modulen wie Bibliotheken oder Paketen handelt es sich bei Modulen auf hoher Ebene um Module, die traditionell Abhängigkeiten aufweisen und von denen sie auf niedriger Ebene abhängen.
Mit anderen Worten, auf hoher Ebene des Moduls wird die Aktion aufgerufen und auf niedriger Ebene wird die Aktion ausgeführt.
Eine vernünftige Schlussfolgerung aus diesem Prinzip ist, dass es keine Abhängigkeit zwischen Konkretionen geben sollte, sondern eine Abhängigkeit von einer Abstraktion. Aber nach dem Ansatz, den wir verfolgen, können wir die Abhängigkeit von Investitionen falsch anwenden, aber eine Abstraktion.
Stellen Sie sich vor, wir passen unseren Code wie folgt an:
Wir hätten eine Bibliothek oder ein Paket für die Datenzugriffsschicht, die die Abstraktion definieren.
// DataAccessLayer.dll
public interface IProductDAO
public class ProductDAO : IProductDAO{
}
Und eine andere Geschäftslogik der Bibliotheks- oder Paketschicht, die von der Datenzugriffsschicht abhängt.
// BusinessLogicLayer.dll
using DataAccessLayer;
public class ProductBO {
private IProductDAO productDAO;
}
Obwohl wir von einer Abstraktionsabhängigkeit abhängig sind, bleibt die Abhängigkeit zwischen Geschäft und Datenzugriff gleich.
Um eine Abhängigkeitsinversion zu erhalten, muss die Persistenzschnittstelle in dem Modul oder Paket definiert werden, in dem sich diese Logik oder Domäne auf hoher Ebene befindet, und nicht im Modul auf niedriger Ebene.
Definieren Sie zunächst, was die Domänenschicht ist, und die Abstraktion ihrer Kommunikation wird als Persistenz definiert.
// Domain.dll
public interface IProductRepository;
using DataAccessLayer;
public class ProductBO {
private IProductRepository productRepository;
}
Nachdem die Persistenzschicht von der Domäne abhängt, können Sie jetzt invertieren, wenn eine Abhängigkeit definiert ist.
// Persistence.dll
public class ProductDAO : IProductRepository{
}
(Quelle: xurxodev.com )
Es ist wichtig, das Konzept gut zu verarbeiten und den Zweck und die Vorteile zu vertiefen. Wenn wir mechanisch bleiben und das typische Fall-Repository kennenlernen, können wir nicht identifizieren, wo wir das Prinzip der Abhängigkeit anwenden können.
Aber warum invertieren wir eine Abhängigkeit? Was ist das Hauptziel über spezifische Beispiele hinaus?
Dies ermöglicht üblicherweise, dass sich die stabilsten Dinge, die nicht von weniger stabilen Dingen abhängig sind, häufiger ändern.
Es ist einfacher, den Persistenztyp zu ändern, entweder die Datenbank oder die Technologie, um auf dieselbe Datenbank zuzugreifen, als die Domänenlogik oder Aktionen, die für die Kommunikation mit der Persistenz ausgelegt sind. Aus diesem Grund ist die Abhängigkeit umgekehrt, da es einfacher ist, die Persistenz zu ändern, wenn diese Änderung auftritt. Auf diese Weise müssen wir die Domain nicht ändern. Die Domänenschicht ist die stabilste von allen, weshalb sie von nichts abhängen sollte.
Es gibt jedoch nicht nur dieses Repository-Beispiel. Es gibt viele Szenarien, in denen dieses Prinzip gilt, und es gibt Architekturen, die auf diesem Prinzip basieren.
Es gibt Architekturen, bei denen die Abhängigkeitsinversion der Schlüssel zu ihrer Definition ist. In allen Domänen ist es das Wichtigste und es sind Abstraktionen, die angeben, dass das Kommunikationsprotokoll zwischen der Domäne und den übrigen Paketen oder Bibliotheken definiert ist.
In der sauberen Architektur befindet sich die Domäne in der Mitte. Wenn Sie in Richtung der Pfeile schauen, die die Abhängigkeit anzeigen, ist klar, welche Ebenen am wichtigsten und stabilsten sind. Die äußeren Schichten gelten als instabile Werkzeuge. Vermeiden Sie daher, von ihnen abhängig zu sein.
(Quelle: 8thlight.com )
Genauso verhält es sich mit der hexagonalen Architektur, bei der sich die Domäne ebenfalls im zentralen Teil befindet und Ports Abstraktionen der Kommunikation vom Domino nach außen sind. Auch hier ist offensichtlich, dass die Domäne am stabilsten ist und die traditionelle Abhängigkeit invertiert ist.
Für mich ist das im offiziellen Artikel beschriebene Prinzip der Abhängigkeitsinversion ein fehlgeleiteter Versuch, die Wiederverwendbarkeit von Modulen zu verbessern, die von Natur aus weniger wiederverwendbar sind, sowie eine Möglichkeit, ein Problem in der C ++ - Sprache zu umgehen.
Das Problem in C ++ ist, dass Header-Dateien normalerweise Deklarationen von privaten Feldern und Methoden enthalten. Wenn ein C ++ - Modul auf hoher Ebene die Header-Datei für ein Modul auf niedriger Ebene enthält, hängt dies von den tatsächlichen Implementierungsdetails dieses Moduls ab. Und das ist natürlich keine gute Sache. In den heute gebräuchlicheren modernen Sprachen ist dies jedoch kein Problem.
High-Level-Module sind von Natur aus weniger wiederverwendbar als Low-Level-Module, da erstere normalerweise anwendungs- / kontextspezifischer sind als letztere. Beispielsweise ist eine Komponente, die einen UI-Bildschirm implementiert, von höchster Ebene und auch sehr (vollständig?) Anwendungsspezifisch. Der Versuch, eine solche Komponente in einer anderen Anwendung wiederzuverwenden, ist kontraproduktiv und kann nur zu einer Überentwicklung führen.
Die Erstellung einer separaten Abstraktion auf derselben Ebene einer Komponente A, die von einer Komponente B abhängt (die nicht von A abhängt), kann daher nur durchgeführt werden, wenn Komponente A für die Wiederverwendung in verschiedenen Anwendungen oder Kontexten wirklich nützlich ist. Wenn dies nicht der Fall ist, wäre die Anwendung von DIP ein schlechtes Design.
Grundsätzlich heißt es:
Die Klasse sollte von Abstraktionen (z. B. Schnittstelle, abstrakte Klassen) abhängen, nicht von bestimmten Details (Implementierungen).
Gute Antworten und gute Beispiele geben hier bereits andere.
Der Grund, warum DIP wichtig ist, liegt darin, dass es das OO-Prinzip "lose gekoppeltes Design" gewährleistet.
Die Objekte in Ihrer Software sollten NICHT in eine Hierarchie geraten, in der einige Objekte die Objekte der obersten Ebene sind, abhängig von Objekten der unteren Ebene. Änderungen an Objekten auf niedriger Ebene werden dann auf Ihre Objekte auf oberster Ebene übertragen, wodurch die Software für Änderungen sehr anfällig ist.
Sie möchten, dass Ihre Objekte der obersten Ebene sehr stabil und nicht anfällig für Änderungen sind. Daher müssen Sie die Abhängigkeiten invertieren.
Eine viel klarere Möglichkeit, das Prinzip der Abhängigkeitsinversion anzugeben, ist:
Ihre Module, die komplexe Geschäftslogik kapseln, sollten nicht direkt von anderen Modulen abhängen, die Geschäftslogik kapseln. Stattdessen sollten sie nur von Schnittstellen zu einfachen Daten abhängen.
Das heißt, anstatt Ihre Klasse Logic
wie gewöhnlich zu implementieren :
class Dependency { ... }
class Logic {
private Dependency dep;
int doSomething() {
// Business logic using dep here
}
}
Sie sollten etwas tun wie:
class Dependency { ... }
interface Data { ... }
class DataFromDependency implements Data {
private Dependency dep;
...
}
class Logic {
int doSomething(Data data) {
// compute something with data
}
}
Data
und DataFromDependency
sollte im selben Modul leben wie Logic
, nicht mit Dependency
.
Warum das?
Dependency
Änderungen müssen Sie nicht ändern Logic
.Logic
funktioniert, ist eine viel einfachere Aufgabe: Es funktioniert nur mit einem ADT.Logic
kann jetzt einfacher getestet werden. Sie können jetzt direkt Data
mit gefälschten Daten instanziieren und diese weitergeben. Keine Notwendigkeit für Verspottungen oder komplexe Testgerüste.DataFromDependency
sich das Dependency
Modul , auf das direkt verwiesen wird , im selben Modul wie Logic
, Logic
hängt das Dependency
Modul zur Kompilierungszeit immer noch direkt vom Modul ab. Laut Onkel Bobs Erklärung des Prinzips ist es der springende Punkt von DIP, dies zu vermeiden. Um DIP zu folgen, Data
sollte es sich im selben Modul wie Logic
, aber DataFromDependency
im selben Modul wie befinden Dependency
.
Inversion of Control (IoC) ist ein Entwurfsmuster, bei dem einem Objekt seine Abhängigkeit von einem externen Framework übergeben wird, anstatt ein Framework nach seiner Abhängigkeit zu fragen.
Pseudocode-Beispiel mit traditioneller Suche:
class Service {
Database database;
init() {
database = FrameworkSingleton.getService("database");
}
}
Ähnlicher Code mit IoC:
class Service {
Database database;
init(database) {
this.database = database;
}
}
Die Vorteile von IoC sind:
Der Punkt der Abhängigkeitsinversion besteht darin, wiederverwendbare Software zu erstellen.
Die Idee ist, dass anstelle von zwei aufeinander stehenden Codeteilen eine abstrahierte Schnittstelle verwendet wird. Dann können Sie jedes Stück ohne das andere wiederverwenden.
Dies wird am häufigsten durch einen IoC-Container (Inversion of Control) wie Spring in Java erreicht. In diesem Modell werden Eigenschaften von Objekten über eine XML-Konfiguration eingerichtet, anstatt dass die Objekte ausgehen und ihre Abhängigkeit ermitteln.
Stellen Sie sich diesen Pseudocode vor ...
public class MyClass
{
public Service myService = ServiceLocator.service;
}
MyClass hängt direkt sowohl von der Service-Klasse als auch von der ServiceLocator-Klasse ab. Es benötigt beide, wenn Sie es in einer anderen Anwendung verwenden möchten. Stellen Sie sich das vor ...
public class MyClass
{
public IService myService;
}
Jetzt basiert MyClass auf einer einzigen Schnittstelle, der IService-Schnittstelle. Wir würden den IoC-Container tatsächlich den Wert dieser Variablen festlegen lassen.
So kann MyClass jetzt problemlos in anderen Projekten wiederverwendet werden, ohne die Abhängigkeit dieser beiden anderen Klassen mit sich zu bringen.
Noch besser ist, dass Sie die Abhängigkeiten von MyService und die Abhängigkeiten dieser Abhängigkeiten nicht ziehen müssen, und ... nun, Sie haben die Idee.
Wenn wir davon ausgehen können, dass ein "hochrangiger" Mitarbeiter eines Unternehmens für die Ausführung seiner Pläne bezahlt wird und dass diese Pläne durch die Gesamtausführung vieler Pläne eines "niedrigen" Mitarbeiters geliefert werden, dann könnten wir sagen Es ist im Allgemeinen ein schrecklicher Plan, wenn die Planbeschreibung des hochrangigen Mitarbeiters in irgendeiner Weise an den spezifischen Plan eines untergeordneten Mitarbeiters gekoppelt ist.
Wenn eine hochrangige Führungskraft einen Plan zur "Verbesserung der Lieferzeit" hat und angibt, dass ein Mitarbeiter in der Reederei jeden Morgen Kaffee trinken und sich dehnen muss, ist dieser Plan eng gekoppelt und weist einen geringen Zusammenhalt auf. Wenn der Plan jedoch keinen bestimmten Mitarbeiter erwähnt und lediglich erfordert, dass "eine Einheit, die Arbeiten ausführen kann, bereit ist zu arbeiten", ist der Plan lose gekoppelt und kohärenter: Die Pläne überschneiden sich nicht und können leicht ersetzt werden . Auftragnehmer oder Roboter können die Mitarbeiter problemlos ersetzen, und der Plan der hohen Ebene bleibt unverändert.
"High Level" im Abhängigkeitsinversionsprinzip bedeutet "wichtiger".
Ich kann sehen, dass in den obigen Antworten eine gute Erklärung gegeben wurde. Ich möchte jedoch eine einfache Erklärung mit einem einfachen Beispiel geben.
Das Prinzip der Abhängigkeitsinversion ermöglicht es dem Programmierer, die fest codierten Abhängigkeiten zu entfernen, so dass die Anwendung lose gekoppelt und erweiterbar wird.
Wie man das erreicht: durch Abstraktion
Ohne Abhängigkeitsinversion:
class Student {
private Address address;
public Student() {
this.address = new Address();
}
}
class Address{
private String perminentAddress;
private String currentAdrress;
public Address() {
}
}
Im obigen Code-Snippet ist das Adressobjekt fest codiert. Stattdessen, wenn wir die Abhängigkeitsinversion verwenden und das Adressobjekt durch Übergabe der Konstruktor- oder Setter-Methode einfügen können. Mal schauen.
Mit Abhängigkeitsinversion:
class Student{
private Address address;
public Student(Address address) {
this.address = address;
}
//or
public void setAddress(Address address) {
this.address = address;
}
}
Abhängigkeitsinversion: Abhängig von Abstraktionen, nicht von Konkretionen.
Umkehrung der Kontrolle: Main vs Abstraction und wie der Main der Klebstoff der Systeme ist.
Dies sind einige gute Beiträge, die darüber sprechen:
https://coderstower.com/2019/03/26/dependency-inversion-why-you-shouldnt-avoid-it/
https://coderstower.com/2019/04/02/main-and-abstraction-the-decoupled-peers/
https://coderstower.com/2019/04/09/inversion-of-control-putting-all-together/
Nehmen wir an, wir haben zwei Klassen: Engineer
und Programmer
:
Class Engineer ist abhängig von der Programmer-Klasse.
class Engineer () {
fun startWork(programmer: Programmer){
programmer.work()
}
}
class Programmer {
fun work(){
//TODO Do some work here!
}
}
In diesem Beispiel ist die Klasse Engineer
von unserer Programmer
Klasse abhängig . Was passiert, wenn ich das ändern muss Programmer
?
Natürlich muss ich das auch ändern Engineer
. (Wow, an dieser Stelle OCP
wird auch verletzt)
Was müssen wir dann tun, um dieses Chaos zu beseitigen? Die Antwort ist eigentlich Abstraktion. Durch Abstraktion können wir die Abhängigkeit zwischen diesen beiden Klassen entfernen. Zum Beispiel kann ich eine Interface
für die Programmer-Klasse erstellen, und von nun an muss jede Klasse, die die verwenden möchte Programmer
, ihre verwenden Interface
. Durch Ändern der Programmer-Klasse müssen wir dann keine Klassen ändern, die sie verwendet haben. Aufgrund der Abstraktion, die wir verwenden gebraucht.
Hinweis: DependencyInjection
Kann uns dabei helfen DIP
und SRP
auch.
Zusätzlich zu den vielen allgemein guten Antworten möchte ich eine kleine eigene Stichprobe hinzufügen, um gute und schlechte Praktiken zu demonstrieren. Und ja, ich bin nicht einer, der Steine wirft!
Angenommen, Sie möchten, dass ein kleines Programm einen String über die Konsolen-E / A in das Base64-Format konvertiert . Hier ist der naive Ansatz:
class Program
{
static void Main(string[] args)
{
/*
* BadEncoder: High-level class *contains* low-level I/O functionality.
* Hence, you'll have to fiddle with BadEncoder whenever you want to change
* the I/O mode or details. Not good. A good encoder should be I/O-agnostic --
* problems with I/O shouldn't break the encoder!
*/
BadEncoder.Run();
}
}
public static class BadEncoder
{
public static void Run()
{
Console.WriteLine(Convert.ToBase64String(Encoding.UTF8.GetBytes(Console.ReadLine())));
}
}
Das DIP besagt grundsätzlich, dass Komponenten auf hoher Ebene nicht von einer Implementierung auf niedriger Ebene abhängig sein sollten, wobei "Ebene" die Entfernung von E / A gemäß Robert C. Martin ("Saubere Architektur") ist. Aber wie kommst du aus dieser Situation heraus? Einfach indem Sie den zentralen Encoder nur von Schnittstellen abhängig machen, ohne sich darum zu kümmern, wie diese implementiert werden:
class Program
{
static void Main(string[] args)
{
/* Demo of the Dependency Inversion Principle (= "High-level functionality
* should not depend upon low-level implementations"):
* You can easily implement new I/O methods like
* ConsoleReader, ConsoleWriter without ever touching the high-level
* Encoder class!!!
*/
GoodEncoder.Run(new ConsoleReader(), new ConsoleWriter()); }
}
public static class GoodEncoder
{
public static void Run(IReadable input, IWriteable output)
{
output.WriteOutput(Convert.ToBase64String(Encoding.ASCII.GetBytes(input.ReadInput())));
}
}
public interface IReadable
{
string ReadInput();
}
public interface IWriteable
{
void WriteOutput(string txt);
}
public class ConsoleReader : IReadable
{
public string ReadInput()
{
return Console.ReadLine();
}
}
public class ConsoleWriter : IWriteable
{
public void WriteOutput(string txt)
{
Console.WriteLine(txt);
}
}
Beachten Sie, dass Sie nicht berühren GoodEncoder
müssen, um den E / A-Modus zu ändern. Diese Klasse ist mit den ihr bekannten E / A-Schnittstellen zufrieden. jede Low-Level-Implementierung von IReadable
und IWriteable
wird es nie stören.
GoodEncoder
in Ihrem zweiten Beispiel nicht davon ab . Um ein DIP-Beispiel zu erstellen, müssen Sie eine Vorstellung davon einführen, was die hier extrahierten Schnittstellen "besitzt" - und sie insbesondere in dasselbe Paket wie den GoodEncoder einfügen, während ihre Implementierungen außerhalb bleiben.
Das Dependency Inversion Principle (DIP) besagt dies
i) High-Level-Module sollten nicht von Low-Level-Modulen abhängen. Beides sollte von Abstraktionen abhängen.
ii) Abstraktionen sollten niemals von Details abhängen. Details sollten von Abstraktionen abhängen.
Beispiel:
public interface ICustomer
{
string GetCustomerNameById(int id);
}
public class Customer : ICustomer
{
//ctor
public Customer(){}
public string GetCustomerNameById(int id)
{
return "Dummy Customer Name";
}
}
public class CustomerFactory
{
public static ICustomer GetCustomerData()
{
return new Customer();
}
}
public class CustomerBLL
{
ICustomer _customer;
public CustomerBLL()
{
_customer = CustomerFactory.GetCustomerData();
}
public string GetCustomerNameById(int id)
{
return _customer.GetCustomerNameById(id);
}
}
public class Program
{
static void Main()
{
CustomerBLL customerBLL = new CustomerBLL();
int customerId = 25;
string customerName = customerBLL.GetCustomerNameById(customerId);
Console.WriteLine(customerName);
Console.ReadKey();
}
}
Hinweis: Die Klasse sollte von Abstraktionen wie Schnittstelle oder abstrakten Klassen abhängen, nicht von bestimmten Details (Implementierung der Schnittstelle).