DEV Community

gleuzio
gleuzio

Posted on

What is Dependency Injection?

Dependency Injection(DI) is a design pattern used in software development, specifically in object-oriented programming, to manage dependencies between objects.

First, let's understand the keywords Dependency and Injection.

Dependency: guaranteed, a piece of code needs other pieces of code to work together in harmony to create a meaningful application. Let's say we are creating an application for a printer. The printer cannot operate without an ink cartridge, so the Printer depends on the Cartridge class to perform its printing tasks. Therefore, we can say there is a dependency between the two classes.

Injection: It refers to the process of passing a dependency to the class that requires it. This process is called known as Dependency Injection.

Let me show how it would look in code for the printer application mentioned above. Then, I will explain the benefits of using Dependency Injection followed after.

Example of Dependency Injection (DI) in a printer application

using System;

public class Printer
{
    private readonly Cartridge _cartridge;

  // Dependency Injection: Dependency is passed through the constructor
    public Printer(Cartridge cartridge)
    {
        _cartridge = cartridge;
    }

    public void Print()
    {
        if (_cartridge.InkLevel > 0)
        {
            Console.WriteLine("Printing successfully.");
        }
        else
        {
            Console.WriteLine("Print failed: Insufficient ink.");
        }
    }
}

public class Cartridge
{
    public int InkLevel { get; set; }

    public Cartridge(int inkLevel)
    {
        InkLevel = inkLevel;
    }
}

public class Program
{
    public static void Main()
    {
        var cartridge = new Cartridge(50);  // 50% ink level
        var printer = new Printer(cartridge);
        printer.Print();  // Expected output: Printing successfully.

        var emptyCartridge = new Cartridge(0);  // 0% ink level
        var emptyPrinter = new Printer(emptyCartridge);
        emptyPrinter.Print();  // Expected output: Print failed: Insufficient ink.
    }
}

Enter fullscreen mode Exit fullscreen mode

Code Explanation:

In this example, the Printer class depends on the Cartridge class. Instead of creating a Cartridge object within the Printer class itself, the Cartridge is injected into the Printer class through the constructor. This is a simple demonstration of Constructor Injection, a common DI technique.

The Print method checks the ink level of the Cartridge. If the ink level is greater than 0, it prints successfully; otherwise, it prints a failure message.

In the Main method, two Printer objects are created with different Cartridge objects: one with 50% ink and another with 0% ink.

Now that we have seen the basic usage of Dependency Injection, let’s discuss the benefits it brings to software development.

Benefits of Dependency Injection (DI):

Improved Maintainability
With DI, classes are not tightly bound to their dependencies, making it easier to modify, extend, or replace dependencies without affecting the entire system. For example, if you wanted to switch to a different cartridge model, you would only need to update the Cartridge class, not the Printer class itself. This decoupling leads to more modular code and makes maintaining your system over time much easier.

Easier Testing
DI allows for easier and more controlled unit testing. By injecting mock or stub dependencies, you can test a class like Printer in isolation. Without DI, you would need to create real Cartridge objects, which might have external dependencies or complex behavior. With DI, you can inject simplified versions of the dependencies, allowing for faster, more predictable tests.

Without DI, testing the Printer class would require creating a Cartridge object and making sure it interacts with the printer as expected. This becomes more difficult if the Cartridge class has external dependencies, like interacting with a hardware device.

With DI, we can easily mock the Cartridge class in our tests:

[Test]
public void Printer_Print_Success_WhenInkLevelIsPositive()
{
    // Arrange
    var mockCartridge = new Mock<ICartridge>();
    mockCartridge.Setup(c => c.InkLevel).Returns(50);
    var printer = new Printer(mockCartridge.Object);

    // Act
    printer.Print();

    // Assert
    Assert.That(Console.Out.ToString(), Does.Contain("Printing successfully."));
}
Enter fullscreen mode Exit fullscreen mode

Separation of Concerns
DI encourages separation of concerns by delegating the responsibility of creating dependencies to an external source, such as a DI container or factory. This allows the Printer class to focus on printing, while the creation and configuration of the Cartridge class are handled elsewhere. This separation of responsibilities makes the code easier to maintain and understand.

Flexibility and Scalability
DI makes it easier to swap out different implementations of a dependency without modifying the dependent class. For example, you can easily change the implementation of Cartridge (e.g., from a standard cartridge to a high-capacity one) without changing the Printer class at all. This is particularly useful in large-scale applications, where dependencies may evolve over time.

Conclusion

Dependency Injection (DI) is a valuable design pattern that improves the maintainability, flexibility, and testability of your code. By promoting loose coupling between components, DI ensures that changes in one class do not affect others, making your codebase more modular and adaptable. Additionally, DI simplifies unit testing by allowing mock dependencies to be injected, rather than relying on real objects with complex behaviors or external dependencies.

In summary, Dependency Injection is essential for building clean, scalable, and easily testable applications. If you want to improve the quality of your code and reduce the complexity of managing dependencies, adopting DI is a great practice to follow.

Top comments (0)