Property Injection своими руками (Xamarin/.Net)

В данной статье мы рассмотрим, чем отличается Property Injection от Constructor Injection и реализуем первое в дополнение к последнему на базе небольшого DI-контейнера в исходниках.

Это обучающий материал начального уровня. Будет полезен тем, кто ещё не знаком с DI-контейнерами или интересуется, как оно устроено изнутри.

Что это и где используется


Внедрение зависимостей (Dependency Injection) — распространённый шаблон проектирования, применяемый для создания программ со слабой связанностью компонентов. Новички обычно встречаются с ним на проектах, где применяются юнит-тесты.

Классический пример системы с жёсткой связанностью — это когда в гостинице намертво соединяют фен с электропитанием (хозяева переживают за сохранность устройства). Длины провода, в общем достаточно, однако при таком подходе нельзя модифицировать фен отдельно от стены, а стену — отдельно от фена. Внедрение слабой связанности в виде розетки и вилки решает эту проблему.

Другой пример — это жёсткая привязка сотрудников к рабочему месту, и также необходимость ходить на работу каждый день. Эта жёсткая связанность в современных компаниях всё чаще ослабляется как возможностью работать из дому, так и внедрением практики отсутствия постоянного рабочего места: хочешь прийти в офис — не забудь себе забукать местечко. С точки зрения работодателя, работа будет выполнена независимо от места сидения сотрудника. Возникают, конечно, дополнительные риски, но такова цена большей гибкости.

Идея внедрения зависимостей в узком смысле заключается в том, что класс-потребитель некоего функционала обращается к классу-поставщику не напрямую, путём создания его экземпляра, а опосредованно, по ссылке определённого интерфейсного типа. От класса-поставщика требуется только соответствовать указанному интерфейсу.

image

В отличие от сервис-локатора, здесь классы не только ничего не знают друг о друге, но ещё и не содержат обращения к классу-посреднику (service locator).

Для того, чтобы поставить в соответствие интерфейсу какой-то конкретный класс (или экземпляр класса), часто применяется так называемый DI-контейнер, в котором заранее регистрируются отношения интерфейса и реализации. Для языков, которые не поддерживают отражения (reflections), то есть не позволяют оперировать свойствами типов, можно ограничиться таким шаблоном как composition root (рассмотрение данного шаблона находится вне рамок данной статьи).

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

Естественный способ передачи ссылок в класс-потребитель — это указать их в параметрах его конструктора. Как правило, DI-контейнер при запросе нужного интерфейса автоматически пробегается по перечню параметров и рекурсивно удовлетворяет все зависимости.
Другой способ — это передача ссылок через свойства, что и называется Property Injection. Чтобы удовлетворить ссылки, DI-контейнер пробегается по списку свойств и в те из них, которые соответствуют определённым критериям, назначают нужные ссылки.

Есть и более редкий способ — Method Injection, где ссылки передаются через специальный метод (например, Init () с перечислением зависимостей в виде параметров). Такой подход похож на constructor injection, но срабатывает уже после создания экземпляра объекта. Под этим термином также понимается нечто совершенно иное, а именно простая передача зависимостей как параметров в произвольный метод.

Не очередной ли это велосипед


Существует множество DI-контейнеров (например, Unity, NInject, Autofac, Spring, Castle Windsor, TinyIoc, MvvmCross). Большинство из них поддерживает Property Injection.

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

Например, корпоративными политиками запрещено использование сторонних библиотек. Или нужных вам библиотек не существует в природе. Или эти библиотеки не удовлетворяют проектным требованиям.

В этом случае нетрудно сделать и свой собственный контейнер — если, конечно, это технически возможно в рамках применяемой технологии разработки, как это возможно в .Net.

Почему не устраивает constructor injection


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

Часто можно наблюдать, как по коду протягивается какой-нибудь из добавляемых параметров, от класса верхнего уровня до конечного потребителя вниз по иерархии наследования. Цепь вызовов тянется, как связка сосисок, от конструктора к конструктору. Мало того, что сама по себе последовательная передача этих параметров загромождает код. Так ещё и добавление очередного параметра в конец этой цепи вызовов её требует модификации на всём протяжении. Что совсем неудобно. Как результат, класс может солидно «распухнуть» только из-за всей этой машинерии, даже ещё не начав ничего делать.

Почему же не избавить программиста от этой рутинной работы и не делегировать создание сущностей DI-контейнеру?

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

Есть хорошее правило, что если вам становится неудобно добавлять очередную зависимость в параметры конструктора, то, значит, их действительно слишком много, и вам следует пересмотреть дизайн. Обычно это 5 штук — естественный предел объектов, которые человек без тренировки может удерживать в области своего внимания.

Мы, однако же, считаем, что в этом правиле ключевое слово — неудобно. Хотелось бы его избегать.

Свой DI-контейнер


Реализацию простейшего DI-контейнера нетрудно найти в исходниках. Автор когда-то воспользовался небольшим примером из Xamarin University (см. здесь).

