Saturday, December 7, 2013

Использование StructureMap

Эта статья является продолжением серии статей по IoC контейнерах. В ней будет рассмотрен  IoC контейнер StructureMap - старейший DI контейнер в мире .NET. Несмотря на солидный возраст, он по-прежнему активно развивается и имеет много современных особенностей, присущих многим современным контейнерам, поэтому мы должны рассматривать его возраст в основном как свидетельство его зрелости. Это также один из наиболее часто используемых DI контейнеров. Лично мне этот контейнер нравится своей простотой в конфигурировании и использовании. Для того, чтобы понять, на чем будут показаны примеры работы, ниже приведена диаграмма классов.

Основная модель представления представлена классом MainViewModel. С помощью этой модели и будет продемонстрирована вся мощь StructureMap. Рассмотрим, как будет реализован самый простой сценарий constructor injection (CI) для класса MainViewModel, чтобы посмотреть на StructureMap в действии. Чтобы инициализировать IoC контейнер перед стартом программы, необходимо перейти в классы App.xaml.cs и переопределить метод OnStartup.
using (var container = new Container())
{
    container.Configure(r => r.For<IBook>().Use<LibraryBook>()
        );
    container.Configure(
        r => r.For<ILibraryBookService>().Use<LibraryBookService>().Named("LibraryBookService"));
    container.Configure(
        r => r.For<ILibraryBookService>().Use<HomeLibraryBookService>().Named("HomeLibraryBookService"));
    container.Configure(r => r.For<Visitor>());
    container.Configure(r => r.For<MainWindow>());
    container.Configure(r => r.For<IBook>().Use<LibraryBook>());
    container.Configure(r => r.For<MainViewModel>().Use<MainViewModel>()
        .Ctor<ILibraryBookService>().Is(container.GetInstance<ILibraryBookService>("LibraryBookService")));
    var model = container.GetInstance<MainViewModel>();
    var view = container.GetInstance<MainWindow>();
    view.DataContext = model;
    view.Show();
}
Для регистрации мы можем использовать способ, описанный выше. Либо писать в таком стиле:
container.Configure(r =>
    {
        r.For<IBook>().Use<LibraryBook>();
        r.For<ILibraryBookService>().Use<LibraryBookService>().Named("LibraryBookService");
    });
Результат будет идентичный, как в первом способе. Возможен еще один вариант кода с использованием ObjectFactory. Но с этим связано несколько особенностей, которые я опишу после приведение примера.
protected override void OnStartup(StartupEventArgs e)
{
    InitializeWithObjectFactory();

    //InitializeStructureMapContainer();
}

private static void InitializeWithObjectFactory()
{
    ObjectFactory.Initialize(InitializeStructureMap);
    ObjectFactory.AssertConfigurationIsValid();
    var model = ObjectFactory.GetInstance<MainViewModel>();
    var view = ObjectFactory.GetInstance<MainWindow>();
    view.DataContext = model;
    view.ShowDialog();
}

