Everything You Need to Know About Dependency Injection

Dependency injection is a technique in which you make the interactions between objects as minimal as possible through specific dependencies. Here’s what to know.  

Written by Nick Hodges
Published on Jan. 12, 2024
Everything You Need to Know About Dependency Injection
Image: Shutterstock / Built In
Brand Studio Logo

In your code, the interaction between objects should be as thin and clean as possible. You should ask for precisely what you need and nothing more. If you need something, you should ask for it — have it “delivered” to you instead of trying to create it yourself. 

That is the essence of dependency injection.

But what does that mean? The concept is similar to how we approach things in the world.

Let’s say you are at the supermarket, and you have a grocery cart full of groceries. You take them to the checkout guy, and he rings them all up. He says, “That’ll be $123.45.” So what do you do? Do you hand him your wallet and have him dig around for cash or a credit card? Of course not. You want to keep the interaction as minimal as possible with your wallet;  you don’t want him rooting around in it. Who knows what will happen? 

Instead, you give the guy cash, or you pull out your credit card and hand it to him. Or you swipe the card yourself so that the checkout guy never even touches your card. You want to keep the interface between you and the clerk to a minimum.

Dependency Injection Explained

Dependency injection is a technique that involves making the interactions between objects as thin as possible through specific dependencies. This allows for loosely coupled code, or code that only depends on the required portion of a separate class to run. This reduces hard-coded dependencies and allows for cleaner, more flexible code.   

Or, let’s say you need a cake for your child’s birthday. You could bake the cake with your child, but that would create a huge mess. An even better solution would be to find a bakery that would deliver birthday cakes to order, dropping off at your doorstep exactly what you wanted for your birthday cake.  

Dependency injection is a $25 term for a 10 cent concept: If you need something in your code, ask for it to do that and nothing else. Just like with the cake and the cashier and the house, it ensures things stay neat and clean and flexible.

Every class of any substance will likely need help from other classes. If your class needs help, ask for it. Remember, every time you try to “do it yourself,” you create a hard-coded dependency. And again, those should be avoided like global variables.

I think code that is neat and clean and flexible sounds pretty good. So, let’s dig in.

 

What Is Dependency Injection?

By now, you probably are wondering what DI exactly is. Well, according to Mark Seemann, author of Dependency Injection in .Net, Dependency Injection is “A set of software design principles and patterns that enable us to develop loosely coupled code.” That’s a good definition but a bit antiseptic. Let’s explore a bit more deeply.

If you are going to inject a dependency, you need to know what a dependency is. A dependency is anything that a given class needs to do its job, such as fields, other classes, etc. If ClassA needs ClassB to be present to compile, then ClassA is dependent on ClassB. Or in other words, ClassB is a dependency of ClassA. Dependencies get created when, well, you create one. 

Here’s an example:

class ClassB {

}

class ClassA {
  private _ClassB: ClassB;

  constructor() {
    this._ClassB = new ClassB();
  }
}

In the code above, we have created a hard-coded dependency to ClassB in ClassA. It's hard-wired into the class. ClassA is completely dependent on ClassB. ClassB is coupled to ClassA. Tightly coupled. About as tightly coupled as you can get. And tight coupling is bad.

 

What Is Coupling?

Coupling is the notion of one thing being dependent on another. Tight coupling is when things really depend on each other and are strictly tied. You can’t compile ClassA without the complete definition of ClassB being present. B is permanently stuck to A. Tight coupling is bad — it creates inflexible code. Think how hard it would be to move around if you were handcuffed to another person. That’s how a class feels when you create hard-coded dependencies.

What you want is to keep the coupling between your classes and modules as “loose” as possible. That is, you want your dependencies to be as thin as you can make them. If you have a class that needs a customer name, pass in just the customer name. Don’t pass the entire customer. And as we shall see, if your class needs another class, pass in an abstraction, an interface usually of that class. An interface is like a wisp of smoke. There is something there, but you really can’t grab on to it.

 

How to Inject a Dependency?

Well, the first thing to do is not to create ClassB inside ClassA. Instead, inject the dependency via the constructor:

class ClassB {

}

class ClassA {
  private _ClassB: ClassB;

  constructor(aClassB: ClassB) {
    this._ClassB = aClassB;
  }
}

