Saturday, November 23, 2013

Использование IoC контейнера Unity

Эту статью я посвящу продукту группы Patterns & Practices Unity. Unity – это IoC контейнер (Inversion of Control), который позволяет управлять зависимостями (DIDependency Injection). Начиная с первой версии, Unity претерпела серьёзных изменений и модификаций. Например, в последних версиях Unity мы можем конфигурировать управление зависимости как через файл конфигурации, так и через код (конфигурирование через файл конфигурации было еще в версии Unity 1.2, но оно претерпело ряд кардинальных изменений). Поскольку я являюсь поклонником WPF, то примеры буду демонстрировать на нем. Постараюсь показать конфигурацию Unity как через файл конфигурации, так и через код. Поскольку эта статья не является вводной по MVVM паттерну, который будет использоваться в статье с готовыми наработками, рекомендую ознакомиться с MVVM (в частности, в моих соответствующих статьях) для понимания принципов работы этого паттерна.  
Для начала опишу, какая у меня получилась MVVM модель. В дальнейшем буду дописывать ее для того, чтобы показать возможности IoC контейнера Unity. Представляю Вашему вниманию проект электронной библиотеки. Структура проекта:
Проект состоит из 4 частей: Model, Utils, View и View Model.
Model состоит из двух интерфейсов и их реализаций. Это интерфейсы IBook и ILibaryBookService. Реализация, соответственно, состоит из классов LibraryBook и LibraryBookService.
В части Utils присутствуют классы, которые выполняют вспомогательную роль. В примере это классы DelegateCommand – для использования интерфейса ICommand для байндинга и класс NotifyModelBase – для добавления интерфейса в наследуемых классахINotifyPropertyChanged.
В части View будут размещаться представления. В данном случае это класс MainWindows.xaml.
В части ViewModel будем размещать модель представления для данного проекта.
Реализация модели выглядит следующим образом:
public interface IBook
{
    string Author { get; set; }
    string Title { get; set; }
    DateTime Year { get; set; }
    string SN { get; set; }
    int Count { get; set; }
}
Реализация интерфейса IBook:
public class LibraryBook : NotifyModelBase, IBook
{
    private string _author;
    public string Author
    {
        get { return _author; }
        set
        {
            _author = value;
            OnPropertyChanged("Author");
        }
    }

    private string _title;
    public string Title
    {
        get { return _title; }
        set
        {
            _title = value;
            OnPropertyChanged("Title");
        }
    }

    private DateTime _year;
    public DateTime Year
    {
        get { return _year; }
        set
        {
            _year = value;
            OnPropertyChanged("Year");
        }
    }

    private string _sn;
    public string SN
    {
        get { return _sn; }
        set
        {
            _sn = value;
            OnPropertyChanged("SN");
        }
    }

    private int _count;
    public int Count
    {
        get { return _count; }
        set
        {
            _count = value;
            OnPropertyChanged("Count");
        }
    }
}
Ниже приведу реализацию интерфейса и класса ILibraryBookService и LibraryBookService
public interface ILibraryBookService
{
    void GetData(Action<ObservableCollection<IBook>, Exception> callback);
    IBook FindBook(IBook findBook);
    void CreateNewBook();
    void RemoveBook(IBook book);
}
public class LibraryBookService : ILibraryBookService
{
    #region Variable
    private ObservableCollection<IBook> _books;
    #endregion

    #region Constructor
    public LibraryBookService()
    {
        _books = new ObservableCollection<IBook>();
        _books.Add(new LibraryBook { Author = "Jon Skeet", Title = "C# in Depth", Count = 3, SN = "ISBN: 9781617291340", Year = new DateTime(2013, 9, 10) });
        _books.Add(new LibraryBook { Author = "Martin Fowler", Title = "Refactoring: Improving the Design of Existing Code", Count = 2, SN = "ISBN-10: 0201485672", Year = new DateTime(1999, 7, 8) });
        _books.Add(new LibraryBook { Author = "Jeffrey Richter", Title = "CLR via C# (Developer Reference)", Count = 5, SN = "ISBN-10: 0735667454", Year = new DateTime(2012, 12, 4) });
    }
    #endregion

    #region Public Methods
    public void GetData(Action<ObservableCollection<IBook>, Exception> callback)
    {
        callback(_books, null);
    }

    public IBook FindBook(IBook findBook)
    {
        if (findBook == null)
            return null;

        return _books.FirstOrDefault(book => book.Author == findBook.Author
                                    && book.Title == findBook.Title
                                    && book.SN == findBook.SN
                                    && book.Year == findBook.Year);
    }

