Click here to Skip to main content
15,881,424 members
Articles / Programming Languages / C#

IoC and DI Tips

Rate me:
Please Sign up or sign in to vote.
4.67/5 (4 votes)
13 Sep 2013CPOL3 min read 12.7K   14  
Some design and architectural guidelines when using an IoC container and DI

Introduction

Inversion of Control reverts the traditional dependencies so that code cannot just be reused as a library of reusable classes that higher level code can call. It allows the higher level code to be shared and reused across projects, e.g. production code and test projects or reuse in product code for different purposes. In this article, I would like to share the experience I gained in the last 2 years using this technology on a brown field project with lots and lots of code with the goal to improve design and architecture for better testability, modifiability and extendibility.

Background

I started to look into design patterns and object oriented design principles (e.g. SOLID) to create a more object oriented design. Although most guide you to a more design object oriented design, I found that no other design principle than the Dependency Injection principle and the Inversion of Control pattern force the user to think in Objects.

Using the Code

The code below shows some simplified classes to teach the concepts. The concepts should be easily translatable to other frameworks and languages.

Before diving into the code, I list below the guiding rules for the design of the classes. I will explain the reason for the guiding rules at the end of the article.

  1. No code, other than the code that configures the IoC container should know about the container
  2. Only constructor injection should be used
  3. If always exactly 1 instance is used by the IoC created class, that instance should be injected to the constructor
  4. If the object used by your class is created dynamically or if an array of instances is used by the IoC created class, a factory method has to be injected in the constructor
  5. If a factory method was injected, a method to destruct the object has to be injected as well
  6. And the last one is:

  7. Never call new directly in your code for a class that should be managed by the IoC container

The first block of code shows some classes that follow these rules. Because of rule 1), there is no dependency to the underlying IoC container.

C#
// class that has a default constructor (simplest case)
public class A
{
    public A()
    {
        Console.WriteLine("newed A");
    }

    public void MethodOnA()
    {
        Console.WriteLine("called A");
    }
}

// class that has always one instance of A injected
public class B
{
    private A _a;
    public B(A a)
    {
        _a = a;
        Console.WriteLine("newed B");
    }

    public void MethodOnB()
    {
        _a.MethodOnA();
        Console.WriteLine("called B");
    }
}

// class that can 'create' A instances and also get a B instance injected
public class C
{
    private Func<A> _getA;
    private Action<A> _releaseA;
    private B _b;
    public C(Func<A> getA, Action<A> releaseA, B b)
    {
        _getA = getA;
        _releaseA = releaseA;
        _b = b;

        Console.WriteLine("newed C");
    }

    public void MethodOnC()
    {
        A a1 = _getA();
        a1.MethodOnA();
        A a2 = _getA();
        a2.MethodOnA();

        _b.MethodOnB();
        Console.WriteLine("called C");

        _releaseA(a2);
        _releaseA(a1);
    }
}

So the classes above are implemented so that dependencies can be injected. None of the classes have knowledge on how an instance of A B or C are created. Also notice that there is no dependency to an IoC container.

If you want to run the program (basically creating an instance of C and call a method on it) the code could look like this:

C++
static void Main(string[] args)
{
    Factory fac = new Factory();

    C c1 = fac.ResolveC();
    c1.MethodOnC();
}

The factory could look like this:

C++
public class Factory
    {
        private B _bShared;
        public C ResolveC()
        {
            if (_bShared == null)
                _bShared = ResolveB();

            return new C(() => ResolveA(), (p) => { Console.WriteLine("A released"); }, _bShared);
        }
        public A ResolveA()
        {
            return new A();
        }

        public B ResolveB()
        {
            return new B(ResolveA());
        }
    }

Now all the code to create instance of A, B and C is centralized. Imagine a class B2 gets introduced and C should use it. The only code change required is to change the creation of C with the additional parameter. Also in the above example, the instance of B that is used in C is a shared object, meaning that a second instance of C would use the same instance of B. This can also easily be changed to use a new instance of B for every instance of C without changing the class C directly.

Instead of coding your own factory, you can also use an existing framework (IoC Container) to do the same thing.

C++
private static void ConfigureContainer(IUnityContainer container)
{
    container.RegisterType<A>();
    container.RegisterType<B>();
    container.RegisterType<C>();
    container.RegisterInstance<Func<A>>(
        () =>
        {
            return container.Resolve<A>();
        });

    container.RegisterInstance<Action<A>>(
        (a) => { Console.WriteLine("A released"); });
}

static void Main(string[] args)
{
    IUnityContainer container = new UnityContainer();
    ConfigureContainer(container);

    C c1 = container.Resolve<C>();
    c1.MethodOnC();
}

Points of Interest

Now imagine that the class C has real program code and that B and A are some data access classes or some other more complex classes that use other classes to do things. Your application code does not depend on any IoC container. Your application code also doesn't know about the scope of injected objects. The Func to return an instance could be changed to return a cached object. For the Action Dispose or a cleanup method could be called to release resources.

Your application code can run with or without an IoC container but a framework simplifies your life by reducing the amount of code you have to write.

History

  • First version of this article

License

This article, along with any associated source code and files, is licensed under The Code Project Open License (CPOL)


Written By
United States United States
Architecting, designing and implemention of software projects. When not stuck at the keyboard sails, climbs, snowboards or does other outdoor activities.

Comments and Discussions

 
-- There are no messages in this forum --