At this point, we have two types of coupling: one where the first class creates an instance of the second class and one where the first class needs the second class (i.e., where it is injected). The latter is preferable to the former in that it involves less (looser) coupling.

That is the essence of dependency injection. Inject your dependencies instead of creating them. That’s it. If you understand the notion that you should code against abstractions and that you should ask for the functionality that you need, you are well on your way to understanding dependency injection and to writing better code. Dependency injection is a means to an end, and that end is loosely coupled code.

So let’s get a bit more practical and take a look at five ways you can go about doing dependency injection. Follow these five basic ideas, and you’ll be doing dependency injection without much additional effort.

More on Software DevelopmentHow to Write Pythonic Code

 

5 Dependency Injection Principles to Follow

There are five basic principles that you should follow for dependency injection. These include:

  1. Code against abstractions, not implementations.
  2. Never create things that shouldn’t be created.
  3. Keep constructors simple.
  4. Don’t assume anything about implementation.
  5. Don’t assume an interface is an abstraction.

Let’s go through each one below:

 

1. Code against abstractions, not implementations

Erich Gamma of the “Gang of Four”, the authors of the book, Design Patterns, is credited with coining this phrase, and it’s a powerful and essential idea. If you teach new developers only one thing, it should be this aphorism: Code against abstractions, not implementations.

Abstractions, usually interfaces but not always, are flexible. Interfaces, or abstract classes, can be implemented in many ways. Interfaces can be coded against before the implementation is even completed. If you code to an implementation, you’re creating a tightly coupled and inflexible system. Don’t lock yourself into a single implementation. Instead, use abstractions, and allow your code to be supple, reusable and flexible.

 

2. Never Create Things That Shouldn’t Be Created

Your classes should follow the single responsibility principle, the idea that a class should only do one thing.

If they do that, then they shouldn’t be creating things because that makes two things they’re doing. Instead, they should ask for the functionality they need and let something else create and provide that functionality.

 

Creatables vs. injectables

So what should be created? Well, there are two different kinds of objects that we should concern ourselves with: creatables and injectables.

Creatables are classes you should go ahead and create. They are run-time library or utility classes that are common and well-known.

Generally, classes in the run-time library should be considered creatables. Classes like this should not be injected but should be created by your classes. They often have short lifetimes, frequently living no longer than the span of a single method. If they’re required by the class as a whole, they can be created in the constructor. One should pass only other creatables to the constructor of a creatable.

Injectables, on the other hand, are classes we never want to create directly. They’re the types of classes we never want to hardcode a dependency to and that should always be passed via DI. They will normally be asked for as dependencies in a constructor. Following the rule above, injectables should be referenced via interfaces and not direct references to an instance.

Injectables will most often be classes you write as part of your business logic. They should always be hidden behind an abstraction, usually an interface. Note, too, that injectables can ask for other injectables in their constructor.

 

3. Keep Constructors Simple

Constructors should be kept simple. The constructor of a class shouldn’t be doing any work. They shouldn’t be doing anything other than checking for null, creating creatables and storing dependencies for later use. They shouldn’t include any coding logic. An if clause in a class’ constructor that isn’t checking for null is a cry for that class to be split into two classes. There are ways to check for nil-value parameters that don’t involve an if statement.

A complex constructor is a clear sign that your class is doing too much. Keep constructors short, simple, and free of any logic.

 

4. Don’t Assume Anything About the Implementation

Interfaces are, of course, useless without an implementation. However, you, as a developer, should never make any assumptions about what that implementation is.

You should only code against the contract made by the interface. You may have written the implementation, but you shouldn’t code against the interface with that implementation in mind. Put another way, code against your interface as if a radically new and better implementation of that interface is right around the corner.

A well-designed interface will tell you what you need to do and how it’s to be used. The implementation of that interface should be immaterial to your usage of the interface.

 

5. Don’t Assume an Interface Is an Abstraction

Interfaces are nice, and I certainly sing their praises all the time. However, it’s important to realize not every interface is an abstraction.

 

For instance, if your interface is an exact representation of the public portion of your class, you really aren’t abstracting anything, right? Such interfaces are called header interfaces because they resemble C++ header files. Interfaces extracted from classes can easily be tightly coupled to that class alone, making the interface useless as an abstraction.