private static void InitializeStructureMap(IInitializationExpression r)
{
    r.For<IBook>().Use<LibraryBook>();
    r.For<ILibraryBookService>().Use<HomeLibraryBookService>().Named("HomeLibraryBookService");
    r.For<ILibraryBookService>().Use<LibraryBookService>().Named("LibraryBookService");
    r.For<Visitor>();
    r.For<MainWindow>();
    r.For<IBook>().Use<LibraryBook>();
    r.For<MainViewModel>();
}
Пожалуй, нужно описать особенности, почему этот подход не стоит сейчас использовать и отдать предпочтение инициализации через контейнер. Одна из проблем состоит в том, что ObjectFactory используется для создания статического экземпляра контейнера, который доступен во всем приложении. А такой подход не приветствуется в разработке программного обеспечения. Об этом вы можете почитать в книге Mark Seemann, Dependency Injection in .NET, которая описывает особенности работы с различными IoC контейнерами, включая MEF, который, по сути, не является IoC контейнером. Одной из причин удержаться от использования ObjectFactory, о которых упоминает Mark Seemann, является то, что побуждает нас неправильно использовать контейнер как паттерн Service Locator. Это очень неоднозначный паттерн, который я описал в общих чертах в своих статьях по MVVM, но для того, чтобы с ним ознакомиться, рекомендую посмотреть статью автора этого паттерна Martin Fowler, если у вас нет проблем с английским языком, либо посмотреть блог Сергея Теплякова, DI Паттерны. Service Locator, пропустив через себя субъективизм автора, так как в некоторых моментах он очень категоричен и не рассматривает альтернативы, когда этот паттерн может иметь право на существование. Поэтому рекомендую ознакомиться с использованием этого паттерна, но решать, использовать его или нет, в зависимости от ситуации. Для начинающих разработчиков постараюсь объяснить, что здесь происходит.
r.For<ILibraryBookService>().Use<LibraryBookService>().Named("LibraryBookService")
Метод For<ILibraryBookService> используется для регистрации компонента и дальнейшего его использования как типа сервиса (service type). Метод Use<LibraryBookService>() связывает интерфейс с реализацией. И метод Named("LibraryBookService") позволяет задать имя для экземпляра, чтобы можно было использовать получения этого экземпляра по имени. 
Автоматическая регистрация
Иногда постоянная монотонная регистрация кучи однотипных интерфейсов и их реализаций бывает очень утомительной. Например, если взять за пример такой IoC контейнер как Unity, то написание объемного кода для регистрации - очень кропотливое занятие. Было бы очень удобно, чтобы можно было указать сборку, как, например это делает тот же Autofac, и IoC контейнер с помощью рефлексии сделает за разработчика всю работу. Лично мне и нравится StructureMap тем, что он также такой способ поддерживает.
private void InitializeScanMethod()
{
    IContainer container = new Container(
        => x.Scan(scan =>
            {
                scan.Assembly(GetType().Assembly);
                scan.WithDefaultConventions();
            }));
    var model = container.GetInstance<MainViewModel>();
    var view = container.GetInstance<MainWindow>();
    view.DataContext = model;
    view.Show();
}
Метод Scan вложен в класс Container блока кода как один из методов класса ConfigurationExpression. Переменная x представляет экземпляр IAssemblyScanner, который мы можем использовать, чтобы определить, каким сборку мы должны отсканировать и настроить. Экземпляр IAssemblyScanner предоставляет несколько методов, которые мы можем использовать для определения того, какие сборки для сканирования использовать для связывания интерфейсов и реализации, а также как настроить типы из этих сборок. Мы можем также использовать универсальный метод AssemblyContainingType для определения сборки из представительного типа (комбинирование сборок), но есть несколько других методов, которые позволяют нам обеспечить настройки Assembly, или даже добавить все сборки, просто указав путь к каталогу, с которого можно эти сборки достать. Другие методы дают нам возможность определить, какие типы добавить и как их связывать.

Xml конфигурация
StructureMap позволяет зарегистрировать элементы через xml двумя способами:
  1.  Конфигурационный файл App.config.
  2. Через создание файла StructureMap.config.
Эти две реализации практически идентичные. Посмотреть, как приводится реализация через StructureMap.config, можно в официальной документации. Поэтому рассмотрим вариант с файлом конфигурации App.config. Сразу после примера будет объяснено отличие между этими двумя вариантами.  Файл конфигурации приведен ниже:
<?xml version="1.0" encoding="utf-8" ?>
<configuration>
    <configSections>
      <section name="StructureMap" type="StructureMap.Configuration.StructureMapConfigurationSection,StructureMap"/>
    </configSections>

    <StructureMap>
      <DefaultInstance
        PluginType="StructureMapDemo.Model.IBook, StructureMapDemo"
        PluggedType="StructureMapDemo.Model.LibraryBook, StructureMapDemo"
        />
      <DefaultInstance
        Name="HomeLibraryBookService"
        PluginType="StructureMapDemo.Model.ILibraryBookService, StructureMapDemo"
        PluggedType="StructureMapDemo.Model.HomeLibraryBookService, StructureMapDemo"
        />
      <DefaultInstance
        Name="LibraryBookService"
        PluginType="StructureMapDemo.Model.ILibraryBookService, StructureMapDemo"
        PluggedType="StructureMapDemo.Model.LibraryBookService, StructureMapDemo"
        />
      <DefaultInstance
        PluginType="StructureMapDemo.Model.IVisitorRepository, StructureMapDemo"
        PluggedType="StructureMapDemo.Model.VisitorRepository, StructureMapDemo"
        />
    </StructureMap>