    public void CreateNewBook()
    {
        _books.Add(new LibraryBook { Author = "Test1", Title = "Test1", Count = 5, SN = "ISBN-10: 0735667454", Year = DateTime.Now });
    }

    public void RemoveBook(IBook book)
    {
        if (book == null)
            return;

        _books.Remove(book);
    }
    #endregion
}
Для того, чтобы не загромождать объяснения контейнера Unity, приведу только реализацию  MainViewModel и продемонстрирую только код, который использует Unity и то, как с ним меняется проект. В конце статьи будет приведена ссылка на скачивание исходников к данной статье, и Вы сможете просмотреть, какой код получился в конечном итоге.
public class MainViewModel : NotifyModelBase
{
    private readonly ILibraryBookService _libraryDataService;

    public MainViewModel(ILibraryBookService dataService)
    {
        _libraryDataService = dataService;
        _libraryDataService.GetData(
            (items, error) =>
            {
                Books = items;
            });
    }

    #region Public Properties


    public ObservableCollection<IBook> Books { get; set; }

    private IBook _selectedBook;
    public IBook SelectedBook
    {
        get { return _selectedBook; }
        set
        {
            _selectedBook = value;
            OnPropertyChanged("SelectedBook");
            RemoveBookCommand.RaiseCanExecuteChanged();
        }
    }
    #endregion

    #region Command
    private DelegateCommand _addBookCommand;
    public DelegateCommand AddBookCommand
    {
        get
        {
            return _addBookCommand ?? (_addBookCommand = new DelegateCommand(AddNewBook));
        }
    }

    private void AddNewBook(object args)
    {
        _libraryDataService.CreateNewBook();
    }

    private DelegateCommand _removeBookCommand;
    public DelegateCommand RemoveBookCommand
    {
        get
        {
            return _removeBookCommand ?? (_removeBookCommand = new DelegateCommand(RemoveBook, CanRemoveBook));
        }
    }

    private void RemoveBook(object args)
    {
        Books.Remove(SelectedBook);
    }

    private bool CanRemoveBook(object args)
    {
        if (SelectedBook == null)
            return false;
        var book = _libraryDataService.FindBook(SelectedBook);
        if (book == null)
            return false;

        return true;
    }
    #endregion
}
Поскольку мы будем использовать связку через Unity, уберем параметр StartupUri из App.xaml. Затем переходим к классу App.xaml.cs и переопределяем метод OnStartup. Вот как это будет выглядеть на примере:
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>();
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow {DataContext = model};
    view.Show();
}
В примере мы создали контейнер Unity с помощью строчки
var unityConteiner = new UnityContainer();
Дальше нам необходимо связать интерфейс с реализацией, что делается с помощью  метода RegisterType<>. Данный метод имеет очень много перегруженных вариантов, часть из которых мы  рассмотрим немного позже. Затем мы создаем экземпляр класса MainViewModel с помощью метода Resolve<T>. Дальше выполнятся методы по присвоению DataContext представления конкретной моделью. Последняя строчка просто показывает главное окно программы. Пожалуй, пришло время более подробно остановиться на строчке,  которая в данном примере имеет ключевое значение: 
unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>();
Нам необходимо сначала зарегистрировать тип, чтобы при создании MainViewModel, модели было известно, какой класс использовать для передачи в конструктор. Приведу пример кода созданного выше класса, чтобы объяснить на примере, зачем это нужно.
private readonly ILibraryBookService _libraryDataService;

public MainViewModel(ILibraryBookService dataService)
{
    _libraryDataService = dataService;
    _libraryDataService.GetData(
        (items, error) =>
        {
            Books = items;
        });
}
Приведенный выше код принимает в параметр интерфейс ILibraryBookService, который связал метод RegisterType<> Unity контейнера с классом LibraryBookService. На примере мы увидели один из способов DI (Dependency Injection) внедрения зависимостей, а именно: Constructor Injection. Мне не нравится, как звучит перевод терминов, поэтому буду употреблять англоязычные термины. Это один из самых популярных DI. Существуют еще два менее популярные, чем CI (возможно, их больше, тогда они еще мене популярные)  это Property Injection и Method Injection. Они будут выглядеть так:
PI (Property Injection):
private readonly ILibraryBookService _libraryDataService;
/// <summary>
/// Initializes a new instance of the MainViewModel class.
/// </summary>
public MainViewModel(ILibraryBookService dataService)
{
    _libraryDataService = dataService;
    _libraryDataService.GetData(
        (items, error) =>
        {
            Books = items;
        });
}

