Сегодня я хочу рассказать о IoC контейнере StructureMap (и это не перевод устаревшей официальной документации), который мне приглянулся гораздо больше чем Unity. Хотя, честно сказать, мои взаимоотношения с Unity не сложились с самого начала, когда я увидел километровые файлы конфигурации к нему или же двухсот знаковые строки конфигурации в коде. Не будем о грустном.
StructureMap не только мне показался более удобным чем другие реализации DI\IoC, достаточно набрать в гугле StructureMap vs Unity и получить кучу ссылок, где люди обсуждают и показывают наглядно, что в работе самым удобным, гибким и естественным является StructureMap.
Ссылка раз, два и три. Ко всему прочему StructureMap еще и достаточно быстрый.
Вот еще очень интересные подборки материалов по сравнению фреймворков http://www.sturmnet.org/blog/2010/03/04/poll-results-ioc-containers-for-net
Я думаю можно опустить рассуждения о том, стоит ли такие контейнеры использовать в своем проекте или нет, и стоит ли писать свою реализацию. Я могу сказать, что в моей практике сначала случилось так, что были самописные реализации, которые заметно уступали по возможностям и удобству, но они решали свои конкретные задачи и этого на тот момент хватало. С развитием проекта, как-то было недосуг переводить все на какую-то другую технологию. Потом был Unity, Unity, но в конце концов я пришел к тому, что надо попробовать StructureMap и не пожалел об этом ни разу.
В чем, на мой взгляд, плюсы StructureMap:
Еще много есть интересных и полезных вещей в StructureMap, но их было бы неправильно отнести к плюсам, лучше назвать их приятным дополнением, которые только облегчают жизнь.
Краткий план последующего материала выглядит следующим образом:
Устанавливать StructureMap в свое приложение я советую с помощью NuGet. Одна команда в Package Manager Console (install-Package StructureMap) или же поиск и установка с помощью визарда – это наиболее легкие пути по получению. Но если хочется, то можно скачать проект с официальной страницы http://github.com/structuremap/structuremap/downloads
Самое главное действие наверно для любого IoC контейнера – это регистрация. От того насколько она удобна и понятна зависит, будут ли этим пользоваться люди и насколько вероятно неверное использование инструмента.
Автор советует использовать DSL так широко, как только это возможно и прибегать к файлу конфигурации, только когда надо отдельно задавать строки соединения, URL адреса, пути для файлов и все остальное в таком же духе.
Начнем с самого простого, регистрация класса с конструктором по умолчанию.
Пусть у нас есть такой простой набор классов:
Это набор будет пока что основным, для демонстрации возможностей. Но вы не волнуйтесь, дальше классы и связи будут сложнее, потому что на таких классах многого тоже не покажешь.
Регистрация возможна с помощью контейнера (Container), с помощью статического класса ObjectFactory или же с помощью класса Registry. Постепенно рассмотрим регистрацию с помощью всех этих объектов. Основной класс для регистрации это Registry, остальные классы прокидывают его функционал, для удобства.
Регистрация с помощью статического класса ObjectFactory.
Все делается с помощью DSL и лямбда выражений. Сам DSL достаточно лаконичный и понятный, полученный код с легкостью складывается в осмысленные выражения. В данном случае можно легко прочитать, что для интерфейса IClass1 надо использовать Class1.
Получение объектов можно осуществить следующим способом, тоже интуитивно понятным:
Основной метод для получения объекта GetInstance, в данном случае с указанием интерфейса. Дальше мы еще рассмотрим различные способы получения готовых объектов. Вы можете заметить, что метод возвращает строку, которую получаем от метода с говорящим названием WhatDoIHave. С помощью данного метода можно проводить диагностику внутренностей контейнера, смотреть что, как и где зарегистрировано.
Долгое время автор фреймворка не мог принять термин контейнер, по отношению к своему детищу, поэтому следующий способ был скрыт достаточно долго и только в более поздних реализациях открыл естественный ход регистрации, как оно было реализовано внутри, за статическим классом. Итак,
На первый взгляд все то же самое, лямбда та же самая, но теперь мы создаем класс, который будем потом отдавать наружу, и по которому будем обращаться к контейнеру. Т.е. еще раз, ObjectFactory это просто статический класс обертка над классом Container.
Получение объектов пойдет по тому же сценарию:
Следующий на очереди объект это Registry. Собственно его вы косвенно и вызывали все предыдущие разы. Для разнообразия зарегистрируем конкретные классы.
В данном случае используется метод ForConcreteType, что является синонимом для .For<T>().Use<T>(). Так же можно видеть, что класс Registry можно использовать как подконтейнер, собирать его и потом передавать на сборку в один контейнер. В данном случае проиллюстрировано добавление Registry в момент создания, но ничто не помешает написать:
Чтение «конкретных» классов ничем не отличается от обычного:
StructureMap позволяет группировать соответствия классов используя именованные профили. Т.е. можно быстро переключаться между маппингов классов.
Здесь стоит обратить внимание, что классы Class1 и Class2 регистрируются по общему интерфейсу, но в разных профилях. Для того чтобы получить необходимый класс, надо переключиться между профилями в контейнере с помощью метода SetDefaultProfile который принимает имя профиля.
Имя профиля может быть только строковой переменной, но это уже не большая проблема. Я к тому, что не стоит в реальности писать имя профиля как в примере открытой строкой. Вредно для кармы.
После установки активного профиля, можно работать с контейнером как обычно. В итоге при выполнении одной и той же строки container.GetInstance<IClass>(); мы получаем разные классы.
Существует еще один способ для решения проблемы получения конкретного класса по общему интерфейсу, это именованный плагин.
Немного о терминологии. В IntelliSense и немного здесь можно встретить термин плагин, PluginType и PluggedType, в общем случае это означает тип, который вы хотите получить. Т.е. во всех предыдущих примерах IClass можно назвать PluginType, а Class1 или Class2 – PluggedType.
На примере видно, что мы регистрируем классы по общему интерфейсу, но при этом задаем им конкретные имена. С помощью метода Named можно легко теперь запрашивать конкретный тип.
В примере показывается, как можно обратиться к контейнеру и получить конкретный тип на общий интерфейс. Однако тут мы заодно затронем вопрос, а что же будет при попытке вызова метода GetInstance с общим интерфейсом без указания имени плагина?
Поведение по умолчанию следует поговорке «кто последний, тот и папа», т.е. в данном случае в переменную instanceDef попадет экземпляр класса Class2. Однако мы можем определять вполне явно класс «по-умолчанию». Для этого надо воспользоваться несколько иной формой регистрации плагинов.
И опять можно сказать, что пример сам себя описывает. Если прочитать его, то буквально дословно будет: для интерфейса IClass добавить реализации типов Class1 по имени Class1, Class2 по имени Class2, использовать же Class1 (в данном случае очень даже конкретный класс, но можно было и как в предыдущих примерах написать .Use<Class1>()).
В данном случае именно метод Use говорит, какой тип будет использоваться для интерфейса по умолчанию. Если теперь выполнить следующий код
то получим экземпляр класса Class1.
Use уже само по себе выставляет тип, используемый по умолчанию.
Логичным продолжением будет являться поиск и автоматическая регистрация типов в контейнере. Представим, что у нас не два класса наследуются от общего интерфейса, а 50! Будет очень грустно и скучно заполнять руками все эти регистрации и зависимости. На такой случай есть у StructureMap метод Scan, который пробегает по интересующим вас сборкам или папкам и регистрирует подходящие объекты. Таким образом можно реализовать структуру плагинов для приложения и даже в чем-то составить конкуренцию MEF либо заменить его.
Для того, чтобы метод Scan нашел и зарегистрировал типы необходимо соблюдение нескольких условий:
Метод сканирование и поведение может быть переопределено, но пока что это не будет рассматриваться.
Указание сборки для сканирования можно задать несколькими способами:
После того, как вы указали на подопытные сборки, можно настроить процесс импорта более детально с помощью методов по включению/исключению типов по различным параметрам. За более детальной информацией лучше обратиться к документации. Она устарела, но общее представление о возможностях дает.
Итак, рассмотрим пример попроще:
В этом классе мы говорим, что хотим импортировать все типы, которые реализуют интерфейс IClass, из папки приложения, руководствоваться следует соглашениями по умолчанию. И последней строчкой идет команда на запуск поиска. Ранее все работало без явного указания. Но сейчас надо четко прописать метод LookForRegistries.
После того, как метод отработает можно посмотреть, что нашлось и зарегистрировалось в контейнере.
Обратите внимание, что сейчас вызывается метод GetAllInstances. Если вызвать метод для получения какого-то конкретного класса из зарегистрированных, то будет ошибка, так как StructureMap не знает какой именно класс возвращать «по-умолчанию».
Честно сказать, при такой реализации пользоваться результатами команды Scan невозможно. Для того чтобы все стало хорошо, и можно было бы к найденным классам обращаться по имени, надо код сканирования надо переписать немного.
К методу AddAllTypesOf добавили уточняющее правило, что все классы надо регистрировать по их имени. После такой модификации можно работать с конкретными типами:
В процессе работы с контейнером можно переопределить тип возвращаемый по умолчанию. Это применяется в основном в тестах. Демонстрация работы:
Ранее мы уже объявляли класс RegisterAsPluginWithDefault, который возвращает класс Class1 по умолчанию. С помощью метода Inject можно переопределить возвращаемый тип, необходимо лишь указать тип плагина и новый класс.
Данные примеры были на общие принципы регистрации, когда сами классы простые. В следующей теме рассмотрим, как быть с классами, у которых конструкторы с параметрами.
Продолжение следует.
StructureMap не только мне показался более удобным чем другие реализации DI\IoC, достаточно набрать в гугле StructureMap vs Unity и получить кучу ссылок, где люди обсуждают и показывают наглядно, что в работе самым удобным, гибким и естественным является StructureMap.
Ссылка раз, два и три. Ко всему прочему StructureMap еще и достаточно быстрый.
Вот еще очень интересные подборки материалов по сравнению фреймворков http://www.sturmnet.org/blog/2010/03/04/poll-results-ioc-containers-for-net
Я думаю можно опустить рассуждения о том, стоит ли такие контейнеры использовать в своем проекте или нет, и стоит ли писать свою реализацию. Я могу сказать, что в моей практике сначала случилось так, что были самописные реализации, которые заметно уступали по возможностям и удобству, но они решали свои конкретные задачи и этого на тот момент хватало. С развитием проекта, как-то было недосуг переводить все на какую-то другую технологию. Потом был Unity, Unity, но в конце концов я пришел к тому, что надо попробовать StructureMap и не пожалел об этом ни разу.
В чем, на мой взгляд, плюсы StructureMap:
- Настройка с помощью DSL
- Очень гибкая настройка всего
- Простота конечного использования
- Возможности для проверки внутренних связей
- Поддержка тестирования out-of-the-box
Еще много есть интересных и полезных вещей в StructureMap, но их было бы неправильно отнести к плюсам, лучше назвать их приятным дополнением, которые только облегчают жизнь.
Краткий план последующего материала выглядит следующим образом:
- Установка
- Регистрация (Основа, Профили, Плагины, Сканирование, Внедрение)
- Конструкторы (Простые типы, Конструктор по умолчанию, Составные типы, Приведение типов, Задание аргументов)
- Свойства (Простое задание свойств, Встроенное задание свойств, Задание свойств фреймворком, Допостроение существующих классов)
- Время жизни
- Перехватчики (OnCreation, EnrichWith)
- Дженерик типы
- Аттрибуты (DefaultConstructor, ValidationMethod, Все остальные)
- Тесты
Установка StructureMap
Устанавливать StructureMap в свое приложение я советую с помощью NuGet. Одна команда в Package Manager Console (install-Package StructureMap) или же поиск и установка с помощью визарда – это наиболее легкие пути по получению. Но если хочется, то можно скачать проект с официальной страницы http://github.com/structuremap/structuremap/downloads
Регистрация
Самое главное действие наверно для любого IoC контейнера – это регистрация. От того насколько она удобна и понятна зависит, будут ли этим пользоваться люди и насколько вероятно неверное использование инструмента.
Автор советует использовать DSL так широко, как только это возможно и прибегать к файлу конфигурации, только когда надо отдельно задавать строки соединения, URL адреса, пути для файлов и все остальное в таком же духе.
Начнем с самого простого, регистрация класса с конструктором по умолчанию.
Пусть у нас есть такой простой набор классов:
public interface IClass {}
public interface IClass1 : IClass {}
public interface IClass2 : IClass {}
public class Class1 : IClass1 {}
public class Class2 : IClass2 {}
Это набор будет пока что основным, для демонстрации возможностей. Но вы не волнуйтесь, дальше классы и связи будут сложнее, потому что на таких классах многого тоже не покажешь.
Основа
Регистрация возможна с помощью контейнера (Container), с помощью статического класса ObjectFactory или же с помощью класса Registry. Постепенно рассмотрим регистрацию с помощью всех этих объектов. Основной класс для регистрации это Registry, остальные классы прокидывают его функционал, для удобства.
Регистрация с помощью статического класса ObjectFactory.
public class RegisterByObjectFactory {
public RegisterByObjectFactory() {
ObjectFactory.Initialize(x => {
x.For<IClass1>().Use<Class1>();
x.For<IClass2>().Use<Class2>();
});
}
}
Все делается с помощью DSL и лямбда выражений. Сам DSL достаточно лаконичный и понятный, полученный код с легкостью складывается в осмысленные выражения. В данном случае можно легко прочитать, что для интерфейса IClass1 надо использовать Class1.
Получение объектов можно осуществить следующим способом, тоже интуитивно понятным:
private static string ObjectFactoryExample() {
new RegisterByObjectFactory();
var class1Inst = ObjectFactory.GetInstance<IClass1>();
var class2Inst = ObjectFactory.GetInstance<IClass2>();
return ObjectFactory.WhatDoIHave();
}
Основной метод для получения объекта GetInstance, в данном случае с указанием интерфейса. Дальше мы еще рассмотрим различные способы получения готовых объектов. Вы можете заметить, что метод возвращает строку, которую получаем от метода с говорящим названием WhatDoIHave. С помощью данного метода можно проводить диагностику внутренностей контейнера, смотреть что, как и где зарегистрировано.
Долгое время автор фреймворка не мог принять термин контейнер, по отношению к своему детищу, поэтому следующий способ был скрыт достаточно долго и только в более поздних реализациях открыл естественный ход регистрации, как оно было реализовано внутри, за статическим классом. Итак,
public class RegisterByContainer {
public IContainer Container;
public RegisterByContainer() {
Container = new Container(x => {
x.For<IClass1>().Use<Class1>();
x.For<IClass2>().Use<Class2>();
});
}
}
На первый взгляд все то же самое, лямбда та же самая, но теперь мы создаем класс, который будем потом отдавать наружу, и по которому будем обращаться к контейнеру. Т.е. еще раз, ObjectFactory это просто статический класс обертка над классом Container.
Получение объектов пойдет по тому же сценарию:
private static string ContainerExample() {
var container = new RegisterByContainer().Container;
var class1Inst = container.GetInstance<IClass1>();
var class2Inst = container.GetInstance<IClass2>();
return container.WhatDoIHave();
}
Следующий на очереди объект это Registry. Собственно его вы косвенно и вызывали все предыдущие разы. Для разнообразия зарегистрируем конкретные классы.
public class RegisterByRegister {
public IContainer Container;
public RegisterByRegister() {
var registry = new Registry();
registry.ForConcreteType<Class1>();
registry.ForConcreteType<Class2>();
Container = new Container(x => x.AddRegistry(registry));
}
}
В данном случае используется метод ForConcreteType, что является синонимом для .For<T>().Use<T>(). Так же можно видеть, что класс Registry можно использовать как подконтейнер, собирать его и потом передавать на сборку в один контейнер. В данном случае проиллюстрировано добавление Registry в момент создания, но ничто не помешает написать:
Container = new Container();
Container.Configure(x => x.AddRegistry(registry));
Чтение «конкретных» классов ничем не отличается от обычного:
private static string ConcreteClassExample() {
var container = new RegisterByRegister().Container;
var class1Inst = container.GetInstance<Class1>();
var class2Inst = container.GetInstance<Class2>();
return container.WhatDoIHave();
}
Профили
StructureMap позволяет группировать соответствия классов используя именованные профили. Т.е. можно быстро переключаться между маппингов классов.
public class RegisteringProfiles {
public IContainer Container;
public RegisteringProfiles() {
var registry = new Registry();
registry.Profile("p1", x => x.For<IClass>().Use<Class1>());
registry.Profile("p2", x => x.For<IClass>().Use<Class2>());
Container = new Container(x => x.AddRegistry(registry));
}
}
Здесь стоит обратить внимание, что классы Class1 и Class2 регистрируются по общему интерфейсу, но в разных профилях. Для того чтобы получить необходимый класс, надо переключиться между профилями в контейнере с помощью метода SetDefaultProfile который принимает имя профиля.
private static string ProfilesExample() {
var container = new RegisteringProfiles().Container;
container.SetDefaultsToProfile("p1");
var class1Inst = container.GetInstance<IClass>();
container.SetDefaultsToProfile("p2");
var class2Inst = container.GetInstance<IClass>();
return container.WhatDoIHave();
}
Имя профиля может быть только строковой переменной, но это уже не большая проблема. Я к тому, что не стоит в реальности писать имя профиля как в примере открытой строкой. Вредно для кармы.
После установки активного профиля, можно работать с контейнером как обычно. В итоге при выполнении одной и той же строки container.GetInstance<IClass>(); мы получаем разные классы.
Плагины
Существует еще один способ для решения проблемы получения конкретного класса по общему интерфейсу, это именованный плагин.
Немного о терминологии. В IntelliSense и немного здесь можно встретить термин плагин, PluginType и PluggedType, в общем случае это означает тип, который вы хотите получить. Т.е. во всех предыдущих примерах IClass можно назвать PluginType, а Class1 или Class2 – PluggedType.
public class RegisterAsPlugin {
public IContainer Container;
public RegisterAsPlugin() {
Container = new Container(x => {
x.For<IClass>().Use<Class1>().Named("Class1");
x.For<IClass>().Use<Class2>().Named("Class2");
});
}
}
На примере видно, что мы регистрируем классы по общему интерфейсу, но при этом задаем им конкретные имена. С помощью метода Named можно легко теперь запрашивать конкретный тип.
private static string PluginExample() {
var container = new RegisterAsPlugin().Container;
var class1Inst = container.GetInstance<IClass>("Class1");
var class2Inst = container.GetInstance<IClass>("Class2");
var instanceDef = container.GetInstance<IClass>();
return container.WhatDoIHave();
}
В примере показывается, как можно обратиться к контейнеру и получить конкретный тип на общий интерфейс. Однако тут мы заодно затронем вопрос, а что же будет при попытке вызова метода GetInstance с общим интерфейсом без указания имени плагина?
Поведение по умолчанию следует поговорке «кто последний, тот и папа», т.е. в данном случае в переменную instanceDef попадет экземпляр класса Class2. Однако мы можем определять вполне явно класс «по-умолчанию». Для этого надо воспользоваться несколько иной формой регистрации плагинов.
public class RegisterAsPluginWithDefault {
public IContainer Container;
public RegisterAsPluginWithDefault() {
Container = new Container(x => x.For<IClass>()
.AddInstances(i => {
i.Type(typeof (Class1)).Named("Class1");
i.Type(typeof (Class2)).Named("Class2");
})
.Use(new Class1())
);
}
}
И опять можно сказать, что пример сам себя описывает. Если прочитать его, то буквально дословно будет: для интерфейса IClass добавить реализации типов Class1 по имени Class1, Class2 по имени Class2, использовать же Class1 (в данном случае очень даже конкретный класс, но можно было и как в предыдущих примерах написать .Use<Class1>()).
В данном случае именно метод Use говорит, какой тип будет использоваться для интерфейса по умолчанию. Если теперь выполнить следующий код
var instanceDef = container.GetInstance<IClass>();
то получим экземпляр класса Class1.
Use уже само по себе выставляет тип, используемый по умолчанию.
Сканирование
Логичным продолжением будет являться поиск и автоматическая регистрация типов в контейнере. Представим, что у нас не два класса наследуются от общего интерфейса, а 50! Будет очень грустно и скучно заполнять руками все эти регистрации и зависимости. На такой случай есть у StructureMap метод Scan, который пробегает по интересующим вас сборкам или папкам и регистрирует подходящие объекты. Таким образом можно реализовать структуру плагинов для приложения и даже в чем-то составить конкуренцию MEF либо заменить его.
Для того, чтобы метод Scan нашел и зарегистрировал типы необходимо соблюдение нескольких условий:
- Тип должен быть явным, дженерик типы не регистрируются автоматически;
- Тип должен иметь публичный конструктор;
- Конструктор не может иметь аргументов примитивных типов;
- Множественное регистрирование не допускается.
Метод сканирование и поведение может быть переопределено, но пока что это не будет рассматриваться.
Указание сборки для сканирования можно задать несколькими способами:
- Явно прописать имя сборки или же передать ее саму;
- Обратиться к вызывающей сборке;
- Найти сборку содержащую определенный тип;
- Найти сборки по определенному пути.
После того, как вы указали на подопытные сборки, можно настроить процесс импорта более детально с помощью методов по включению/исключению типов по различным параметрам. За более детальной информацией лучше обратиться к документации. Она устарела, но общее представление о возможностях дает.
Итак, рассмотрим пример попроще:
public class RegisterByScan {
public IContainer Container;
public RegisterByScan() {
Container = new Container(x => x.Scan(s => {
s.AddAllTypesOf(typeof (IClass));
s.AssembliesFromPath(".");
s.WithDefaultConventions();
s.LookForRegistries();
}));
}
}
В этом классе мы говорим, что хотим импортировать все типы, которые реализуют интерфейс IClass, из папки приложения, руководствоваться следует соглашениями по умолчанию. И последней строчкой идет команда на запуск поиска. Ранее все работало без явного указания. Но сейчас надо четко прописать метод LookForRegistries.
После того, как метод отработает можно посмотреть, что нашлось и зарегистрировалось в контейнере.
private static string RegisterByScanExample() {
var container = new RegisterByScan().Container;
var instances = container.GetAllInstances<IClass>();
return container.WhatDoIHave();
}
Обратите внимание, что сейчас вызывается метод GetAllInstances. Если вызвать метод для получения какого-то конкретного класса из зарегистрированных, то будет ошибка, так как StructureMap не знает какой именно класс возвращать «по-умолчанию».
Честно сказать, при такой реализации пользоваться результатами команды Scan невозможно. Для того чтобы все стало хорошо, и можно было бы к найденным классам обращаться по имени, надо код сканирования надо переписать немного.
public class RegisterByScanWithNaming {
public IContainer Container;
public RegisterByScanWithNaming() {
Container = new Container(x => x.Scan(s => {
s.AddAllTypesOf(typeof (IClass)).NameBy(t => t.Name);
s.AssembliesFromPath(".");
s.WithDefaultConventions();
s.LookForRegistries();
}));
}
}
К методу AddAllTypesOf добавили уточняющее правило, что все классы надо регистрировать по их имени. После такой модификации можно работать с конкретными типами:
var instance = container.GetInstance<IClass>("Class1");
Внедрение
В процессе работы с контейнером можно переопределить тип возвращаемый по умолчанию. Это применяется в основном в тестах. Демонстрация работы:
private static string InjectExample() {
var container = new RegisterAsPluginWithDefault().Container;
var instance1 = container.GetInstance<IClass>("Class1");
var instance2 = container.GetInstance<IClass>("Class2");
var class1Inst = container.GetInstance<IClass>();
container.Inject(typeof (IClass), new Class2());
var class2Inst = container.GetInstance<IClass>();
return container.WhatDoIHave();
}
Ранее мы уже объявляли класс RegisterAsPluginWithDefault, который возвращает класс Class1 по умолчанию. С помощью метода Inject можно переопределить возвращаемый тип, необходимо лишь указать тип плагина и новый класс.
Данные примеры были на общие принципы регистрации, когда сами классы простые. В следующей теме рассмотрим, как быть с классами, у которых конструкторы с параметрами.
Продолжение следует.