Изучая Autofac. Динамическое создание объектов с помощью контейнера

Tags: autofac

Тестовый проект

Autofac 2 – один из множества IoC-контейнеров для .NET. Я не стану рассказывать о том, что такое IoC-контейнер (Inversion of Control container), IoC (Inversion of Control) и DI (Dependency Injection), предполагая, что вы уже знаете об этом или можете узнать из других источников, например, здесь. Я буду рассказывать об одном из них – Autofac – по мере моего знакомства с ним. До этого я пользовался StructureMap, но по ряду причин он мне больше не подходит. В частности он давно уже не обновлялся, требует полного .NET Framework (в противовес .NET Framework Client Profile), нет версии для Windows Phone, версия для WinRT (Windows 8) не предвидится. Autofac, как я понял, избавлен от этих недостатков. Вряд ли у меня получится излагать материал последовательно, да я к этому и не стремлюсь. Просто буду описывать те возможности, которые меня заинтересовали в данный момент.

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

Для использования контейнера, во-первых, его необходимо сконфигурировать. Конфигурация контейнера заключается в регистрации компонентов, которые должны быть инстанцированы в ответ на запросы конкретных сервисов. Я буду пользоваться терминологией, принятой среди разработчиков Autofac, а именно:

  • Сервис – контракт поведения, разделяемый между компонентом-провайдером и компонентом-потребителем. В коде сервис – это интерфейс, который должен быть реализован компонентом-провайдером и использован компонентом-потребителем.
  • Компонент – код, который объявляет сервисы, которые он реализует и зависимости, которые он потребляет. В коде сервис – это конкретный класс, который реализует один или несколько сервисов (интерфейсов).
  • Зависимость – сервис, необходимый компоненту.

Итак, в простейшем случае конфигурация контейнера выглядит следующим образом:

        [TestMethod]
        public void RegisterComponentAsSelf()
        {
            // Создать экземпляр построителя контейнера.
            ContainerBuilder lBuilder = new ContainerBuilder();
            // Зарегистрировать компонент, создаваемый с помощью Reflection, и указать, 
            // что тип предоставляет себя как сервис.
            lBuilder.RegisterType<MainPageViewModel>().AsSelf();
            // Создать контейнер с зарегистрированными ранее компонентами.
            IContainer lContainer = lBuilder.Build();
            // Получить сервис из контейнера.
            MainPageViewModel lMainPageViewModel = lContainer.Resolve<MainPageViewModel>();
            // Убедиться, что сервис получен.
            Assert.IsNotNull(lMainPageViewModel);
        }

Вообще-то, большого смысла в таком использовании контейнера нет. Это вариант использования скорее напоминает шаблон ServiceLocator, в котором у нас есть известная точка входа, которая используется для получения экземпляров компонентов по типу сервиса и, возможно, некоторых параметров. Самое плохое в этом шаблоне то, что ваш код будет привязан к конкретному контейнеру. Из этого положения есть выход – можно использовать Common Service Locator library, которая предоставляет обобщённый интерфейс для различных IoC-контейнеров. Но остаётся серьёзный минус: вам придётся настраивать контейнер для модульного тестирования класса, который использует этот шаблон. Более подробно почитать чем плох ServiceLocator можно здесь.

Autofac реализует всю базовую функциональность IoC-контейнера. Подробно писать об этом не интересно. Рассмотрим более интересную возможность.

Динамическое создание объектов с помощью контейнера

Итак, экземпляр контейнера мы за собой таскать не станем, но как же в таком случае нам получать экземпляры компонентов? Для этого в нашей программе надо создать корневой компонент, используя контейнер. В конструкторе корневого компонента контейнером будут вставлены его зависимости. Например, для WPF приложения таким корневым компонентом будет ViewModel главного экрана приложения. Далее нам уже не потребуется использовать контейнер, и о нём можно забыть. Вы можете сказать: “подождите, а если мне нужно динамически создать какой-то объект?”. Например, в ClientListViewModel, реализующем функциональность экрана списка клиентов, нам необходимо создать по одному ClientViewModel для каждого объекта Client, полученному из базы данных. Получается придётся по-старинке создавать эти объекты с помощью оператора new и таким образом внедрять жёсткую зависимость? Нет, не обязательно. Для этого в Autofac предусмотрены делегатные фабрики (DelegateFactories).

