Pull to refresh

StructureMap — краткий справочник для работы (1/3)

Reading time10 min
Views12K
Сегодня я хочу рассказать о 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 адреса, пути для файлов и все остальное в таком же духе.

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

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

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

Продолжение следует.
Tags:
Hubs:
+17
Comments24

Articles