Finally, abstractions can be leaky. They can reveal specific implementation details about their implementation. Leaky abstractions are also normally tied to a specific implementation. 

Now that we’ve looked at the basic principles, let’s take a look at the three types of dependency injection — constructor injection, method injection and property injection — including what they are, how they work, and when to use them.

 

3 Types of Dependency Injection

There are three common types of dependency injection that you should be familiar with, including: 

  1. Constructor injection
  2. Property injection
  3. Method injection

Let’s look at each one below.

 

1. Constructor Injection

Constructor injection is the process of using the constructor to pass in the dependencies of a class. The dependencies are declared as parameters of the constructor. As a result, you cannot create a new instance of the class without passing in a variable of the type required by the constructor.

That last point is key. When you declare a dependency as a parameter on the constructor, you are saying, “I’m sorry, folks, but if you want to create this class, you must pass in this parameter.” Thus, a class is able to specify the dependencies that it requires and be guaranteed that it will get them. You can’t create the class without them. If you have this code:

class BankingService {}

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    this._BankingService = aBankingService;
  }
}

You can’t create a PayrollSystem without passing it an instance of BankingService. Well, sadly, you can pass null, but we’ll deal with that in a minute. PayrollSystem very clearly declares that it requires a BankingService, and users of the class must supply one.

 

Never Accept Null

As I mentioned, it’s unfortunate that the above class can and will take null as a parameter. I say take because, while a user of the class can pass in null, the class itself doesn’t have to accept null.

In fact, I argue that all methods should explicitly reject null as a value for any reference parameter at any time, including constructors and regular methods. At no time should a parameter be allowed to be null without the method raising an exception. If you pass null to the PayrollSystem above and the class tries to use it, an error will occur. And errors are bad. They should — and can — be avoided.

The above code really should look something like this:

class BankingService {}

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    if (!aBankingService) {
      throw new Error('How dare you pass me a null parameter!!!')
    }
    this._BankingService = aBankingService;
  }
}

This code will never allow the internal field to be null. It will raise an exception if someone dares pass in null as the value for the constructor’s parameter. This is how it should be. You could accept null, but then you’d have to check for it everywhere in your code, and who wants that? 

Checking for null is boilerplate code. Protecting against null being passed as a parameter is called the guard pattern, and you can write code to guard against null being passed to your methods. Here’s a basic implementation of a guard function:

function CheckNeitherNullNorUndefined<T>(value: T | undefined | null): void {
  if (<T>value == undefined || <T>value == null) {
    throw new Error('Guard Failure: null was passed.');
  }
}

The guard pattern actually is defined as any Boolean expression that must evaluate to true before the program execution can continue. It is usually used to ensure that certain preconditions are met before a method can continue, ensuring that the code that follows can properly execute. Checking that a reference isn’t null is probably the most common, but not the only, use for the guard pattern.

In the case at hand, we are using the guard pattern to protect against a parameter being null, so we can use the guard pattern to simplify our code:

class PayrollSystem {
  private _BankingService: BankingService;

  constructor(aBankingService: BankingService) {
    CheckNeitherNullNorUndefined(aBankingService);
    this._BankingService = aBankingService;
  }
}

 

When to Use Constructor Injection

You should use constructor injection when your class has a dependency that the class requires in order to work properly. If your class cannot work without a dependency, then inject it via the constructor. If your class needs three dependencies, then demand all three in the constructor.

Additionally, you should use constructor injection when the dependency in question has a lifetime longer than a single method. Dependencies passed into the constructor should be useful to the class in a general way, with its use spanning multiple methods in the class. If a dependency is used in only one spot, method injection (covered below) might be a better choice.

Constructor injection should be the main way that you do dependency injection. It’s simple: A class needs something and thus asks for it before it can even be constructed. By using the guard pattern, you can use the class with confidence, knowing that the field variable storing that dependency will be a valid instance. Plus, it’s really simple and clear to do.

Constructor injection should be your go-to technique for clear, decoupled code. But it shouldn’t be the only tool in the toolbox.

 

2. Property Injection

