Search code examples
c#dependency-injectioninversion-of-control

How does Inversion of Control help me?


I'm trying to understand Inversion of Control and how it helps me with my unit testing. I've read several online explanations of IOC and what it does, but I'm just not quite understanding it.

I developed a sample project, which included using StructureMap for unit testing. StructureMap setup code like the following:

private readonly IAccountRepository _accountRepository

public Logon()
{
    _accountRepository = ObjectFactory.GetInstance<IAccountRepository>();
}

The thing I'm not understanding though, is as I see it, I could simply declare the above as the following:

AccountRepository _accountRepository = new AccountRepository();

And it would do the same thing as the prior code. So, I was just wondering if someone can help explain to me in a simple way, what the benefit of using IOC is (especially when dealing with unit testing).

Thanks


Solution

  • Inversion of Control is the concept of letting a framework call back into user code. It's a very abstract concept but in essence describes the difference between a library and a framework. IoC can be seen as the "defining characteristic of a framework." We, as program developers, call into libraries, but frameworks instead call into our code; the framework is in control, which is why we say the control is inverted. Any framework supplies hooks that allow us to plug in our code.

    Inversion of Control is a pattern that can only be applied by framework developers, or perhaps when you're an application developer interacting with framework code. IoC does not apply when working with application code exclusively, though.

    The act of depending on abstractions instead of implementations is called Dependency Inversion, and Dependency Inversion can be practiced by both application and framework developers. What you refer to as IoC is actually Dependency Inversion, and as Krzysztof already commented: what you're doing is not IoC. I'll discuss Dependency Inversion for the remainder of my answer.

    There are basically two forms of Dependency Inversion:

    • Service Locator
    • Dependency Injection.

    Let's start with the Service Locator pattern.

    The Service Locator pattern

    A Service Locator supplies application components outside the [startup path of your application] with access to an unbounded set of dependencies. As its most implemented, the Service Locator is a Static Factory that can be configured with concrete services before the first consumer begins to use it. (But you’ll equally also find abstract Service Locators.) [source]

    Here's an example of a static Service Locator:

    public class Service
    {
        public void SomeOperation()
        {
            IDependency dependency = 
                ServiceLocator.GetInstance<IDependency>();
                
            dependency.Execute();
        }
    }
    

    This example should look familiar to you, because this what you're doing in your Logon method: You are using the Service Locator pattern.

    We say that a Service Locator supplies access to an unbounded set of dependencies, because the caller can pass in any type it wishes at runtime. This is opposite to the Dependency Injection pattern.

    The Dependency Injection pattern

    With the Dependency Injection pattern (DI), you statically declaring a class's required dependencies; typically, by defining them in the constructor. The dependencies are made part of the class's signature. The class itself isn't responsible for getting its dependencies; that responsibility is moved up up the call stack. When refactoring the previous Service class with DI, it would likely become the following:

    public class Service
    {
        private readonly IDependency dependency;
    
        public Service(IDependency dependency)
        {
            this.dependency = dependency;
        }
    
        public void SomeOperation()
        {
            this.dependency.Execute();
        }
    }
    

    Comparing both patterns

    Both patterns are Dependency Inversion, since in both cases the Service class isn't responsible of creating the dependencies and doesn't know which implementation it is using. It just talks to an abstraction. Both patterns give you flexibility over the implementations a class is using and thus allow you to write more flexible software.

    There are, however, many problems with the Service Locator pattern, and that's why it is considered an anti-pattern. You are already experiencing these problems, as you are wondering how Service Locator in your case helps you with unit testing.

    The answer is that the Service Locator pattern does not help with unit testing. On the contrary: it makes unit testing harder compared to DI. By letting the class call the ObjectFactory (which is your Service Locator), you create a hard dependency between the two. Replacing IAccountRepository for testing, also means that your unit test must make use of the ObjectFactory. This makes your unit tests harder to read. But more importantly, since the ObjectFactory is a static instance, all unit tests make use of that same instance, which makes it hard to run tests in isolation and swap implementations on a per-test basis.

    I used to use a static Service Locator pattern in the past, and the way I dealt with this was by registering dependencies in a Service Locator that I could change on a thread-by-thread basis (using [ThreadStatic] field under the covers). This allowed me to run my tests in parallel (what MSTest does by default) while keeping tests isolated. The problem with this, unfortunately, was that it got complicated really fast, it cluttered the tests with all kind of technical stuff, and it made me spent a lot of time solving these technical problems, while I could have been writing more tests instead.

    But even if you use a hybrid solution where you inject an abstract IObjectFactory (an abstract Service Locator) into the constructor of Logon, testing is still more difficult compared to DI because of the implicit relationship between Logon and its dependencies; a test can't immediately see what dependencies are required. On top of that, besides supplying the required dependencies, each test must now supply a correctly configured ObjectFactory to the class.

    Conclusion

    The real solution to the problems that Service Locator causes is DI. Once you statically declare a class's dependencies in the constructor and inject them from the outside, all those issues are gone. Not only does this make it very clear what dependencies a class needs (no hidden dependencies), but every unit test is itself responsible for injecting the dependencies it needs. This makes writing tests much easier and prevents you from ever having to configure a DI Container in your unit tests.