Допустим, у нас есть такие классы:

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

Логгер — просто пишет сообщения:

public class ConsoleLogger : ILogger
{
    public void Log(string message)
    {
        Console.WriteLine($"[LOG] {message}");
    }
}

Репозиторий пользователей — хранит/создаёт пользователей и пишет логи через ILogger:

public interface IUserRepository
{
    void CreateUser(string name);
}

public class UserRepository : IUserRepository
{
    private readonly ILogger _logger;

    public UserRepository(ILogger logger)
    {
        _logger = logger;
    }

    public void CreateUser(string name)
    {
        _logger.Log($"Создан пользователь: {name}");
    }
}

Сервис авторизации — использует репозиторий и логгер для входа пользователей:

public class AuthService
{
    private readonly ILogger _logger;
    private readonly IUserRepository _users;

    public AuthService(ILogger logger, IUserRepository users)
    {
        _logger = logger;
        _users = users;
    }

    public void Login(string name)
    {
        _logger.Log($"Пользователь {name} вошёл в систему");
    }
}

Сервис уведомлений — отправляет уведомление и пишет в лог:

public class NotificationService
{
    private readonly ILogger _logger;

    public NotificationService(ILogger logger)
    {
        _logger = logger;
    }

    public void Notify(string message)
    {
        _logger.Log($"Уведомление отправлено: {message}");
    }
}

Сервис заказов — создаёт заказ, пишет лог и отправляет уведомление:

public class OrderService
{
    private readonly ILogger _logger;
    private readonly NotificationService _notifications;

    public OrderService(ILogger logger, NotificationService notifications)
    {
        _logger = logger;
        _notifications = notifications;
    }

    public void CreateOrder(string user, string item)
    {
        _logger.Log($"Создан заказ: {item} для {user}");
        _notifications.Notify($"Ваш заказ '{item}' создан!");
    }
}

Всё красиво: каждая зависимость передаётся извне. DI соблюдён. Теперь посмотрим на точку входа:

var logger = new ConsoleLogger();

var userRepo = new UserRepository(logger);
var auth = new AuthService(logger, userRepo);

var notifications = new NotificationService(logger);

var orderService = new OrderService(logger, notifications);

И вроде бы ничего страшного... Но попробуй представить, что таких сервисов уже 20–30, и каждый зависит от трёх других. Тогда Program.cs превращается в огромную простыню из new. Каждая новая зависимость требует лезть в точку входа и переписывать код.

И вот на этом моменте у разработчиков возникает очень естественная мысль:

«Почему программа сама не может понять, какие зависимости кому нужны, и собрать эту цепочку автоматически?»

Так и появилась идея контейнера зависимостей — специального механизма, который отвечает за создание и связывание объектов друг с другом.

Представьте: раньше вы вручную создавали логгер, репозиторий, сервисы, передавали их в конструкторы. Контейнер говорит:

«Давай я сам буду создавать твои зависимости. Ты просто скажи мне, какие классы существуют и что кому нужно».

В реальных проектах вы почти никогда не будете писать контейнеры вручную — для этого существуют готовые решения. Но чтобы понять, как они работают, полезно увидеть простейшую реализацию:

public class SimpleContainer
{
    private readonly Dictionary<Type, Type> _registrations = new();
    private readonly Dictionary<Type, object> _instances = new();

    public void Register<TService, TImplementation>()
        where TImplementation : TService
    {
        _registrations[typeof(TService)] = typeof(TImplementation);
    }

    public TService Resolve<TService>()
    {
        return (TService)Resolve(typeof(TService));
    }

    private object Resolve(Type serviceType)
    {
        if (_instances.TryGetValue(serviceType, out var readyInstance))
            return readyInstance;

        if (!_registrations.TryGetValue(serviceType, out var implementationType))
        {
            if (!serviceType.IsAbstract)
                implementationType = serviceType;
            else
                throw new InvalidOperationException($"Тип {serviceType.Name} не зарегистрирован");
        }

        var ctor = implementationType.GetConstructors().First();
        var ctorParams = ctor.GetParameters();
        var args = new object[ctorParams.Length];

        for (int i = 0; i < ctorParams.Length; i++)
        {
            var paramType = ctorParams[i].ParameterType;
            args[i] = Resolve(paramType);
        }

        var instance = Activator.CreateInstance(implementationType, args)!;
        _instances[serviceType] = instance;

        return instance;
    }
}

Теперь достаточно просто зарегистрировать классы, и контейнер сам соберёт всю цепочку зависимостей за вас:

var container = new SimpleContainer();

container.Register<ILogger, ConsoleLogger>();
container.Register<IUserRepository, UserRepository>();
container.Register<AuthService, AuthService>();
container.Register<NotificationService, NotificationService>();
container.Register<OrderService, OrderService>();

var orderService = container.Resolve<OrderService>();

Акцентируем ваше внимание: раньше вам буквально нужно было вручную создавать каждый объект и передавать его в конструктор следующего. Мы писали целые цепочки из new, следили за порядком зависимостей.

Теперь мы просто говорим контейнеру: «Вот у нас есть такой сервис, вот его реализация — запомни». Это и называется регистрацией.

И контейнер сам: находит, что нужно передать в конструктор, создаёт все недостающие зависимости, подставляет их в правильном порядке, кэширует экземпляры, если надо, и в итоге отдаёт вам полностью собранный готовый объект.

Именно поэтому DI-контейнеры так популярны: они убирают самый скучный и рутинный пласт работы, оставляя вам только логику приложения.

DI-контейнер — специальный механизм, который берёт на себя создание объектов и связывание их зависимостей. Вместо ручной сборки через цепочки new вы регистрируете сервисы, а контейнер сам подставляет нужные реализации.

Регистрация — процесс, при котором мы сообщаем контейнеру, какие классы существуют и какие реализации нужно использовать.

Разрешение (Resolve) — запрос к контейнеру на создание полностью собранного объекта со всеми зависимостями.

Обсуждение урока

0
Комментарии видны всем. Чтобы участвовать в обсуждении, войдите или зарегистрируйтесь.
Модерация сообщества

Пожаловаться на комментарий

Расскажите модераторам, что именно требует внимания.