DEV Community

Cover image for What is dependency injection
mahdi
mahdi

Posted on

What is dependency injection

In the realm of object-oriented programming (OOP), dependency injection (DI) emerges as a cornerstone technique for managing dependencies within software systems. Dependencies, in this context, represent essential resources – often other components within the application – that a piece of code relies upon to execute its intended function.

The Tight Coupling Quandary:

Traditionally, code would directly instantiate or locate the dependencies it requires. This approach, while seemingly straightforward, fosters tight coupling between the code and specific implementations of those dependencies. Imagine a class responsible for sending emails. In a tightly coupled approach, this class might directly create a connection to an email server. This tight coupling presents significant challenges when it comes to:

  • Modification: Altering code that is tightly coupled to specific dependencies becomes a cumbersome task. If the email server API changes, the code responsible for sending emails would need to be adapted as well. This rippling effect can lead to maintenance headaches and bugs.
  • Testing: Unit testing becomes more intricate. How can you effectively test the email sending functionality if it's inextricably linked to a concrete email server implementation? Mocking external dependencies becomes necessary, adding complexity to the testing process.
  • Reusability: Reusing code across diverse contexts becomes difficult due to its reliance on specific dependencies. The email sending class, tightly coupled to a particular email server, cannot be easily integrated into other applications that might use different email providers.

Dependency Injection: A Paradigm Shift:

Dependency injection offers a paradigm shift by decoupling the creation and usage of dependencies. Here's how it achieves this:

  • Explicit Dependency Declaration: Code explicitly declares the dependencies it necessitates, often through constructor arguments or dedicated methods. This transparency fosters a clear understanding of a component's requirements. Imagine the email sending class now taking an interface as a constructor argument, representing the email sending functionality. This interface can be implemented by various concrete email server providers.
  • External Dependency Management: An external mechanism, frequently a framework or custom code, assumes responsibility for the creation and lifecycle management of these dependencies. This separation of concerns promotes loose coupling. A dedicated dependency injection framework can handle creating the appropriate email server provider based on configuration and inject it into the email sending class.
  • Dependency Injection: During object creation, the external provider injects the requisite dependencies into the code. This process ensures that the code receives the necessary resources without being burdened with the intricacies of their creation. The dependency injection framework would inject the concrete email server provider implementation into the email sending class.

Dependency Injection within Frameworks:

Many application frameworks seamlessly integrate dependency injection functionalities. These frameworks often leverage a combination of dependency injection and inversion of control (IoC). In IoC, the framework exerts control over the object lifecycle, encompassing the creation of objects and the injection of dependencies. By embracing dependency injection, developers gain greater control over the flow of their application and

pave the way for the creation of more adaptable, maintainable, and well-structured codebases.

In essence, dependency injection empowers you to write code that is more modular, easier to test, and more adaptable to evolving requirements. It fosters the creation of loosely coupled systems that are a hallmark of well-designed, maintainable software.

roles

Services and clients Services are the components that provide functionality, and clients are the components that use the services. Dependency injection decouples the clients from the services they use.

Interfaces Interfaces define contracts that specify how clients can interact with services. This abstraction allows different implementations of services to be injected interchangeably.

Injectors Injectors are responsible for providing the dependencies required by a component. They locate the dependencies and inject them into the dependent component, typically using a framework or a custom implementation.

Analogy As an analogy, cars can be thought of as services which perform the useful work of transporting people from one place to another. Car engines can require gas, diesel or electricity, but this detail is unimportant to the client—a driver—who only cares if it can get them to their destination. Cars present a uniform interface through their pedals, steering wheels and other controls. As such, which engine they were 'injected' with on the factory line ceases to matter and drivers can switch between any kind of car as needed.

What is dependency injection used for?

Dependency injection is used to make a class independent of its dependencies or to create a loosely coupled program. Dependency injection is useful for improving the reusability of code. Likewise, by decoupling the usage of an object, more dependencies can be replaced without needing to change class.

Dependency injection also aids in following the SOLID principles of object-oriented design. There are five aspects of OOP, with each letter representing a principle:

  • Single responsibility.
  • Open/closed.
  • Liskov substitution.
  • Interface segregation.
  • Dependency inversion. None of these principles are exclusive, as some of these represent multiple strategies that pursue a single goal. Or, in other cases, adherence to one SOLID practice may naturally lead to another.

