Светлая сторона property-injection

О плюсах инверсии зависимостей и той гибкости, которую она даёт приложениям сказано очень многое, и я искренне надеюсь, что она повсеместно используется :)
Как известно, существует несколько способов инверсии зависимостей:

  • Constructor-based injection — инъекция через конструктор. Класс объявляет свои зависимости как параметры в конструкторе:
        public class MyImageProcessor
        {
            private readonly IScreenshooter _screenshooter;
            private readonly IImageResizer _imageResizer;
            private readonly IImageComparer _imageComparer;
    
            public MyImageProcessor(IScreenshooter screenshooter, IImageResizer imageResizer, IImageComparer imageComparer)
            {
                _screenshooter = screenshooter;
                _imageResizer = imageResizer;
                _imageComparer = imageComparer;
            }
        }
    

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

  • Property-based injection — инъекция через публичные свойства. Свойства в этом случае обычно необходимо украшать атрибутами:
        public class MyImageProcessor2
        {
            [Inject]
            public IScreenshooter Screenshooter { get; set; }
            [Inject]
            public IImageResizer ImageResizer { get; set; }
            [Inject]
            public IImageComparer ImageComparer { get; set; }
    
            public MyImageProcessor2()
            {
            }
        }

    Выглядит короче, но имеет очевидные минусы — зависимости класса непонятны (легко забыть инициализировать свойства, например, из тестов; при инъекции через конструктор с этим проблем нет), доступ к сервисам «открыт внешнему миру», что также не всегда является желаемым поведением.

Есть еще field-injection, когда инъекция идет не через публичные свойства, а через публичные поля, но этот вариант уже почти официально считается «говнокодом» :)

До недавнего времени в подавляющем большинстве случаев я использовал инъекцию через конструктор, и с трудом представлял причины, по которым можно предпочесть property-injection.

Однако критический анализ собственного кода вкупе с последней заметкой Ayende заставили меня задуматься.
Приведу типичный пример, над которым я размышлял, и который весьма вероятно встречался и у вас.
Предположим, что у нас есть много классов-процессоров для различных операций распознавания изображений, наследующихся от некого BaseProcessor. Базовому процессору для выполнения операций нужны (как в примерах выше), сервисы для получения скриншота экрана, ресайза изображений и некий сервис сравнения изображений. Логично предположить, что во всех классах-наследниках Screenshooter и ImageResizer скорее всего будут одинаковы, а вот используемая реализация сервиса сравнения — будет отличаться в зависимости от метода распознавания. Как же будут выглядить наши классы в этом случае:

//constructor-injection
public class GaussProcessor : MyImageProcessor
    {
        public MyImageProcessor(IScreenshooter screenshooter, IImageResizer imageResizer, GaussImageComparer imageComparer) 
            : base(screenshooter, imageResizer, imageComparer)
        {
        }
    }

//property-injection
public class CannyProcessor : MyImageProcessor
    {
        public MyImageProcessor(CannyImageComparer imageComparer) : base(imageComparer)
        {
        }
    }

Очевидно, что второй способ не только короче, но и намного понятнее и прозрачнее.

Случаи, когда в базовых — инфраструктурных — классах требуется достаточно большое количество сервисов, не меняющихся в классах-наследниках довольно типичны. При этом, если наследников тоже достаточно много, то постоянное указывание этих «заглушек» в конструкторах потомков действительно начинает несколько «надоедать», и невольно возникает путаница между сервисами которые надо просто «пробросить» и теми, которые действительно различаются между реализациями.
Перенос инъекции подобных сервисных вещей в property-injection выглядит весьма разумным решением, в первую очередь с точки зрения удобства использования и сокращения количества потенциальных вопросов/ошибок при имплементации наследников.
Из минусов, конечно, сохраняется появление в публичном интерфейсе класса геттеров/сеттеров для таких свойств, но это можно считать минимальным злом, с которым приходится мириться. Пункт же с «неясностью зависимостей класса» здесь отходит на второй план, поскольку, наоборот, реальные зависимости класса, меняющиеся в наследниках, наоборот выделяются на фоне «неважных» обще-инфраструктурных зависимостей.
Ну и даже существующие небольшие минусы однозначно лучше, чем service-location или жесткое связывание. :)

P.S. Эта заметка никоим образом не говорит о необходимости повсеместного применения property-injection, инъекция через конструктор по прежнему остается приоритетным для меня способом использования ioc. Однако нельзя отрицать и случаи, когда инъекция через свойства может быть предпочтительнее.

Опубликовать в Facebook
Опубликовать в Google Plus

2 комментария

  1. Интересно, не задумывался об этом.

    >>доступ к сервисам «открыт внешнему миру», что также не всегда является желаемым поведением.

    Мне кажется, в этом плане у constructor и property-injection все одинаково: если конструктор/свойство internal, то и интерфейс зависимости может быть internal.

  2. Я имел ввиду, что если мы имеем API класса ImageProcessor, то в нем, условно, ожидается функция Process, а свойства Screenshooter и ImageResizer совсем даже не ожидаются (и в некотором роде даже нарушают инкапсуляцию).
    В случае constructor-injection таких публичных свойств не будет. Или ты про то, что такие свойства можно тоже сделать internal?

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *