Через конструктор или через свойство?

10 сент. 2012 г. | | |

Инверсия управления (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. Существует несколько вариантов, как инжектировать эти зависимости в метод, на двух из которых (к слову, самых популярных) мы и остановимся. Это инъекция зависимости через конструктор (con­struc­tor depen­den­cy [пример на Unity 2.0]) и через свойства объекта (prop­erty dependency [пример на Unity 2.0]).

Традиционная школа мысли

Обычно для решения описанной в примере проблемы среди C#-разработчиков используется следующая рекомендация:

Use con­struc­tor for required depen­den­cies and prop­er­ties for optional dependencies.(Используйте конструктор для необходимых зависимостей и свойства для дополнительных)

Этот подход на сегодняшний день является наиболее популярным, и до недавнего времени я использовал именно его.

На практике всё не так хорошо, как в теории, и я считаю, что инъекция через конструктор основана на неверной предпосылке:

By look­ing at the class' con­struc­tor and prop­er­ties you will be able to eas­ily see the min­i­mal set of required depen­den­cies (those that go into the con­struc­tor) and optional set that can be sup­plied, 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, чтобы не получить Null­Ref­er­ence­Ex­cep­tions, что привело бы к засорению метода и нечитабельности кода.
3) Игнорируется тот факт, что класс вряд ли будет создаваться вручную - обычно это поручается IoC-контейнеру. Большинство IoC-контейнеров в состоянии справиться с перегруженными конструкторами и аргументами по умолчанию. Это делает разделение аргументов на необходимые и необязательные ещё более сомнительным.

Вот другой, уже более практичный совет:

Use con­struc­tor for not-changing depen­den­cies and prop­er­ties for ones that can change dur­ing object's lifetime.
(Используйте конструктор для зависимостей, не меняющих своё состояние, а свойства - для тех зависимостей, которые могут измениться в процессе жизни объекта)

Другими словами, session и bus нужно сделать private readonly полями - компилятор C# позаботится о том, чтобы после того, как мы установим эти поля в конструкторе (желательно, сначала проверив на валидность), они всегда оставались теми же гарантировано валидными (not null) объектами. С другой стороны, logger остается в подвешенном состоянии: в любой момент кто угодно может изменить его или вобще установить в null. Поэтому инжектирование зависимостей через свойства - зло. Всё должно быть установлено в конструкторе.

Я использовал этот подход до тех пор, пока не обнаружил следующие недостатки:
1) Ужас в классах-потомках. Пример, с которым я недавно столкнулся: базовый класс модели представления в WPF с зависимостью в dispatcher. Каждый потомок, унаследовавший этот класс (а их было много), должен имень конструктор с dispatcher в качестве аргумента и передавать его в конструктор базового класса.  А теперь представьте, что будет, если так же понадобится event aggregator: добавить зависимость в КАЖДЫЙ класс-потомок - тут ReShaper не поможет.
2) Доверие к компилятору больше, чем к разработчикам. Разработчики не бросятся устанавливать свойство в разные случайные значения только потому, что оно имеет открытй сеттер. Всё зависит от соглашений, используемых в команде. Например, в команде, где я на данный момент работаю, имеется правило, о котором все знают, и которому все следуют: "Не использовать свойства (properties), даже если это просто сеттер". Для внедрения зависимостей мы используем методы. Кроме того, зная, что разработчикам можно доверять, когда я в коде вижу свойство, я знаю, что оно не изменит своего состояния, т.е. улучшается читаемость и понимаение кода.

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

Что дальше?

Все эти недостатки лишают вышеупомянутых подходов первоначальной привлекательности. Поэтому я стараюсь разделять область применения для обоих подходов.

Use con­struc­tor for application-level depen­den­cies and prop­er­ties for infra­struc­ture or inte­gra­tion dependencies.
(Используйте конструктор для application-level зависимостей, а свойства - для инфраструктуры или интеграционных зависиомстей)

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

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

Такой подход даёт четкое разделение между сущностью, бизнес логикой и бухгалтерией. Поля и свойства именуются различно и (особенно с фичей решарпера 'Color iden­ti­fiers') имеют разную цветовую схему. Это упрощает поиск действительно важных вещей в коде.

Зависимости уровня инфраструктуры перемещаются в базовый класс (как в примере про  ViewModel с Dis­patcher и Even­tAg­gre­ga­tor), и код потомков получается более компактным и читаемым.

Итоговый вариант класса 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 коммент.:

Суйхуйвчай комментирует...

Спасибо. Интересно. Коротко и по делу. А чего можете рассказать через инъекцию через интерфейс внедрения?

Shemyakina Tatiana комментирует...

Сформулируйте, пожалуйста, более четко ваш вопрос.

суйхуйвчай комментирует...

хм. пока отложим предыдущий вопрос.
Напишу к этому посту вопрос:


Допустим имеем класс-обертку для сокета


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 и порт, если они заранее неизвестны?

тоже самое комментирует...

Тьфу бля, точнее не к этому, а просто здесь, коли тут ответили

Отправить комментарий