//Property Injection
public IBook FavoriteBook { get; set; }
Property FavoriteBook используется для внедрения Property Injection. Если Вы любитель MEF, то там довольно распространенная практика – использование PI, и Вам, наверное, неоднократно приходилось неоднократно с этим сталкиваться.
На третьем месте – менее распространенный способ внедрения зависимостей – это MI (Method Injection). Использование MI в коде будет выглядеть следующим образом:
public bool ClearBooks(ILibraryBookService libraryBookService, string bookTitle)
{
}
Вот как выглядят эти три кита:

IoC контейнеры я изобразил в сторонке, потому что они позволяют упростить и автоматизировать написание кода с использованием данного подхода (DI) настолько, насколько это возможно.
Вернемся, пожалуй, к нашему примеру. Код привязки через Unity получился довольно громоздким, что желательно исправить. Для этого Unity предоставляет нам множество разных вариантов, по которым мы сейчас пройдемся. Первый вариант  это связывание через конфигурационный файл. Удобен для начальной привязки для небольших проектов. Что касается более крупных проектов, я видел в основном либо привязку через код, либо начальные параметры через конфигурационный файл, а все остальное   через код. (Наверное, из-за того, что Unity не предоставляет мастера конфигурации для конфигурационного файла). Пожалуй, стоит рассмотреть данный вариант реализации.  Это значительно упросит наш код. Вот как выглядит начальная настройка через файл конфигурации:
<?xml version="1.0"?>
<configuration>
<!--<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/></startup>-->
 <configSections>
    <section name="unity" type="Microsoft.Practices.Unity.Configuration.UnityConfigurationSection, Microsoft.Practices.Unity.Configuration"/>
  </configSections>
  <unity>
      <alias alias="singleton"
                 type="Microsoft.Practices.Unity.ContainerControlledLifetimeManager, Microsoft.Practices.Unity" />
      <alias alias="nonshared"
                 type="Microsoft.Practices.Unity.ExternallyControlledLifetimeManager, Microsoft.Practices.Unity" />
      <alias alias="ILibraryBookService"
                 type="UnityTest.Model.ILibraryBookService, UnityTest" />
      <alias alias="IBook"
                 type="UnityTest.Model.IBook, UnityTest" />
      <container>
        <register  type="IBook" mapTo="UnityTest.Model.IBook, UnityTest">
            <lifetime type="nonshared" />
          </register>
        <register  type="ILibraryBookService" mapTo="UnityTest.Model.LibraryBookService, UnityTest">
            <lifetime type="singleton" />
          </register>

        <register  type="UnityTest.ViewModel.MainViewModel, UnityTest">
              <constructor>
                <param name="dataService" type="ILibraryBookService"/>
              </constructor>
          </register>
      </container>
  </unity>