Okay, so we use constructor injection when we want to declare required dependencies. But what to do when a dependency isn’t required? Sometimes a class has a dependency that isn’t strictly required but is indeed used by the class. An example might be a document class that may or may not have a grammar checker installed. If there is one, great, the class can use it. If there isn’t one, great,  the class can include a default implementation as a placeholder.

The solution here is property injection. You add a property to your class that can be set to a valid instance of the class in question. Since the dependency is a property, you can set it as desired. If the dependency is not wanted or needed, you can leave the property as is.

Your code should act as if the dependency is there, so you should provide a do-nothing default implementation so that the code can still be run with or without a real dependency. Remember, we never want anything to be null, so that default implementation should be valid. Thus, if the user of the class wants to provide a working implementation, they can. If not, there is a working default that will allow the containing class to still function.

 

When to Use Property Injection

Use property injection when a dependency is optional and/or when a dependency can be changed after the class is instantiated. Use it when you want users of the containing class to be able to provide their own implementation of the interface in question. You should only use property injection when you can provide a default implementation of the interface in question. Property injection is sometimes referred to as setter injection.

Any default implementation will likely be a non-functional implementation. But it doesn’t have to be. If you want to provide a working default implementation, that’s fine. However, be aware that by using property injection and creating that class in the containing object’s constructor, you are coupling yourself to that implementation.

 

Property Injection Example

An example, of course, will show how things are done. Let’s take a look at code that does what I described above — a document class that has an optional grammar checker.

First, we’ll start with an interface:

interface GrammarChecker {
  CheckGrammar(): void;
}

Now, we’ll implement it twice: once as a do-nothing default and again as a real grammar checker.

  class defaultGrammarChecker implements IGrammarChecker {
    public CheckGrammar() {
      console.log('Do Nothing');
    }
  }

  class realGrammarChecker implements IGrammarChecker {
    CheckGrammar() {
      console.log('Grammar has been checked');
    }
  }

Both of the implementations just log to the console, even the non-op implementation. I merely wanted to make sure that things were all working properly. Again, defaultGrammarChecker is meant to be a non-operational, default implementation that will keep us from having to check for null all the time.

Now, we need a class that has a property on it for the grammar checker.

class Document {
    private _text: string;

    private _grammarChecker: IGrammarChecker;

    set grammarChecker(value: IGrammarChecker) {
      CheckNeitherNullNorUndefined(value);
      this._grammarChecker = value;
    }

    public constructor(aText: string) {
      this._text = aText;
      this._grammarChecker = new defaultGrammarChecker();
    }

    set text(value: string) {
      this._text = value;
    }

    get text(): string {
      return this._text;
    }

    public CheckGrammar() {
      this._grammarChecker.CheckGrammar();
    }
  }

Here are some things to note about this code:

  • Its constructor takes the document text as a parameter. It’s then exposed as a read/write property, so you can access it and change it if you want.
  • The constructor also creates an instance of the default grammar checker. Note again that this creates a hard-coded dependency — one of the perils of property injection. But the dependency is a do-nothing default and prevents us from having to constantly check for null.
  • The setter for the grammarChecker property contains a guard call, ensuring that the internal value _grammarChecker can never be null.
  • One further thing to note is that the grammarChecker property is a write-only property. That is, you can only set the value and never actually read it externally. You might create a write-only property when the value being written is only used internally and will never be called by code outside of the class.

Here’s some code that exercises everything and shows property injection in action:

  var document: Document = new Document('This is the document text.');
  
  console.log(document.text);
  // Use the default, no-op grammar checker
  document.CheckGrammar();
  // Change the dependency to use the "real" grammar checker
  document.grammarChecker = new realGrammarChecker();
  // Now the grammar checker is a "real" one
  document.CheckGrammar();

Here are things to note about the above code:

  • It creates a document, taking some text as a constructor parameter.
  • It calls CheckGrammar, but the default grammar checker doesn’t do anything, so it says so in the console.
  • But then we use property injection to inject a real grammar checker, and when we call CheckGrammar, the grammar gets checked for real.

Thus, property injection allows you to provide optional dependencies. It also allows you to change a dependency if required. For instance, your document class may take texts from different languages and thus will require that the grammar checker changes as the document’s language changes. Property injection will allow for this.

 

3. Method Injection

