При использовании инъекции зависимостей у меня частенько возникают проблемы с конструкторами.
Посмотрим на простой пример класса — сервиса почтовой рассылки:
public class EmailSender { public EmailSender(ISmtpClient smtpClient, string serverAddress) { } public void Send(string from, string to, string text) { } }
Какие у класса есть зависимости? Ему нужен SMTP-клиент, чтобы осуществлять отправку писем, а также необходим адрес смтп-сервера, который будет отправлять корреспонденцию.
Допустим, что у меня в проекте полная Инверсия Зависимостей и активно используются IoC-контейнеры. Какая возникает проблема?
ISmtpClient зарегистрирован в моем контейнере с ним проблем нет, но как сконфигурировать EmailSender адресом сервера? Непонятно.
Классическим способом решения выглядит создание фабрики:
public class EmailSenderFactory { public EmailSender CreateEmailSender(string serverAddress) { return new EmailSender(_container.Resolve<ISmtpClient>(), serverAddress); } }
Вариант неплохой, но не без минусов: при изменениях в конструкторе EmailSender’a (например, добавлении еще одной зависимости), те же изменения придется «продублировать» в методах фабрики.
Еще один из вариантов решения — вынос «value-type» параметров из конструктора в отдельный метод (например, Initialize). Выглядит это примерно следующим образом:
public class EmailSender { private string _serverAddress; public EmailSender(ISmtpClient smtpClient) { } public void Initialize(string serverAddress) { _serverAddress = serverAddress; } }
В этом случае считается, что сразу после создания объекта (например, после Resolve из Ioc-контейнера), необходимо обязательно вызвать метод Initialize. Если подобная конвенция прочно укоренится в проекте, то большой проблемы она не вызывает, поскольку становится привычной. Однако идеальной и очевидной такую методику назвать нельзя.
Меня озадачила проблема и я попробовал её решить, избавившись от минусов первого способа. А именно, я создаю подобную фабрику, но её не приходится писать и обновлять вручную — она генерируется с помощью T4.
Таким образом, пометив нужный конструктор атрибутом UseForFactory, мы получаем автоматически сгенерированную фабрику (пример основан на использовании Ninject):
public class EmailSender { [UseForFactory] public EmailSender(ISmtpClient smtpClient, string serverAddress) { } } //этот класс генерируется автоматически public class EmailSenderFactory { private readonly Ninject.IKernel _container; public EmailSenderFactory(Ninject.IKernel container) { _container = container; } public virtual Factory2.EmailSender Create(string serverAddress) { return new Factory2.EmailSender(_container.Get<Factory2.ISmtpClient>(), serverAddress); } }
Скачать t4-шаблон можно по ссылке, а для настройки на использование вашего Ioc-контейнера достаточно поменять всего две строчки:
const string IocContainerInterface = "Ninject.IKernel"; const string ResolveObjectMethod = "Get<{0}>()";
А как вы решаете подобные проблемы и возникали ли они у вас?
Выбираю обычно первый способ, изредка переписывать конструктор не такая уж и большая проблема — он обычно не так часто меняется у подобных классов.
Если уж совсем припрет — можно инжектить какой-нибудь ConfigProvider, хотя это тоже не самая хорошая идея, но вполне допустимая, как мне кажется.
А вот Initialize() мне не нравится вообще — если у нас EmailSender превратится в интерфейс, то этот метод тоже уйдет в интерфейс, потом появятся наследники у этого интерфейса, которым для инициализации требуется не только адрес сервера, но и логин-пароль, в итоге ничем хорошим это не закончится
Пользуюсь Autofac, он умеет это делать из коробки. То есть ресолвит фабрику с параметрами по конструктору.
Александр, согласен, вариант с Initialize далек от идеала. Тем не менее, он вполне встречается в реальном коде.
А по поводу изменения конструктора — очень зависит от стадии развития проекта. В момент начальной разработки изменения обычно случаются очень часто, когда проект «созревает» — изменений становится меньше. Впрочем, это относится к изменениям не только относительно конструкторов
Иван, правильно ли я понял, что имеется ввиду что-то подобное:(new [] {new NamedParameter(«serverAddress», «smtp.google.com»)});
_container.ResolveAllWithParameters
подобный механизм, безусловно, существует и в других контейнерах (и в Ninject тоже), но в этом случае мы теряем строгую типизацию и привязываемся к имени параметра, которое при рефакторинге очень легко может измениться