Инверсия управления (Inversion of control) - один из полезнейших приёмов при разработке в ОО стиле. Инверсия управления позволяет уменьшить связанность модулей приложения, за счет инъекций объектов-зависимостей в объекты-клиенты. Клиенты должны знать минимум о своих зависимостях - в пределах реализуемого интерфейса, если такой имеется - и совершенно ничего о процессе создания объектов-зависимостей. Существует несколько вариантов внедрения зависимостей.
Под катом - перевод статьи Krzysztof Koźmi "To constructor or to property dependency?", в которой автор рассуждает, как лучше передавать объектам зависимости.
В [ ] находятся заметки переводчика.
У нас, девелоперов, есть что-то общее с этим комиксом (xkcd):
Мы можем бесконечно спорить о табах и пробелах, о целесообразности дополнительной ";" и о других сомнительных вещах. Споры могут быть горячими, с большим количеством конструктивных и не очень (зачастую) аргументов.
Прежде чем данный пост будет воспринят как попыта начать одну из таких дискуссий, хочу отметить, что цель поста - поделиться опытом, а не разжечь флейм.
Зависимости: механизмы и семантика
Когда пишешь код на ОО языке, к примеру, на C#, и следуешь устоявшимся практикам (SOLID и тд), как правило, создаешь много классов, каждый из которых выполняет строго определённую задачу, а остальное делегирует другим.Для примера рассмотрим ситуацию, когда у пользователя (user) меняется адрес (место жительства), и нам нужно обновить эти данные.
public class UserService: IUserService
{
// some other members
public void UserMoved(UserMovedCommand command)
{
var user = session.Get<User>(command.UserId);
logger.Info("Updating address for user {0} from {1} to {2}",
user.Id, user.Address, command.Address);
user.UpdateAddress(command.Address);
bus.Publish(new UserAddressUpdated(user.Id, user.Address));
}
}
В четырёх строчка кода метода UserMoved есть три зависимости: session, logger и bus. Существует несколько вариантов, как инжектировать эти зависимости в метод, на двух из которых (к слову, самых популярных) мы и остановимся. Это инъекция зависимости через конструктор (constructor dependency [пример на Unity 2.0]) и через свойства объекта (property dependency [пример на Unity 2.0]).
Традиционная школа мысли
Use constructor for required dependencies and properties for optional dependencies.(Используйте конструктор для необходимых зависимостей и свойства для дополнительных)
Этот подход на сегодняшний день является наиболее популярным, и до недавнего времени я использовал именно его.
На практике всё не так хорошо, как в теории, и я считаю, что инъекция через конструктор основана на неверной предпосылке:
By looking at the class' constructor and properties you will be able to easily see the minimal set of required dependencies (those that go into the constructor) and optional set that can be supplied, but the class doesn't require them (those are exposed as properties).Перепишем класс, следуя этой рекомендации:
(Глядя на конструктор класса и его свойства вы легко увидите минимальный набор обязательных зависимостей, которые пойдут в конструктор, и набор опциональных зависимостей, которые могут быть предоставлены, но класс не требует их (эти зависимости внедряются через свойства))
public class UserService: IUserService
{
// some other members
public UserService(ISession session, IBus bus)
{
//the obvious
}
public ILogger Logger {get; set;}
}
Таким образом, зависимости session и bus являются обязательными, а logger - нет (обычно логгер инициализируется чем-то вроде NullLogger [паттерн NullObject]).
На практике я выявил несколько вещей, которые ставят под сомнение полезность этого подхода:
1) Игнорируется перегрузка конструктора. Если у класса есть еще один конструктор, который в качестве аргумента принимает только session, то делает ли это зависимость bus необязательной? Даже без перегрузки, в C# 4 можно устанавливать аргументы в null [или задавать значение по умолчанию]. Какого рода будут такие аргументы: необходимыми или необязательными?
2)Игнорируется тот факт, что на практике очень редко встречаются необзательные зависимости. Если б зависимость действительно была необязательной, то, наверно, пришлось бы делать проверки на null, чтобы не получить NullReferenceExceptions, что привело бы к засорению метода и нечитабельности кода.
3) Игнорируется тот факт, что класс вряд ли будет создаваться вручную - обычно это поручается IoC-контейнеру. Большинство IoC-контейнеров в состоянии справиться с перегруженными конструкторами и аргументами по умолчанию. Это делает разделение аргументов на необходимые и необязательные ещё более сомнительным.
Вот другой, уже более практичный совет:
Use constructor for not-changing dependencies and properties for ones that can change during object's lifetime.
(Используйте конструктор для зависимостей, не меняющих своё состояние, а свойства - для тех зависимостей, которые могут измениться в процессе жизни объекта)
Другими словами, session и bus нужно сделать private readonly полями - компилятор C# позаботится о том, чтобы после того, как мы установим эти поля в конструкторе (желательно, сначала проверив на валидность), они всегда оставались теми же гарантировано валидными (not null) объектами. С другой стороны, logger остается в подвешенном состоянии: в любой момент кто угодно может изменить его или вобще установить в null. Поэтому инжектирование зависимостей через свойства - зло. Всё должно быть установлено в конструкторе.
Я использовал этот подход до тех пор, пока не обнаружил следующие недостатки:
1) Ужас в классах-потомках. Пример, с которым я недавно столкнулся: базовый класс модели представления в WPF с зависимостью в dispatcher. Каждый потомок, унаследовавший этот класс (а их было много), должен имень конструктор с dispatcher в качестве аргумента и передавать его в конструктор базового класса. А теперь представьте, что будет, если так же понадобится event aggregator: добавить зависимость в КАЖДЫЙ класс-потомок - тут ReShaper не поможет.
2) Доверие к компилятору больше, чем к разработчикам. Разработчики не бросятся устанавливать свойство в разные случайные значения только потому, что оно имеет открытй сеттер. Всё зависит от соглашений, используемых в команде. Например, в команде, где я на данный момент работаю, имеется правило, о котором все знают, и которому все следуют: "Не использовать свойства (properties), даже если это просто сеттер". Для внедрения зависимостей мы используем методы. Кроме того, зная, что разработчикам можно доверять, когда я в коде вижу свойство, я знаю, что оно не изменит своего состояния, т.е. улучшается читаемость и понимаение кода.
Подведём итог. Я стараюсь, чтобы все зависимости в классе были обязательными, и ни одна из них не меняла состояния после того, как объект, в который инжектируются зависимости, был создан.
Что дальше?
Все эти недостатки лишают вышеупомянутых подходов первоначальной привлекательности. Поэтому я стараюсь разделять область применения для обоих подходов.Use constructor for application-level dependencies and properties for infrastructure or integration dependencies.
(Используйте конструктор для application-level зависимостей, а свойства - для инфраструктуры или интеграционных зависиомстей)
Другими словами, зависимости, без которых объект не сможет выполнить свою задачу, передаются через конструктор, остальные устанавливаются через свойства.
Вернёмся к нашему примеру. Следуя этому подходу, session следует инжектировать через конструктор. Сессия является частью сущности того, что делает класс, и используется для получения пользователя, чей адрес мы хотим обновить. Логгирование и публикация информация на шине гораздо больше относятся к инфраструктуре, чем к сущности класса. Поэтому их нужно сделать полями.
Такой подход даёт четкое разделение между сущностью, бизнес логикой и бухгалтерией. Поля и свойства именуются различно и (особенно с фичей решарпера 'Color identifiers') имеют разную цветовую схему. Это упрощает поиск действительно важных вещей в коде.
Зависимости уровня инфраструктуры перемещаются в базовый класс (как в примере про ViewModel с Dispatcher и EventAggregator), и код потомков получается более компактным и читаемым.
Итоговый вариант класса UserService [от переводчика]:
public class BaseService
{
public ILogger logger {get;set;}
public IBus bus {get;set;}
}
public class UserService: BaseService, IUserService
{
// some other members
public UserService(ISession _session)
{
session = _session;
}
public void UserMoved(UserMovedCommand command)
{
var user = session.Get<User>(command.UserId);
base.logger.Info("Updating address for user {0} from {1} to {2}",
user.Id, user.Address, command.Address);
user.UpdateAddress(command.Address);
base.bus.Publish(new UserAddressUpdated(user.Id, user.Address));
}
}
4 коммент.:
Спасибо. Интересно. Коротко и по делу. А чего можете рассказать через инъекцию через интерфейс внедрения?
Сформулируйте, пожалуйста, более четко ваш вопрос.
хм. пока отложим предыдущий вопрос.
Напишу к этому посту вопрос:
Допустим имеем класс-обертку для сокета
class SocketWrapper : ISocketWrapper
{
public SocketWrapper(String ip, int port)
{
}
// interface method
void DoSomethig()
{
}
}
и класс юзающий его через интерфейс
class DataConnector
{
public DataConnector(ISocketConnector conn)
{
}
}
_________
пишу в коде
unity.RegisterType(typeof(ISocketWrapper), typeof(SocketWrapper))
unity.Resolve(typeof(DataConnector))
___
А теперь вопрос, как сказать юнити откуда брать аргументы для SocketWrapper? эти самые ip и порт, если они заранее неизвестны?
Тьфу бля, точнее не к этому, а просто здесь, коли тут ответили
Отправить комментарий