Что такое DI-контейнер (контейнер внедрения зависимостей)?

Ответ

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

Основные функции DI-контейнера:

  • Регистрация (Registration): Указание, какой класс соответствует какому интерфейсу и как управлять его жизненным циклом.
  • Разрешение (Resolution): Автоматическое создание запрошенного объекта со всеми его зависимостями.
  • Управление жизненным циклом (Lifetime Management): Контроль над тем, как долго "живет" экземпляр сервиса.

Пример использования встроенного контейнера .NET:

using Microsoft.Extensions.DependencyInjection;

// Создаем коллекцию сервисов
var services = new ServiceCollection();

// Регистрируем зависимости с указанием времени жизни
services.AddTransient<ILogger, ConsoleLogger>(); // Новый экземпляр при каждом запросе
services.AddScoped<IOrderRepository, SqlOrderRepository>(); // Один экземпляр на область (scope), например, на HTTP-запрос
services.AddSingleton<ICacheService, MemoryCacheService>(); // Один экземпляр на все время работы приложения

// Регистрируем наш основной сервис
services.AddTransient<OrderProcessor>();

// Строим провайдер (контейнер)
var serviceProvider = services.BuildServiceProvider();

// Запрашиваем сервис. Контейнер автоматически создаст OrderProcessor,
// внедрит в него ILogger и IOrderRepository, создав их при необходимости.
using (var scope = serviceProvider.CreateScope())
{
    var processor = scope.ServiceProvider.GetRequiredService<OrderProcessor>();
    processor.ProcessOrder(new Order());
}

Популярные контейнеры для .NET: Autofac, Ninject, Simple Injector, Unity. ASP.NET Core по умолчанию использует легковесный, но функциональный контейнер Microsoft.Extensions.DependencyInjection.

Зачем он нужен? Контейнер избавляет от необходимости писать сложный "проводочный" код (wire-up code) для создания графа объектов, особенно в больших приложениях с глубокими цепочками зависимостей.

Ответ 18+ 🔞

А, DI-контейнер, ну это же та самая магия, которая избавляет тебя от ручного склеивания всего этого конструктора, как какой-нибудь паяльщик-самоучка! Вместо того чтобы самому, с потными ладошками, создавать каждый объект и пихать ему в конструктор другие объекты, которые тоже надо создать, а у тех свои зависимости... Короче, адский пазл, который собирать заебись.

Короче, контейнер — это такой умный менеджер по зависимостям. Ты ему на вход кричишь: «Слушай, когда кто-то попросит ILogger, ты ему суй ConsoleLogger! А IOrderRepository — это вот этот SqlOrderRepository, понял?». А он тебе: «Да похуй, ща всё будет». И дальше ты просто говоришь: «Дай мне OrderProcessor». А он уже сам, как заправский бармен, миксерует тебе коктейль из всех нужных ингредиентов, даже те, про которые ты забыл.

Что он умеет, этот бармен-контейнер:

  • Записать в меню (Registration): Ты ему объясняешь, какой класс к какому интерфейсу привязать и сколько раз его использовать — один на всех или каждому новому клиенту свежий.
  • Намешать коктейль (Resolution): Ты кричишь «Мне нужен OrderProcessor!», а он уже лезет в свои запасы, создаёт ConsoleLogger, SqlOrderRepository, собирает из них твой процессор и подаёт. Всё сам, блядь.
  • Решать, кому и сколько наливать (Lifetime Management): Один стакан на весь бар (Singleton), новый стакан на каждую компанию (Scoped) или вообще отдельный шот на каждый чих (Transient).

Вот смотри, как это на родном .NET выглядит, почти как на настоящем:

using Microsoft.Extensions.DependencyInjection;

// Создаём наш барной лист, список того, что есть
var services = new ServiceCollection();

// Пишем в этот лист рецепты
services.AddTransient<ILogger, ConsoleLogger>(); // Каждый раз новый стакан, без повторного использования
services.AddScoped<IOrderRepository, SqlOrderRepository>(); // Один стакан на всю тусовку (например, на один HTTP-запрос)
services.AddSingleton<ICacheService, MemoryCacheService>(); // Один стакан на весь вечер, для всех

// И главный наш коктейль регистрируем
services.AddTransient<OrderProcessor>();

// Готовим бар, строим провайдер из нашего списка
var serviceProvider = services.BuildServiceProvider();

// А теперь идём в бар и заказываем
using (var scope = serviceProvider.CreateScope())
{
    // Бармену (провайдеру) кричим: «Давай сюда OrderProcessor, и побыстрее!»
    var processor = scope.ServiceProvider.GetRequiredService<OrderProcessor>();
    // Он тебе его приносит, и там УЖЕ ВСЁ ПОДКЛЮЧЕНО: и логгер, и репозиторий. Волшебство, ёпта!
    processor.ProcessOrder(new Order());
}

Есть ещё крутые спецбары (контейнеры) для .NET: Autofac, Ninject, Simple Injector. Но в ASP.NET Core по умолчанию уже встроен свой, довольно шустрый бармен — Microsoft.Extensions.DependencyInjection. Его обычно хватает за глаза, если ты, конечно, не строишь какую-нибудь дичь с овердохуищем правил и условий.

А зачем это всё? Да чтобы не превращать точку входа в приложение в эту вот сраную фабрику по производству объектов, где ты сам, как обезьяна с гранатой, соединяешь провода. Контейнер делает это за тебя автоматически. Особенно кайфуешь, когда зависимостей не три, а тридцать три, и все друг в друге сидят. Вот тут-то и понимаешь, что контейнер — это не прихоть, а спасение от ебанины.