Dependency Injection

Posted by Sneha Mahapatra on July 31, 2024 · 12 mins read

"I chooose a lazy person to do a hard job. Because a lazy person will find an easy way to do it."

Bill Gates

Introduction

To understand Dependency Injection it is important to understand Inversion of Control. Inversion of Control or IoC is a design principle which offloads control of the creation / management of an object to an external resource. This definition seems convoluted but it is a simple concept.

It is important to note that Inversion of Control is a principle, and therefore relies on a pattern to implement them. Let's use the Service Locator Pattern to implement IoC.

Inversion Control


// Service Locator class
public class ServiceLocator
{
    private static readonly Dictionary 
        services = new Dictionary();

    // Register a service
    public static void RegisterService(T service)
    {
        var type = typeof(T);
        if (!services.ContainsKey(type))
        {
            services.Add(type, service);
        }
    }

    // Get a service
    public static T GetService()
    {
        return (T)services[typeof(T)];
    }
}   
In the code above we have a simple ServiceLocator class. The class has a service member that keeps track of the type of services and the service object itself. It has RegisterService function that registers the class and a GetService function that gets a service. Let us say we are creating a Program and we need to log some information in our program. Lets create a Logger class.

// Interface for logging service
public interface ILogger
{
    void Log(string message);
}

// Implementation of the logging service
public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"Log: {message}");
    }
}
We have created two classes. One class is the ILogger class which is an interface. There is a function called Log which takes a string message. This class is important as it serves as the template to what other logging classes might look like. Inheriting this class we have created a type of Logger Class called ConsoleLogger. Essentially this class takes the Log function, overrides it, and has the Console Loggers objects log any information to the console. Now comes our IoC.

class Program
{
    static void Main(string[] args)
    {
        // Register the logging service with the Service Locator
        ServiceLocator.RegisterService(new ConsoleLogger());

        // Create and run the business process
        BusinessProcess process = new BusinessProcess();
        process.Process();
    }
}


// Business class using the Service Locator
public class BusinessProcess
{
    public void Process()
    {
        // Retrieve the logger from the service locator
        ILogger logger = ServiceLocator.GetService();
        logger.Log("Business process started");
        
        // Additional processing logic...
        logger.Log("Business process completed");
    }
}
Here we have a Program class with our Main function. We register a ConsoleLogger service with type ILogger. Next we have a BusinessProcess class. It has function and it calls for the service ILogger. Note here that BusinessProcess is not specifically calling ConsoleLogger. It is asking for any type Logger, and takes whatever is given and Logs information. It does not care where its logging, how its logging, how the function works, it just wants to log some information, and will use whatever type of logger is there.

We could create different classes of Loggers such as an EmailLogger–which emails a log file to a certain person, or TextLogger-which creates a text file of the logs that has transpired. The BusinessProcess class does not care for how to the logging is done, rather it just wants the information to be somehow logged. The BusinessProcess class has offloaded the creation / management of the object and handed it off to an extenral source-the ServiceLocator class. This is how the name came to be, it is an inversion of the application's flow to a framework. Instead of the logger class being created in the BusinessProcess class, it was created in the Program class first and then it was set aside while the BusinessProcess class was called, and then it was used after the creation of the BusinessProcess. It is not happening in step by step order, rather we have given control to the external sources.

Dependency Injection

Dependency Injection is a type of software design pattern, like how ServiceLocator is, that relies on IoC. Every class that implements Dependency Injection implements IoC. Dependency Injection is much more specific version of Ioc–instead of creating any kind of callback, we are looking at situation that where implementations are passed into an object through 3 different types of injections: constructors, property, and method injections. (Note this works for C# but may not be the same for other languages like Java). Below is an implementation of the 3 different types of injections.

Construction Injection

Construction Injection is exactly what is sounds like: we will dependency inject our dependency(object) through the constructor of our class. Below is the example we had used before. We have again ILogger and Logger.

    // Logger interface
    public interface ILogger
    {
        void Log(string message);
    }
    
    // ConsoleLogger implementation
    public class ConsoleLogger : ILogger
    {
        public void Log(string message)
        {
            Console.WriteLine($"Log: {message}");
        }
    }
However, the difference is in the BusinessService class.

// Business class using constructor injection
public class BusinessService
{
    private readonly ILogger _logger;

    // Constructor Injection
    public BusinessService(ILogger logger)
    {
        _logger = logger;
    }

    public void Process()
    {
        _logger.Log("Processing started...");
        // Additional processing logic...
        _logger.Log("Processing completed.");
    }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        ILogger logger = new ConsoleLogger();
        BusinessService service = new BusinessService(logger);
        service.Process();
    }
}
Beforehand, we had used the ServiceLocater pattern which used a ServiceLocater object to add different services. Now we are creating the ConsoleLogger object and injecting it into the logger. The BusinessService constructor has a parameter of ILogger logger and is not dependent on the type of the logger.

Property Injection

The difference in Property Injection is that instead of dependency injecting the Logger object through the constructor, we do it through the property (or the member) of the class.

    // Business class using property injection
    public class BusinessServiceWithProperty
    {
        public ILogger Logger { get; set; } // Property Injection
    
        public void Process()
        {
            Logger?.Log("Processing started...");
            // Additional processing logic...
            Logger?.Log("Processing completed.");
        }
    }
    
    // Usage
    class Program
    {
        static void Main(string[] args)
        {
            ILogger logger = new ConsoleLogger();
            BusinessServiceWithProperty service = new BusinessServiceWithProperty
            {
                Logger = logger // Injecting dependency through property
            };
            service.Process();
        }
    }

Here the logger object is created as type ILogger. It is then set in the Program class with the logger object instantiated in the Program class.

Method Injection

This is very similar to the Constructor Injection, after all a constructor is a special type of method. In this injection type we are Dependency injection into a method.

// Business class using method injection
public class BusinessServiceWithMethod
{
    public void Process(ILogger logger) // Method Injection
    {
        logger.Log("Processing started...");
        // Additional processing logic...
        logger.Log("Processing completed.");
    }
}

// Usage
class Program
{
    static void Main(string[] args)
    {
        ILogger logger = new ConsoleLogger();
        BusinessServiceWithMethod service = new BusinessServiceWithMethod();
        service.Process(logger); // Injecting dependency through method parameter
    }
}

Here we have injected an ILogger object into a method called Process.

Is this Dependency Injection?

Now let's take a look at a certain example.

    // Business class using method injection
    public class BusinessServiceWithMethod
    {
        public void Process(ConsoleLogger logger) // Method Injection
        {
            logger.Log("Processing started...");
            // Additional processing logic...
            logger.Log("Processing completed.");
        }
    }
    
    // Usage
    class Program
    {
        static void Main(string[] args)
        {
            BusinessServiceWithMethod service = new BusinessServiceWithMethod();
            service.Process(new ConsoleLogger()); // Injecting dependency through method parameter
        }
    }
    
Is the provided example above implementing Dependency Injection?
Is the provided example above implementing Dependency Injection?

Key Concepts of IoC and Dependency Injection

After learning about IoC and Dependency Injection it is important to understand why it is important and why it is used. There are many key ideas to the importance of IoC but it can be brought down to three points:

  • Decoupling IoC helps in reducing the tight coupling between classes by allowing an external entity to handle the creation and management of dependencies.
  • Flexibility By separating the dependency configuration from the application logic, it becomes easier to switch out implementations and configure them differently for various environments (e.g., testing, production).
  • Maintainability With IoC, code becomes easier to maintain because dependencies can be modified without changing the classes that use them.

WorkCited