Введение в IoC

(Пост на RSDN)

Есть класс A и зависит от от класса B (использует его).

Например так:
public class A
{
   B b = new B();
   void foo()
   { 
      //используем b
   }
}

public class B
{
    ...
}

В этом коде существует несколько проблем.
1)Невозможно тестировать класс A в отрыве от B. Если B работает с БД, то для тестов A вам понадобится база.
2)Временем жизни объекта B управляет А, нельзя например использовать один и тот же объект B в разных местах.

Чтобы победить это нам сначало надо выделить интерфейс B и назвать его IB.
public interface IB
{
    ...
}

public class A
{
    IB b = new B();    
  
    void foo()  
    {    
        //используем b  
    }
}

public class B:IB
{
    ...
}
Но ни одну из наших проблем это не решило.

Теперь можно применить паттерн Service Locator. Суть этого паттерна состоит в том что имеем фабричный метод который по идентификатору возвращает нужную реализацию интерфейса. В .NET есть IServiceProvider с одном методом GetService, которому параметром передается тип.

Предположим что мы сделали хорошую реализацию IServiceProvider и у нас получился такой код
public interface IB
{
    ...
}
public class A
{
    //serviceLocator - реалзиация IServiceProvider
    //которая по типу IB возвращает B
    IB b = (IB)serviceLocator.GetService(typeof(IB)); 
   
    void foo()  
    {
        //используем b
    }
}

public class B:IB
{
    ...
}

Теперь временем жизни B управляет serviceLocator, A не знает о классе B. Можно через serviceLocator подпихнуть любую реализацию IB.
Этот подход называет Dependency Lookup — поиск зависимостей, это один из вариантов подхода IoC.

Но у нас появилась зависимость от serviceLocator, теперь хоть тестировать A без класса B возможно, но это потребует настройки локатора (возможно совсем нетривиальной).

Если подойти с другой стороны, то можно сделать так чтобы класс A не искал зависимости сам, а получал их извне. Например через конструктор, свойство или метод.
public interface IB
{
    ...
}

public class A
{    
    IB b;    
    
    //Например так  
    public A(IB b)
    {
        this.b = b;
    }

    //Или так  
    public IB B { {get {return b;} set {b = value;}}  

    //Или так  
    public void SetB(IB b)
    {
        this.b = b;
    }  
    
    void foo()
    {    
        //используем b
    }
}

public class B:IB
{
    ...
}
Тогда вызывающий код дожен выгядеть примерно так
...
var a = new A(new B());
...

Теперь у нас A не зависит ни от чего, а все зависимости можно передать например через конструктор, что значительно облегчает тестирование A.
Этот подход называется Dependency Injection — инъекция зависимостей, это другой вариант IoC.

Но в большем масштабе проблемы не решает, только перемещает её на уровень выше, то есть все зависимости перемещаются в вызывающий код.

Если такой подход применить во всей программе, то в итоге метод main (или другая точка входа) будет выглядеть так:

new Program(new A(new B(new C(), new D()), new E()......)
И это мы еще не рассматривали управление временем жизни объектов.

На помощь нам приходят IoC-контейнеры.
Сами IoC-контейнеры похожи на ServiceLocator, только делают чуть больше работы.
При запросе объекта какого-то типа у контейнера он решает объект какого типа вернуть. Для каждого типа, зарегистрированного в IoC-контейнере, есть карта зависимостей, то есть описание какие параметры надо передавать в конструктор, каким свойствам надо присваивать и какие методы вызывать чтобы инъектировать зависимости в объект. Карта зависимостей задается внешне или получается с помощью рефлексии.
Кроме того контейнер содержит ассоциации для какого запрошенного идентификатора объект какого типа надо вернуть. В качестве идентификатора чаще всего используется сам тип. Для каждой зависимости запрошенного объекта, контейнер создает дургой объект у которого тоже могут быть зависимости, для них эта операция вызывается рекурсивно.
В принципе контейнер не обязан каждый раз создавать объект, он может управлять его временем жизни.

Хороший IoC контейнер должен возможности чтобы код выше можно было переписать так:

container.Resolve<program>();
А также потребуется где-то задать параметры контейнеру, чтобы он по запросу объекта типа IB возвращал объект типа B, для IA возвращал A и так далее.
Обычно параметры контейнера можно задавать как в коде, так и во внешнем конфигурационном файле.

Кроме того IoC-контейнер можно использовать как Service Locator, но такого надо избегать.

Для .NET существует множество контейнеров. От MS — Unity, даже версия для Silverlight уже появилась, autofac, Castle Windsor, StructureMap и Spring.NET. Spring.NET — клон java библиотеки со всеми вытекающими. Куча конфигов в XML, огромное число классов в библиотеке, слабое использование возможностей .NET, лично меня от него воротит после использования достаточно легковесного unity.


Кроме того MS ведет разработку библиотеки MEF. В ней применяются принципы IoC, но для более крупных компонент, а также возможности менять набор компонент в Runtime. MEF будет в составе .NET 4.0