Эту статью я посвящу продукту
группы Patterns
& Practices
–
Unity.
Unity
–
это IoC
контейнер (Inversion of Control), который позволяет
управлять зависимостями (DI
– Dependency
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 –
для добавления интерфейса в наследуемых классахm 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;
});
}
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.
Источники:
No comments:
Post a Comment