Смысл этой возможности в следующем. Если тип T зарегистрирован в контейнера, Autofac будет автоматически разрешать зависимости на делегатах Func<T>, как фабрики, создающие экземпляры T с помощью контейнера. Описание довольно путанное. На деле это означает, что если вы хотите создавать объекты какого-то типа без непосредственного вызова его конструктора, вам необходимо зарегистрировать этот тип в контейнере и определить в типе тип делегата принимающего те же параметры, с теми же именами, что и конструктор, и возвращающего экземпляр данного типа. В конструкторе класса, который должен динамически создавать объекты, добавляем параметр с типом делегата фабрики дочернего объекта. Когда необходимо создать дочерний объект, необходимо вызвать этот делеагат, который вернёт новый дочерний объект. Autofac автомагически вызывает конструктор. Далее код, который показывает, как использовать эту возможность на примере, использованном в предыдущем абзаце (Метод ClientRepository.GetClients() возвращает три объекта Client, для которых и создаются три ViewModel типа ClientViewModel ).

    /// <summary>
    /// ViewModel информации о клиенте.
    /// </summary>
    public class ClientViewModel
    {
        #region Delegates

        /// <summary>
        /// Создаёт экземпляр ViewModel информации о клиенте.
        /// </summary>
        /// <param name="aClient">
        /// Информация о клиенте.
        /// </param>
        /// <returns>
        /// ViewModel информации о клиенте.
        /// </returns>
        public delegate ClientViewModel Factory(Client aClient);

        #endregion

        #region Constructors

        /// <summary>
        /// Инициализирует новый экземпляр ViewModel информации о клиенте.
        /// </summary>
        /// <param name="aClient">
        /// Информация о клиенте.
        /// </param>
        public ClientViewModel(Client aClient)
        {
            Client = aClient;
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Получает/устанавливает ID клиента.
        /// </summary>
        public Guid Id
        {
            get { return Client.Id; }
            set { Client.Id = value; }
        }

        /// <summary>
        /// Получает/устанавливает имя клиента.
        /// </summary>
        public string Name
        {
            get { return Client.Name; }
            set { Client.Name = value; }
        }

        #endregion

        #region Private properties

        private Client Client { get; set; }

        #endregion
    }


    /// <summary>
    /// ViewModel списка клиентов.
    /// </summary>
    public class ClientListViewModel
    {
        #region Constructors

        /// <summary>
        /// Инициализирует новый экземпляр ViewModel списка клиентов.
        /// </summary>
        /// <param name="aClientRepository">
        /// Репозиторий информации о клиентах.
        /// </param>
        /// <param name="aClientViewModelFactory">
        /// Фабрика создания дочерних ViewModel.
        /// </param>
        public ClientListViewModel
            (IClientRepository aClientRepository, ClientViewModel.Factory aClientViewModelFactory)
        {
            ClientRepository = aClientRepository;
            ClientViewModelFactory = aClientViewModelFactory;
        }

        #endregion

        #region Public properties

        /// <summary>
        /// Получает список ViewModel информации о клиентах.
        /// </summary>
        public IList<ClientViewModel> Items
        {
            get
            {
                if (mItems == null)
                {
                    mItems = new List<ClientViewModel>();
                    foreach (Client lClient in ClientRepository.GetClients())
                    {
                        ClientViewModel lViewModel = ClientViewModelFactory(lClient);
                        mItems.Add(lViewModel);
                    }
                }

                return mItems;
            }
        }

        #endregion

        #region Private properties

        /// <summary>
        /// Получает/устанавливает репозиторий информации о клиентах.
        /// </summary>
        private IClientRepository ClientRepository { get; set; }

        /// <summary>
        /// Получает/устанавливает фабрику ViewModel информации о клиентах.
        /// </summary>
        private ClientViewModel.Factory ClientViewModelFactory { get; set; }

        #endregion

        #region Private data

        private List<ClientViewModel> mItems;

        #endregion
    }

 

        [TestMethod]
        public void CreatingListViewModelUsingDelegateFactory()
        {
            ContainerBuilder lBuilder = new ContainerBuilder();
            lBuilder.RegisterType<ClientListViewModel>().AsSelf();
            lBuilder.RegisterType<ClientViewModel>().AsSelf();
            lBuilder.RegisterType<ClientRepository>().As<IClientRepository>();
            IContainer lContainer = lBuilder.Build();
            ClientListViewModel lClientListViewModel = lContainer.Resolve<ClientListViewModel>();
            // Убедиться, что сервис получен.
            Assert.IsNotNull(lClientListViewModel);
            // Убедиться, что созданы три дочерних ViewModel.
            Assert.AreEqual(3, lClientListViewModel.Items.Count);
        }