Практика 14. Практическая работа 14. Dependency Injection
Цель
- Научиться распознавать и устранять жёсткие зависимости (классы, создающие свои зависимости внутри).
- Освоить внедрение через конструктор (constructor injection) и зависимость от абстракций, а не от конкретных классов.
- Применить принцип инверсии зависимостей (SOLID-D) на практике, вынеся сборку объектов в composition root.
- Реализовать простой DI-контейнер с операциями регистрации и разрешения зависимостей.
- Убедиться, что слабая связанность даёт тестируемость: подмену реальных объектов моками без правки бизнес-логики.
Краткая теория
- Жёсткая зависимость. Класс, пишущий
self.repo = DatabaseRepo()внутри__init__, намертво привязан к реализации: её нельзя заменить и нельзя подставить мок в тесте. - Внедрение через конструктор. Зависимость передают снаружи:
def __init__(self, repo: Repository). Класс зависит от абстракции, а кто создаёт конкретный объект — забота вызывающего кода. - SOLID-D (инверсия зависимостей). Модули верхнего уровня не зависят от нижнего; оба зависят от абстракций. DI — практический механизм: стрелка зависимости «переворачивается» через интерфейс.
- Composition root. Единственная точка приложения, где конкретные реализации соединяются друг с другом; классы остаются «чистыми».
- DI-контейнер (IoC). Объект, которому регистрируют соответствие
«абстракция → реализация», а затем просят разрешить (
resolve) готовый объект со всеми вложенными зависимостями. Примеры — вlecture_14.md.
Задания
Задание 1. Устранение жёсткой зависимости
Дан «плохой» связанный код. Сервис сам создаёт свои зависимости и потому непригоден ни для замены реализации, ни для тестирования.
Исходный код для рефакторинга:
class ConsoleLogger: def log(self, msg: str) -> None: print(f"[LOG] {msg}")
class EmailSender: def send(self, to: str, text: str) -> None: print(f"[SMTP] -> {to}: {text}")
class NotificationService: def __init__(self): self.logger = ConsoleLogger() # жёстко зашито self.sender = EmailSender() # жёстко зашито
def notify(self, user_email: str, text: str) -> None: self.sender.send(user_email, text) self.logger.log(f"sent to {user_email}")Требования:
- Выделите абстракции
Logger(ABC)иSender(ABC)с методамиlog/send. - Перепишите
NotificationServiceтак, чтобыloggerиsenderвнедрялись через конструктор и были аннотированы абстрактными типами. - Сделайте две реализации
Sender:EmailSenderиSmsSender. - Соберите сервис в коде вызова (вне класса), подставив нужные реализации.
Проверьте: в NotificationService нет ни одного ()-создания зависимости;
смена EmailSender на SmsSender не требует правок внутри сервиса.
Задание 2. Зависимость от абстракции и подмена мока в тесте
Покажите главную выгоду DI — тестируемость без реальных побочных эффектов.
Требования:
- Реализуйте
FakeSender(Sender), который ничего не отправляет, а запоминает вызовы в списокself.sent: list[tuple[str, str]]. - Реализуйте
FakeLogger(Logger), накапливающий сообщения вself.records. - Напишите проверку (без поднятия реального SMTP): создайте
NotificationService(FakeLogger(), FakeSender()), вызовитеnotify(...)и убедитесь по атрибутам мока, что отправка и запись в лог произошли.
Скелет:
class FakeSender(Sender): def __init__(self): self.sent: list[tuple[str, str]] = []
def send(self, to: str, text: str) -> None: self.sent.append((to, text))
sender = FakeSender()service = NotificationService(FakeLogger(), sender)service.notify("a@b.c", "привет")assert sender.sent == [("a@b.c", "привет")]Проверьте: тест не выводит ничего по сети/в консоль; проверка идёт через состояние моков. Поясните в комментарии, почему это было невозможно в задании 1.
Задание 3. Инверсия зависимостей и composition root
Соберите многослойное приложение, в котором верхний слой не знает о конкретных реализациях нижнего.
Исходный код для рефакторинга:
class OrderService: def __init__(self): self.repo = InMemoryOrderRepo() # высокий уровень -> низкий self.notifier = NotificationService()
def place(self, order): self.repo.save(order) self.notifier.notify(order.email, "Заказ принят")Требования:
- Опишите абстракцию
OrderRepository(ABC)с методамиsave/find_by_id; сделайте реализациюInMemoryOrderRepo. - Перепишите
OrderServiceтак, чтобы он зависел только от абстракций (OrderRepository,NotificationServiceс внедрёнными зависимостями), получая их через конструктор. - Вынесите всю сборку (логгер, сендер, репозиторий, сервисы) в одну функцию
build_app()— это composition root. Нигде, кроме неё, не должно быть создания конкретных реализаций. - В комментарии покажите направление зависимостей «до» и «после» и поясните, почему стрелка инвертировалась.
Проверьте: OrderService импортирует/использует только абстракции; чтобы
переключить хранилище на другое, правится одна строка в build_app().
Задание 4. Простой DI-контейнер: регистрация и разрешение
Автоматизируйте сборку зависимостей вместо ручных вызовов конструкторов.
Требования:
- Реализуйте класс
Containerс методами:register_singleton(service_type, instance)— сохранить готовый объект;register(service_type, implementation)— связать абстракцию с классом;resolve(service_type)— вернуть объект, рекурсивно разрешив параметры его конструктора по аннотациям типов.
- В
resolveсначала проверяйте singletons, затем зарегистрированные реализации, иначе создавайте сам запрошенный тип. _createдолжен прочитатьinspect.signature(cls.__init__), для каждого параметра (кромеself) разрешить его по аннотации; если аннотации нет, но есть значение по умолчанию — взять его.
Скелет:
import inspect
class Container: def __init__(self): self._singletons: dict[type, object] = {} self._impls: dict[type, type] = {}
def register_singleton(self, service_type, instance): self._singletons[service_type] = instance
def register(self, service_type, implementation): self._impls[service_type] = implementation
def resolve(self, service_type): if service_type in self._singletons: return self._singletons[service_type] cls = self._impls.get(service_type, service_type) return self._create(cls)
def _create(self, cls): ... # разобрать сигнатуру __init__ и разрешить параметрыПроверьте: зарегистрировав Logger, Sender, OrderRepository и сервисы,
container.resolve(OrderService) возвращает полностью собранный объект; singleton
при двух resolve возвращается один и тот же (сравните по is).
Задание 5*. Управление временем жизни и фабрики (повышенной сложности)
- Добавьте в контейнер
register_factory(service_type, factory)— разрешение через переданную функцию-фабрику (например, чтобы создать объект с параметром из конфигурации). - Реализуйте режим
transient(новый объект на каждыйresolve) и сравните его поведение сsingleton. - Добавьте обнаружение циклической зависимости: если при разрешении тип
уже находится в процессе создания — выбросьте понятную ошибку
CircularDependencyErrorвместо бесконечной рекурсии.
Критерии оценки
| Критерий | Вес |
|---|---|
| Задание 1: выделены абстракции, зависимости внедрены через конструктор | 20% |
Задание 2: моки Fake*, проверка через состояние без побочных эффектов | 20% |
Задание 3: инверсия зависимостей, единый composition root (build_app) | 20% |
Задание 4: контейнер с register/resolve, рекурсивное разрешение, singleton | 20% |
| Задание 5*: фабрики, transient/singleton, обнаружение цикла | 10% |
Качество кода: имена, docstrings, аннотации типов, отсутствие () внутри классов | 10% |
Штрафы: создание зависимости внутри __init__ (self.x = Concrete()),
зависимость сервиса от конкретного класса вместо абстракции, сборка объектов
вне composition root, «глобальный» доступ к контейнеру из бизнес-логики.
Вопросы для самопроверки
- Какие три проблемы возникают у класса, который сам создаёт свои зависимости в конструкторе?
- Что значит «зависеть от абстракции, а не от реализации» и как это выглядит в
сигнатуре
__init__? - Как Dependency Injection реализует принцип инверсии зависимостей (SOLID-D)?
- Что такое composition root и почему сборку объектов выносят в одно место?
- Почему внедрение через конструктор обычно предпочтительнее внедрения через сеттер?
- Как DI делает код тестируемым? Приведите пример с моком из задания 2.
- Что означают этапы «регистрация» и «разрешение» в работе DI-контейнера?
- Как контейнер из задания 4 определяет, какие объекты подставить в конструктор?
- Чем отличаются режимы времени жизни
singletonиtransient? - Что такое циклическая зависимость и как контейнер может её обнаружить?