Аббревиатуры DI и IoC известны каждому уважающему себя программисту. Кто-то считает эти термины веянием моды, кто-то — неотъемлемой частью своей повседневной деятельности. Но, несмотря на то, что и DI и IoC тесно переплетаются с самим понятием качественной архитектуры, многие программисты вообще не понимают значения этих аббревиатур. Из-за этого появляется бесконечное множество записей в блогах, статей и комментариев примерно следующего содержания (компиляция из реальных постов):

Есть ли вообще польза от использования DI и IoC в бизнес-приложениях? По-моему и без него все прекрасно работает, а IoC — это только лишнее усложнение. У меня вот есть подруга.. а точнее друг. Так вот в его бизнес-приложении di+ioc безбожно запутали код, ухудшив читаемость, отладку и рефакторинг и при этом ничего не дали.

Каждый раз, когда читаешь такое внутри просто все переворачивается) Эти феерические программисты явно не понимают, чем IoC отличается от ID, а главное — они не понимают, что IoC и IoC-контейнер — это абсолютно разные вещи.

Ну теперь нужно от эмоциональной части перейти к технической — давайте попробуем разобраться в сути названных понятий и определить границу применяемости каждого из них. Начнем с DI.

DI (Dependency injection, инъекция зависимостей) — это техника внедрения (предоставления, инъекции) внешней зависимости в программный компонент (объект).

Чтобы понять значение написанного воспользуемся упрощением классического примера Мартина Фаулера. Допустим, нам нужно создать программу, которая выводит на экран список фильмов (Movie Finder), удовлетворяющих некоторым условиям. Для построения гибкого решения мы разбиваем программу на 2 части — класс MovieFinder, осуществляющий поиск фильмов, и MovieLister, осуществляющий фильтрацию найденных фильмов. Понятно, что MovieLister должен каким-то образом получать ссылку на MovieFinder. То есть MovieFinder является внешней зависимостью по отношению к MovieLister. Посмотрим как мы можем реализовать получение этой внешней зависимости.

Самый простой способ — MovieFinder сам инстанцирует и хранит объект MovieLister:

class MovieLister
{
    private MovieFinder _finder = new MovieFinder();

    public Movie[] MoviesDirectedBy(String arg) 
    {
        var allMovies = _finder.FindAll();
        var filteredMovies = new List<Movie>();
        foreach (var movie in allMovies) 
        {
            if (movie.Director.Equals(arg))
            {
                filteredMovies.Add(movie);
            }
        }

        return filteredMovies.ToArray;
    }
}

У такого подхода есть серьезные недостатки. Во первых, классы MovieLister и MovieFinder жестко связаны, а значит мы, например, не можем заменить реализацию MovieFinder на альтернативную. Во вторых, по понятным причинам такой код трудно тестировать. Сделаем следующий шаг — создадим интерфейс для класса MovieFinder:

interface IMovieFinder 
{
    Movie[] FindAll();
}

И рассмотрим следующий способ разрешения зависимости — использование сервис-локатора (service locator):

class MovieLister
{
    private IMovieFinder _finder = ServiceLocator.CreateMovieFinder(); 1

    public Movie[] MoviesDirectedBy(String arg) 
    {
        var allMovies = _finder.FindAll();
        var filteredMovies = new List<Movie>();
        foreach (var movie in allMovies) 
        {
            if (movie.Director.Equals(arg))
            {
                filteredMovies.Add(movie);
            }
        }

        return filteredMovies.ToArray;
    }
}

Как видим реализация метода MoviesDirectedBy не изменилась, но зато теперь MovieLister работает не с реализацией MovieFinder, а с соответствующим интерфейсом. Также видим 1, что MovieLister не создает MovieFinder напрямую, а использует класс ServiceLocator. Реализация сервис-локатора может быть следующей:

public static class ServiceLocator
{
    public static IMovieFinder CreateMovieFinder()
    {
        return new MovieFinder();
    }
}

В результате мы получили то, что MovieLister больше не связан с конкретным классом MovieFinder и мы легко можем предоставить другую его реализацию, изменив метод CreateMovieFinder в классе ServiceLocator. Но проблема с тестированием осталась — мы не можем легко написать тесты, потому что внутри класса идет обращение к статическому методу. Эта проблема решается переходом на следующую ступень — использование паттерна Dependency Injection (DI)

