что такое dependency injection и зачем оно нужно
Dependency injection
От переводчика
Представляемый вашему вниманию перевод открывает серию статей от Jakob Jenkov, посвященных внедрению зависимостей, или DI. Примечательна серия тем, что в ней автор, анализируя понятия и практическое применение таких понятий как «зависимость», «внедрение зависимостей», «контейнер для внедрения зависимостей», сравнивая паттерны создания объектов, анализируя недостатки конкретных реализаций DI-контейнеров (например, Spring), рассказывает, как пришел к написанию собственного DI-контейнера. Таким образом, читателю предлагается познакомиться с довольно цельным взглядом на вопрос управления зависимостями в приложениях.
В данной статье сравнивается подход к настройке объектов изнутри и извне (DI). По смыслу настоящая статья продолжает статью Jakob Jenkov Understanding Dependencies, в которой дается определение самому понятию «зависимости» и их типам.
Серия включает в себя следующие статьи
Внедрение зависимостей
«Внедрение зависимостей» — это выражение, впервые использованное в статье Мартина Фаулера Inversion of Control Containers and the Dependency Injection Pattern. Это хорошая статья, но она упускает из виду некоторые преимущества контейнеров внедрения зависимостей. Также я не согласен с выводами статьи, но об этом — в следующих текстах.
Объяснение внедрения зависимостей
Внедрение зависимостей — это стиль настройки объекта, при котором поля объекта задаются внешней сущностью. Другими словами, объекты настраиваются внешними объектами. DI — это альтернатива самонастройке объектов. Это может выглядеть несколько абстрактно, так что посмотрим пример:
UPD: после обсуждения представленных автором фрагментов кода с flatscode и fogone, я принял решение скорректировать спорные моменты в коде. Изначальный замысел был в том, чтобы не трогать код и давать его таким, каков он написан автором. Оригинальный авторский код в спорных местах закомментирован с указанием «в оригинале», ниже дается его исправленная версия. Также оригинальный код можно найти по ссылке в начале статьи.
Этот DAO (Data Access Object), MyDao нуждается в экземпляре javax.sql.DataSource для того, чтобы получить подключения к базе данных. Подключения к БД используются для чтения и записи в БД, например, объектов Person.
Заметьте, что класс MyDao создает экземпляр DataSourceImpl, так как нуждается в источнике данных. Тот факт, что MyDao нуждается в реализации DataSource, означает, что он зависит от него. Он не может выполнить свою работу без реализации DataSource. Следовательно, MyDao имеет «зависимость» от интерфейса DataSource и от какой-то его реализации.
Класс MyDao создает экземпляр DataSourceImpl как реализацию DataSource. Следовательно, класс MyDao сам «разрешает свои зависимости». Когда класс разрешает собственные зависимости, он автоматически также зависит от классов, для которых он разрешает зависимости. В данном случае MyDao завсист также от DataSourceImpl и от четырех жестко заданных строковых значений, передаваемых в конструктор DataSourceImpl. Вы не можете ни использовать другие значения для этих четырех строк, ни использовать другую реализацию интерфейса DataSource без изменения кода.
Как вы можете видеть, в том случае, когда класс разрешает собственные зависимости, он становится негибким в отношении к этим зависимостям. Это плохо. Это значит, что если вам нужно поменять зависимости, вам нужно поменять код. В данном примере это означает, что если вам нужно использовать другую базу данных, вам потребуется поменять класс MyDao. Если у вас много DAO-классов, реализованных таким образом, вам придется изменять их все. В добавок, вы не можете провести юнит-тестирование MyDao, замокав реализацию DataSource. Вы можете использовать только DataSourceImpl. Не требуется много ума, чтобы понять, что это плохая идея.
Давайте немного поменяем дизайн:
Заметьте, что создание экземпляра DataSourceImpl перемещено в конструктор. Конструктор принимает четыре параметра, это — четыре значения, необходимые для DataSourceImpl. Хотя класс MyDao все еще зависит от этих четырех значений, он больше не разрешает зависимости сам. Они предоставляются классом, создающим экземпляр MyDao. Зависимости «внедряются» в конструктор MyDao. Отсюда и термин «внедрение (прим. перев.: или иначе — инъекция) зависимостей». Теперь возможно сменить драйвер БД, URL, имя пользователя или пароль, используемый классом MyDao без его изменения.
Внедрение зависимостей не ограничено конструкторами. Можно внедрять зависимости также используя методы-сеттеры, либо прямо через публичные поля (прим. перев.: по поводу полей переводчик не согласен, это нарушает защиту данных класса).
Класс MyDao может быть более независимым. Сейчас он все еще зависит и от интерфейса DataSource, и от класса DataSourceImpl. Нет необходимости зависеть от чего-то, кроме интерфейса DataSource. Это может быть достигнуто инъекцией DataSource в конструктор вместо четырех параметров строкового типа. Вот как это выглядит:
Теперь класс MyDao больше не зависит от класса DataSourceImpl или от четырех строк, необходимых конструктору DataSourceImpl. Теперь можно использовать любую реализацию DataSource в конструкторе MyDao.
Цепное внедрение зависимостей
Пример из предыдущего раздела немного упрощен. Вы можете возразить, что зависимость теперь перемещена из класса MyDao к каждому клиенту, который использует класс MyDao. Клиентам теперь приходится знать о реализации DataSource, чтобы быть в состоянии поместить его в конструктор MyDao. Вот пример:
Как вы можете видеть, теперь MyBizComponent зависит от класса DataSourceImpl и четырех строк, необходимых его конструктору. Это еще хуже, чем зависимость MyDao от них, потому что MyBizComponent теперь зависит от классов и от информации, которую он сам даже не использует. Более того, реализация DataSourceImpl и параметры конструктора принадлежат к разным слоям абстракции. Слой ниже MyBizComponent — это слой DAO.
Решение — продолжить внедрение зависимости по всем слоям. MyBizComponent должен зависеть только от экземпляра MyDao. Вот как это выглядит:
Снова зависимость, MyDao, предоставляется через конструктор. Теперь MyBizComponent зависит только от класса MyDao. Если бы MyDao был интерфейсом, можно было бы менять реализацию без ведома MyBizComponent.
Такой паттерн внедрения зависимости должен продолжается через все слои приложения, с самого нижнего слоя (слоя доступа к данным) до пользовательского интерфейса (если он есть).
CLI приложение + Dependency Injector — руководство по применению dependency injection + Вопросы / ответы
Я создатель Dependency Injector. Это dependency injection фреймворк для Python.
Это завершающее руководство по построению приложений с помощью Dependency Injector. Прошлые руководства рассказывают как построить веб-приложение на Flask, REST API на Aiohttp и мониторинг демона на Asyncio применяя принцип dependency injection.
Сегодня хочу показать как можно построить консольное (CLI) приложение.
Дополнительно я подготовил ответы на часто задаваемые вопросы и опубликую их постскриптум.
Основная фича этой версии — связывание (wiring). Узнать больше о новой фиче можно в этом посте.
Для старта необходимо иметь:
Что мы будем строить?
Мы будем строить CLI (консольное) приложение, которое ищет фильмы. Назовем его Movie Lister.
Как работает Movie Lister?
Вот как выглядит диаграмма классов приложения Movie Lister:
Обязанности между классами распределены так:
Подготовка окружения
Начнём с подготовки окружения.
В первую очередь нам нужно создать папку проекта и virtual environment:
Теперь давайте активируем virtual environment:
Окружение готово. Теперь займемся структурой проекта.
Структура проекта
В этом разделе организуем структуру проекта.
Создадим в текущей папке следующую структуру. Все файлы пока оставляем пустыми.
Установка зависимостей
Пришло время установить зависимости. Мы будем использовать такие пакеты:
И выполним в терминале:
Установка зависимостей завершена. Переходим к фикстурам.
Фикстуры
В это разделе мы добавим фикстуры. Фикстурами называют тестовые данные.
Мы создадим скрипт, который создаст тестовые базы данных.
Добавляем директорию data/ в корень проекта и внутрь добавляем файл fixtures.py :
Далее редактируем fixtures.py :
Теперь выполним в терминале:
Скрипт должен вывести OK при успешном завершении.
Проверим, что файлы movies.csv и movies.db появились в директории data/ :
Фикстуры созданы. Продолжаем.
Контейнер
В этом разделе мы добавим основную часть нашего приложения — контейнер.
Контейнер позволяет описать структуру приложения в декларативном стиле. Он будет содержать все компоненты приложения их зависимости. Все зависимости будут указаны явно. Для добавления компонентов приложения в контейнер используются провайдеры. Провайдеры управляют временем жизни компонентов. При создании провайдера не происходит создание компонента. Мы указываем провайдеру как создавать объект, и он создаст его как только в этом будет необходимость. Если зависимостью одного провайдера является другой провайдер, то он будет вызван и так далее по цепочке зависимостей.
Контейнер пока пуст. Мы добавим провайдеры в следующих секциях.
Контейнер — первый объект в приложении. Он используется для получения всех остальных объектов.
Работа с csv
Теперь добавим все что нужно для работы с csv файлами.
Создаем файл entities.py в пакете movies :
и добавляем внутрь следующие строки:
Создаем файл finders.py в пакете movies :
и добавляем внутрь следующие строки:
Теперь добавим CsvMovieFinder в контейнер.
У csv_finder еще есть зависимость от нескольких опций конфигурации. Мы добавили провайдер Сonfiguration чтобы передать эти зависимости.
Сначала используем, потом задаем значения.
Теперь давайте добавим значения конфигурации.
Значения установлены в конфигурационный файл. Обновим функцию main() чтобы указать его расположение.
Создаем файл listers.py в пакете movies :
и добавляем внутрь следующие строки:
Все компоненты созданы и добавлены в контейнер.
Все готово. Теперь запустим приложение.
Выполним в терминале:
Работа с sqlite
У провайдера sqlite_finder есть зависимость от опций конфигурации, которые мы еще не определили. Обновим файл конфигурации:
Готово. Давайте проверим.
Выполняем в терминале:
Провайдер Selector
В этом разделе мы сделаем наше приложение более гибким.
Больше не нужно будет делать изменения в коде для переключения между csv и sqlite форматами. Мы реализуем переключатель на базе переменной окружения MOVIE_FINDER_TYPE :
Выполним в терминале следующие команды:
Вывод при выполнении каждой команды будет выглядеть так:
В следующем разделе добавим несколько тестов.
Тесты
В завершение добавим несколько тестов.
Создаём файл tests.py в пакете movies :
и добавляем в него следующие строки:
Теперь запустим тестирование и проверим покрытие:
Работа закончена. Теперь давайте подведем итоги.
Заключение
Мы построили консольное (CLI) приложение применяя принцип dependency injection. Мы использовали Dependency Injector в качестве dependency injection фреймворка.
Преимущество, которое вы получаете с Dependency Injector — это контейнер.
Контейнер начинает окупаться, когда вам нужно понять или изменить структуру приложения. С контейнером это легко, потому что все компоненты приложения и их зависимости определены явно и в одном месте:
Контейнер как карта вашего приложения. Вы всегда знайте что от чего зависит.
PS: вопросы и ответы
В комментариях к прошлому руководству были заданы классные вопросы: «зачем это нужно?», «зачем нужен фреймворк?», «чем фреймворк помогает в реализации?».
Я подготовил ответы:
Что такое dependency injection?
Концепция Dependency Injector
В дополнение опишу концепцию Dependency Injector как фреймворка.
Dependency Injector основан на двух принципах:
Что дальше?
Если вы заинтересовались, но сомневайтесь, моя рекомендация такая:
Попробуйте применить этот подход на протяжении 2-х месяцев. Он неинтуитивный. Нужно время чтобы привыкнуть и прочувствовать. Польза стает ощутимой, когда проект вырастает до 30+ компонентов в контейнере. Если не понравится — много не потеряйте. Если понравится — получите существенное преимущество.
Этот класс создает MessageWriter и напрямую зависит от этого класса. Включенные в код зависимости, как в предыдущем примере, представляют собой определенную проблему. Их следует избегать по следующим причинам:
Внедрение зависимостей устраняет эти проблемы следующим образом:
При использовании шаблона внедрения зависимостей рабочая служба имеет следующие характеристики:
Реализацию интерфейса IMessageWriter можно улучшить с помощью встроенного API ведения журнала:
Обновленный метод ConfigureServices регистрирует новую реализацию IMessageWriter :
Использование цепочки внедрений зависимостей не является чем-то необычным. Каждая запрашиваемая зависимость запрашивает собственные зависимости. Контейнер разрешает зависимости в графе и возвращает полностью разрешенную службу. Весь набор зависимостей, которые нужно разрешить, обычно называют деревом зависимостей, графом зависимостей или графом объектов.
В терминологии внедрения зависимостей — служба:
Несколько правил обнаружения конструктора
Если тип определяет более одного конструктора, поставщик служб включает логику для определения используемого конструктора. Выбирается тот конструктор, который имеет больше всего параметров, в которых типы могут разрешаться с внедрением зависимостей. Рассмотрим следующий пример службы на C#:
Если при определении конструктора возникает неоднозначность, выдается исключение. Рассмотрим следующий пример службы на C#:
Код ExampleService с неоднозначными параметрами типов, которые могут разрешаться с внедрением зависимостей, выдаст исключение. Не делайте этого, это всего лишь демонстрация того, что подразумевается под «неоднозначными типами, разрешаемыми с внедрением зависимостей».
Регистрация групп служб с помощью методов расширения
Расширения Microsoft используют конвенцию для регистрации группы связанных служб. Соглашение заключается в использовании одного метода расширения Add
Платформенные службы
В следующей таблице перечислены некоторые примеры этих зарегистрированных платформой служб.
Тип службы | Время существования |
---|---|
Microsoft.Extensions.DependencyInjection.IServiceScopeFactory | Одноэлементный |
IHostApplicationLifetime | Одноэлементный |
Microsoft.Extensions.Logging.ILogger | Одноэлементный |
Microsoft.Extensions.Logging.ILoggerFactory | Одноэлементный |
Microsoft.Extensions.ObjectPool.ObjectPoolProvider | Одноэлементный |
Microsoft.Extensions.Options.IConfigureOptions | Временный |
Microsoft.Extensions.Options.IOptions | Одноэлементный |
System.Diagnostics.DiagnosticListener | Одноэлементный |
System.Diagnostics.DiagnosticSource | Одноэлементный |
Время существования служб
Службы можно зарегистрировать с одним из следующих вариантов времени существования:
Они описываются в следующих разделах. Для каждой зарегистрированной службы выбирайте подходящее время существования.
Временный
Временные службы времени существования создаются при каждом их запросе из контейнера служб. Это время существования лучше всего подходит для простых служб без отслеживания состояния. Регистрируйте временные службы с помощью AddTransient.
В приложениях, обрабатывающих запросы, временные службы удаляются в конце запроса.
Область действия
Для веб-приложений время существования, привязанное к области, означает, что службы создаются один раз для каждого запроса (подключения) клиента. Регистрируйте службы с заданной областью с помощью AddScoped.
В приложениях, обрабатывающих запросы, службы с заданной областью удаляются в конце запроса.
При использовании Entity Framework Core метод расширения AddDbContext по умолчанию регистрирует типы DbContext с заданной областью времени существования.
Разрешать службу с заданной областью из одноэлементной службы запрещено, и будьте внимательны, чтобы не сделать это неявно, например, через временную службу. При обработке последующих запросов это может вызвать неправильное состояние службы. Допускается следующее:
По умолчанию в среде разработки разрешение службы из другой службы с более длинным временем существования вызывает исключение. Дополнительные сведения см. в разделе Проверка области.
Одноэлементный
Одноэлементные службы времени существования создаются в следующих случаях.
Каждый последующий запрос на реализацию службы из контейнера внедрения зависимостей использует тот же экземпляр. Если в приложении нужно использовать одноэлементные службы, разрешите контейнеру служб управлять временем их существования. Не реализуйте одноэлементный подход и предоставьте код для удаления одноэлементных объектов. Службы никогда не должны удаляться кодом, который разрешил службу из контейнера. Если тип или фабрика зарегистрированы как одноэлементный объект, контейнер автоматически удалит одноэлементные объекты.
Зарегистрируйте одноэлементные службы с помощью AddSingleton. Одноэлементные службы должны быть потокобезопасными и часто использоваться в службах без отслеживания состояния.
В приложениях, обрабатывающих запросы, отдельные службы удаляются, когда ServiceProvider удаляется по завершении работы приложения. Поскольку память не освобождается до завершения работы приложения, рекомендуется учитывать использование памяти одноэлементным объектом.
Методы регистрации службы
Платформа предоставляет методы расширения регистрации службы, которые полезны в определенных сценариях.
Метод | Автоматически object удаление | Несколько реализации | Передача аргументов |
---|---|---|---|
Add services.AddSingleton (); | Да | Да | Нет |
Add services.AddSingleton (sp => new MyDep()); | Да | Да | Да |
Add services.AddSingleton (); | Да | Нет | Нет |
AddSingleton (new services.AddSingleton (new MyDep()); | Нет | Да | Да |
AddSingleton(new services.AddSingleton(new MyDep()); | Нет | Нет | Да |
Дополнительные сведения об удалении типа см. в разделе Удаление служб.
Регистрация службы только с типом реализации эквивалентна регистрации этой службы с той же реализацией и типом службы. Именно поэтому несколько реализаций службы не могут быть зарегистрированы с помощью методов, которые не принимают явный тип службы. Эти методы могут регистрировать несколько экземпляров службы, но все они будут иметь одинаковую реализацию типа.
Платформа также предоставляет методы расширения TryAdd
Параметр TryAddSingleton не применяется, так как он уже был добавлен, поэтому выполнение «try» завершится ошибкой. В ExampleService будут следующие утверждения:
Дополнительные сведения см. в разделе:
Регистрация службы обычно не зависит от порядка, за исключением случаев регистрации нескольких реализаций одного типа.
IServiceCollection является коллекцией объектов ServiceDescriptor. В следующем примере показано, как зарегистрировать службу, создав и добавив ServiceDescriptor :
Встроенные методы Add
Поведение внедрения через конструктор
Службы можно разрешать с помощью:
Конструкторы могут принимать аргументы, которые не предоставляются внедрением зависимостей, но эти аргументы должны назначать значения по умолчанию.
Проверка области
Когда приложение выполняется в среде Development и вызывает CreateDefaultBuilder для создания узла, поставщик службы по умолчанию проверяет следующее:
Корневой поставщик службы создается при вызове BuildServiceProvider. Время существования корневого поставщика службы соответствует времени существования приложения — поставщик запускается с приложением и удаляется, когда приложение завершает работу.
Сценарии применения области
Интерфейс IServiceScopeFactory всегда регистрируется как отдельный (singleton), но IServiceProvider зависит от времени существования содержащего класса. Например, если при разрешении служб из области какая-то из служб принимает интерфейс IServiceProvider, это будет экземпляр с заданной областью.
Для получения служб с заданной областью в реализациях IHostedService, например службы BackgroundService, не внедряйте зависимости служб через конструктор. Вместо этого внедрите IServiceScopeFactory, создайте область, а затем используйте разрешение зависимостей из области, чтобы применить подходящее время существования служб.
В приведенном выше коде во время выполнения приложения фоновая служба:
В примере исходного кода можно увидеть, как реализации IHostedService могут использовать преимущества времени существования служб с заданной областью.
Что такое dependency injection и зачем оно нужно
Зачем нужно Dependency Injection (и что это?)
В этом уроке мы разберемся с тем, что такое зависимости классов и внедрение зависимостей. Пожалуйста, помни, что все описанные здесь принципы появились не просто так. Мы используем их не потому, что слепо в них верим, а потому что они помогают решить проблемы, стоящие при разработке больших приложений. Начнем издалека.
Принцип одной обязанности
Принцип единственной обязанности заключается в том, что каждый класс занимается только своим делом. Например, один класс отвечает только за взаимодействие с БД, другой за проверку правильности введенных данных, третий за выставление и проверку кук для авторизованных пользователей.
Теперь, если мы хотим понять, как происходит например проверка данных при регистрации, или что-то в ней поменять, нам надо изучить лишь один класс, который этим занимается. Также, мы можем протестировать процесс проверки отдельно от остального кода (более того, мы можем его тестировать даже если остальные методы пока не написаны).
Если мы разбили код на классы, то может оказаться, что одному из них нужен другой. Ну например, если у нас есть класс авторизации, а в нем метод, проверяющий логин и пароль пользователя на правильность, которые хранятся в базе данных, ему понадобится класс, достающий их оттуда.
Теперь обсудим, в чем вред от глобальных переменных и злоупотребления статическими методами, чтобы у тебя не возникло даже мысли использовать их для связи классов между собой. А после узнаем про правильный способ внедрения зависимостей.
Чем плохи глобальные переменные
На первый взгляд они хорошо подходят для хранения таких вещей, как: настройки (например имя и пароль к базе данных), или язык выводимых сообщений на многоязычном сайте, но если присмотреться, то это почти всегда плохое решение.
Чем меньше область, где доступна переменная, тем проще поменять работающий с ней код. В случае с настройками базы данных, достаточно передать их только в класс, отвечающий за соединение с ней.
Также, доступная глобально переменная (например, настройки базы данных) провоцирует неопытных программистов писать код работы с базой данных в любом месте программы, вместо того, чтобы сконцентрировать его в одном классе.
В этой ситуации код был бы понятнее, если бы язык передавался явно через аргументы функции, либо передавался через конструктор класса, в котором находится метод.
Как я уже писал выше, статические поля в классах зачастую аналогичны глобальным переменным. Они точно так же добавляют побочные эффекты в использующие их функции. Ну например, если мы хотим временно поменять значение такого поля, нам придется сначала куда-то сохранить его старое значение, а потом восстановить. Очень неудобно.
Чем плохи классы из статических методов
Вот недостатки классов, где все методы статические (в сравнении с классами из обычных методов, где надо сначала создать объект и только потом вызывать у него методы):
Допустим, что мы сделали метод проверки правильности данных пользователя статическим. Он принимает на вход объект пользователя и возвращает массив сообщений об ошибках:
Далее, мы пишем метод регистрации пользователя, который в том числе вызывает метод валидации:
Также, статические методы можно использовать как дополнительные конструкторы в классе (паттерн Static Constructor).
Хорошая функция получает нужные ей значения через аргументы, а хороший класс получает свои обязательные зависимости через конструктор. Это имеет такие преимущества:
Вот пример передачи зависимостей классу-валидатору:
Как мы видим, внедрение зависимостей дает нам максимальную гибкость использования и позволяет сделать классы слабо связанными друг с другом, так, что изменение в одном не потребует переделки другого.
Поскольку тут зависимости передаются снаружи, а не класс сам ищет их, это называется инверсия управления (IoC, inversion of control) («Инверсия» значит «замена на противоположный вариант»). Такой подход дает нам максимальную гибкость в том, как создавать и связывать между собой объекты разных классов.
Увы, иногда можно встретить менее удачные способы получения зависимостей. Разберем их недостатки.
Registry и чем он плох
Когда классу UserValidator нужно что-то получить из БД, он находит объект для работы с ней в Registry :
Чем плох ServiceLocator
А вот, как он используется:
Этот подход исправляет часть недостатков Registry (например, он доступен только в тех классах, куда мы его передали), но имеет такие недостатки:
Однако, использование ServiceLocator иногда оправданно. В некоторых фреймворках объект ServiceLocator (в качестве которого выступает DI Container) передается в конструктор контроллера, чтобы тот мог найти и вызвать нужные ему сервисы. Это проще, чем передавать каждый по отдельности в конструктор. Контроллер является чем-то вроде стартовой точки обработки запроса, так что там это может быть приемлемо.
Кстати, самой простой реализацией ServiceLocator может быть простой массив:
Но на мой взгляд, это плохо соответствует ООП и имеет недостатки: мы, например, не можем ставить тайп хинты на него.
Чем хорош DI container
При использовании DI в небольшом приложении мы можем вручную создать все нужные объекты в самом начале:
Как видно, объект контейнера позволяет работать с ним, используя синтаксис доступа к массиву (хотя он и не является массивом), за счет реализации интерфейса ArrayAccess.
Иногда в контейнер, кроме классов, кладут еще настройки приложения, например, настройки соединения с БД.
Здесь у нас для каждого класса регистрируется соответствующий сервис в контейнере, но вообще, для одного класса можно создать несколько сервисов с разными именами и настройками. Ну например, у нас может быть 2 объекта PDO с разными настройками, если мы по каким-то причинам используем 2 разные базы данных.
DI container внешне напоминает ServiceLocator. Но если присмотреться, то между ними есть принципиальная разница: при использовании ServiceLocator все классы начинают зависеть от него, так как мы передаем им в конструктор этот самый ServiceLocator. Когда класс хочет получить объект, он сам вызывает методы ServiceLocator. В случае же с DI container классы о нем ничего не знают, и получают от него в конструктор только нужные им объекты-сервисы. Потому DI container не имеет недостатков ServiceLocator. Ты можешь использовать DI контейнер с любыми классами, в них не надо специально что-то дописывать.
Заметь что контейнер — внешняя вещь по отношению к сервису. Мы не передаем сам контейнер в конструктор (иначе это будет ServiceLocator ). Класс от него не зависит, мы можем в любой момент выкинуть контейнер и создать объект руками или взять другой контейнер от другого производителя. Мы можем описать в конфиге и создать несколько экземпляров класса с разными настройками. Ты чувствуешь силу ООП и зришь свет разума, падаван?
Если что-то осталось непонятным, и хочется разобраться, я предлагаю попробовать написать свой DI контейнер. Ниже я опишу, как это сделать.
Есть много реализаций DI container, например более сложный и мощный Symfony DI Container (англ.), где можно описывать зависимости классов в файлах конфигурации.
В некоторых фреймворках, правда, объект DI container передается в контроллер (фактически получается паттерн ServiceLocator) вместо того, чтобы передавать явно только нужные контроллеру сервисы. Это делается, чтобы сэкономить время и не прописывать каждый контроллер в DI контейнере.
Некоторые DI контейнеры могут также автоматически находить нужные классу зависимости, например, по тайп-хинтам в конструкторе. Вот здесь описана опция autowiring в Symfony (англ.).
Написание своего собственного DI контейнера
При этом контейнер должен сохранять созданные ранее объекты-сервисы и при повторном запросе сервиса не создавать вторую его копию, а возвращать ранее созданный объект.
Иногда контейнер наделяют дополнительными функциями, например, хранить параметры конфигурации (вроде хоста, имени и пароля для доступа к БД). Параметры помещаются в контейнер в начале программы и могут использоваться при создании сервисов. В нашем контейнере этого не будет.
Наш простой контейнер будет содержать всего лишь 2 метода:
Вот пример использования нашего контейнера:
Попробуем написать класс контейнера:
Дописать недостающие части кода оставим в качестве домашнего задания читателю.
Пример использования преимуществ DI
Попробуем сделать пример кода, который использует возможности DI. Допустим, у нас есть класс, который загружает данные из интернета и как-то их обрабатывает (например, это скрипт, который раз в сутки загружает курсы валют с официального сайта центрального банка и сохраняет в нашу базу данных). Если писать код без использования DI, он будет выглядеть примерно так:
и используется он так:
Можно написать и другие декораторы, например, декоратор, логгирующий выполняющиеся запросы или декоратор, кеширующий результаты запроса. Мы можем гибко использовать отдельные классы, строя из них нужную нам конструкцию.
Конечно, тут важно обходиться без фанатизма. Не стоит разделять классы на слишком маленькие части и злоупотреблять использованием паттернов без необходимости, так как это может сделать код менее понятным. Иногда такие возможности, как повторное скачивание, выгоднее встроить в сам класс. Интерфейсы и изменение поведения класса с помощью декоратора обычно используют в библиотеках, так как автор библиотеки хочет предоставить пользователям возможность гибко использовать свои классы (у каждого пользователя обычно есть свои пожелания), а править код сторонней библиотеки пользователи не могут.
Внедрение через интерфейс