Advantages of dependency injection

  1. Decoupling and Modularization:

    • Dependency injection decouples components by removing the responsibility of resource instantiation from the dependent objects.
    • This promotes modularization, as components become independent of how their dependencies are created or located.
  2. Ease of Dependency Swapping:

    • Dependency injection allows for easy swapping of dependencies, including substituting real dependencies with mock dependencies for testing purposes.
    • The framework or container manages the instantiation and injection of dependencies, facilitating seamless substitution without modifying client code.
  3. Centralized Configuration:

    • Dependency injection centralizes configuration data, typically through external configuration files or annotations.
    • This centralized configuration simplifies maintenance by ensuring that updates or modifications to dependencies are applied in one place, reducing the risk of inconsistencies.
  4. Customization and Extensibility:

    • Injected resources can be easily customized and configured through external configuration files or annotations, without requiring modifications to the source code.
    • This enables developers to tailor the behavior of components without altering their implementation, enhancing flexibility and extensibility.
  5. Improved Testability:

    • Dependency injection enhances the testability of code by facilitating the isolation of dependencies during unit testing.
    • Mock dependencies can be injected to simulate different scenarios, enabling comprehensive testing of component behavior under various conditions.
  6. Maintainability and Reusability:

    • By removing the knowledge of dependency implementation details from clients, dependency injection improves code maintainability.
    • Components become more reusable as they are no longer tightly coupled to specific implementations of their dependencies, promoting code reuse across different contexts.
  7. Enhanced Developer Productivity:

    • Dependency injection enables developers to work more independently on separate components, as they only need to adhere to the interface of the dependencies.
    • This autonomy fosters parallel development and collaboration, leading to increased productivity and faster iteration cycles.
  8. Configuration Flexibility:

    • Dependency injection allows for externalization of configuration details into configuration files or annotations.
    • This flexibility enables the system to be reconfigured without the need for recompilation, simplifying deployment and maintenance processes.

Methods of dependency injection

There is more than one way to employ dependency injection. The most common way is using constructor injection, which requires that all software dependencies be provided when an object is first created. However, constructor injection assumes the entire system is using this software design pattern, which means the entire system must be refactored at the same time. This is difficult, risky and time-consuming.

An alternative approach to constructor injection is service locator, a pattern that software designers can implement slowly, refactoring the application one piece at a time as convenient. Slow adaptation of existing systems is better than massive conversion efforts. So, when adapting an existing system, dependency injection via service locator is the best pattern to use.
There are those who criticize the service locator pattern, saying it replaces the dependencies rather than eliminating the tight coupling. However, some programmers insist that, when updating an existing system, it is valuable to use the service locator during the transition, and when the entire system has been adapted to the service locator, only a small additional step is needed to convert to constructor injection.

Types of dependency injection

1. Constructor Injection: A Transparent and Well-Defined Approach

  • Mechanism: Dependencies are explicitly provided as arguments to a class's constructor during object instantiation. This fosters a transparent and well-defined approach, fostering clarity from the outset.
  • Benefits:

    • Loose Coupling: Classes depend on interfaces, not concrete implementations, promoting flexibility and adaptability within your software architecture. You can easily swap out specific implementations of a dependency without modifying the core functionality of the class.
    • Enhanced Testability: Mock dependencies can be seamlessly injected during unit testing, leading to more isolated and reliable tests. This ultimately strengthens your test suite's robustness by ensuring each component is tested independently of external dependencies.
    • Improved Code Readability: Dependencies are explicitly declared within constructors, enhancing code comprehension for both current and future developers working on your project. By glancing at the constructor, you can instantly understand the class's required dependencies.
  • Drawbacks:

    • Refactoring Requirements: Existing code might necessitate modification to accept dependencies through constructors, potentially requiring a significant upfront investment in terms of development time. Especially in large codebases, adapting existing classes to work with constructor injection can be a daunting task.
    • Constructor Overload: A class with numerous dependencies can lead to lengthy and cumbersome constructors, potentially impacting readability. An excessive number of arguments in a constructor can make it difficult to understand the class's purpose and how to use it effectively.

2. Setter Injection: A Pragmatic Approach for Gradual Adoption

  • Mechanism: Dependencies are injected through setter methods within a class. The injector calls these methods to provide the necessary dependencies. This technique offers a more flexible approach, particularly valuable during the refactoring of legacy codebases.
  • Benefits:
    • Flexibility: Setter injection can be a pragmatic solution for existing code that might not be easily adaptable to constructor injection. It enables a smoother transition to a DI approach by allowing you to incrementally modify code to accept dependencies through setters.
  • Drawbacks:
    • Reduced Transparency: Compared to constructor injection, dependency relationships might become less evident when using setter injection. Since dependencies are injected at a later stage, the code might not explicitly declare its requirements upfront, potentially impacting code maintainability in the long run.
    • Testability Considerations: Mocking dependencies injected through setters can be more involved compared to constructor injection. In some cases, you might need to refactor the code or use additional techniques to effectively mock setter-injected dependencies during testing.

3. Interface Injection: Amplifying Loose Coupling

  • Mechanism: Similar to constructor injection, interfaces are injected instead of concrete implementations. However, the injection might occur through a dedicated injector method provided by the interface itself. This approach furthers the principle of loose coupling by ensuring classes only rely on abstract definitions, not specific implementations.
  • Benefits:

    • Enhanced Loose Coupling: Classes depend solely on interfaces, not concrete implementations, promoting an even greater degree of flexibility and adaptability within your codebase. By depending on interfaces, you can easily swap out concrete implementations without affecting the core functionality of the class. This makes your code more reusable and adaptable to evolving requirements.
  • Drawbacks:

    • Increased Code Complexity: This approach requires the creation and management of injector methods within interfaces, potentially adding complexity to your codebase. Especially in large projects, managing these injector methods can introduce additional maintenance overhead.

