Допустим, у нас есть такие классы:
Сейчас мы покажем несколько классов — логгер, репозиторий, сервис авторизации, сервис уведомлений и так далее. Они выглядят как настоящая мини-система, но вам не нужно глубоко вникать в их смысл. Нам важно не то, что именно они делают, а то, как они связаны друг с другом.
Логгер — просто пишет сообщения:
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) — запрос к контейнеру на создание полностью собранного объекта со всеми зависимостями.