О плюсах инверсии зависимостей и той гибкости, которую она даёт приложениям сказано очень многое, и я искренне надеюсь, что она повсеместно используется :)
Как известно, существует несколько способов инверсии зависимостей:
- 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. Однако нельзя отрицать и случаи, когда инъекция через свойства может быть предпочтительнее.
Интересно, не задумывался об этом.
>>доступ к сервисам «открыт внешнему миру», что также не всегда является желаемым поведением.
Мне кажется, в этом плане у constructor и property-injection все одинаково: если конструктор/свойство internal, то и интерфейс зависимости может быть internal.
Я имел ввиду, что если мы имеем API класса ImageProcessor, то в нем, условно, ожидается функция Process, а свойства Screenshooter и ImageResizer совсем даже не ожидаются (и в некотором роде даже нарушают инкапсуляцию).
В случае constructor-injection таких публичных свойств не будет. Или ты про то, что такие свойства можно тоже сделать internal?