Здесь многого не хватает по сравнению с промышленными DI-контейнерами (синглтоны, домены и т.д.), но это находится за рамками настоящей статьи.

Контейнер позволяет зарегистрировать отношение между интерфейсом и реализацией (метод Register в разных вариантах, который просто добавляет элемент в Dictionary), чтобы затем, в методе Resolve, создать экземпляр класса, который соответствует запрашиваемому интерфейсу и проделать то же самое параметров его конструктора.

Данный контейнер работает на базе Reflections (отражений) и метода Activator.CreateInstance (). Последний метод используется для создания экземпляра класса по его типу, а отражения позволяют вычитывать свойства типа.

Для реализации property injection добавим специальный атрибут, которым будем помечать те свойства, которые контейнеру следует обрабатывать как зависимости.

Назовём атрибут «ResolveAttribute». Для этого необходимо создать класс-наследник System.Attribute (см. Appendix A).

Добавим обработку атрибута в методе Resolve контейнера.

        public object Resolve(Type type)
        {
            object result = null;
            ...

            //Inject [Resolve] marked properties
            var props = targetType.GetRuntimeProperties()
                .Where(x => x.CanWrite && x.IsDefined(typeof(ResolveAttribute)));

            foreach (var item in props)
            {
                item.SetValue(result, Resolve(item.PropertyType));
            }

            return result;
        }


Это всё, что нужно сделать в исходном контейнере для того, чтобы заработало внедрение зависимостей через свойства (исходники см. в Appendix B).

Пример использования


Предположим, существует класс, в котором используется внедрением зависимостей через конструктор (разумеется, зависимости должны быть заранее зарегистрированы — см. Appendix C). Заметим, что параметры и тело конструктора — это сугубо обслуживающий код:

    public class MediaRecorder : IMediaRecorder
    {
        private readonly IMediaPlayer player;

        private readonly IRestServiceClient restClient;

        private readonly ILog log;

        private readonly IFileService fileService;

        private readonly ISettingsProvider settingsProvider;

        public MediaRecorder(IMediaPlayer player, IRestServiceClient restClient, ILog log, IFileService fileService, ISettingsProvider settingsProvider)
        {
            this.player = player;
            this.restClient = restClient;
            this.log = log;
            this.fileService = fileService;
            this.settingsProvider = settingsProvider;
        }
 
   }


Модифицируем класс, используя внедрение через свойства:

    public class MediaRecorder : IMediaRecorder
    {
        [Resolve]
        public IMediaPlayer Player { get; set; }

        [Resolve]
        public IRestServiceClient RestClient { get; set; }

        [Resolve]
        public ILog Log { get; set; }

        [Resolve]
        public IFileService FileService { get; set; }

        [Resolve]
        public ISettingsProvider SettingsProvider { get; set; }

        public MediaRecorder()
        {
        }
    }


Как видим, количество служебного кода сократилось, а сам код стал более читабельным.

Обратной стороной успеха служит снижение возможности контролировать целостность программы со стороны компилятора. Впрочем, это общее следствие для всех типов реализации IoC.

Также надо иметь в виду, что нормальной практикой считается использовать constructor injection для обязательных зависимостей, а property injection — для необязательных. Именно поэтому свойства в нашем примере имеют модификатор доступа «public».

Публичный доступ к зависимостям даёт возможность подменять их значения извне, что, одной стороны, служит инструментом для внедрения зависимостей вручную, а с другой — позволяет непреднамеренно повредить работу всего класса, если он написан недостаточно надёжно.

Код в нашей реализации не проверяет уровень доступа свойства, то есть можно использовать как «public», так и «private». Последнее рекомендуется делать для тех свойств, произвольная модификация которых нежелательна. Это защитит класс от непреднамеренного неправильного использования и, в то же время, позволит использовать property injection.

Производительность


Как видно по нашей реализации, для поддержки Property Injection нужно при помощи отражений (reflections) пробежаться по всем свойствам класса. Может оказаться, что свойств довольно много — на практике бывает и более тысячи, что несколько замедляет работу программы.

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

Например, можно было бы сократить список свойств путём помещения в служебный класс тех из них, которые не помечены «ResolveAttribute» (в нашей реализации), задуматься над отложенным чтением (lazy loading). А контейнер мог бы кэшировать список свойств для внедрения. Но об этом мы поговорим как-нибудь в другой раз.

Вместе с тем, закон Мура всё ещё работает, и вычислительная мощь компьютеров растёт. Это позволяет нам использовать всё более и более сложные алгоритмы.

В заключение отметим, что различные DI-контейнеры имеют различную производительность, причём эта разница может быть значительной (см. небольшое исследование).

Appendix A
using System;

namespace Bricks
{
    [AttributeUsage(AttributeTargets.All)]
    public sealed class ResolveAttribute : Attribute
    {
    }
}


Appendix B
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;

