Navigation
Recherche
|
Why you should use dependency injection
mercredi 5 mars 2025, 10:00 , par InfoWorld
A few weeks ago, I wrote about how a good software team will defer decisions as long as possible. Smart teams will design and build systems that don’t lock them into any particular implementation until it is utterly necessary. (And even then, if they have done things right, they don’t have to lock themselves into anything…) This enables them to keep possibilities open and has the lovely result of driving a flexible, extensible design.
Critical to that, of course, is writing code that is flexible and not locked into any one implementation. This is where dependency injection comes in. I’ve mentioned this powerful and under-appreciated technique before, and I thought I’d take the time to discuss what it is in a bit more depth. Dependency injection is a twenty-five cent term for a priceless idea. It’s a straightforward technique that decouples your code from specific implementations and maximizes flexibility. The idea is this: Delay the actual implementation of functionality for as long as possible. Instead, code against abstractions as much as possible. Coding yourself into a corner Let’s look at an example. Building an e-commerce application is quite commonplace. Whether it is an online website or a physical point-of-sale system, the app will need to process credit cards. Now, credit card processing is a fairly complex thing, but it is something that lends itself to abstractions. Let’s say your system will be using the PayStuff payment processor. If you are a strict adherent to the YAGNI principle (which I wouldn’t recommend) then you’ll just go ahead and hard-code the implementation of PayStuff like so: 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); This works fine, I guess. You can process and collect money for orders and all is well. It’s not changeable. And never mind that you can’t unit test it at all. But hey, you aren’t going to need anything more, right? YAGNI for the win! But oops! PayStuff goes out of business! You need to start using the ConnectBucks processor instead of PayStuff. And the very same day you realize that, your product manager asks you to add support for paying with PayPal and Google Pay. Suddenly, your system is not only hard to test, but it doesn’t even work anymore, and supplying all this new functionality will require some pretty major surgery to your system, right? Abstraction saves the day What you should have done instead is realize that you are going to need an abstraction. Thus, you create an interface and write all your code against it and not a specific implementation. Then, instead of creating the implementation on the spot, you defer the implementation decision and “inject” into the constructor the implementation of the abstraction that you want to use. That is, you delay as long as possible the actual implementation, and instead code against an interface. For instance, here’s a simple interface you might use: interface IPaymentProcessor { processPayment(amount: number): void; } You can write the whole payment module with this interface and not know or care how the payment is processed. From here, you create a class that is designed to receive, not create, an implementation of the interface: 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!'); } } This class takes as a parameter to the constructor an implementation of the payment processor. You might say that the class has a dependency on the payment processor, and that the code injects that dependency into the class. But what actual processor is used to take the payment? The class neither knows nor cares. Plug and play classes Next, you can create classes that implement the IPaymentProcessor interface: 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...`); } } Note that there is no limit to the ways that you can implement this interface, allowing for flexibility in an unknown future. You can even create a mock processing class for testing purposes. From here, you create the implementation classes as needed and pass them into the processing class: const creditCardCheckout = new Checkout(new CreditCardPayment()); creditCardCheckout.processOrder(100); const paypalCheckout = new Checkout(new PayPalPayment()); paypalCheckout.processOrder(50); You could even build a factory class that creates the correct implementation on request. const processor: IPaymentProcessor = PaymentProcessorFactory.createProcessor(PaymentType.PayPal); const checkout = new Checkout(processor); checkout.processOrder(50); Now you have a system that is easy to change if payments change in the future, easy to test via a mock payment system, and easy to understand, as the actual inner workings of the payment process are tucked neatly away in an implementing class. Save it for later That is dependency injection in a nutshell. By coding against abstractions and providing implementations at the last minute rather than hard-coding the implementation, you can create a flexible, testable, and extensible system that will be vastly easier to maintain. Beyond that, there are sophisticated dependency injection systems that provide a “container” where all implementing classes are automatically generated upon first use. But the essence of dependency injection is really that simple.
https://www.infoworld.com/article/3838096/why-you-should-use-dependency-injection.html
Voir aussi |
56 sources (32 en français)
Date Actuelle
jeu. 3 avril - 01:15 CEST
|