Dependency Injection – im Grunde ein simples Konzept.Thomas Bethge | shutterstock.com Kluge Dev-Teams entwerfen und erstellen Systeme, die nicht an spezifische Implementierungen gebunden sind – solange, bis es nicht mehr anders geht. So zumindest meine Überzeugung. Denn auf diese Weise halten sich Entwickler alle Möglichkeiten offen und realisieren quasi nebenbei, flexible, erweiterbare Designs. Die entscheidende Zutat dafür ist natürlich entsprechend flexibel gestalteter Code – was Dependency Injection auf den Plan ruft. Diese simple Technik entkoppelt Ihren Code von bestimmten Implementierungen und maximiert seine Flexibilität. Die Idee dahinter besteht darin, Funktionalität so spät wie möglich zu implementieren – und stattdessen ausgiebig gegen Abstraktionen zu programmieren. Im Folgenden lesen Sie, warum Sie Dependency Injection nutzen sollten – in aller Kürze. So programmiert man sich in die Bredouille Dazu werfen wir einen Blick auf ein Beispiel: E-Commerce-Anwendungen sind weit verbreitet und müssen im Regelfall Kreditkarten verarbeiten können. Das ist eine relativ komplexe Angelegenheit – die sich aber sehr gut für Abstraktionen eignet. Angenommen, unser System nutzt den Zahlungsverarbeiter PayStuff – und wir halten uns strikt an das YAGNI-Prinzip (was nicht zu empfehlen ist). In diesem Fall könnten wir die Implementierung wie folgt festkodieren: class PayStuffPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayStuff…`); } } class Checkout { private paymentProcessor: PayStuffPaymentProcessor; constructor() { this.paymentProcessor = new PayStuffPaymentProcessor(); } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log(“Order processed successfully!”); } } // Usage const checkout = new Checkout(); checkout.processOrder(100); checkout.processOrder(50); Das funktioniert wahrscheinlich gut, um die Zahlungen für Bestellungen zu verarbeiten. Dass ein Unit Test erst gar nicht möglich ist – wen interessiert’s? YAGNI ftw! Doch dann die Überraschung: PayStuff stellt seinen Betrieb ein. Und wir müssen auf den neuen ConnectBucks-Processor umsteigen. An genau dem Tag, an dem das klar wird, kommt dann auch noch ein Produktmanager ums Eck und verlangt nach Support für PayPal und Google Pay. Jetzt ist unser System nicht mehr nur schwer zu testen, sondern funktioniert auch nicht mehr und muss umfangreich überarbeitet werden. Abstraktion, die den Tag rettet Das wäre uns erspart geblieben, wenn wir erkannt hätten, dass wir eine Abstraktion benötigen. Dazu erstellen wir ein Interface und schreiben sämtlichen Code dagegen – nicht gegen eine spezifische Implementierung. Statt diese direkt zu erstellen, schieben wir die Entscheidung darüber lieber und „injizieren“ die Implementierung der Abstraktion in den Konstruktor, die wir verwenden möchten. Mit anderen Worten: Wir verzögern die tatsächliche Implementierung so lange wie möglich und programmieren stattdessen gegen eine Schnittstelle. Ein simples Beispiel für ein Interface, das wir nutzen könnten: interface IPaymentProcessor { processPayment(amount: number): void; } Sie können das gesamte Zahlungsmodul mit diesem Interface schreiben – ohne zu wissen (oder sich darum zu kümmern), wie die Zahlung verarbeitet wird. An diesem Punkt erstellen Sie eine Klasse, die so konzipiert ist, dass sie eine Implementierung der Schnittstelle empfängt und nicht erstellt: class Checkout { private paymentProcessor: IPaymentProcessor; constructor(paymentProcessor: IPaymentProcessor) { if (!paymentProcessor) { throw new Error(“PaymentProcessor cannot be null or undefined.”); } this.paymentProcessor = paymentProcessor; } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log(“Order processed successfully!”); } } Diese Klasse verwendet eine Implementierung des Zahlungsverarbeiters als Parameter für den Konstruktor. Man könnte auch sagen, dass die Klasse eine Abhängigkeit vom Payment Processor aufweist und der Code diese in die Klasse einfügt. Welcher Zahlungsverarbeiter tatsächlich verwendet wird, „weiß“ die Klasse nicht – und es ist ihr auch egal. Plug-&-Play-Klassen Im nächsten Schritt können wir Klassen erstellen, die die IPaymentProcessor-Schnittstelle implementieren: class PayPalPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayPal…`); } } class GooglePayPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via Google Pay…`); } } class ConnectBucksPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via ConnectBucks…`); } } Für die Zukunft ist bei diesem Code vorgesorgt: Es ist sogar möglich, eine „Mock“-Verarbeitungsklasse zu Testing-Zwecken zu erstellen. Von diesem Punkt aus können Sie die Implementierungsklassen je nach Bedarf kreieren und sie an die Verarbeitungsklasse übergeben: const creditCardCheckout = new Checkout(new CreditCardPayment()); creditCardCheckout.processOrder(100); const paypalCheckout = new Checkout(new PayPalPayment()); paypalCheckout.processOrder(50); Sie könnten darüber hinaus auch eine Factory-Klasse erstellen, die auf Anfrage die korrekte Implementierung erstellt. const processor: IPaymentProcessor = PaymentProcessorFactory.createProcessor(PaymentType.PayPal); const checkout = new Checkout(processor); checkout.processOrder(50); Und während die eigentliche Funktionsweise des Zahlungsprozesses in einer Implementierungsklasse verborgen ist, steht im Ergebnis ein flexibles System, das: anpassbar, testfähig, wartbar und leicht verständlich ist. Das ist Dependency Injection „in a Nutshell“. Machen Sie es sich zunutze – es lohnt sich. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!
Nutzen Sie Dependency Injection!
Dependency Injection – im Grunde ein simples Konzept.Thomas Bethge | shutterstock.com Kluge Dev-Teams entwerfen und erstellen Systeme, die nicht an spezifische Implementierungen gebunden sind – solange, bis es nicht mehr anders geht. So zumindest meine Überzeugung. Denn auf diese Weise halten sich Entwickler alle Möglichkeiten offen und realisieren quasi nebenbei, flexible, erweiterbare Designs. Die entscheidende Zutat dafür ist natürlich entsprechend flexibel gestalteter Code – was Dependency Injection auf den Plan ruft. Diese simple Technik entkoppelt Ihren Code von bestimmten Implementierungen und maximiert seine Flexibilität. Die Idee dahinter besteht darin, Funktionalität so spät wie möglich zu implementieren – und stattdessen ausgiebig gegen Abstraktionen zu programmieren. Im Folgenden lesen Sie, warum Sie Dependency Injection nutzen sollten – in aller Kürze. So programmiert man sich in die Bredouille Dazu werfen wir einen Blick auf ein Beispiel: E-Commerce-Anwendungen sind weit verbreitet und müssen im Regelfall Kreditkarten verarbeiten können. Das ist eine relativ komplexe Angelegenheit – die sich aber sehr gut für Abstraktionen eignet. Angenommen, unser System nutzt den Zahlungsverarbeiter PayStuff – und wir halten uns strikt an das YAGNI-Prinzip (was nicht zu empfehlen ist). In diesem Fall könnten wir die Implementierung wie folgt festkodieren: class PayStuffPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayStuff...`); } } class Checkout { private paymentProcessor: PayStuffPaymentProcessor; constructor() { this.paymentProcessor = new PayStuffPaymentProcessor(); } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log("Order processed successfully!"); } } // Usage const checkout = new Checkout(); checkout.processOrder(100); checkout.processOrder(50); Das funktioniert wahrscheinlich gut, um die Zahlungen für Bestellungen zu verarbeiten. Dass ein Unit Test erst gar nicht möglich ist – wen interessiert’s? YAGNI ftw! Doch dann die Überraschung: PayStuff stellt seinen Betrieb ein. Und wir müssen auf den neuen ConnectBucks-Processor umsteigen. An genau dem Tag, an dem das klar wird, kommt dann auch noch ein Produktmanager ums Eck und verlangt nach Support für PayPal und Google Pay. Jetzt ist unser System nicht mehr nur schwer zu testen, sondern funktioniert auch nicht mehr und muss umfangreich überarbeitet werden. Abstraktion, die den Tag rettet Das wäre uns erspart geblieben, wenn wir erkannt hätten, dass wir eine Abstraktion benötigen. Dazu erstellen wir ein Interface und schreiben sämtlichen Code dagegen – nicht gegen eine spezifische Implementierung. Statt diese direkt zu erstellen, schieben wir die Entscheidung darüber lieber und „injizieren“ die Implementierung der Abstraktion in den Konstruktor, die wir verwenden möchten. Mit anderen Worten: Wir verzögern die tatsächliche Implementierung so lange wie möglich und programmieren stattdessen gegen eine Schnittstelle. Ein simples Beispiel für ein Interface, das wir nutzen könnten: interface IPaymentProcessor { processPayment(amount: number): void; } Sie können das gesamte Zahlungsmodul mit diesem Interface schreiben – ohne zu wissen (oder sich darum zu kümmern), wie die Zahlung verarbeitet wird. An diesem Punkt erstellen Sie eine Klasse, die so konzipiert ist, dass sie eine Implementierung der Schnittstelle empfängt und nicht erstellt: class Checkout { private paymentProcessor: IPaymentProcessor; constructor(paymentProcessor: IPaymentProcessor) { if (!paymentProcessor) { throw new Error("PaymentProcessor cannot be null or undefined."); } this.paymentProcessor = paymentProcessor; } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log("Order processed successfully!"); } } Diese Klasse verwendet eine Implementierung des Zahlungsverarbeiters als Parameter für den Konstruktor. Man könnte auch sagen, dass die Klasse eine Abhängigkeit vom Payment Processor aufweist und der Code diese in die Klasse einfügt. Welcher Zahlungsverarbeiter tatsächlich verwendet wird, „weiß“ die Klasse nicht – und es ist ihr auch egal. Plug-&-Play-Klassen Im nächsten Schritt können wir Klassen erstellen, die die IPaymentProcessor-Schnittstelle implementieren: class PayPalPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayPal...`); } } class GooglePayPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via Google Pay...`); } } class ConnectBucksPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via ConnectBucks...`); } } Für die Zukunft ist bei diesem Code vorgesorgt: Es ist sogar möglich, eine „Mock“-Verarbeitungsklasse zu Testing-Zwecken zu erstellen. Von diesem Punkt aus können Sie die Implementierungsklassen je nach Bedarf kreieren und sie an die Verarbeitungsklasse übergeben: const creditCardCheckout = new Checkout(new CreditCardPayment()); creditCardCheckout.processOrder(100); const paypalCheckout = new Checkout(new PayPalPayment()); paypalCheckout.processOrder(50); Sie könnten darüber hinaus auch eine Factory-Klasse erstellen, die auf Anfrage die korrekte Implementierung erstellt. const processor: IPaymentProcessor = PaymentProcessorFactory.createProcessor(PaymentType.PayPal); const checkout = new Checkout(processor); checkout.processOrder(50); Und während die eigentliche Funktionsweise des Zahlungsprozesses in einer Implementierungsklasse verborgen ist, steht im Ergebnis ein flexibles System, das: anpassbar, testfähig, wartbar und leicht verständlich ist. Das ist Dependency Injection „in a Nutshell“. Machen Sie es sich zunutze – es lohnt sich. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!
Nutzen Sie Dependency Injection! Dependency Injection – im Grunde ein simples Konzept.Thomas Bethge | shutterstock.com Kluge Dev-Teams entwerfen und erstellen Systeme, die nicht an spezifische Implementierungen gebunden sind – solange, bis es nicht mehr anders geht. So zumindest meine Überzeugung. Denn auf diese Weise halten sich Entwickler alle Möglichkeiten offen und realisieren quasi nebenbei, flexible, erweiterbare Designs. Die entscheidende Zutat dafür ist natürlich entsprechend flexibel gestalteter Code – was Dependency Injection auf den Plan ruft. Diese simple Technik entkoppelt Ihren Code von bestimmten Implementierungen und maximiert seine Flexibilität. Die Idee dahinter besteht darin, Funktionalität so spät wie möglich zu implementieren – und stattdessen ausgiebig gegen Abstraktionen zu programmieren. Im Folgenden lesen Sie, warum Sie Dependency Injection nutzen sollten – in aller Kürze. So programmiert man sich in die Bredouille Dazu werfen wir einen Blick auf ein Beispiel: E-Commerce-Anwendungen sind weit verbreitet und müssen im Regelfall Kreditkarten verarbeiten können. Das ist eine relativ komplexe Angelegenheit – die sich aber sehr gut für Abstraktionen eignet. Angenommen, unser System nutzt den Zahlungsverarbeiter PayStuff – und wir halten uns strikt an das YAGNI-Prinzip (was nicht zu empfehlen ist). In diesem Fall könnten wir die Implementierung wie folgt festkodieren: class PayStuffPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayStuff...`); } } class Checkout { private paymentProcessor: PayStuffPaymentProcessor; constructor() { this.paymentProcessor = new PayStuffPaymentProcessor(); } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log("Order processed successfully!"); } } // Usage const checkout = new Checkout(); checkout.processOrder(100); checkout.processOrder(50); Das funktioniert wahrscheinlich gut, um die Zahlungen für Bestellungen zu verarbeiten. Dass ein Unit Test erst gar nicht möglich ist – wen interessiert’s? YAGNI ftw! Doch dann die Überraschung: PayStuff stellt seinen Betrieb ein. Und wir müssen auf den neuen ConnectBucks-Processor umsteigen. An genau dem Tag, an dem das klar wird, kommt dann auch noch ein Produktmanager ums Eck und verlangt nach Support für PayPal und Google Pay. Jetzt ist unser System nicht mehr nur schwer zu testen, sondern funktioniert auch nicht mehr und muss umfangreich überarbeitet werden. Abstraktion, die den Tag rettet Das wäre uns erspart geblieben, wenn wir erkannt hätten, dass wir eine Abstraktion benötigen. Dazu erstellen wir ein Interface und schreiben sämtlichen Code dagegen – nicht gegen eine spezifische Implementierung. Statt diese direkt zu erstellen, schieben wir die Entscheidung darüber lieber und „injizieren“ die Implementierung der Abstraktion in den Konstruktor, die wir verwenden möchten. Mit anderen Worten: Wir verzögern die tatsächliche Implementierung so lange wie möglich und programmieren stattdessen gegen eine Schnittstelle. Ein simples Beispiel für ein Interface, das wir nutzen könnten: interface IPaymentProcessor { processPayment(amount: number): void; } Sie können das gesamte Zahlungsmodul mit diesem Interface schreiben – ohne zu wissen (oder sich darum zu kümmern), wie die Zahlung verarbeitet wird. An diesem Punkt erstellen Sie eine Klasse, die so konzipiert ist, dass sie eine Implementierung der Schnittstelle empfängt und nicht erstellt: class Checkout { private paymentProcessor: IPaymentProcessor; constructor(paymentProcessor: IPaymentProcessor) { if (!paymentProcessor) { throw new Error("PaymentProcessor cannot be null or undefined."); } this.paymentProcessor = paymentProcessor; } processOrder(amount: number) { this.paymentProcessor.processPayment(amount); console.log("Order processed successfully!"); } } Diese Klasse verwendet eine Implementierung des Zahlungsverarbeiters als Parameter für den Konstruktor. Man könnte auch sagen, dass die Klasse eine Abhängigkeit vom Payment Processor aufweist und der Code diese in die Klasse einfügt. Welcher Zahlungsverarbeiter tatsächlich verwendet wird, „weiß“ die Klasse nicht – und es ist ihr auch egal. Plug-&-Play-Klassen Im nächsten Schritt können wir Klassen erstellen, die die IPaymentProcessor-Schnittstelle implementieren: class PayPalPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via PayPal...`); } } class GooglePayPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via Google Pay...`); } } class ConnectBucksPayment implements IPaymentProcessor { processPayment(amount: number) { console.log(`Processing $${amount} payment via ConnectBucks...`); } } Für die Zukunft ist bei diesem Code vorgesorgt: Es ist sogar möglich, eine „Mock“-Verarbeitungsklasse zu Testing-Zwecken zu erstellen. Von diesem Punkt aus können Sie die Implementierungsklassen je nach Bedarf kreieren und sie an die Verarbeitungsklasse übergeben: const creditCardCheckout = new Checkout(new CreditCardPayment()); creditCardCheckout.processOrder(100); const paypalCheckout = new Checkout(new PayPalPayment()); paypalCheckout.processOrder(50); Sie könnten darüber hinaus auch eine Factory-Klasse erstellen, die auf Anfrage die korrekte Implementierung erstellt. const processor: IPaymentProcessor = PaymentProcessorFactory.createProcessor(PaymentType.PayPal); const checkout = new Checkout(processor); checkout.processOrder(50); Und während die eigentliche Funktionsweise des Zahlungsprozesses in einer Implementierungsklasse verborgen ist, steht im Ergebnis ein flexibles System, das: anpassbar, testfähig, wartbar und leicht verständlich ist. Das ist Dependency Injection „in a Nutshell“. Machen Sie es sich zunutze – es lohnt sich. (fm) Sie wollen weitere interessante Beiträge zu diversen Themen aus der IT-Welt lesen? Unsere kostenlosen Newsletter liefern Ihnen alles, was IT-Profis wissen sollten – direkt in Ihre Inbox!