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

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

public class EmailSender
{
	public EmailSender(ISmtpClient smtpClient, string serverAddress)
	{
	}

	public void Send(string from, string to, string text)
	{
		
	}
}

Какие у класса есть зависимости? Ему нужен SMTP-клиент, чтобы осуществлять отправку писем, а также необходим адрес смтп-сервера, который будет отправлять корреспонденцию.

Допустим, что у меня в проекте полная Инверсия Зависимостей и активно используются IoC-контейнеры. Какая возникает проблема?
ISmtpClient зарегистрирован в моем контейнере с ним проблем нет, но как сконфигурировать EmailSender адресом сервера? Непонятно.
Continue reading

Светлая сторона property-injection

О плюсах инверсии зависимостей и той гибкости, которую она даёт приложениям сказано очень многое, и я искренне надеюсь, что она повсеместно используется :)
Как известно, существует несколько способов инверсии зависимостей:

  • 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.
Continue reading

IoC-контейнер для ASP.Net MVC — Ninject

Уверен, что очень скоро сам забуду, почему же выбрал Ninject, так что оставлю здесь пару причин. До недавнего времени использовал Unity, но пришло время что-то менять :)

Ninject vs Unity — если «made by Microsoft» для многих является аргументом «за», то для меня, подсознательно, совсем наоборот :) По факту — Unity слабо развивается, и интеграции в MVC «из коробки» в нём банально нет. Ключевым аргументом к отказу от Unity стало отсутствие PerRequestLifetimeManager (синглтон в рамках одного запроса, очень полезен для Session/ObjectContext).

Ninject vs Castle Windsor — windsor как-то совсем не обрадовал. Опять-таки нет встроенной интеграции в MVC, нет инъекции в проперти (а в атрибуты иначе никак), и нет резолвинга классов, без их предварительной регистрации. Собственно, до «тестового приложения» здесь даже и не дошло. Совсем плохо.

StructureMap и AutoFac не рассматривались, уж больно понравилась ninject-овская интеграция :) Встает из nuget, настройки не требует вовсе (в контроллеры и атрибуты всё замечательно инъектится), очевидное место вставки регистраций, приятный синтаксис и множество тонких настроек. Да, выбор очевиден :)

Да, по скорости Ninject уступает и кастлу, и юнити, но производительность здесь не настолько критична — даже если отличие «в разы» — то все равно разница при создании десятка объектов — микросекунды. Преждевременно оптимизировать такие тонкие моменты не будем :)

Generic-типы — это просто :)

    internal class ChooseServerStage : ActStageBase
    {
        public ChooseServerStage(FinderWrapper<RegionFinder<PixelCounterComparer<whitelettercolorpixelcriteria>>> finder)
            : base(finder)
        {
        }
    }

Дженерик-тип тройной вложенности в конструкторе :) Только что написал подобную конструкцию в своем собственном коде.
Continue reading

Модульность WPF-приложений (или Использование IoC-контейнеров, часть 3)

Последнюю неделю (или даже чуть больше) мы занимаемся разбором кода пришедшего нам WPF-проекта, а также поиском и корректировкой архитектурных недоработок. Проекту где-то год-полтора, но работа над ним велась не слишком активно (долгое время — одним разработчиком), поэтому кодовая база не слишком велика.

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

Итак, что у нас имеется? Допустим, есть модуль, который призван искать подключенные к компьютеру устройства — SD-карты с определенным содержимым, или приборы, подключенный по USB.
Как бы мы хотели видеть использование данного класса?

var detector = new DeviceDetector();
var devices = detector.Detect();

И если вторая строчка определяется контрактом класса и фактически так и выглядит, то вот конструктор класса DeviceDetector выглядит совсем по-другому:

public class DeviceDetector {
  public DeviceDetector(IComPortSearcher searcher,
                                IUsbValidator validator,
                                IDriveSearcher driveSearcher,
                                IDeviceValidator validator) {
  }
}
var devices = detector.Detect();

Continue reading

Использование IoC-контейнеров, часть 2

Со времен прошлой записи про Inversion of Control, контейнеры таки плотно вошли и пустили корни в нашей повседневной жизни. В новом проекте он используется сплошь и рядом, да и при переписывании частей legacy-кода то и дело норовит внедриться.

Однако, используя контейнеры очень легко забыть о модульности и разделении областей видимости.
Как это обычно происходит: проект только начинается, пишутся первые корневые классы, появляется первый UI. При этом проект еще маленький и кажется, что все его части имеют очень много общего (так и есть на самом деле — мы ведь только начинаем). Мы решили использовать контейнер и при старте приложения все типы в нём сразу зарегистрировали.
Наступило счастье — любой класс мы можем с лёгкостью разрезолвить со всеми его зависимостями. Вроде бы всё хорошо.

Но с ростом появляются издержки. Поскольку всё регистрируется в одном месте — все классы во всех проектах публичные. Поскольку контейнер един — мы не задумываемся об обособленности отдельных проектов (всё равно всё успешно отрезолвится в рантайме).
Как следствие из этого — проблемы с пониманием из-за отсутствия абстрагированных регионов и проекты становится проблематично «выдернуть» из продукта и использовать отдельно (что, во-первых, часто пригождается, а во-вторых, помогает в проектировании).

Как «рецепт успеха» я бы рекомендовал всегда рассматривать проект (или группу проектов с единым назначением) как обособленную единицу, и всегда задумываться о его собственном внешнем интерфейсе (он, обычно, состоит из буквально одного-двух интерфейсов), а не отталкиваться от того, как он будет использован в конкретный момент в конкретном продукте.

Как следствие, каждый такой проект или группа проектов будут, скорее всего, иметь свой собственный контейнер, который упростит разработку конкретной области.