13. Внедрение зависимостей (и начальная загрузка) (стр. 246-267)
Проблема: неявные зависимости
До сих пор мы управляли зависимостями по-разному:
UoW — явная зависимость (хорошо):
def allocate(cmd: Allocate, uow: AbstractUnitOfWork):
with uow:
...
# Тесты — легко подделать
uow = FakeUnitOfWork()
messagebus.handle(cmd, uow)
Email — неявная зависимость (плохо):
from allocation.adapters import email
def send_out_of_stock_notification(event, uow):
email.send('stock@made.com', f'Артикула {event.sku} нет в наличии')
# Тесты — mock.patch для каждого теста
with mock.patch("allocation.adapters.email.send"):
...
Проблемы неявных зависимостей:
mock.patchпривязывает к реализации (импорт, имя функции)- Нужно патчить в каждом тесте
- Рефакторинг ломает тесты
Решение: явные зависимости
def send_out_of_stock_notification(
event: OutOfStock,
send_mail: Callable # ← Явная зависимость!
):
send_mail('stock@made.com', f'Артикула {event.sku} нет в наличии')
Преимущества:
- Тестируемость — легко подменить в тестах
- Инверсия зависимостей — зависимость от абстракций
- Явное лучше неявного (Дзен Python)
Вопрос: кто будет создавать и передавать зависимости?
Подготовка обработчиков: 3 способа внедрения
Способ 1: Замыкания
# handlers.py
def allocate(cmd: Allocate, uow: AbstractUnitOfWork):
with uow:
...
def send_out_of_stock_notification(event, send_mail: Callable):
send_mail('stock@made.com', f'Артикула {event.sku} нет в наличии')
# bootstrap.py
def bootstrap():
uow = SqlAlchemyUnitOfWork()
# Замыкание захватывает uow
def allocate_composed(cmd):
return allocate(cmd, uow)
# Замыкание захватывает send_mail
def sosn_composed(event):
return send_out_of_stock_notification(event, email.send)
return allocate_composed, sosn_composed
Способ 2: functools.partial
import functools
def bootstrap():
uow = SqlAlchemyUnitOfWork()
allocate_composed = functools.partial(allocate, uow=uow)
sosn_composed = functools.partial(
send_out_of_stock_notification,
send_mail=email.send
)
return allocate_composed, sosn_composed
Способ 3: Классы
# handlers.py
class AllocateHandler:
def __init__(self, uow: AbstractUnitOfWork):
self.uow = uow
def __call__(self, cmd: Allocate):
with self.uow:
# Логика обработчика
...
# bootstrap.py
uow = SqlAlchemyUnitOfWork()
allocate = AllocateHandler(uow) # ← Внедрение зависимости!
# Позже
allocate(cmd) # ← Вызов без передачи зависимостей
Сценарий начальной загрузки (Bootstrap)
Задачи bootstrap:
- Объявляет зависимости по умолчанию
- Выполняет инициализацию (ORM, logging)
- Внедряет зависимости в обработчики
- Возвращает шину сообщений
Реализация:
# bootstrap.py
import inspect
from functools import partial
def bootstrap(
start_orm: bool = True,
uow: AbstractUnitOfWork = SqlAlchemyUnitOfWork(),
send_mail: Callable = email.send,
publish: Callable = redis_eventpublisher.publish,
) -> MessageBus:
if start_orm:
orm.start_mappers()
# Зависимости
dependencies = {
'uow': uow,
'send_mail': send_mail,
'publish': publish
}
# Внедряем зависимости в обработчики событий
injected_event_handlers = {
event_type: [
inject_dependencies(handler, dependencies)
for handler in event_handlers
]
for event_type, event_handlers in handlers.EVENT_HANDLERS.items()
}
# Внедряем зависимости в обработчики команд
injected_command_handlers = {
command_type: inject_dependencies(handler, dependencies)
for command_type, handler in handlers.COMMAND_HANDLERS.items()
}
return MessageBus(
uow=uow,
event_handlers=injected_event_handlers,
command_handlers=injected_command_handlers,
)
def inject_dependencies(handler, dependencies):
# Проверяем сигнатуру функции
params = inspect.signature(handler).parameters
# Находим совпадающие зависимости
deps = {
name: dependency
for name, dependency in dependencies.items()
if name in params
}
# Возвращаем частично применённую функцию
return lambda message: handler(message, **deps)
«Ручное» внедрение (альтернатива без inspect)
Если inspect() кажется сложным:
def bootstrap(uow, send_mail, publish):
injected_event_handlers = {
events.Allocated: [
lambda e: handlers.publish_allocated_event(e, publish),
lambda e: handlers.add_allocation_to_read_model(e, uow),
],
events.Deallocated: [
lambda e: handlers.remove_allocation_from_read_model(e, uow),
lambda e: handlers.reallocate(e, uow),
],
events.OutOfStock: [
lambda e: handlers.send_out_of_stock_notification(e, send_mail)
]
}
injected_command_handlers = {
commands.Allocate: lambda c: handlers.allocate(c, uow),
commands.CreateBatch: lambda c: handlers.add_batch(c, uow),
commands.ChangeBatchQuantity: lambda c: handlers.change_batch_quantity(c, uow),
}
return MessageBus(
uow=uow,
event_handlers=injected_event_handlers,
command_handlers=injected_command_handlers,
)
Преимущество: проще понять, нет «магии» с inspect().
Использование bootstrap в точках входа
Flask:
# flask_app.py
from allocation import bootstrap
bus = bootstrap.bootstrap()
@app.route('/allocate', methods=['POST'])
def allocate_endpoint():
cmd = commands.Allocate(
request.json['orderid'],
request.json['sku'],
request.json['qty']
)
bus.handle(cmd)
return '', 202
Redis Consumer:
from allocation import bootstrap
bus = bootstrap.bootstrap()
def main():
pubsub = r.pubsub()
pubsub.subscribe('change_batch_quantity')
for m in pubsub.listen():
cmd = commands.ChangeBatchQuantity(...)
bus.handle(cmd)
Внедрение зависимостей в тестах
Тестовый bootstrap:
# tests/conftest.py
def bootstrap_for_tests():
return bootstrap.bootstrap(
start_orm=False, # Не запускаем ORM
uow=FakeUnitOfWork(), # Поддельный UoW
send_mail=FakeEmailSender(), # Поддельный email
publish=lambda *args: None, # Никакого Redis
)
# tests/unit/test_handlers.py
def test_allocate():
bus = bootstrap_for_tests()
bus.handle(commands.CreateBatch('b1', 'SKU', 100, None))
bus.handle(commands.Allocate('order1', 'SKU', 10))
assert bus.uow.committed
«Правильное» создание адаптера
Проблема: как создать реальный email-адаптер?
Решение: фабрика адаптеров в bootstrap:
# adapters/email.py
class EmailSender:
def __init__(self, smtp_host, smtp_port):
self.smtp = smtplib.SMTP(smtp_host, smtp_port)
def send(self, to, subject):
self.smtp.send_message(...)
# bootstrap.py
def make_email_sender():
return EmailSender(
smtp_host=os.environ.get('SMTP_HOST', 'localhost'),
smtp_port=int(os.environ.get('SMTP_PORT', 25))
)
def bootstrap(...):
send_mail = make_email_sender()
...
В тестах:
def test_bootstrap():
bus = bootstrap.bootstrap(
send_mail=FakeEmailSender() # ← Подмена!
)
Выводы
- Явные зависимости лучше неявных — тестируемость, рефакторинг
- Bootstrap — единое место для инициализации и внедрения
- Три способа внедрения: замыкания,
partial, классы - Inspect — автоматическое внедрение по имени параметра
- Тесты — подменяем зависимости в bootstrap
Вопросы
- Зачем нужны явные зависимости вместо импортов?
- Что делает функция
bootstrap()? - Какие есть способы внедрения зависимостей (3 способа)?
- Как упростить тестирование с помощью bootstrap?
- Что такое Composition Root?
