top of page

Introduction to Dependency Injection(DI)

Updated: Jun 29, 2023

Dependency Injection (DI) is a widely adopted design pattern that effectively reduces tight coupling between software components, resulting in more modular and maintainable code.


At its core, DI enables the implementation of Inversion of Control (IoC) by allowing the creation of dependent objects outside of a class and providing them to the class through various means. This approach shifts the responsibility of creating and binding dependent objects away from the class that relies on them.


The Dependency Injection pattern involves three types of classes:

  1. Client Class: The client class, also known as the dependent class, is a class that requires the functionality or services provided by another class, known as the service class. The client class relies on the service class to perform specific tasks or fulfill certain responsibilities.

  2. Service Class: The service class, on the other hand, is a class that provides the required services to the client class. It encapsulates the implementation details of a particular functionality and exposes methods or properties that the client class can utilize. The service class typically defines a well-defined interface that the client class can interact with.

  3. Injector Class: The injector class plays a crucial role in implementing DI. It is responsible for injecting (providing) the instance of the service class into the client class. The injector class determines how the dependency is resolved and instantiated, whether through constructor injection, method injection, or property injection.

    • Constructor Injection: The injector class creates an instance of the service class and passes it as a parameter to the constructor of the client class during instantiation.

    • Method Injection: The injector class invokes a specific method on the client class, passing the instance of the service class as an argument.

    • Property Injection: The injector class sets a property of the client class with the instance of the service class after both objects have been created.

By utilizing DI, the client class remains decoupled from the specific implementation details of the service class. This decoupling enhances code maintainability, reusability, and testability. It becomes easier to substitute different implementations of the service class, enabling flexibility and promoting modular design.


The following figure illustrates the relationship between these classes:

Dependency Injection

Dependency Injection


Types of Dependency Injection

  • Constructor Injection - Inject the instance of the dependency class in the constructor of the dependent class.

  • Property Injection - Inject the instance of the dependency class in a property of the dependent class.

  • Method Injection - Inject the instance of the dependency class in a method/action of the dependent class.


Constructor Injection

Here is an example demonstrating constructor injection in C#:

public class CustomerBusinessLogic 
{     
    private ICustomerDataAccess _dataAccess;      
    
    public CustomerBusinessLogic(ICustomerDataAccess custDataAccess)     
    {         _dataAccess = custDataAccess;     
    }      
    
    public string ProcessCustomerData(int id)     
    {         
        return _dataAccess.GetCustomerName(id);     
    } 
}  

public interface ICustomerDataAccess 
{     
    string GetCustomerName(int id); 
}  

public class CustomerDataAccess : ICustomerDataAccess 
{     
    public string GetCustomerName(int id)     
    {         
        // Get the customer name from the database in a real application
        return "Dummy Customer Name";     
    } 
}  

public class CustomerService 
{     
    private CustomerBusinessLogic _customerBL;      
    
    public CustomerService()     
    {         
        _customerBL = new CustomerBusinessLogic(new CustomerDataAccess());     
    }      
    
    public string GetCustomerName(int id)     
    {         
        return _customerBL.ProcessCustomerData(id);     
    } 
} 

In the above example, the CustomerBusinessLogic class includes a constructor that accepts an ICustomerDataAccess object as a parameter. This is the constructor injection. The _dataAccess field is then assigned the provided ICustomerDataAccess object.

The ICustomerDataAccess interface defines the contract for accessing customer data, and the CustomerDataAccess class implements this interface by providing a dummy implementation of the GetCustomerName method.

The CustomerService class demonstrates how the dependency is injected into the CustomerBusinessLogic class. In the constructor of CustomerService, an instance of CustomerDataAccess is created, and that instance is passed to the CustomerBusinessLogic constructor. This ensures that the CustomerBusinessLogic class is provided with the necessary dependency, and it doesn't need to create an instance of CustomerDataAccess itself.

By using constructor injection, the classes CustomerBusinessLogic and CustomerDataAccess become loosely coupled. The dependency between them is resolved externally, allowing for easier substitution of different implementations of the ICustomerDataAccess interface and promoting flexibility and modularity in the code.

Property Injection

Here is an example demonstrating property injection in C#:

public class CustomerBusinessLogic
{
    public CustomerBusinessLogic()
    {
    }

    public string GetCustomerName(int id)
    {
        return DataAccess.GetCustomerName(id);
    }

    public ICustomerDataAccess DataAccess { get; set; }
}

public class CustomerService
{
    private CustomerBusinessLogic _customerBL;

    public CustomerService()
    {
        _customerBL = new CustomerBusinessLogic();
        _customerBL.DataAccess = new CustomerDataAccess();
    }

