Codementor Events

The value of Dependency Injection

Published May 22, 2018Last updated Nov 17, 2018
The value of Dependency Injection

What is Dependency Injection (DI) and why would you want to use it? How do you get started? Hopefully I can answer those questions for you in a clear and easy to understand way.

In a world where TDD is mentioned everywhere, having a foundational knowledge of DI will serve you well. So, what is it? Well this is pretty easy to Google, but I will put it here in my own-ish words:

  • A technique where one object supplies the dependencies of another
  • A dependency is an object that can be used
  • Injection is the passing of a dependency to a dependent object that would use it

So why would you want to do this? I want to focus on two reasons DI is important. I will do so with very simple (completely for demonstration) code using C#, but these principles hold true for many other languages.

Feel free to follow along.

Single Responsibility

A major benefit of DI is removing the need for all of your objects to have to know how to implement all of its dependencies. To demonstrate this, we will look at a simple console app that has two services that both log using loggers.

Program.cs

class Program
{
  static void Main(string[] args)
    {
    	var logger = new Logger("Injected Logger");
        var service1 = new Service1(logger);
        var service2 = new Service2(logger);
        
        service1.Log1("Service 1");
        service1.Log2("Service 1");

        service2.Log1("Service 2");
        service2.Log2("Service 2");

        Console.ReadLine();
    }
}

Service1.cs

    public class Service1
    {
        private ILogger Logger1 { get; }
        private readonly ILogger Logger2 = new Logger("Not Injected Logger");

        public Service1(ILogger logger)
        {
            Logger1 = logger;
        }

        public void Log1(string message)
        {
            Logger1.Log(message);
        }

        public void Log2(string message)
        {
            Logger2.Log(message);
        }
    }

Service2.cs

    public class Service2
    {
        private ILogger Logger1 { get; }
        private ILogger Logger2 = new Logger("Not Injected Logger");

        public Service2(ILogger logger)
        {
            Logger1 = logger;
        }

        public void Log1(string message)
        {
            Logger1.Log(message);
        }

        public void Log2(string message)
        {
            Logger2.Log(message);
        }
    }

Logger.cs

    public interface ILogger
    {
        string SystemName { get; }

        void Log(string message);
    }

    public class Logger : ILogger
    {
        public Logger(string systemName)
        {
            SystemName = systemName;
        }

        public string SystemName { get; }

        public void Log(string message)
        {
            Console.WriteLine($"{SystemName}: {message}");
        }
    }

Running this program gives us the following:
Output 1.PNG

We have one logger that is injected by our Program.cs and another that is instantiated by both service1.cs and service2.cs. Pretty simple so far, but you can hopefully see where the "Not Injected Logger" could become problematic in a project that has tens or hundreds of classes that will need to log.

Let's say we need to change the name of both loggers to be UPPERCASE.

Doing so is simple with the injected logger. Simply update Program.cs.

var logger = new Logger("INJECTED LOGGER");

Output2.PNG
By making that one change, we updated the name of the injected logger in both services! We will have to go into both service classes to update the "Not Injected Logger"

Testing

This to me is the biggest benefit and core reason I use DI. When testing at the unit level (this does not include UI or End to End testing), you do not want your tests to leave the process (i.e. call out to a database or API).

To demonstrate this, we will change the Console.WriteLine() in the Logger.cs to throw new NotImplementedException();. Our goal now is to test the Service1.cs class to verify that the Log occurs in both Log1() and Log2() methods.

Now, we will add a unit test project and include the unit test class:
Service1Test.cs

    [TestClass]
    public class Service1Tests
    {
        [TestMethod]
        public void Log1Logs()
        {
            // Arrange
            var message = "Hi from log 1";
            var fakeLogger = new FakeLogger();
            var sut = new Service1(fakeLogger); // SUT stand for System Under Test

            // Act
            sut.Log1(message);

            // Assert
            Assert.AreEqual(message, fakeLogger.LoggedMessage);
        }
    }

    internal class FakeLogger : ILogger
    {
        public FakeLogger()
        {
            SystemName = "Fake Logger";
        }

        public string LoggedMessage = null;
        public string SystemName { get; }

        public void Log(string message)
        {
            LoggedMessage = message;
        }
    }

Once you add that code and run the test, it should pass.
TestRun1.PNG

If this is new to you I strongly recommend putting a breakpoint at the beginning of the test and debugging through everything to see how these pieces interact.

Since the test passed, that means that our Service1.cs used the FakeLogger instead of the normal Logger.

Now the real challenge... Write a similar test for the Log2() method on Service1.cs. Here is what I came up with:

        [TestMethod]
        public void Log2Logs()
        {
            // Arrange
            var message = "Hi from log 2";
            var fakeLogger = new FakeLogger();
            var sut = new Service1(fakeLogger); // SUT stand for System Under Test

            // Act
            sut.Log2(message);

            // Assert
            Assert.AreEqual(message, fakeLogger.LoggedMessage);
        }

You will notice that this test fails!
TestRun2.PNG

This is because, from the unit test, we have no way of changing Logger2 in Service1.cs. The Log2() method is not unit testable the way it is currently implemented due to the Logger2.Log() method.

Now imagine that Logger2.Log() called out to a database, or sent an email, or hit an API that cost money. Every time you ran your test, this would happen.

Conclusion

Dependency Injection can be an overwhelming and foreign concept. Hopefully you now have a better understanding of what it is and why it can be useful. Also, there are several kinds of DI. I showed constructor injection and I would recommend you look into it to figure out what is the best fit for you.

So what next? I would look into finding an IoC Tool (see Inversion of Control) as they complement what I went over here. I have created one to really simplify the process for WebAPI, MVC, and WCF web services.

You can check it out at NuGet and GitHub.

I am always looking for feedback and ways to improve, so feel free to message me or comment!

Hope you learned something new!

Sources:
Wikipedia on Dependency Injection

Discover and read more posts from Joe Cisar
get started
post commentsBe the first to share your opinion
Show more replies