Инъекция массивов в Unity

Unity умеет резолвить зависимости-массивы. Для такой зависимости контейнер возвращает объекты всех подходящих типов. Даже при разрешении зависимостей с указанием имени будут возвращены все подходящие типы.

Также этого Unity поддерживает разрешение обобщенных массивов.

Пример

Интерфейс и классы логгеров

public interface ILogger
{
    void Write(string message);
}

public class TraceLogger: ILogger
{
    public void Write(string message)
    {
        Trace.Write(message);
    }
}

public class ConsoleLogger: ILogger
{
    public void Write(string message)
    {
        Console.WriteLine(message);
    }
}

Сервис, принимающий обобщенный массив

public class SomeService2<T>
{
    public SomeService2(T[] array)
    {

    }
}

Код, получающий нужный экземпляр

var container = new UnityContainer();
container
    .RegisterType<ILogger, ConsoleLogger>("ConsoleLogger")
    .RegisterType<ILogger, TraceLogger>("TraceLogger");

var service = container.Resolve<SomeService2<ILogger>>();

При вызове такого кода в конструктор SomeService2 будет передан массив из двух логгеров.



Использование generic-ов в Unity

Unity поддерживает работу с generic-типами. 
Можно в контейнер поместить обобщенный тип,  а потом запросить тип с конкретными параметрами.  Подробнее описано по ссылке http://msdn.microsoft.com/en-us/library/dd203156.aspx

Пример кода, использующего такую фичу.

Рассмотрим обобщенный класс репозитария для работы с данными

public interface IRepository<T>
{
    IQueryable<T> Items();
    void Save(T item);
    void Delete(T item);
}

Реализация этого интерфейса для Entity Framework

public class EFRepository<T> : IRepository<T> where T : EntityObject
{
    ObjectContext _context;
    string _entitySetName;

    public EFRepository(ObjectContext context)
    {
        _context = context;
        var container = _context
            .MetadataWorkspace
            .GetEntityContainer(
                _context.DefaultContainerName, 
                DataSpace.CSpace);
        var edmEntityType = (EdmEntityTypeAttribute)typeof(T)
            .GetCustomAttributes(
                typeof(EdmEntityTypeAttribute), 
                false)
            .Single();

        _entitySetName = container.BaseEntitySets
            .Single(s => s.ElementType.Name == edmEntityType.Name
                && 
                s.ElementType.NamespaceName == edmEntityType.NamespaceName)
            .Name;
    }

    public IQueryable<T> Items()
    {
        return _context.CreateQuery<T>(_entitySetName);
    }

    public void Save(T item)
    {
        switch (item.EntityState)
        {
            case System.Data.EntityState.Detached:
                _context.AddObject(_entitySetName, item);
                goto case System.Data.EntityState.Added;

            case System.Data.EntityState.Added:
            case System.Data.EntityState.Modified:
                _context.SaveChanges();
                break;
            default:
                break;
        }
    }

    public void Delete(T item)
    {
        _context.DeleteObject(item);
        _context.SaveChanges();
    }
}

Есть EF модель с типом контекста  AddressBookEntities и двумя сущностями Person и PhoneNumber и сервис SomeService1, работающий с этими сущностями.
Конструктор сервиса выглядит так:

public SomeService1(IRepository<Person> personsRepository, 
                    IRepository<PhoneNumber> phoneNumbersRepository)
{

}

Чтобы получить от Unity экземпляр сервиса можно написать такой код:

var container = new UnityContainer();
container
    .RegisterType<ObjectContext, AddressBookEntities>(
            new ContainerControlledLifetimeManager(),
            new InjectionConstructor())
    .RegisterType(typeof(IRepository<>), typeof(EFRepository<>));

var service = container.Resolve<SomeService1>();

Unity сам создаст инстансы конкретных типов при сборке зависимостей SomeService1.



Рефакторинг legacy кода для использования Unity

Представим себе код, который был написан без применения принципов IoC.
Например такой

public class AccountManager
{
    public void CreateAccount(string userName, string password)
    {
        var accountStore = new AccountStore();
        accountStore.AddNewAccount(userName, password);
    }

