Dependency Injection

 - 

Junior developers frequently struggle with the concept of de...

Dependency Injection

Created: 1/17/2022Updated: 3/25/2022

Junior developers frequently struggle with the concept of dependency injection. I've even encountered more senior engineers who conflate this agnostic tactic in programming with tools and libraries they use. In this article I'll explain the benefits of dependency injection and demonstrate how to get started in object-oriented and functional settings.


What does it mean to "inject" a dependency?

Dependencies are considered "injected" when the calling scope passes, or at least has the option to pass, the dependency into the called scope vice it referencing said dependency directly. In object-oriented contexts this is generally done through a constructor, but dependency injection can be done at the method or function level as well.

Consider these examples

Object-oriented (C#)

interfaceINumberSummarizer// abstraction{	intSummarize(IEnumerable<int> numbers);}classNumberAdder : INumberSummarizer// concrete implementation{	publicintSummarize(IEnumerable<int> numbers)	{		return numbers.Aggregate(0, (pv, cv) => pv + cv);	}}classNoInjectionExample{	publicintUseSummarizer(IEnumerable<int> numbers)	{		returnnew NumberAdder().Summarize(numbers);	}}classInjectionExample{	privatereadonly INumberSummarizer numberSummarizer;        publicInjectionExample(INumberSummarizer numberSummarizer)	{		this.numberSummarizer = numberSummarizer;	}        publicintUseSummarizer(IEnumerable<int> numbers)	{		returnthis.numberSummarizer.Summarize(numbers);	}}

Functional (TypeScript)

typeNumberSummarizer = (numbers: number[]) =>number; // abstractionconstNumberAdder: NumberSummarizer = (numbers: number[]) =>// concrete implementation  numbers.reduce((pv, cv) => pv + cv, 0);functionNoInjectionExample(numbers: number[]) {  returnNumberAdder(numbers);}functionInjectionExample(numbers: number[], numberSummarizer: NumberSummarizer) {  returnnumberSummarizer(numbers);}

In the above examples there is an abstraction for a NumberSummarizer dependency and a concrete implementation. Notice how the injection examples don't reference the concrete NumberAdder implementation directly.

You might be thinking, "if I only have one concrete implementation, what's the point; doesn't this just complicate my code?"

Dependency injection is still a best practice for dependencies that only have a single implementation. I'll dive into this question more thoroughly throughout this article. I'll also demonstrate a simple trick that will let you take advantage of dependency injection without having to pass around concrete implementations.

Why inject dependencies?

In an effort to stay DRY (don't repeat yourself), and reuse code, you'll frequently compose classes and functions together. This is often the preferred method for sharing code, due to the extensibility it provides.

However, without abstraction and dependency injection, you "couple" objects to one another. This coupling can become a huge burden, especially when it comes to testing.

Unit testing

Imagine you have a service responsible for fetching data from some third party resource. The service code essentially fires off an http request, transforms the response a bit, then returns it. Do you really want to execute an actual http request over the network to test that service? The answer could be yes, but from a unit testing perspective, it's going to be no. If your unit test fails when the internet is down...it's not a unit test.

Now imagine you have a UI layer with a button whose click calls that service before printing the results out somewhere. Do you want to execute the service code, or its underlying http request, to verify that the button's click handler does its job? You may, if you're writing "integration" or "end-to-end (e2e)" tests, however, the answer is again no when writing unit tests.

I could go on and on with more examples, and, as I started to point out, this "coupling" of code has a way of exploding very quickly. The point I'm driving at here is, you should be able to unit test code without executing its dependencies' implementations. In order to achieve that you need dependency injection.

Architecture

Dependency injection allows us to follow a principle called "dependency inversion." One of the SOLID principles [1]. To follow dependency inversion, the code in one unit (class, function, etc.) must not directly reference concrete implementations of other units. Sound familiar?

The purpose of this principle is to keep our code decoupled from its dependencies. Doing so supports extensibility, testability, and general maintainability.

Example

Imagine you're tasked with implementing various user tracking business logic to support making "data driven decisions." The business has already made the decision to choose "Google Analytics (GA)" as the underlying tool, but it was a close call between it and Microsoft's "App Insights (AI)" platform. So close, that the conversation often drifted into how difficult it might be to switch platforms in the future.

Given this consideration, you recognize that lack of coupling between the business logic (domain) and GA (dependency) is a must.

Achieving this goal is much less complicated than may be expected. You first define an abstraction for your business logic to reference when it's "tracking" an event. You then create a concrete implementation that uses GA's software development kit (SDK).

The benefit here is that if the day ever comes where the business wants to switch to AI, all you have to take care of is implementing a new concrete implementation of your abstraction that uses AI's SDK vice GA's.

What if you receive the curve ball requirement that one of your applications (or application components) will make the switch to AI before another...? If your code is following the dependency inversion principle, and injecting dependencies..., you'll just shrug, since this seemingly stark requirement won't add any complexity at all. That's the power of dependency inversion/injection in architecture.

Here's a diagram for you visual folks:

Example architecture of how DI applies to extensibility

Default values for injected dependencies

Earlier I mentioned I would demonstrate a "trick" that would allow you to use dependency injection without needing to pass in implementations each time.

The trick, as implied by this section's heading, is to set default implementations in the signature declaration.

Here are modified versions of the "injection" examples from earlier:

Object-oriented (C#)

classInjectionExample{	privatereadonly INumberSummarizer numberSummarizer;        publicInjectionExample(INumberSummarizer numberSummarizer = new NumberAdder())	{		this.numberSummarizer = numberSummarizer;	}        publicintUseSummarizer(IEnumerable<int> numbers)	{		returnthis.numberSummarizer.Summarize(numbers);	}}// Usagenew InjectionExample().UseSummarizer(newint[] {1, 2, 3}); // 6

Functional (TypeScript)

functionInjectionExample(numbers: number[], numberSummarizer: NumberSummarizer = NumberAdder) {  returnnumberSummarizer(numbers);}// UsageInjectionExample([1, 2, 3]); // 6

The only changes from the originals, are "= new NumberAdder()" in the C# version and "= NumberAdder" in the TypeScript version. These changes allow this code to be referenced as if it didn't have injected dependencies, however, an alternative or mock dependency can still be passed in when needed (ex. during tests).

There are other conventions for handling this, however, this tactic is language agnostic and not dependent on any additional tools, frameworks, etc.

Summary

I hope this article has been a pleasant intro or refresher in dependency injection and some closely related concepts.

For any readers who are wondering why I didn't broach the topic of DI or IOC containers, suffice to say that I consider these tools to be additive, and in no way "fundamental," to the topic.

Dependency injection is a powerful tool, and easy to use once you get the hang of it. I recommend using it anytime one class, function, etc. depends on others. Doing so will result in more testable and maintainable software.

Credits

  1. Principles WIKI: SOLID