Внедрение зависимостей (DI)
Внедрение зависимостей: почему классы не должны создавать свои зависимости сами.
Суть: Dependency Injection (DI) — это приём, при котором объект получает свои зависимости извне, а не создаёт их сам через
new. ASP.NET Core имеет встроенный DI-контейнер, который собирает граф объектов за вас.
Представьте контроллер, которому нужен сервис для работы с пользователями. Без DI он бы написал new UserService(new AppDbContext(...)) — жёстко связав себя с конкретными классами. Поменять реализацию или подсунуть заглушку в тесте станет невозможно. DI переворачивает это: контроллер просто просит зависимость, а контейнер её предоставляет.
Конструкторное внедрение
public interface IUserService
{
Task<User?> GetAsync(int id);
}
public class UserService : IUserService
{
private readonly AppDbContext _db;
public UserService(AppDbContext db) => _db = db; // зависимость извне
public Task<User?> GetAsync(int id) => _db.Users.FindAsync(id).AsTask();
}
public class UsersController : ControllerBase
{
private readonly IUserService _service;
public UsersController(IUserService service) => _service = service; // DI
}
Контроллер просит IUserService в конструкторе. Контейнер видит это, создаёт UserService, тому нужен AppDbContext — контейнер создаёт и его. Так выстраивается весь граф.
Регистрация
builder.Services.AddScoped<IUserService, UserService>();
Эта строка говорит: «когда кто-то просит IUserService, дай UserService». Связь интерфейс в реализацию задаётся в одном месте.
Как работает под капотом
DI-контейнер — это, по сути, словарь «тип в способ его создания». При запросе типа он смотрит регистрацию, рекурсивно создаёт все зависимости конструктора и возвращает готовый объект. Это инверсия контроля: не ваш код управляет созданием, а контейнер. Промоделируем простейший контейнер на Python.
# Мини DI-контейнер: регистрация и разрешение зависимостей
class Container:
def __init__(self):
self._registry = {}
def register(self, key, factory):
self._registry[key] = factory
def resolve(self, key):
factory = self._registry[key]
return factory(self) # фабрике передаём контейнер для под-зависимостей
c = Container()
c.register("db", lambda _: {"conn": "postgres"})
c.register("user_service", lambda cont: {"db": cont.resolve("db")})
c.register("controller", lambda cont: {"service": cont.resolve("user_service")})
ctrl = c.resolve("controller") # контейнер собрал весь граф
print(ctrl)
Попробуй сам ▶ — запросив «controller», контейнер сам разрешил «user_service», а тот — «db». Это и есть инверсия контроля.
Частые ошибки
- Создавать зависимости через
newвнутри класса. Это убивает тестируемость и гибкость — просите их через конструктор. - Забыть зарегистрировать сервис. Тогда при запросе будет исключение «не удалось разрешить».
- Service Locator-антипаттерн. Тянуть зависимости вручную из контейнера в коде вместо конструктора — прячет зависимости.
Best practices
- Зависьте от интерфейсов (
IUserService), а не от конкретных классов — легче подменять и тестировать. - Используйте конструкторное внедрение как основной способ.
- Держите конструкторы тонкими: много зависимостей — сигнал, что класс делает слишком много.
Инверсия контроля и почему это меняет дизайн
DI — частный случай более широкого принципа инверсии управления (IoC): не ваш код решает, когда и как создавать зависимости, это решает контейнер. Связан DI и с принципом инверсии зависимостей (буква D в SOLID): модули верхнего уровня зависят не от конкретных реализаций, а от абстракций (интерфейсов). Контроллер зависит от IUserService, а какая именно реализация подставится — настоящая, кэширующая или тестовый мок — решается в одном месте регистрации.
Практический выигрыш виден в тестах. Чтобы проверить контроллер, не нужна реальная БД: достаточно подставить заглушку IUserService, возвращающую заранее заданные данные. Без DI, с new внутри, это было бы невозможно — зависимость намертво вшита. Так DI напрямую конвертируется в тестируемость, а тестируемость — в скорость и уверенность при изменениях.
Способы внедрения и анти-паттерны
Основной и рекомендуемый способ — конструкторное внедрение: зависимости приходят параметрами конструктора, объект после создания всегда в рабочем состоянии, а список зависимостей виден прямо в сигнатуре. Есть и внедрение через метод/свойство, но их применяют редко и осознанно. Полезное наблюдение: если конструктор разросся до 7-8 зависимостей, это запах — класс, вероятно, делает слишком много и его стоит разбить.
Главный анти-паттерн вокруг DI — Service Locator: когда вместо честного запроса зависимостей в конструкторе код сам лезет в контейнер (provider.GetService<T>()) посреди логики. Так зависимости становятся скрытыми — по сигнатуре класса не видно, что ему на самом деле нужно, и тесты усложняются. Запускаемая врезка выше показала суть контейнера: словарь «ключ → фабрика» с рекурсивным разрешением. Реальный контейнер ASP.NET Core делает то же, но с учётом времён жизни — а это тема следующего урока.
Итог: DI отдаёт создание зависимостей контейнеру, делая код гибким и тестируемым. Дальше — ключевая тема: времена жизни сервисов.