    public string GetCustomerName(int id)
    {
        return _customerBL.GetCustomerName(id);
    }
}

In the above example, the CustomerBusinessLogic class includes a public property named DataAccess of type ICustomerDataAccess. This property is used for property injection. It allows an external entity to set an instance of a class that implements the ICustomerDataAccess interface.


The CustomerService class demonstrates how the dependency is injected into the CustomerBusinessLogic class using property injection. In the constructor of CustomerService, an instance of CustomerBusinessLogic is created, and then the DataAccess property of _customerBL is set to a new instance of CustomerDataAccess. This ensures that the CustomerBusinessLogic class has access to the necessary dependency through the DataAccess property.


By using property injection, the dependency is set after the object is constructed, providing more flexibility in injecting dependencies. It allows for easier configuration and reconfiguration of dependencies at runtime. However, it's important to note that the DataAccess property must be set before it is used, otherwise, it could lead to null reference exceptions if not properly initialized.


Property injection is an alternative to constructor injection and can be useful in certain scenarios where the dependency may change over time or needs to be configured separately from the object construction.


Method Injection

Here is an example demonstrating method injection using an interface in C#:

interface IDataAccessDependency
{
    void SetDependency(ICustomerDataAccess customerDataAccess);
}

public class CustomerBusinessLogic : IDataAccessDependency
{
    private ICustomerDataAccess _dataAccess;

    public CustomerBusinessLogic()
    {
    }

    public string GetCustomerName(int id)
    {
        return _dataAccess.GetCustomerName(id);
    }

    public void SetDependency(ICustomerDataAccess customerDataAccess)
    {
        _dataAccess = customerDataAccess;
    }
}

public class CustomerService
{
    private CustomerBusinessLogic _customerBL;

    public CustomerService()
    {
        _customerBL = new CustomerBusinessLogic();
        ((IDataAccessDependency)_customerBL).SetDependency(new CustomerDataAccess());
    }

    public string GetCustomerName(int id)
    {
        return _customerBL.GetCustomerName(id);
    }
}

In the above example, an interface called IDataAccessDependency is defined, which includes a method named SetDependency. The CustomerBusinessLogic class implements this interface and provides an implementation for the SetDependency method.


The CustomerBusinessLogic class also includes a private field _dataAccess of type ICustomerDataAccess, which represents the dependency.


The CustomerService class demonstrates how the method injection works. In the constructor of CustomerService, an instance of CustomerBusinessLogic is created. Then, the SetDependency method is called on the CustomerBusinessLogic object, casting it to the IDataAccessDependency interface. This allows the CustomerDataAccess object to be passed as an argument and set as the dependency for the CustomerBusinessLogic object.


By using method injection, the dependency is set through a method call, which provides flexibility in injecting dependencies at runtime. It allows for decoupling between the dependent class and the dependency, as the dependent class does not need to have knowledge of the specific implementation of the dependency. Instead, it relies on the interface provided by the IDataAccessDependency interface.


Method injection is particularly useful when the dependency may change over time or needs to be dynamically provided to the dependent class.


Benefits of using DI

  1. Unit Testing: DI facilitates unit testing by allowing dependencies to be easily replaced with mock objects or test doubles. This enables isolated testing of individual components, improving testability and making it easier to write comprehensive unit tests.

  2. Reduced Boilerplate Code: DI reduces the amount of boilerplate code required for initializing dependencies. Instead of manually creating and managing dependencies within a class, the injector component takes care of providing the necessary dependencies. This leads to cleaner and more concise code.

  3. Easy Application Extension: DI makes it easier to extend the application by adding or replacing components without modifying existing code. New dependencies can be injected into classes without changing their implementation, promoting modularity and enabling flexible application design.

  4. Loose Coupling: DI promotes loose coupling between components. By injecting dependencies rather than creating them internally, classes become less dependent on specific implementations and more focused on interfaces or abstractions. This enhances flexibility, reusability, and maintainability.


Disadvantages of DI

  1. Complexity: DI can be initially challenging to grasp, especially for developers new to the concept. It requires understanding the principles, patterns, and frameworks associated with DI. Overusing DI or applying it improperly can lead to management issues and introduce unnecessary complexity into the codebase.

  2. Runtime Errors: DI can shift some compile-time errors to runtime errors. Since dependencies are resolved dynamically, errors related to missing or incompatible dependencies may not be discovered until runtime. Proper testing and thorough verification of dependencies are crucial to avoid such issues.

  3. Impact on IDE Automation: Dependency injection frameworks often rely on reflection or dynamic programming techniques, which can hinder some IDE automation features. For example, automated refactoring tools or finding references might not work as effectively with injected dependencies. This limitation can impact development productivity in certain scenarios.

0 comments

コメント


bottom of page