    public bool Authenticate(string userName, string password)
    {
        var accountStore = new AccountStore();
        if (accountStore.IsValidAccount(userName, password))
        {
            Session.CurrentSession.SetAuthToken(userName);
            return true;
        }
        else
        {
            return false;
        }
    }

    public bool IsUserInRole(string userName, string roleName)
    {
        var roleStore = new RoleStore();
        return roleStore
                .GetRolesForUser(userName)
                .Contains(roleName);
    }
}

Классы AccountStore и RoleStore – это персистентные хранилища, а класс Session хранит информацию о текущем сеансе работы пользователя.

Класс AccountManager применяется во многих местах, поэтому  мы не можем сразу вынести все зависимости в конструктор.

Поэтому сначала немного отрефакторим класс AccountManager и вынесем зависимости в открытые свойства, чтобы через них можно было подпихивать зависимости.

public class AccountManager
{
    [Dependency]
    public AccountStore AccountStore { get; set; }

    [Dependency]
    public RoleStore RoleStore { get; set; }

    [Dependency("GetCurrentSession")]
    public Func<Session> GetCurrentSession { get; set; }

    public AccountManager()
    {
        AccountStore = new AccountStore();
        RoleStore = new RoleStore();
        GetCurrentSession = () => Session.CurrentSession;
    }

    public void CreateAccount(string userName, string password)
    {
        AccountStore.AddNewAccount(userName, password);
    }

    public bool Authenticate(string userName, string password)
    {
        if (AccountStore.IsValidAccount(userName, password))
        {
            GetCurrentSession().SetAuthToken(userName);
            return true;
        }
        return false;
    }

    public bool IsUserInRole(string userName, string roleName)
    {
        return RoleStore
                .GetRolesForUser(userName)
                .Contains(roleName);
    }
}

В этом коде атрибутами сразу обозначены зависимости.  Эти атрибуты будет анализировать Unity при инъекции зависимостей.
Так как при инъекции компоненты подбираются по типу, то для функцции GetCurrentSession желательно указать дополнительный строковый идентификатор.

Теперь будем прикручивать IoC-контейнер.
Нам сначала понадобится создать синглтон контейнера Unity.
Многие, в том числе и я, считают синглтоны злом, но в данном случае немного зла необходимо.

public static class UnitySingleton
{
    static IUnityContainer _instance = new UnityContainer();

    public static UnitySingleton()
    {
	//Здесь будем конфигурировать контейнер
        _instance
            .RegisterType<AccountStore>()
            .RegisterType<RoleStore>()
            .RegisterInstance<Func<Session>>(
                    "GetCurrentSession",
                    () => Session.CurrentSession);
    }

    public static IUnityContainer Instance
    {
        get
        {
            return _instance;
        }
    }
}

Для регистрации метода получения текущей сессии используется RegisterInstance. Этот метод помещает в контейнер экземпляр объекта, указанный параметром. Контейнер удерживает этот объект все время, но с помощью LifetimeManager это поведение можно изменить.

Теперь в конструкторе AccountManager напишем только одну строчку

public AccountManager()
{
    UnitySingleton.Instance.BuildUp(this);
}

После таких преобразований можно выделять интерфейсы классов, используемых внутри AccountManager чтобы ослабить зависимости между классами, а также проводить аналогичный рефакторинг для классов, использующих AccountManager.

Когда сам класс AccountManager нигде не будет инстанцироваться явно через new, а только доставаться из контейнера, тогда можно будет рефакторить AccountManager, чтобы он получал  все зависимости через конструктор.



Используем IoC-контейнер Unity

Unity – IoC-контейнер от Microsoft, разработанный группой Patterns&Practicies. Найти его можно по адресу http://www.codeplex.com/unity. Также Unity включен в состав Enterprise Library.

Будет использоваться версия 1.2, последняя на данный момент.

Итак приступим.
Как завещал дядька Фаулер будем рассматривать использования контейнера на примере программы, работающей  с базой данных фильмов.

Чтобы использовать Unity в своей программе надо подключить сборки Microsoft.Practices.ObjectBuilder2 и Microsoft.Practices.Unity.

Нам надо написать класс, который позволяет искать фильмы по различным параметрам.

Предположим что фильмы описываются классом Movie