What if the dependency that your class needs is going to be different much of the time? What if the dependency is an interface, and you have several implementations that you may want to pass into the class? You could use property injection, but then you’d be setting the property all the time before calling the method that utilized the frequently changing dependency, setting up the possibility of temporal coupling.

Temporal coupling is the idea that the order of execution must occur in a specific way for things to work correctly.

Constructor and property injection are generally used when you have one dependency that isn’t going to change often, so they aren’t appropriate for use when your dependency may be one of many implementations.

This is where method injection comes in.

Method injection allows you to inject a dependency right at the point of use so that you can pass any implementation you want without having to worry about storing it for later use. It’s often used when you pass in other information that needs special handling. For example:

interface IFoodPreparer {
    prepareFood(aRecipe: Recipe);
  }

  class Baker implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      console.log('Use baking skills to do the following: ' + aRecipe.Text);
    }
  }

  class ShortOrderCook implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      console.log('Use the grill to do the following: ' + aRecipe.Text);
    }
  }

  class Chef implements IFoodPreparer {
    prepareFood(aRecipe: Recipe) {
      'Use well-trained culinary skills to prepare the following: ' +
        aRecipe.Text;
    }
  }

  class Restaurant {
    private _name: string;
    constructor(aName: string) {
      this._name = aName;
    }

    set Name(value: string) {
      this._name = value;
    }

    get Name(): string {
      return this._name;
    }

    MakeFood(aRecipe: Recipe, aPreparer: IFoodPreparer) {
      aPreparer.prepareFood(aRecipe);
    }
  }

Here we have the notion of a recipe, which may require a different preparer depending on that recipe. Only the calling entity will know what the proper preparer type will be for a given recipe. For instance, one recipe might require a short-order cook, and another recipe might require a baker or a chef. We don’t know when writing the code what kind of IFoodPreparer will be needed, and thus we can’t really pass the dependency in the constructor and be stuck with that one implementation.

It is also clumsy to set a property every time a new or different IFoodPreparer is required. And setting the property in such a way induces temporal coupling and will suffer from thread-safety issues because it would require a lock around the code in a threaded environment.

The best solution is to just pass the IFoodPreparer into the method at the point of use.

Method injection should be used when the dependency could change with every use, or at least when you can’t be sure which dependency will be needed at the point of use.

Here’s an example of using method injection when the dependency needs to change every time it is used. Imagine a situation where a car-painting robot requires a new paint-gun tip after every car it paints. You might start out like this, using constructor injection:

interface IPaintGunTip {
    SprayCar(aColor: string);
  }

  class PaintGunTip implements IPaintGunTip {
    SprayCar(aColor: string) {
      console.log('Spray the car with ', aColor);
    }
  }

  class CarPaintingRobot {
    private _paintGunTip: IPaintGunTip;

    constructor(aPaintGunTip: IPaintGunTip) {
      CheckNeitherNullNorUndefined(aPaintGunTip);
      this._paintGunTip = aPaintGunTip;
    }

    paintCar(aColor: string) {
      this._paintGunTip.SprayCar(aColor);

      // what now?  The tip is no good, how do we get a new one?
    }
  }

Here, when we paint the car, we have to get a new paint gun tip. But how? When we paint the car, the tip is no good anymore, but it’s an interface, and we have no way to manually free it, and even if we did, what would we do the next time we need to paint a car? We don’t know what kind of tip is needed for a given car, and if we have properly separated our concerns, we don’t even know anything about creating a new tip. What to do? Well, use method injection instead:

  class CarPaintingRobotWithMethodInjection {
    public PaintCar(aColor: string, aPaintGunTip: IPaintGunTip) {
      CheckNeitherNullNorUndefined(aPaintGunTip);
      aPaintGunTip.SprayCar(aColor);
    }
  }

When implementing a method using method injection, you must include a guard clause. The dependency will be immediately used, and, of course, if you try to use it when it’s null, you’ll get an immediate error. This should obviously be avoided.

Now, when we pass the dependency directly to the method, the interface goes out of scope when we are done painting and the paint gun tip is destroyed. In addition, the next time a car needs to be painted, the consumer will pass in a new tip, which will be freed upon use. Method injection to the rescue!

 

When to Use Method Injection