</configuration>
Использовать этот файл конфигурации можно так:
private void InitializeFromConfigFile()
{
    ObjectFactory.Initialize(x =>
    {
        x.PullConfigurationFromAppConfig = true;
    });
    var model = ObjectFactory.GetInstance<MainViewModel>();
    var view = ObjectFactory.GetInstance<MainWindow>();
    view.DataContext = model;
    view.Show();
}
Если Вы хотите использовать StructureMap.config, замените вызов
ObjectFactory.Initialize(x =>
    {
        x.PullConfigurationFromAppConfig = true;
    });
на вызов
ObjectFactory.Initialize(x =>
            {
                x.UseDefaultStructureMapConfigFile = true;
            });
Вы также можете конфигурировать привязку через Ioc контейнер с помощью методов
x.AddConfigurationFromXmlFile();
x.AddConfigurationFromNode();
С помощью этих методов можно без перекомпиляции приложения, по сути, конфигурировать контейнер "на лету". Чтобы посмотреть, как использовать эти методы, давайте рассмотрим пример.
private void InitializeContainerFromConfigFile()
{
    var config = AppDomain.CurrentDomain.SetupInformation.ConfigurationFile;
    var container = new Container();
    container.Configure(x => x.AddConfigurationFromXmlFile(config));
    var model = container.GetInstance<MainViewModel>();
    var view = container.GetInstance<MainWindow>();
    view.DataContext = model;
    view.Show();
}
Об этом удивительном факте я прочитал в книге Mark Seemann, Dependency Injection in .NET. Поэтому рекомендую эту книгу к прочтению, если Вы хотите знать тонкости использования популярных IoC контейнеров.
Property Injection (PI)
Внедрение property injection с помощью StructureMap настолько просто, что в примере просто будет приведена строчка кода, как это можно реализовать:
[SetterProperty]
public IVisitorRepository VisitorRepository { get; set; }
Вот и вся реализация. Все остальное за Вас сделает StructureMap. Дело в том, что эту строчку можно было даже не писать, так как StructureMap поддерживает автоматическую регистрацию property, если оно объявлено как public (auto wire).
Method injection можно сделать так:
model.PrintBook(container.GetInstance<ILibraryBookService>("HomeLibraryBookService"));
Или как вариант сделать Constructor Injection для интерфейса IContainer, в нужный класс, а затем использовать его для получения нужных экземпляров. Такой подход был замечен мной у программистов, которые используют за базовый Unity контейнер.
Registry
Контейнер StructureMap имеет возможность разделения связывания интерфейсов с их реализацией по модулям. В данном IoC контейнере такая возможность реализуется с помощью класса Registry. Рассмотрим, как это работает. Пример класса для регистрации  приложения для электронной библиотеки:
public class LibraryRegistry : Registry
{
    public LibraryRegistry()
    {
        For<IBook>().Use<LibraryBook>();
        For<ILibraryBookService>().Use<HomeLibraryBookService>().Named("HomeLibraryBookService");
        For<ILibraryBookService>().Use<LibraryBookService>().Named("LibraryBookService");
        For<Visitor>();
        For<IVisitorRepository>().Use<VisitorRepository>();
        For<MainWindow>();
        For<IBook>().Use<LibraryBook>();
        For<MainViewModel>();
    }
}
Использование данного модуля:
private void InitializeRegistry()
{
    using (var container = new Container())
    {
        container.Configure(x => x.AddRegistry<LibraryRegistry>());
        var model = container.GetInstance<MainViewModel>();
        var view = container.GetInstance<MainWindow>();
        view.DataContext = model;
        view.ShowDialog();
    }
}
Данный подход позволяет программистам разделить логику программы по модулям. Такой подход элегантно сочетается с программированием по модели, если есть четкое разделение программной архитектуры на слои.
Итоги
В данной статье мы рассмотрели использование IoC контейнера StructureMap. Этот контейнер предоставляет множество возможностей для управления зависимостями. Также этот контейнер по своей простоте использования подобен контейнеру Autofac, по конфигурированию и настройке этот контейнер ничем не хуже, чем Unity или CastleWindsor. Напоследок осталась новость для любителей и ценителей MEF. Если вы пишете плагины и используете для этого MEF (Managed Extensibility Framework), или используете MEF для связывания интерфейсов и реализации, посмотрите в сторону StructureMap. Он умеет делать все то же, что и MEF, но намного быстрее. Надеюсь, эта статья была полезная для Вас, и она станет началом для использования возможностей этого контейнера.

Источники:

No comments:

Post a Comment