public class Movie
{
    public string Title { get; set; }
    public string Director { get; set; }
    public int Year { get; set; }
}

Работу с БД завернем в интерфейс

public interface IMovieRepository
{
    IQueryable<Movie> GetMovies();
}

Теперь можем написать класс, который нам нужен. Следуя принципам IoC будем передавать зависимости через конструктор.

public class MovieFinder
{
    IMovieRepository _repository;

    public MovieFinder(IMovieRepository repository)
    {
        _repository = repository;
    }

    public IEnumerable<Movie> FindByTitle(string q)
    {
        return _repository
                .GetMovies()
                .Where(m => m.Title.Contains(q));
    }
}

В целях тестирования напишем простую реализацию IMovieRepository.

public class InMemoryMovieRepository : IMovieRepository
{
    public IQueryable<Movie> GetMovies()
    {
        return new[]
        {
            new Movie
            {
                Title = "Гарри Поттер и узник Азкабана",
                Director = "Альфонсо Куарон",
                Year = 2004
            },
            new Movie
            {
                Title = "Звездные войны: Эпизод 2 - Атака клонов",
                Director = "Джордж Лукас",
                Year = 2002
            },
            new Movie
            {
                Title = "Властелин колец: Братство кольца",
                Director = "Питер Джексон",
                Year = 2001
            },
        }.AsQueryable();
    }
}

Напишем короткий код, который собирает все вместе.

//Создаем контейнер
var container = new UnityContainer();

//Помещаем в контейнер реализацию для используемого интерфейса
container.RegisterType<IMovieRepository, InMemoryMovieRepository>();

//Собираем нужный объект
var finder = container.Resolve<MovieFinder>();

Обратите внимание, что сам тип MovieFinder необязательно помещать в контейнер.

Теперь можно заняться базой данных.
Создадим EF модель с одной сущностью Movie, эту сущность будем использовать вместо нашего класса Movie.

Реализация IMovieRepository для БД будет выглядеть так:

public class EFMovieRepository: IMovieRepository
{
    MoviesContainer _context;

    public EFMovieRepository(MoviesContainer context)
    {
        _context = context;
    }

    public IQueryable<Movie> GetMovies()
    {
        return _context.Movies;
    }
}

Где MoviesContainer – тип контекста EF.

Чтобы использовать этот репозитарий слегка изменим код основной программы.

var container = new UnityContainer();
container
    .RegisterType<IMovieRepository, EFMovieRepository>()
    .RegisterType<MoviesContainer>(new InjectionConstructor());

var finder = container.Resolve<MovieFinder>();

Параметр new InjectionConstructor() для второго вызова RegisterType говорит контейнеру что надо использовать конструктор без параметров для создания объекта этого класса. Если конструктор в классе всего один, то задавать такой параметр не требуется.

В приведенном выше коде есть один недостаток. Все объекты создаются каждый раз при вызове Resolve. Если мы не хотим пересоздавать каждый раз контекст EF (это длительная операция), то можем сделать его синглтоном.
Делается это очень просто, достаточно заменить вызов
.RegisterType<MoviesContainer>(new InjectionConstructor())
на
.RegisterType<MoviesContainer>(
                          new ContainerControlledLifetimeManager(),
                          new InjectionConstructor())

Первый параметр задает LifetimeManager, который, как понятно из названия, управляет временем жизни объекта. ContainerControlledLifetimeManager означает что объект будет жить пока живет контейнер, и для всех вызовов Resolve будет возвращаться один и тот же экземпляр контекста.

В принципе этого достаточно чтобы начать использовать Unity в своем проекте.



Основная задача IoC-контейнеров

Многократно встречал мнение что использовать IoC-контейнер стоит только тогда, когда нужно изменение конфигурации без перекомпиляции программы.
Такое мнение пришло из мира Java, где частью любой крупной программы является множество конфигурационных XML файлов. В других платформах зачастую возможность внешнего конфигурирования используемых компонент избыточна.

На самом деле основной задачей IoC-контейнеров является декларативное управление временем жизни компонент, и зависимостями между ними.

Внешние конфиги, позволяющие менять состав компонент без перекомпиляции, AOP времени исполнения и другие приблуды являются необязательными.