Method injection is useful in two scenarios: 

  1. When the implementation of a dependency will vary. 
  2. When the dependency needs to be renewed after each use. In both cases, it’s up to the caller to decide what implementation to pass to the method.

 

Benefits of Dependency Injection

We’ve covered the “what” and the “how,” but you might be asking: “Why should we do all this?” Why go to all the trouble to arrange our code in the particular way called for by the principles of dependency injection?

Well, because there are benefits. Let’s talk a bit about those benefits because they are many, and they are compelling.

 

1. Code Using Dependency Injection is Maintainable

Probably the main benefit of dependency injection is maintainability. If your classes are loosely coupled and follow the single responsibility principle — the natural result of using DI — then your code will be easier to maintain.

Simple, stand-alone classes are easier to fix than complicated, tightly coupled classes.

Maintainable code has a lower total cost of ownership. Maintenance costs often exceed the cost of building the code in the first place, so anything that improves the maintainability of your code is a good thing. We all want to save time and money, right?

 

2. Code Using Dependency Injection Is Testable

Along the same lines as maintainability is testability. Code that is easy to test is tested more often. More testing means higher quality.

Loosely coupled classes that only do one thing — again, the natural result of using DI — are very easy to unit test. By using dependency injection, you make creating test doubles, commonly called “mocks,” much more straightforward.

If you pass dependencies to classes, it’s quite simple to pass in a test double implementation. If dependencies are hard-coded, it’s impossible to create test doubles for those dependencies.

Testable code that actually is tested is quality code. Or at least, it’s of higher quality than untested code.

I find it hard to accept the argument that unit tests are a waste of time. They are always worth the time to me. Surely, I’m not the only one who finds it strange that this is even in dispute?

 

3. Code Using Dependency Injection Is Readable

Code that uses dependency injection is more straightforward. It follows the single responsibility principle and, thus, results in smaller, more compact, and to-the-point classes.

Constructors aren’t as cluttered and filled with logic. Classes are more clearly defined, openly declaring what they need. Because of all this, dependency injection-based code is more readable. And more readable code is more maintainable.

 

4. Code Using Dependency Injection Is Flexible

Loosely coupled code — yet again, the result of using dependency injection — is more flexible and usable in different ways. Small classes that do one thing can more easily be reassembled and reused in different situations.

Small classes are like Lego blocks — they can easily be pieced together to make a multitude of things, as opposed to Duplo blocks, which are bulkier and less flexible. Being able to reuse code saves time and money.

All software needs to be able to change and adapt to new requirements. Loosely coupled code that uses dependency injection is flexible and able to adapt to those changes.

 

5. Code Using Dependency Injection Is Extensible

Code that uses dependency injection results in a more extendable class structure. By relying on abstractions instead of implementations, code can easily vary a given implementation.

When you code against abstractions, you can code with the notion that a radically better implementation of what you are doing is just around the corner.

Small, flexible classes can be extended easily, either by inheritance or composition.

An application’s codebase never remains static, and you will very likely need to add new features as your codebase grows and new requirements arise. Extensible code is up to that challenge.

 

6. Code Using Dependency Injection Is Shareable

If you are on a team and that team needs to work together on a project (when is that not true?), then dependency injection will facilitate team development. Even if you are working alone, your work will likely be passed to someone in the future.

Dependency injection calls for you to code against abstractions and not implementations.

If you have two teams working together, each needing the other’s work, you can define the abstractions before doing the implementations. Then each team can write their code using the abstractions, even before the implementations are written.

Also, because code is loosely coupled, those implementations won’t rely on each other, and thus they are easily split between teams.

A tutorial on dependency injection. | Video: CodeAesthetics

More on Software DevelopmentA Guide to React Hooks With Examples

 

Why Use Dependency Injection

Dependency injection is fundamentally about injecting dependencies rather than creating them, a simple yet powerful concept that underpins better coding practices. It's about coding against abstractions, asking for what you need, and embracing loosely coupled code. DI is not just a technique, but a pathway to writing maintainable, testable, readable, flexible, and extensible code. 

Understanding and applying the three methods of DI can significantly enhance your code’s quality. Though it requires thoughtful planning and design, the effort pays off in making code maintenance significantly easier and more efficient, a dream for any developer.

Hiring Now
GitLab
Cloud • Security • Software • Cybersecurity • Automation
SHARE