https://www.techtarget.com/searchapparchitecture/definition/dependency-injection
Dependency injection is a technique used in object-oriented programming (OOP) to reduce the hardcoded dependencies between objects. A dependency in this context refers to a piece of code that relies on another resource to carry out its intended function. Often, that resource is a different object in the same application.
Dependencies within an OOP application enable objects to perform their assigned tasks by providing additional functionality. For example, an application might include two class definitions: Class A and Class B. As part of its definition, Class B creates an instance of Class A to carry out a specific task, which means that Class B is dependent on Class A to carry out its function. The dependency is hardcoded into the Class B definition, resulting in code that is tightly coupled. Such code is more difficult to test, modify or reuse than loosely coupled code.
Instead of the dependency being hardcoded, it can be injected through a mechanism such as a class constructor or public property. In this scenario, Class A gets passed into Class B via a parameter, rather than Class B creating the object itself. Class B can then be compiled without including the entire Class A definition, resulting in a class that functions independently of its dependencies. The result is code that is more readable, maintainable, testable, reusable and flexible than tightly coupled code.
Dependency injection is consistent with the SOLID principles of object-oriented design. The principles provide a set of guidelines for developing code that is more maintainable and extensible than other types of code. SOLID is an acronym that represents the following five concepts:
Dependency inversion is of particular importance when it comes to dependency injection. Dependency inversion focuses on decoupling and abstracting code, rather than relying too heavily on concretions, which are hardcoded concrete implementations. Dependency inversion also ensures that high-level modules do not depend on low-level modules.
Dependency injection supports the dependency inversion principle by injecting dependencies into the class definitions instead of hardcoding them. In this way, it abstracts the details and ensures that high-level modules don't depend on low-level modules.
Dependency injection is also closely aligned with the inversion of control (IoC) software design principle, which states that a class should be configured by another class from the outside, as opposed to configuring dependencies statically. It asserts that a program is more pluggable, testable, usable and loosely coupled if the management of an application's flow is transferred to a different part of the application.
When interactions occur that need custom business logic, an IoC framework invokes code provided by the developer -- this is the inversion aspect. Ruby on Rails is an example of IoC in an application framework. The use of event-based user interfaces instead of ones controlled by procedural code are also examples of the IoC principle.
While there is some confusion and debate as to the relationship between IoC and dependency injection, the latter is generally considered to be a specific style of IoC.
According to Martin Fowler, chief scientist at Thoughtworks, a technology consultancy, the confusion arises due to the increase use of IoC containers. "The name is somewhat confusing since IoC containers are generally regarded as a competitor to Enterprise JavaBeans, yet EJB uses inversion of control just as much -- if not more," according to Fowler.
Dependency injection describes a specific use case of IoC where a container provides a configurable application context that creates, starts, catches and manages pluggable objects.
Dependency injection is implemented in OOP development through an application's class definitions. The components that participate in the injection typically play one of these four roles:
The service, client and interface roles are also used when implementing the dependency inversion principle.
OOP supports the following approaches to dependency injection:
Constructor injection is the most common approach to dependency injection. However, it requires that all software dependencies be provided when an object is first created. It also assumes the entire system is using this approach, which means the entire system must be refactored if a component needs to be updated, a process that can be 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 often better than a massive conversion effort.
Some programmers criticize the service locator pattern, saying it replaces the dependencies rather than eliminating the tight coupling. However, other programmers insist that, when updating an existing system, it is valuable to use the service locator during the transition. Then, when the entire system has been adapted to the service locator, only a small additional step is needed to convert to constructor injection.
When a client class requires outside services to carry out its intended purpose, the client must know what resources are needed, where to locate them and how to communicate with them. One way of structuring code is to embed the logic for accessing the services within the clients themselves.
However, this tightly coupled approach can be problematic. If a resource changes location or other information changes, the embedded code must be rewritten, which can get quite complex.
For example, when a user clicks a button, the event creates a new instance of a business logic layer, and one of the event's methods is called. Within that method, a new instance of a data access layer class is also formed, and one of its methods is called. In turn, this method makes a database query.
Another way to structure the code is to have clients declare their dependency on resources and enable an external piece of code to assume the responsibility for instantiating and configuring software components and their dependencies.
The external piece of code, which is decoupled, can be handcoded or implemented with a special software module called a dependency injection container or dependency injection framework. Essentially, the container or framework provides a map of the dependencies a client might need and logic for adding new dependencies to the map.
These relationships are summed up in the dependency inversion principle, which states, "High-level modules should not depend on low-level modules; both should depend on abstractions. Abstractions should not depend on details. Details should depend upon abstractions."
Many development teams use dependency injection because it offers several important benefits:
Although dependency injection can be beneficial, it also comes with several challenges:
Learn more about how dependencies can be a problem in microservices, particularly with the creation of circular dependencies. Also, explore how to use abstracted repositories in dependency injection, and check out in-demand programming languages devs should get to know.
20 Sep 2024