namespace Bricks
{
    /// 
    /// This is a very simple example of a DI/IoC container.
    /// 
    public sealed class DependencyContainer : IDependencyContainer
    {
        readonly Dictionary> registeredCreators = new Dictionary>();
        readonly Dictionary> registeredSingletonCreators = new Dictionary>();
        readonly Dictionary registeredSingletons = new Dictionary();
        object locker = new object();

        /// 
        /// Register a type with the container. This is only necessary if the 
        /// type has a non-default constructor or needs to be customized in some fashion.
        /// 
        /// Abstraction type
        /// Type to create
        public void Register()
            where TImpl : new()
        {
            registeredCreators.Add(typeof(TAbstraction), () => new TImpl());
        }

        /// 
        /// Register a type with the container. This is only necessary if the 
        /// type has a non-default constructor or needs to be customized in some fashion.
        /// 
        /// Function to create the given type.
        /// Type to create
        public void Register(Func creator)
        {
            registeredCreators.Add(typeof(T), creator);
        }

        /// 
        /// Register a type with the container. This is only necessary if the 
        /// type has a non-default constructor or needs to be customized in some fashion.
        /// 
        /// Abstraction type
        public void RegisterInstance(object instance)
        {
            registeredCreators.Add(typeof(TAbstraction), () => instance);
        }

        /// 
        /// Creates a factory for a type so it may be created through
        /// the container without taking a dependency on the container directly.
        /// 
        /// Creator function
        /// The 1st type parameter.
        public Func FactoryFor()
        {
            return () => Resolve();
        }

        /// 
        /// Creates the given type, either through a registered function
        /// or through the default constructor.
        /// 
        /// Type to create
        public T Resolve()
        {
            return (T)Resolve(typeof(T));
        }

        /// 
        /// Creates the given type, either through a registered function
        /// or through the default constructor.
        /// 
        /// Type to create
        public object Resolve(Type type)
        {
            object result = null;
            var targetType = type;

            TypeInfo typeInfo = type.GetTypeInfo();

            if (registeredSingletonCreators.TryGetValue(type, out Func creator))
            {
                lock (locker)
                {
                    if (registeredSingletons.ContainsKey(type))
                    {
                        result = registeredSingletons[type];
                    }
                    else
                    {
                        result = registeredSingletonCreators[type]();
                        registeredSingletons.Add(type, result);
                    }
                }

                if (result != null)
                {
                    targetType = result.GetType();
                }
            }
            else
            if (registeredCreators.TryGetValue(type, out creator))
            {
                result = registeredCreators[type]();

                if (result != null)
                {
                    targetType = result.GetType();
                }
            }
            else
            {
                var ctors = typeInfo.DeclaredConstructors.Where(c => c.IsPublic).ToArray();
                var ctor = ctors.FirstOrDefault(c => c.GetParameters().Length == 0);

                if (ctor != null || ctors.Count() == 0)
                {
                    result = Activator.CreateInstance(type);
                }
                else
                {
                    // Pick the first constructor found and create any parameters.
                    ctor = ctors[0];
                    var parameters = new List();
                    foreach (var p in ctor.GetParameters())
                    {
                        parameters.Add(Resolve(p.ParameterType));
                    }

                    result = Activator.CreateInstance(type, parameters.ToArray());
                }
            }

            //Create [Resolve] marked property
            var props = targetType.GetRuntimeProperties()
                .Where(x => x.CanWrite && x.IsDefined(typeof(ResolveAttribute)));

            foreach (var item in props)
            {
                item.SetValue(result, Resolve(item.PropertyType));
            }

            return result;
        }

        public void Clear()
        {
            registeredCreators.Clear();
            registeredSingletonCreators.Clear();
            registeredSingletons.Clear();
        }

        public void RegisterSingleton(Type tInterface, object service)
        {
            RegisterSingleton(tInterface, () => service);
        }

        public void RegisterSingleton(TAbstraction service) where TAbstraction : class
        {
            registeredSingletonCreators.Add(typeof(TAbstraction), () => service);
        }
        public void RegisterSingleton() where TAbstraction : class where TImpl : new()
        {
            registeredSingletonCreators.Add(typeof(TAbstraction), () => new TImpl());
        }

        public void RegisterSingleton(Type tInterface, Func serviceConstructor)
        {
            registeredSingletonCreators.Add(tInterface, serviceConstructor);
        }

        public void RegisterSingleton(Func serviceConstructor) where TAbstraction : class
        {
            registeredSingletonCreators.Add(typeof(TAbstraction), serviceConstructor);
        }
    }
}


Appendix C
    public static class Bootstrap
    {
        public static readonly IDependencyContainer Container = new DependencyContainer();

        public static void Init()
        {
            Container.Clear();

            Container.RegisterSingleton();
            Container.RegisterSingleton(() => new SettingsProvider());
            Container.RegisterSingleton(() => new FileService());
            Container.RegisterSingleton(() => new MessagesProvider());

            Container.Register();
            Container.Register();
            Container.Register();

            ...
        }
    }

© Habrahabr.ru