</configuration>
Как видите, в примере закомментирована строка
<!--<startup><supportedRuntime version="v4.0" sku=".NETFramework,Version=v4.5"/></startup>-->
Из-за этой строки понять, почему у меня не работает файл конфигурации, заняло два часа. С этой строкой Ваше приложение просто не запустится. Если сможете преодолеть эту проблему, расскажите мне, как это сделать. Вот как теперь будет выглядеть инициализация контейнера через код с использованием конфигурационного файла:
using (IUnityContainer container = new UnityContainer())
{
    var section = (UnityConfigurationSection)ConfigurationManager.GetSection("unity");
    section.Containers.Default.Configure(container);
    var model = container.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
Для начальной конфигурации Unity контейнера выбирайте тот способ конфигурации, который Вам по душе.
Пожалуй, продолжим экспериментировать дальше с конфигурированием IoC контейнера. Для этого мы создадим класс HomeLibraryBookService, который наследуем от интерфейса ILibraryBookService. Этот класс практически такой же, как и класс LibraryBookService. Я его создал только для того, чтобы продемонстрировать возможности Unity.
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>();
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>();
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
В этом коде вызов  
unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>();
перекрыл первый вызов. Для того, чтобы такого не произошло и мы могли привязывать к нужной реализации, нужно добавить имя для зарегистрированного типа с целью удобства поиска его в контейнере. Реализация всего этого будет показана ниже.
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>("LibraryBookService");
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>("HomeLibraryBookService");

    unityConteiner.RegisterType<MainViewModel>(
        new InjectionConstructor(new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")));
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
Немного усложним задание и добавим  еще один интерфейс IVisitorRepository для посетителей библиотеки. Этот интерфейс будет иметь только один метод, который будет возвращать посетителей. Есть два способа внедрения Property Injection с помощью Unity.
Сначала рассмотрим простой способ с помощью атрибута [Dependency].
В коде это будет выглядеть следующим образом:
[Dependency]
public IVisitorRepository VisitorRepository { get; set; }
И сама реализация:
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>("LibraryBookService");
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>("HomeLibraryBookService");
    unityConteiner.RegisterType<IVisitorRepository, VisitorRepository>();
    unityConteiner.RegisterType<MainViewModel>(
        new InjectionConstructor(new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")));
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
Теперь усложним сценарий и сделаем  внедрение Property Injection через Unity Container.
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>("LibraryBookService");
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>("HomeLibraryBookService");
    unityConteiner.Configure<InjectedMembers>()
                    .ConfigureInjectionFor<MainViewModel>(
                         new InjectionProperty(
                             "VisitorRepository",
                                new ResolvedParameter<VisitorRepository>()));
    unityConteiner.RegisterType<MainViewModel>(
        new InjectionConstructor(new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")));
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
Напоследок у нас остался пример с внедрением Method Injection. Его можно сделать также несколькими способами. Начнем, пожалуй, с наиболее громоздкого способа.
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>("LibraryBookService");
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>("HomeLibraryBookService");
    unityConteiner.Configure<InjectedMembers>()
                    .ConfigureInjectionFor<MainViewModel>(
                         new InjectionProperty(
                             "VisitorRepository",
                                new ResolvedParameter<VisitorRepository>()));
    unityConteiner.Configure<InjectedMembers>()
                    .ConfigureInjectionFor<MainViewModel>(
                        new InjectionMethod(
                            "PrintBook",
                            new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")));

    unityConteiner.RegisterType<MainViewModel>(
        new InjectionConstructor(new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")));
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
Приведенный выше код более компактно выглядит так:
protected override void OnStartup(StartupEventArgs e)
{
    var unityConteiner = new UnityContainer();
    unityConteiner.RegisterType<IBook, LibraryBook>();
    unityConteiner.RegisterType<ILibraryBookService, LibraryBookService>("LibraryBookService");
    unityConteiner.RegisterType<ILibraryBookService, HomeLibraryBookService>("HomeLibraryBookService");
    unityConteiner.RegisterType<MainViewModel>(
        new InjectionConstructor(new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")),
        new InjectionMethod("PrintBook", new ResolvedParameter<ILibraryBookService>("HomeLibraryBookService")),
        new InjectionProperty("VisitorRepository", new ResolvedParameter<VisitorRepository>()));
    var model = unityConteiner.Resolve<MainViewModel>();
    var view = new MainWindow { DataContext = model };
    view.Show();
}
И напоследок – мой любимый способ внедрения зависимостей, которым позволяет управлять Unity, – атрибуты. Пример:
[Dependency]
public IVisitorRepository VisitorRepository { get; set; }

public ObservableCollection<IBook> Books { get; set; }

[InjectionMethod]
public void PrintBook([Dependency("HomeLibraryBookService")]ILibraryBookService libraryBookService)
{

}
В итоге главная задача сводится к тому, чтобы связать интерфейс с реализацией через Unity.
Стоит упомянуть еще об одной главной детали, без которой не обойтись при связывании интерфейсов с реализацией. Это LifetimeManager, который управляет временем жизни объектов. Приведу краткое описание этих менеджеров.
TransientLifetimeManager ничего не сохраняет, GetValue всегда возвращает null, поэтому объект создается каждый раз. Этот менеджер используется по умолчанию при вызове RegisterType.
ContainerControlledLifetimeManager сохраняет объект в локальной переменной. Поэтому объект живет столько же, сколько и контейнер.
ExternallyControlledLifetimeManager позволяет зарегистрировать сопоставления типов и существующие объекты с контейнером с помощью сохранения слабой ссылки на объекты, которые он создает при вызове Resolve или ResolveAll методов, или когда механизм внедрения зависимостей внедряется в экземпляры других классов, основанных на атрибутах или параметрах конструктора в рамках этого класса.
PerThreadLifetimeManager сохраняет объекты в ThreadStatic словаре. Каждый поток в программе будет иметь свой набор объектов.

Итоги
В данной статье приведен краткий анализ использования IoC контейнера Unity для управления зависимостями. К сожалению, не получилось вместить много интересных возможностей этого контейнера. Пожалуй, оставлю это на следующую статью. Надеюсь, что эта статья поможет разобраться с основами работы с Unity. Для читателей моего блога постараюсь провести небольшой экскурс в мир IoC контейнеров на примерах таких контейнеров, как Autofac, Ninject, Winsdor и StructureMap. Возможно, также добавлю в этот список портированный с Java IoC контейнер – Spring.Net.

Источники:
Unity - Документация

No comments:

Post a Comment