Wenn Sie mit objektorientierter Programmierung vertraut sind oder gerade erst anfangen, sich damit auseinanderzusetzen, sind Sie wahrscheinlich schon auf das Akronym SOLID gestoßen. SOLID stellt eine Reihe von Prinzipien dar, die Entwicklern dabei helfen sollen, sauberen, wartbaren und skalierbaren Code zu schreiben. In diesem Artikel konzentrieren wir uns auf das „D“ in SOLID, das für das Dependency Inversion Principle steht.
Aber bevor wir uns mit den Details befassen, nehmen wir uns zunächst einen Moment Zeit, um das „Warum“ hinter diesen Prinzipien zu verstehen.
Bei der objektorientierten Programmierung unterteilen wir unsere Anwendungen normalerweise in Klassen, von denen jede eine spezifische Geschäftslogik kapselt und mit anderen Klassen interagiert. Stellen Sie sich zum Beispiel einen einfachen Online-Shop vor, in dem Benutzer Produkte in ihren Warenkorb legen können. Dieses Szenario könnte so modelliert werden, dass mehrere Klassen zusammenarbeiten, um den Geschäftsbetrieb zu verwalten. Betrachten wir dieses Beispiel als Grundlage, um zu untersuchen, wie das Abhängigkeitsinversionsprinzip das Design unseres Systems verbessern kann.
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
Wie wir sehen können, sind Abhängigkeiten wie OrderService und ProductService innerhalb des Klassenkonstruktors eng miteinander verbunden. Diese direkte Abhängigkeit macht es schwierig, diese Komponenten zu ersetzen oder zu verspotten, was beim Testen oder Austauschen von Implementierungen eine Herausforderung darstellt.
Das Dependency Injection (DI)-Muster bietet eine Lösung für dieses Problem. Indem wir dem DI-Muster folgen, können wir diese Abhängigkeiten entkoppeln und unseren Code flexibler und testbarer machen. So können wir den Code umgestalten, um DI zu implementieren:
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
Wir übergeben Abhängigkeiten explizit an den Konstruktor jedes Dienstes, was zwar ein Schritt in die richtige Richtung ist, aber dennoch zu eng gekoppelten Klassen führt. Dieser Ansatz verbessert die Flexibilität zwar geringfügig, geht aber nicht vollständig auf das zugrunde liegende Problem ein, unseren Code modularer und einfacher testbar zu machen.
Das Dependency Inversion Principle (DiP) geht noch einen Schritt weiter, indem es die entscheidende Frage beantwortet: Was sollen wir bestehen? Das Prinzip legt nahe, dass wir statt konkreter Implementierungen nur die notwendigen Abstraktionen übergeben sollten – insbesondere Abhängigkeiten, die der erwarteten Schnittstelle entsprechen.
Betrachten Sie beispielsweise die Klasse ProductService mit einer Methode getProducts, die ein Array von Produkten zurückgibt. Anstatt ProductService direkt an eine bestimmte Implementierung zu koppeln (z. B. Daten aus einer Datenbank abzurufen), könnten wir es auf verschiedene Arten implementieren. Eine Implementierung ruft möglicherweise Produkte aus einer Datenbank ab, während eine andere möglicherweise ein hartcodiertes JSON-Objekt zum Testen zurückgibt. Der Schlüssel liegt darin, dass beide Implementierungen dieselbe Schnittstelle nutzen, was Flexibilität und Austauschbarkeit gewährleistet.
Um dieses Prinzip in die Praxis umzusetzen, verlassen wir uns oft auf ein Muster namens Inversion of Control (IoC). IoC ist eine Technik, bei der die Kontrolle über die Erstellung und Verwaltung von Abhängigkeiten von der Klasse selbst auf eine externe Komponente übertragen wird. Dies wird typischerweise durch einen Dependency Injection-Container oder einen Service Locator implementiert, der als Registrierung fungiert, von der aus wir die erforderlichen Abhängigkeiten anfordern können. Mit IoC können wir die entsprechenden Abhängigkeiten dynamisch einfügen, ohne sie fest in die Klassenkonstruktoren zu codieren, wodurch das System modularer und einfacher zu warten ist.
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
Wie wir sehen können, werden Abhängigkeiten im Container registriert, sodass sie bei Bedarf ersetzt oder ausgetauscht werden können. Diese Flexibilität ist ein entscheidender Vorteil, da sie eine lockere Kopplung zwischen Komponenten fördert.
Dieser Ansatz hat jedoch einige Nachteile. Da Abhängigkeiten zur Laufzeit aufgelöst werden, kann es zu Laufzeitfehlern kommen, wenn etwas schief geht (z. B. wenn eine Abhängigkeit fehlt oder inkompatibel ist). Darüber hinaus gibt es keine Garantie dafür, dass die registrierte Abhängigkeit genau der erwarteten Schnittstelle entspricht, was zu subtilen Problemen führen kann. Diese Methode der Abhängigkeitsauflösung wird oft als Service-Locator-Muster bezeichnet und wird in vielen Fällen als Anti-Muster angesehen, da sie auf der Laufzeitauflösung basiert und Abhängigkeiten möglicherweise verschleiert.
Eine der beliebtesten Bibliotheken in JavaScript zur Implementierung des Inversion of Control (IoC)-Musters ist InversifyJS. Es bietet ein robustes und flexibles Framework für die saubere, modulare Verwaltung von Abhängigkeiten. Allerdings hat InversifyJS einige Nachteile. Eine wesentliche Einschränkung ist die Menge an Boilerplate-Code, die zum Einrichten und Verwalten von Abhängigkeiten erforderlich ist. Darüber hinaus ist es häufig erforderlich, Ihre Bewerbung auf eine bestimmte Weise zu strukturieren, was möglicherweise nicht für jedes Projekt geeignet ist.
Eine Alternative zu InversifyJS ist Friendly-DI, ein leichter und schlankerer Ansatz für die Verwaltung von Abhängigkeiten in JavaScript- und TypeScript-Anwendungen. Es ist von den DI-Systemen in Frameworks wie Angular und NestJS inspiriert, ist aber minimalistischer und weniger ausführlich gestaltet.
Zu den wichtigsten Vorteilen von Friendly-DI gehören:
Es ist jedoch wichtig zu beachten, dass Friendly-DI speziell für TypeScript entwickelt wurde und Sie seine Abhängigkeiten installieren müssen, bevor Sie es verwenden können.
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
Und erweitern Sie auch tsconfig.json:
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
Das obige Beispiel kann mit Friendly-DI geändert werden:
class ServiceLocator { static #modules = new Map(); static get(moduleName: string) { return ServiceLocator.#modules.get(moduleName); } static set(moduleName: string, exp: never) { ServiceLocator.#modules.set(moduleName, exp); } } class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { const ProductService = ServiceLocator.get('ProductService'); this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { const OrderService = ServiceLocator.get('OrderService'); this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } } ServiceLocator.set('ProductService', ProductService); ServiceLocator.set('OrderService', OrderService); new UserService();
Wie wir sehen können, haben wir den Dekorator @Injectable() hinzugefügt, der unsere Klassen als injizierbar markiert und signalisiert, dass sie Teil des Abhängigkeitsinjektionssystems sind. Durch diesen Dekorator weiß der DI-Container, dass diese Klassen bei Bedarf instanziiert und eingefügt werden können.
Wenn wir eine Klasse als Abhängigkeit in einem Konstruktor deklarieren, binden wir nicht direkt an die konkrete Klasse selbst. Stattdessen definieren wir die Abhängigkeit anhand ihrer Schnittstelle. Dies entkoppelt unseren Code von der spezifischen Implementierung und ermöglicht eine größere Flexibilität, sodass es bei Bedarf einfacher ist, Abhängigkeiten auszutauschen oder zu simulieren.
In diesem Beispiel haben wir unseren UserService in der Klasse App platziert. Dieses Muster ist als Composition Root bekannt. Der Composition Root ist der zentrale Ort in der Anwendung, an dem alle Abhängigkeiten zusammengestellt und eingefügt werden – im Wesentlichen die „Wurzel“ des Abhängigkeitsdiagramms unserer Anwendung. Indem wir diese Logik an einem Ort aufbewahren, behalten wir eine bessere Kontrolle darüber, wie Abhängigkeiten aufgelöst und in die gesamte App eingefügt werden.
Der letzte Schritt besteht darin, die Klasse App im DI-Container zu registrieren, wodurch der Container den Lebenszyklus und die Injektion aller Abhängigkeiten beim Start der Anwendung verwalten kann.
npm i friendly-di reflect-metadata
Wenn wir Klassen in unserer Anwendung ersetzen müssen, müssen wir nur eine Scheinklasse erstellen, die der Ursprungsschnittstelle folgt:
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } }
und verwenden Sie dann die Ersetzungsmethode, bei der wir eine ersetzbare Klasse als Scheinklasse deklarieren:
class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor(private productService: ProductService) {} getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor(private orderService: OrderService) {} getUserOrders() { return this.orderService.getOrdersForUser(); } } new UserService(new OrderService(new ProductService()));
Friendly-DI können wir viele Male ersetzen lassen:
class ServiceLocator { static #modules = new Map(); static get(moduleName: string) { return ServiceLocator.#modules.get(moduleName); } static set(moduleName: string, exp: never) { ServiceLocator.#modules.set(moduleName, exp); } } class ProductService { getProducts() { return ['product 1', 'product 2', 'product 3']; } } class OrderService { constructor() { const ProductService = ServiceLocator.get('ProductService'); this.productService = new ProductService(); } getOrdersForUser() { return this.productService.getProducts(); } } class UserService { constructor() { const OrderService = ServiceLocator.get('OrderService'); this.orderService = new OrderService(); } getUserOrders() { return this.orderService.getOrdersForUser(); } } ServiceLocator.set('ProductService', ProductService); ServiceLocator.set('OrderService', OrderService); new UserService();
Das ist alles, wenn Sie Kommentare oder Klarstellungen zu diesem Thema haben, schreiben Sie Ihre Gedanken bitte in die Kommentare.
Das obige ist der detaillierte Inhalt vonBeherrschung des Prinzips der Abhängigkeitsinversion: Best Practices für sauberen Code mit DI. Für weitere Informationen folgen Sie bitte anderen verwandten Artikeln auf der PHP chinesischen Website!