Изменим код так, чтобы MovieLister получал ссылку на MovieFinder извне:

class MovieLister
{
    private IMovieFinder _finder; 

    public MovieLister(IMovieFinder movieFinder)
    {
        _finder = movieFinder;  2
    }

    public Movie[] MoviesDirectedBy(String arg) 
    {
        ...
    }
}

Ссылка на MovieFinder передается в конструктор 2 класса MovieLister. Именно это и называется «инъекцией зависимости». Именно такой подход является самым правильным и естественным в большинстве случаев. И заметьте — здесь вообще не упоминается IoC-контейнер, потому что он не имеет отношения к самой технике DI! Рассмотрим следующий термин — IoC.

IoC (Inversion of Control, инверсия управления) — это абстрактный принцип построения программных систем, согласно которому логика управления потоком выполнения выносится из конкретных компонентов на уровень системы или вспомогательного фреймворка. То есть, каждый раз, когда мы выносим зависимость из какого-либо класса и вводим интерфейс, мы используем прицип IoC. Более того, DI — это частный случай IoC. Когда мы вынесли логику получения ссылки на MovieFinder в конструктор класса MovieLister мы именно выполнили «обращение» (инверсию) этой зависимости. Мы освободили класс MovieLister от ответственности за создание зависимости и позволили ему сосредоточиться на собственной логике, а это и есть IoC. Однако, инверсия управления не ограничивается инъекцией зависимостей. К IoC можно также отнести:

  • использование AOP;
  • использование событийно-управляемых компонентов;
  • и ... что-то еще, что я не могу сейчас вспомнить.

И опять в определении IoC не упоминается и никак не фигурирует IoC-контейнер. Потому что, как и в случае с DI, IoC — это базовый принцип построения качественной архитектуры, который должен применяться обязательно в любом проекте.

Что же такое IoC-контейнер? Это просто библиотека, помогающая реализовать принципы DI и IoC. Это может быть удобная декларативная конфигурация инъекции зависимостей, AOP-движок или централизованная шина событий. Целесообразность использования IoC-контейнера для конкретного проекта определяется только исходя из характеристик этого проекта. Но сами паттерны DI и IoC в любом случае должны использоваться.

Очень интересно услышать замечания по поставленному вопросу, пожалуйста, если есть что добавить — комментируйте.

Progg it

Alexander

Так вот оно что... Так-то оно да, конечно.

Alexander

Ну, а если серьезно, то какие могут быть замечания? Мне вот только интересно, почему сейчас существует тенденция к конфигурации IoC-контейнеров с помощью атрибутов. Взять хотя бы Unity, MEF, да и spring.net 2.0 вроде бы к этому идет. Получается, что конфигурация разбросана по всему коду, а мне это не кажется клевым.

hVostt

в начале сам приходишь к DI. потом понимаешь, что это называется DI. потом немного юзаешь и понимаешь, что это скорее всего как-то можно автомтизировать и приходишь в конце-концов к IoC, контейнерам, AOP ну проч. в зависимости от предпочтений и проекта. а сама статья - ЗАЧЕТ! самая короткая статья, объясняющая что и как в двух словах. а лучше читать Фаулера, вдумчиво :)

Ubloobok

Alexander, не знаю как другие, но лично я выбрал именно MEF и его атрибуты для конфигурации контейнера, импорта/экспорта. Ситуация тут довольно банальная: если у нас совсем модульное приложение - импорт или экспорт могут быть где угодно. Раз так, то описать всю конфигурацию в одном месте не получится, следовательно придется в каждом модуле делать класс-конфигуратор. Уже некоторое нарушение принципа единообразия.

Ubloobok

Дальше нам потребовалось вот этот класс экспортировать как один instance на все приложение, а этот как создаваемый каждый раз заново, а этот временно запретит для экспорта. Чтобы это определить нам потребуется прочитать код класса-конфигуратора. Это лишние трудности, долгое понимание общей картины приложения. В конце получим свои атрибуты, а это тот же самый MEF, но написанный собственноручно. Вывод: все равно в сложном приложении рано или поздно придем к атрибутам.