4. Method Injection: A Less Common, Yet Flexible Approach

  • Mechanism: A client class implements an interface. A method within that interface provides the dependency, and an injector uses the interface to supply the dependency to the class. This style is less commonly used but offers an alternative approach for injecting dependencies, particularly useful in specific scenarios.
  • Benefits:

    • Flexibility: Method injection offers an alternative approach for injecting dependencies, particularly useful in specific scenarios where constructor or setter injection might not be ideal. For instance, method injection might be useful if you need to inject dependencies conditionally based on certain runtime factors.
  • Drawbacks:

    • Less Common Usage: This pattern is less familiar for some developers, potentially impacting code maintainability, especially for those new to the codebase. Due to its less widespread use, developers unfamiliar with method injection might find the code harder to understand and reason about.

Top comments (20)

Collapse
 
krlz profile image
krlz

I think is useful in some situations (as any other pattern in any other situation) Combining Dependency Injection (DI) with the Singleton pattern can make testing easier by mocking dependencies and focusing on specific outputs. It's like having a single, global instance of something that's needed everywhere, but with the flexibility to swap it out for testing. This setup makes it simpler to test individual parts of your code without worrying about the whole system. Plus, it keeps your code clean and modular, making it easier to maintain and extend

Collapse
 
stefanak-michal profile image
Michal Štefaňák

Worst design pattern by my opinion.

Collapse
 
m__mdy__m profile image
mahdi

So, in your opinion, what design pattern is suitable?

Collapse
 
stefanak-michal profile image
Michal Štefaňák

I'm okay with others but this one really grinds me gears. 😁

Collapse
 
keyurparalkar profile image
Keyur Paralkar

@stefanak-michal can you tell me why it is the worst design pattern?

Collapse
 
stefanak-michal profile image
Michal Štefaňák

Let me clarify, it's just my personal opinion. For me it feels like lazyness. It adds extra layer of abstraction which can affects the performance. I know what the object needs so I'll give to it, I don't want to relay on some magic to do its job.

Thread Thread
 
emi_black_ace profile image
Jacob Van Wagoner

You can do dependency injection without abstraction. All it is is clean declaration of who owns what and who needs what. Further, it can be done right there at compile time so there isn't even the bootstrap startup time.

Collapse
 
ppaanngggg profile image
ppaanngggg

aha, I also don't like DI....

Collapse
 
artem1458 profile image
Artem Korniev

Good article, thanks!

If you're interested in typescript dependency injection with a bit of magic - check out Clawject dev.to/artem1458/clawject-simplify...

Collapse
 
m__mdy__m profile image
mahdi

Thank you, I will definitely look

Collapse
 
emi_black_ace profile image
Jacob Van Wagoner

That's such a weird picture at the beginning. Looks like an old AI image generator because of how awful the words look and the fact that the needle is in a nonsense spot.

Collapse
 
m__mdy__m profile image
mahdi

Yes, I made that picture with artificial intelligence, I just wanted it to look like dependency injection

Collapse
 
emi_black_ace profile image
Jacob Van Wagoner

I suppose it's a good representation of how it turns out when the junior dev team is unsupervised.

Collapse
 
efpage profile image
Eckehard

What distinguishes dependency injection from subscription patterns like event handlers?

Collapse
 
pcockerell profile image
Peter Cockerell

Almost everything. Dependency injection is about getting particular code modules available to the system via abstraction. Once they're bound, they largely stay that way until the system is reconfigured (for example to change an email provider, using the author's example).

Event handlers are about getting particular types of data into the system continuously during execution, and the event source is often bound and unbound throughout the runtime of the system.

Collapse
 
efpage profile image
Eckehard

Just to get it right: Assume your "system" is the actor, doing a particular job. With DI it get´s access to external code modules that it can call to do a job (like sending an email).

With an event handler, an external module (like the mailing system) can announce an event function to the "system". In case, the "system" want´s to send an email, it checks if there is an event function installed and calls it with the predefined parameters.

So, the main difference seems to be the knowledge of external dependencies, while the effect is very similar. Or did I get something wrong?

Thread Thread
 
m__mdy__m profile image
mahdi

Interesting, thanks for the explanation, I didn't know much about subscription patterns

Collapse
 
emi_black_ace profile image
Jacob Van Wagoner

DI isn't subscription at all. With DI, the one with the handle to the other object is in control of when it snags things. With subscription patterns it's inverted -- the other stuff, which the code currently in control is calling to, has no idea what's subscribed to it.

DI might be "one instance, many places each method can be called from" where subscription is "method called from one place (internally), trigger everyone else to call their methods."

Collapse
 
thatcomputerguy profile image
Griff Polk

Hehe the AI cover

Collapse
 
m__mdy__m profile image
mahdi

yep