Инъекция зависимостей и меняющиеся параметры в конструкторе

При использовании инъекции зависимостей у меня частенько возникают проблемы с конструкторами.
Посмотрим на простой пример класса — сервиса почтовой рассылки:

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}>()";

А как вы решаете подобные проблемы и возникали ли они у вас?

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

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

  1. Выбираю обычно первый способ, изредка переписывать конструктор не такая уж и большая проблема — он обычно не так часто меняется у подобных классов.
    Если уж совсем припрет — можно инжектить какой-нибудь ConfigProvider, хотя это тоже не самая хорошая идея, но вполне допустимая, как мне кажется.
    А вот Initialize() мне не нравится вообще — если у нас EmailSender превратится в интерфейс, то этот метод тоже уйдет в интерфейс, потом появятся наследники у этого интерфейса, которым для инициализации требуется не только адрес сервера, но и логин-пароль, в итоге ничем хорошим это не закончится

  2. Пользуюсь Autofac, он умеет это делать из коробки. То есть ресолвит фабрику с параметрами по конструктору.

  3. Александр, согласен, вариант с Initialize далек от идеала. Тем не менее, он вполне встречается в реальном коде.

    А по поводу изменения конструктора — очень зависит от стадии развития проекта. В момент начальной разработки изменения обычно случаются очень часто, когда проект «созревает» — изменений становится меньше. Впрочем, это относится к изменениям не только относительно конструкторов

  4. Иван, правильно ли я понял, что имеется ввиду что-то подобное:
    _container.ResolveAllWithParameters(new [] {new NamedParameter(«serverAddress», «smtp.google.com»)});

    подобный механизм, безусловно, существует и в других контейнерах (и в Ninject тоже), но в этом случае мы теряем строгую типизацию и привязываемся к имени параметра, которое при рефакторинге очень легко может измениться

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

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