Расширение возможностей Unity

    В этом посте я покажу пример того, как можно расширить стандартные возможности IoC-контейнера Unity. Покажу как создается объект в Unity «изнутри». Расскажу про Unity Extensions, Strategies & Policies.

    Допустим в нашем приложении есть компонент Persistence, который отвечает за сохранении объектов. Он описывается интерфейсом IPersistence и имеет реализации — FilePersistence, DbPersistence, WsPersistence, InMemoryPersistence.

    В классическом варианте мы в начале приложения регистрируем нужную реализацию в Unity и далее, вызывая Resolve для IPersistence, всегда получаем ее.
    IUnityContainer uc = new UnityContainer();

    uc.RegisterType<IPersistence, FilePersistence>();
    IPersistence p = uc.Resolve<IPersistence>();
    p.Add(obj);


    * This source code was highlighted with Source Code Highlighter.


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

    В Unity есть возможность регистрировать зависимости по имени. Пример:
    uc.RegisterType<IPersistence, InMemoryPersistence>("none");
    uc.RegisterType<IPersistence, FilePersistence>("file");
    uc.RegisterType<IPersistence, DbPersistence>("db");
    uc.RegisterType<IPersistence, WsPersistence>("ws");

    IPersistence p = uc.Resolve<IPersistence>("file"); // Получили file реализацию.
    IPersistence p = uc.Resolve<IPersistence>("db");  // Получили dbреализацию.


    * This source code was highlighted with Source Code Highlighter.


    Осталось добиться, чтобы при получении реализации без имени, Unity каким-то образом определял какую нам нужно реализацию.
    Пусть у нас будет делегат, который мы передадим Unity, определявший нужное имя реализации.
    Пример:
    uc.SetNameResolver<IPersistence>(GetPersistenceImplementationName);
    IPersistence p = uc.Resolve<IPersistence>();  // Тут мы получим ту реализацию, имя который вернул GetPersistenceImplementationName


    * This source code was highlighted with Source Code Highlighter.


    Стандартного способа в Unity для этого нет. Но мы решим проблему написанием своего расширения.

    Unity Extensions


    Расширение Unity — это класс, унаследованный от UnityContainerExtension. У него есть контекст расширения (ExtensionContext) и виртуальные методы Initialize() и Remove() (соответственно вызываются при инициализации и удалении расширения).
    Добавляются расширения через методы контейнера AddNewExtension и AddExtension, удаляются через RemoveAllExtensions.
    public class NameResolverExtension : UnityContainerExtension
    {
      protected override void Initialize()
      {   
      }

      protected override void Remove()
      {
      }

      public NameResolverExtension()
        : base()
      {
      }
    }

    uc.AddNewExtension<NameResolverExtension>();


    * This source code was highlighted with Source Code Highlighter.


    Чтобы расширение можно было конфигурировать, оно должно реализовывать интерфейс-конфигуратор, унаследованный от IUnityContainerExtensionConfigurator. Конфигурирование происходит через метод контейнера Configure.
    // Наш делегат для получения имени
    public delegate string NameResolverDelegate(Type typeToBuild);

    // Интерфейс-конфигуратор
    public interface INameResolverExtensionConfigurator : IUnityContainerExtensionConfigurator
    {
      INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(
        NameResolverDelegate resolver);
    }

    static private string GetPersistenceImplementationName(Type typeToBuild)
    {
      // На самом деле тут мы должны читать конфиг...
      return "db";
    }

    uc.Configure<INameResolverExtensionConfigurator>()
      .RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);


    * This source code was highlighted with Source Code Highlighter.


    Strategy


    Каждый зарегистрированный тип в Unity имеет свой build-ключ (buildKey). Он состоит из зарегистрированного типа и имени, под которым он был зарегистрирован.

    Процесс Resolve в Unity реализован при помощи стратегий.
    Стратегия — это класс реализующий интерфейс IBuilderStrategy. Он имеет четыре метода: PreBuildUp, PostBuildUp, PreTearDown, PostTearDown.
    При вызове Resolve:
    1. Создается список зарегистрированных стратегий;
    2. Формируется build-ключ искомого типа и контекст построения (BuilderContext);
    3. Контекст последовательно обрабатывается стратегиями до тех пор, пока одна из них не установит флаг BuildComplete в true.

    В Unity есть 4 предопределенных стратегии, которые вызываются для каждого Resolve:
    • BuildKeyMappingStrategy. Заменяет build-ключ в контексте с искомого типа на ключ реализации. По сути весь resolving тут и происходит;
    • LifetimeStrategy. Проверяет наличие реализации в Lifetime-менеджере;
    • ArrayResolutionStrategy. Разрешение зависимостей-массивов;
    • BuildPlanStrategy. Создании экземпляра реализации (если он еще не создан) и автоматическое разрешение его зависимостей.


    Мы напишем свою стратеги, которая будет подменять в build-ключе пустое имя на имя нужной реализации.
    internal class ResolveNameBuilderStrategy : BuilderStrategy
    {
      private NamedTypeBuildKey ReplaceBuildKeyName(IBuilderContext context, NamedTypeBuildKey buildKey)
      {
      }

      public override void PreBuildUp(IBuilderContext context)
      {
        if (context.BuildKey is NamedTypeBuildKey)
          context.BuildKey = ReplaceBuildKeyName(context, (NamedTypeBuildKey)(context.BuildKey));
      }

      public ResolveNameBuilderStrategy()
        : base()
      {
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Т.к. поиск реализации по build-ключу происходит в стратегии BuildKeyMappingStrategy, то мы должны зарегистрировать свою стратегию так, чтобы она выполнилась раньше BuildKeyMappingStrategy. Стратегии сортируются в зависимости от этапа, который был указан при регистрации.
    Всего есть 7 этапов — Setup, TypeMapping, Lifetime, PreCreation, Creation, Initialization, PostInitialization. BuildKeyMappingStrategy регистрируется на этап TypeMapping, значит нашу стратегию зарегистрируем на Setup. Регистрация будет происходить в методе Initialize нашего расширения.
    public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
    {
      protected override void Initialize()
      {
        Context.Strategies.AddNew<ResolveNameBuilderStrategy>(UnityBuildStage.Setup);
      }

    }


    * This source code was highlighted with Source Code Highlighter.


    Policies


    Еще одним важным механизмом в Unity являются политики.
    Политика — это интерфейс, наследуемый от IBuilderPolicy и класс его реализующий.
    В интерфейсе политики можно определять методы для любых действий. Сам IBuilderPolicy пустой.
    Стратегия может получить из BuilderContext политику для заданного типа по build-ключу.
    Создадим свою политику для получения нового имя по build-ключу.
    internal interface IResolveNamePolicy : IBuilderPolicy
    {
      string ResolveName(NamedTypeBuildKey buildKey);
    }


    * This source code was highlighted with Source Code Highlighter.


    Используем ее в нашей стратегии.
    internal class ResolveNameBuilderStrategy : BuilderStrategy
    {
      private NamedTypeBuildKey ReplaceBuildKeyName(
        IBuilderContext context, NamedTypeBuildKey buildKey)
      {
        IResolveNamePolicy policy = context.Policies.Get<IResolveNamePolicy>(buildKey);

        if (policy != null)
          return new NamedTypeBuildKey(buildKey.Type, policy.ResolveName(buildKey));

        return buildKey;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Добавлять стратегии можно в расширении через context. Политика добавляется для конкретного ключа, или как политика по-умолчанию.
    Реализуем политику получения имя через делегат:
    internal class ResolveNamePolicyDelegated : IResolveNamePolicy
    {
      protected readonly NameResolverDelegate Resolver;

      public ResolveNamePolicyDelegated(NameResolverDelegate resolver)
        : base()
      {
        Resolver = resolver;
      }

      public string ResolveName(NamedTypeBuildKey buildKey)
      {
        return Resolver(buildKey.Type);
      }
    }


    * This source code was highlighted with Source Code Highlighter.

    Для интерфейса политики IResolveNamePolicy может быть несколько реализация, например через делегат, через интерфейс, через обращение к конфигу.

    Добавлять политику для конкретного build-ключа будем при конфигурировании нашего расширения.
    public class NameResolverExtension : UnityContainerExtension, INameResolverExtensionConfigurator
    {
      public INameResolverExtensionConfigurator RegisterDelegatedName<TTypeToBuild>(NameResolverDelegate resolver)
      {
        Context.Policies.Set<IResolveNamePolicy>(
          new ResolveNamePolicyDelegated(resolver),
          NamedTypeBuildKey.Make<TTypeToBuild>());

        return this;
      }
    }


    * This source code was highlighted with Source Code Highlighter.


    Результат


    Наше расширение готово.
    Теперь мы можем делать так:
    IUnityContainer uc = new UnityContainer();

    uc.RegisterType<IPersistence, InMemoryPersistence>("none");
    uc.RegisterType<IPersistence, FilePersistence>("file");
    uc.RegisterType<IPersistence, DbPersistence>("db");
    uc.RegisterType<IPersistence, WsPersistence>("ws");

    uc.AddNewExtension<NameResolverExtension>();
    uc.Configure<INameResolverExtensionConfigurator>().RegisterDelegatedName<IPersistence>(GetPersistenceImplementationName);

    IPersistence p = uc.Resolve<IPersistence>();
    p.Add(new Object()); // Будет использоваться та реализация, имя которой вернет GetPersistenceImplementationName


    * This source code was highlighted with Source Code Highlighter.

    Можно создать class helper для IUnityContainer чтобы можно было писать «SetNameResolver», как в начале и хотели.

    Теперь при вызове Resolve:
    • Первой запускается наша стратегия;
    • Она получает политику для искомого build-ключа;
    • Если для этого build-ключа существует политика, то build-ключ заменяется в контексте на ключ с именем из политики;
    • Дальше Resolve работает так же как и раньше, но создает объект уже не для безымянного ключа, а для ключа с новым именем.

    Исходный код — тут

    Похожие публикации

    AdBlock похитил этот баннер, но баннеры не зубы — отрастут

    Подробнее
    Реклама

    Комментарии 11

      0
      Спасибо за статью.

      Небольшая критика.
      Я слышал, что разрешение зависимостей по строковому имени — это каг бы не правильно.
      Правильно это marker interface.

      Что вы об этом думаете?
        0
        Всегда пожалуйста.

        А для чего тут marker interface использовать?
        Можно пример кода?
          +1
          Действительно, marker interface это не ваш случай.

          Если реализция может изменяться в runtime, то можно повторно вызывать RegisterType/RegisterInstance — это затрёт предыдушее значение в IoC контейнере. Так не пробовали?

          А строковые имена хороши для usecase типа ResolveAll. Чтобы вернулся массив всех объектов, реализующих IPersistence. Здесь мне кажется лучше без них.
            0
            Если реализция может изменяться в runtime, то можно повторно вызывать RegisterType/RegisterInstance — это затрёт предыдушее значение в IoC контейнере. Так не пробовали?

            Полностью согласен. Ваш вариант лучше, т.к. в моем варианте мы часть ответственности IoC берем на себя.
            Плюс в Вашем варианте можно при изменении конфигурации, после перерегистрации вызвать BuildUp для существующих объектов, что должно заменить в них реализации зависимостей.

            Но пост писался как пример создания Unity Extension.
        +3
        В плане доступности сервисов, тут есть варианты. Например, можно получать первый существующий сервис вот так:
        IService svc1 = uc.Resolve<IService>("database");
        IService svc2 = uc.Resolve<IService>("memory");
         
        var svc = svc1 ?? svc2;

        Не очень эленантно, но все же. Также есть например вот такой вариант:
        var svc = uc.ResolveAll<IService>().FirstOrDefault(s => s != null);

        А за статью спасибо. Интересно.
          0
          А что вы решили насчет написания статьи об использовании Unity по мотивам одного из предыдущих постов на Хабре?
            0
            Ну, я написал небольшой пост на эту тему. Возможно в скором будущем напишу еще.
              0
              Честно говоря, ожидал большего. Очередной притянутый за уши пример :(
                0
                притянутый за уши? взят из продакшна.
                  0
                  А разве в продакшн нельзя притянуть за уши? Пример все равно неудачный, и демонстрирует преимущества IoC очень слабо.
            0
            Всегда пожалуйста.
            Но не очень понял комментарий.
            Нам не нужно получить «любую из существующих реализаций», нам нужно получить «реализацию, нужную в данный момент, в зависимости от условий о которых IoC не знает»

            И Resolve не возвращает null, если такой реализации нет — будет exception.

          Только полноправные пользователи могут оставлять комментарии. Войдите, пожалуйста.

          Самое читаемое