Question

I apologize if this seems like yet another repeat of the question, but every time I find an article regarding the topic, it mostly just talks about what DI is. So, I get DI, but I'm trying to understand the need for an IoC container, which everyone seems to be getting into. Is the point of an IoC container really just to "auto-resolve" the concrete implementation of the dependencies? Maybe my classes tend to not have several dependencies and maybe that's why I don't see the big deal, but I want to make sure that I'm understanding the utility of the container correctly.

I typically break my business logic out into a class that might look something like this:

public class SomeBusinessOperation
{
    private readonly IDataRepository _repository;

    public SomeBusinessOperation(IDataRespository repository = null)
    {
        _repository = repository ?? new ConcreteRepository();
    }

    public SomeType Run(SomeRequestType request)
    {
        // do work...
        var results = _repository.GetThings(request);

        return results;
    }
}

So it only has the one dependency, and in some cases it might have a second or third, but not all that often. So anything that calls this can pass it's own repo or allow it to use the default repo.

As far as my current understanding of an IoC container goes, all the container does is resolve IDataRepository. But if that's all it does, then I'm not seeing a ton of value in it since my operational classes already define a fallback when no dependency passed in. So the only other benefit I can think of is that if I have several operations like this use the same fallback repo, I can change that repo in one place which is the registry/factory/container. And that's great, but is that it?

Was it helpful?

Solution

The IoC container isn't about the case where you have one dependency. It's about the case where you have 3 dependencies and they have several dependencies which have dependencies etc.

It also help you to centralize the resolution of a dependency, and life cycle management of dependencies.

OTHER TIPS

There are a number of reasons why you might want to use an IoC container.

Unreferenced dlls

You can use an IoC container to resolve a concrete class from an unreferenced dll. This means that you can take dependencies entirely on the abstraction - i.e. the interface.

Avoid the use of new

An IoC container means that you can completely remove the use of the new keyword for creating a class. This has two effects. The first is that it decouples your classes. The second (which is related) is that you can drop in mocks for unit testing. This is incredibly useful, particularly when you are interacting with a long running process.

Write Against Abstractions

Using an IoC container to resolve your concrete dependencies for you allows you to write your code against abstractions, rather than implementing every concrete class you need as you need it. For example, you may need your code to read data from a database. Instead of writing the database interaction class, you simply write an interface for it and code against that. You can use a mock to test the functionality of the code you're developing as you're developing it, rather than relying on developing the concrete database interaction class before you can test the other code.

Avoid brittle code

Another reason for using an IoC container is that by relying on the IoC container to resolve your dependencies, you avoid the need to change every single call to a class constructor when you add or remove a dependency. The IoC container will automatically resolve your dependencies. This isn't a huge issue when you're creating a class once, but it is a gigantic issue when you're creating the class in a hundred places.

Lifetime management and unmanaged resouce cleanup

The final reason I will mention is the management of object lifetimes. IoC containers often provide the ability to specify the lifetime of an object. It makes a lot of sense to specify the lifetime of an object in an IoC container rather than trying to manually manage it in code. Manual lifetime management can be very difficult. This can be useful when dealing with objects that require disposal. Instead of manually managing the disposal of your objects, some IoC containers will manage the disposal for you, which can help prevent memory leaks and simplify your codebase.

The problem with the sample code that you have provided is that the class you're writing has a concrete dependency on the ConcreteRepository class. An IoC container would remove that dependency.

According to the single responsibility principle every class must have only a single responsibility. Creating new instances of classes is just another responsibility, so you have to encapsulate this kind of code in one or more classes. You can do that using any creational patterns, for example factories, builders, DI containers etc...

There are other principles like inversion of control and dependency inversion. In this context they are related to the instantiation of dependencies. They state that the high level classes must be decoupled from the low level classes (dependencies) they use. We can decouple things by creating interfaces. So the low level classes have to implement specific interfaces and the high level classes have to utilize instances of classes which implement these interfaces. (note: REST uniform interface constraint applies the same approach on a system level.)

The combination of these principles by example (sorry for the low quality code, I used some ad-hoc language instead of C#, since I don't know that):

  1. No SRP, no IoC

    class SomeHighLevelService
    {
        public doFooBar(){
            Crap crap = doFoo();
            doBar(crap);
        }
    
        public Crap doFoo(){
            //...
            return crap;
        }
    
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
    
  2. Closer to SRP, no IoC

    class SomeHighLevelService
    {
        public SomeHighLevelService(){
            Foo foo = new Foo();
            Bar bar = new Bar();
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    SomeHighLevelService service = new SomeHighLevelService();
    service.doFooBar();
    
  3. Yes SRP, no IoC

    class HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new SomeHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new Foo();
        }
    
        private Bar getBar(){
            return new Bar();
        }
    }
    
    class SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    HighLevelServiceProvider provider = new HighLevelServiceProvider();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
    
  4. Yes SRP, yes IoC

    interface HighLevelServiceProvider {
        SomeHighLevelService getSomeHighLevelService();
    }
    
    interface SomeHighLevelService {
        doFooBar();
    }
    
    interface Foo {
        Crap doFoo();
    }
    
    interface Bar {
        doBar(Crap crap);
    }
    
    
    class ConcreteHighLevelServiceContainer implements HighLevelServiceProvider {
        public SomeHighLevelService getSomeHighLevelService(){
            SomeHighLevelService service = new ConcreteHighLevelService();
            service.setFoo(this.getFoo());
            service.getBar(this.getBar());
            return service;
        }
    
        private Foo getFoo(){
            return new ConcreteFoo();
        }
    
        private Bar getBar(){
            return new ConcreteBar();
        }
    }
    
    class ConcreteHighLevelService implements SomeHighLevelService
    {           
        public setFoo(Foo foo){
            this.foo = foo;
        }
    
        public setBar(Bar bar){
            this.bar = bar;
        }
    
        public doFooBar(){
            Crap crap = foo.doFoo();
            bar.doBar(crap);
        }
    
    }
    
    class ConcreteFoo implements Foo {
        public Crap doFoo(){
            //...
            return crap;
        }
    }
    
    class ConcreteBar implements Bar {
        public doBar(Crap crap){
            //...
        }
    }
    
    
    HighLevelServiceProvider provider = new ConcreteHighLevelServiceContainer();
    SomeHighLevelService service = provider.getSomeHighLevelService();
    service.doFooBar();
    

So we ended up having a code in which you can replace every concrete implementation with another which implements the same interface ofc. So this is good because the participating classes are decoupled from each other, they know only the interfaces. Another advantage that the code of the instantiation is reusable.

Licensed under: CC-BY-SA with attribution
scroll top