StructureMap 1/3

Сегодня я хочу рассказать о 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:

  • Настройка с помощью 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 адреса, пути для файлов и все остальное в таком же духе.

Начнем с самого простого, регистрация класса с конструктором по умолчанию.

Пусть у нас есть такой простой набор классов:

Это набор будет пока что основным, для демонстрации возможностей. Но вы не волнуйтесь, дальше классы и связи будут сложнее, потому что на таких классах многого тоже не покажешь.

Основа

Регистрация возможна с помощью контейнера (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 можно переопределить возвращаемый тип, необходимо лишь указать тип плагина и новый класс.

Данные примеры были на общие принципы регистрации, когда сами классы простые. В следующей теме рассмотрим, как быть с классами, у которых конструкторы с параметрами.

 

Продолжение следует.

Hard’n’heavy!

 

 

Оставить комментарий