В Python web разработке есть стандартные подходы к архитектуре и дизайну кода, привнесённые распространёнными web-фреймворками, но они не давали нам достаточной простоты и гибкости. Поэтому мы решили попробовать другое: немного Domain-Driven Design, немного слоёв в архитектуре, немного (микро-) сервисов. В результате проекты стали проще для понимания, оптимизации и изменения. Теперь мы можем быстрее их начинать и быстрее добавлять новых разработчиков даже на последнем этапе (мы почти отменили закон Брукса!).
В докладе я хочу поделиться нашим опытом и практическими рекомендациями, которые мы сформировали для наших проектов.
Доклад, в основном, практический, может быть интересен как тимлидам и python-разработчикам, знакомым с вышеупомянутыми концепциями, но не использующим их на практике, так и видящим их впервые.
6. Как мы обычно делали
• SQLAlchemy
• HTTP API и/или MVC
• Pure Python или Django
• Deploy:
• Один WSGI App (несколько инстансов)
• БД (с репликами по необходимости)
• несколько long-run python процесов (aka workers)
6
7. • Просто начать.
• Подходит большинству
проектов.
• Знаком почти всем python
разработчикам.
• В большинстве случае
нормальная
производительность.
7
• Сложно добиться очень высокой
производительности.
• Поставка новых игровых фичей со
временем замедляется.
• Со временем повышается
вероятность внесения багов.
Что хорошо Что не очень
13. Может ORM?
• Проектирование БД мешает на этапе выявления сущностей и их
взаимодействия.
• При первоначальном кодировании сущностей могут возникнуть
вопросы к бизнесу, которые изменят требования.
• Может оказаться что БД не нужна, или нужна в другом виде.
16
15. User Story
Я как пользователь хочу зарегистрироваться в реферальной
программе чтобы иметь возможность приглашать других игроков.
18
16. 19
• Ожидаем ли пика регистрации? (хранить кланы у себя или ходить за ними?)
• Откуда может прийти запрос на регистрацию: знают ли они nickname игрока?
• Должен ли процесс регистрации быть транзакцией?
=> Идём задавать вопросы в бизнес и проекты-интеграции.
class AccountService:
def register_account(self, account_id: AccountID) -> Account:
clan = self._get_clan(account_id)
nickname = self._get_nickname(account_id)
if self._is_account_banned(account_id):
raise Exception('Account can not be created.')
account = Account(account_id, nickname, clan)
self._save_account(account)
return account
def _get_clan(self, account_id: AccountID) -> Optional[Clan]:
pass
25. 30
class BaseAsyncRepository(Generic[T, T_ID], BaseQueryBuilderMixin,
BaseSerializerMixin[T, T_ID]):
async def get_by_id(self, instance_id: T_ID) -> Optional[T]:
query, args = self._get_query_and_args(
self.get_by_id_query, self.instance_id_as_dict(instance_id)
)
result = await self._fetchone(query, *args)
if result is None:
return None
return self.get_instance(result)
async def insert(self, instance: T) -> int:
params = self.instance_to_dict(instance)
query, args = self._get_query_and_args(self.insert_query, params)
result = await self._execute(query, *args)
return self._get_row_count(result)
26. 31
class CachedSyncRepository(Generic[T, T_ID], BaseSyncRepository[T, T_ID]):
def insert(self, instance: T) -> ResultProxy:
result = super(CachedSyncRepository, self).insert(instance)
self._save_instance_in_cache(instance)
return result
def get_by_id(self, instance_id: T_ID) -> Optional[T]:
result = self._get_instance_from_cache(instance_id)
if result is not None:
return result
instance = super(CachedSyncRepository, self).get_by_id(instance_id)
if instance is None:
return None
self._save_instance_in_cache(instance)
return instance
29. Теперь нам нужен scheduler
• cron/at?
• celery?
• Написать свой?
34
Отложим выбор на потом
30. 35
class SchedulingTask:
def __init__(
self,
task_name: str,
must_complete_at: datetime = None,
task_id: str = None,
params: Dict = None,
) -> None:
self.task_name = task_name
self.task_id = task_id or uuid.uuid4()
self.params = params or {}
self.must_complete_at = must_complete_at
class SchedulingTaskRepository:
def insert_many(self, instances: List[SchedulingTask]) -> None:
for instance in instances:
self.insert(instance)
def insert(self, task: SchedulingTask) -> None:
pass
31. 36
class SchedulingTaskRepository:
def insert(self, task: SchedulingTask) -> None:
celery_app.send_task(task.task_name, [task.params], **task.celery_opts)
class SchedulingTask:
def __init__(
self,
task_name: str,
must_complete_at: datetime = None,
task_id: str = None,
params: Dict = None,
celery_opts: Dict = None,
) -> None:
self.task_name = task_name
self.task_id = task_id or uuid.uuid4()
self.params = params or {}
self.celery_opts = celery_opts or {}
self.celery_opts['task_id'] = self.task_id
if must_complete_at is not None:
self.celery_opts['eta'] = must_complete_at
32. • Если взаимодействие может быть ограничено CRUD запросами.
• Если объект можно легко сконструировать.
• Если абстракция репозитория не протекает.
37
Когда можно так делать
В остальных случаях используем класс-сервис.
37. User Story
Я как пользователь хочу зарегистрироваться в реферальной
программе чтобы иметь возможность приглашать других игроков.
42
38. 43
class AccountService:
def register_account(self, account_id: AccountID) -> Account:
clan = self._get_clan(account_id)
nickname = self._get_nickname(account_id)
if self._is_account_banned(account_id):
raise Exception('Account can not be created.')
account = Account(account_id, nickname, clan)
self.account_repo.insert(account)
self._create_referral_link(account)
return account
def _create_referral_link(self, account: Account) -> None:
if not account.can_be_recruiter:
return
link = ReferralLink(account.id)
self.referral_link_repo.insert(link)
39. 44
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
Account Context
Referral Link Context
Соблюдение TTL ссылки
40. 45
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
AccountService
ReferralLinkService
Соблюдение TTL ссылки
41. 46
class ReferralLinkService:
def get_or_create_referral_link(self, account_id: AccountID) -> ReferralLink:
existed_link = self.referral_link_repo.get_by_account_id(account_id)
if existed_link is not None:
return existed_link
link = ReferralLink(account_id)
self.referral_link_repo.insert(link)
return link
class AccountService:
def register_account(self, account_id: AccountID) -> Account:
# ...
if account.can_be_recruiter:
self.referral_link_service.get_or_create_referral_link(account.id)
return account
42. 47
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
AccountService
ReferralLinkService
Соблюдение TTL ссылки
Некоторый маппер
43. 48
class ReferralLinkService:
def get_or_create_referral_link(self, account: Account) -> ReferralLink:
if not account.can_be_recruiter:
raise Exception(‘Referral link can not be created.')
existed_link = self.referral_link_repo.get_by_account_id(account.id)
if existed_link is not None:
return existed_link
link = ReferralLink(account.id)
self.referral_link_repo.insert(link)
return link
Если что – потом отрефакторим. Система должна эволюционировать.
45. User Story
Я как игрок хочу иметь возможность приглашать рефералов
после достижения 1000 боёв.
50
46. 51
class AccountService:
def add_battle_to_account(self, account: Account,
battle: PlayerBattle) -> None:
account.add_battle(battle)
with db.transaction:
self.account_repo.update(account)
self._drop_caches_for_account(account)
if account.can_be_recruiter:
self.referral_link_service.get_or_create_referral_link(
account.id)
47. 52
Пример: контекст и переиспользование методов
class AccountService:
def is_banned(self, account_id: AccountID) -> bool:
for reason in self.excluded_account_repo.get_by_account_id(account_id):
if reason.is_ban:
return True
return False
• Требуется бОльшая производительность.
• Логика не сложная и покрыта тестами.
Когда использовать:
53. Что мы получили
• Оптимизация после performance теста проводится почти мгновенно и
без внесения багов.
• Высокая степень покрытия тестами при небольших затратах на
тестирование.
• При разработке игрового события средней сложности у нас теперь
уходит гораздо меньше ресурсов на бэкенде.
• Проще добавлять новых разработчиков.
• Проще bootstrap нового проекта.
61
56. Если добавляете новую фичу
• Определите единый язык (может помочь составление глоссария)!
• Найдите в каком контексте появляется новая фича.
• Найдите основные сущности и создайте pure python классы.
• Выделите бизнесовые связи между ними и найдите корень агрегата.
• Создайте класс Service с методами на каждую User Story/Use Case и
выделите методы, которые можно переиспользовать в проекте.
• Постепенно мигрируйте логику, связанную с этим контекстом, из всего
кода в этот Service.
64
57. Если нужно много и сложно общаться с базой
• Пересмотрите предметную область (помогает разделение
контекстов).
• Не стесняйтесь доставать из базы отдельные значения
(например,get_link_text_by_id, но только при необходимости).
• Ограничьте контекст где «программирование на SQLAlchemy»
необходимо.
65
58. В любом случае
• Найти единый язык (может помочь составление глоссария).
• Найти контексты и ограничить их проникновение друг в друга.
• Разделить ответственности по слоям (hexagon/layered architecture).
• Уменьшать когнитивную нагрузку при любом удобном случае.
• Рефакторинг должен быть непрерывным.
• Использовать type hinting!
• Следование стратегическому дизайну DDD — инструмент а не цель.
66
59. Domain Layer
• Pure Python (standard library, third-party libraries)
• Интерфейс класса использует единый язык, на котором говорят все в
проекте (бизнес, дизайнеры, реклама и т.д.)
• Язык можно менять, хорошо составить глоссарий.
• Забыть об инфраструктурной части (БД, очереди, протоколы).
67
60. Service Layer
• Сервис соответствует не агрегату, а некоторому контексту
использования, но не более одного агрегата на сервис.
• Интерфейсы методов должны быть полны и ограничены.
• Допустимо передавать агрегат из сервиса в сервис.
• Допустимо использовать один сервис из другого.
• Сервис не всегда связан с объектом предметной области, иногда он
может лежать в инфраструктурном слое.
• Сервисы должны подвергаться постоянному рефакторингу.
68
61. Infrastructure Layer
• Единый интерфейс, не требующий документации (методы get_by_…,
get_or_raise_by_…).
• Типизация параметров и возвращаемых значений.
• __init__ без параметров без явной необходимости.
• Не стесняться писать дополнительные методы при необходимости.
• По возможности оперирует целыми объектами предметной области а не
их частью (не возвращает специфичные словари и списки).
• Зависит только от объектов предметной области и сторонних библиотек.
69
63. Вопросы?
• Если кодовая база уже есть как переписать?
• Почему не микросервисы?
• Почему классы а не модули? Чем не подходит logic.py?
• Почему столько отклонений от DDD?
• Подходит ли это мне?
71
Здравствуйте, меня зовут Борис Цема, и, как говорит любимый персонаж моего любимого фильма, я решаю проблемы. В основном используя для этого язык Python.
Всем известный продукт Wargaming это World Of Tanks: многопользовательская онлайн игра, технически представляющая собой толстый клиент и сервер. Сражения на танках — это основной геймплей. Но кроме них игроку нужны ещё социализация, игровые события и мета-геймплей. Всё это в Wargaming создаётся отдельными проектами, которые делаются отдельными департаментами.
Так проекты выглядят для игрока
Так — для нас изнутри.
Проект в WG отвечает какой-то определённой цели: это может быть какая-нибудь долгоживущая внутренняя система (например поддержки пользователя или сбора и хранения бизнес-информации), это может быть некоторый новый мета-геймплей (УРы или Глобалка) или временное событие (Ивент для футбола в 2018 году, Хэллоуин). Каждый проект делается некоторой командой разработки (как правило, она собирается заново) и интегрируется с десятком-другим сервисов WG.
После того как собрали требования, мы обычно берём SQLAlchemy, последнюю из используемых в компании версий языка, определяем взаимодействие с фронтендом (HTTP API или MVC) и с интеграциями, и дальше делаем как-то так.
Есть много известных и опробованных десятилетиями способов, о которых вам расскажут коучи и консультанты.
После оценки производительности оказалось, что в худшем сценарии использования нам не хватит IOPS даже на запись, не говоря про изменения. Было не ясно справятся ли известные нам SQL решения (соседний проект решал похожую проблему в in-memory БД). Проект имел сложную предметную область, изначально не было уверенности в том, что её понимание останется прежним.
Высокий RPS, большой поток входящих на обработку сообщений.
Мы попытались охватить всё это у доски, но не получилось.
core, которое мы будем использовать — что туда класть? А если предметная область поменяется и это перестанет быть тем, что подходит в core?
ORM?
Было очевидно что понадобится шардинг, было не очевидно что не ошибёмся с ним изначально.
Нам точно нужен был asyncio, но весь проект не должен был быть асинхронным.
В результате performance в тестах превзошёл прогнозируемый в несколько раз.
Когда потребовалось расширить проект для следующего игрового события мы сделали это не напрягаясь в рекордные сроки.
После нескольких небольших ошибок в проде (где их нет) на новом проекте их действительно не было.
Много кода получилось не проектно-специфично, в результате в следующих новых проектах смогли переиспользовать большую часть кода.
Но я не консультант, я работал с этим какое-то время, и попробовав и адаптировав часть этих методик я сейчас расскажу как мы это делаем.
Для примера я буду использовать недавно вышедшую Реферальную программу 2.0. Суть её в следующем: игрок, соответствующий определённым критериям, может создать реферальную ссылку по которой пригласить других игроков, соответствующих другим критериям. За игру вдвоём игроки получают бонусы, реферальная программа разбита на этапы, за завершение этапов — награды, за завершение всей программы — награды.
Первым делом мы пытаемся понять предметную область и выделить основные её сущности.
В этом процессе часто участвуют та часть команды которая непосредственно не программирует. Её могут называть « бизнес-парни », « владельцы продукта » и ещё как-то. Там могут быть бизнес-аналитики, гейм-дизайнеры, UI/UX дизайнеры. Мы собираемся, смотрим требования, задаём вопросы, пытаемся понять что и как должно работать, внимательно слушаем.
В процессе таких встреч (их может быть несколько), в кабинетах всё чаще начинают повторяться специфичные для проекта слова: провинция, бой, очки реферальной программы, реферальная ссылка. Это очень важные слова. Их надо обязательно запомнить, осмыслить, и перевести в код.
Если в коде что-то не смотрится, всегда можно настоять на использовании других слов для какой-то части.
Существование на проекте единого языка между всеми его участниками — один из важнейших пунктов для успеха. Так обеспечивается скорость и однозначность понимания между бизнесом, тестировщиками и разработкой.
Ещё важный пункт: если вы в коде смоделировали классы и объекты максимально близко к реально произносимой предметной области, то и при изменении последней будет легче менять код. Поэтому важно следить чтобы основные свойства в коде выглядели так же, как их произносят « бизнес-люди ».
В коде это может выглядеть так.
Здесь мы забываем про всё лишнее и начинаем танцевать от предметной области. Обратите внимание: в кабинете звучит слово « количество боёв », в коде мы по необходимости храним какое-то представление этих боёв, но у класса Account есть свойство battles_count. Когда вы в требованиях встретите « у игрока должно быть 1000 боёв а ещё » и тут 20 пунктов, в коде вам будет проще видеть if account.battles_count > 1000 and … чем if len(account.arenas) > 1000.
Обычно на этом этапе сразу хочется определить таблицы в БД и создать модели в какой-нибудь SQLAlchemy.
Однако основная задача при выявлении предметной области — найти основные сущности, их составляющие и их взаимодействие. Вы ещё не очень в контексте проекта, он не очень удерживается в голове, причём не только у вас. Моделируя классы, выставляя ограничения вы выявляете граничные случаи и задаёте вопросы бизнесу, который, на самом деле, довольно крупными мазками рисует проект. На этом этапе могут вскрыться ситуации: а если игрок ещё не успел подтвердить процесс регистрации но уже пришёл к нам? Сколько максимально может быть рекрутов и нужно ли отображать какую-то информацию о них? Какое время жизни ссылки? От чего зависит? и так далее.
Если вы начинаете отвлекаться на « хранить в JSON или в Text», « как разбить на таблицы » то пропускаете этот важный этап. Когда дойдёте до очередного вопроса, бизнес скажет « а, точно, давай-ка лучше сделаем так », и вам опять надо перекраивать.
Поэтому проектировать БД на этом этапе не нужно. Потом вы начнёте реализовывать юзкейсы, взаимодействие между сущностями и фиксация на БД будет мешать, тем более что может оказаться что СУБД и не нужна а достаточно строчки в redis или статичного json файла.
В крайнем случае в SQLAlchemy можно сделать класс а потом смаппить.
Основная идея этого этапа — отложить максимум технических решений на потом. Это выглядит как прототип, который не будет выбрасываться.
В результате у нас появляется слой предметной области, реализующий основные сущности. Эти основные сущности где-то внутри себя содержат какие-то другие данные, нужные только им. Они их агрегируют. В терминах DDD такие сущности называют агрегатами, а весь этот слой мы называем domain layer.
После выделения существительных, давайте перейдём к глаголам. Прочитаем требования.
Действия над предметной областью будем реализовывать в классах-сервисах.
Что такое Сервис?
Класс, публичные методы которого полностью или частично реализуют описанные человеческим языком требования.
Ответственность класса — предметная логика, описанная в требовании, а не внутреннее состояние Account.
AccountService оркестрирует работу над Account.
Даже при таком простом действии начинают возникать вопросы. Эти вопросы начинают менять требования, и нам легко это сделать т.к. ещё немного написано и мы не потратились на создание схемы в БД.
Что у нас получилось? Класс, публичные методы которого реализуют user story. В нём мы держим всю логику, а вызывать этот метод можно через HTTP API, RPC (XMLRPC, zeromq), CLI и как угодно. Этот вызов не есть ответственность Service класса.
Класс Service оркестрирует работу над аккаунтом: куда сходить, когда сохранить.
При внимательном взгляде на получившийся код из собственного опыта можно предположить что какие-то методы точно ещё не раз понадобятся.
В данном случае вот этот метод может понадобится саппорту для разбора проблем у пользователей. При необходимости мы можем сделать его публичным и переиспользовать, как только где-то ещё в требовании появится проверка на забаненость.
Вообще, переиспользование — сильная сторона такого подхода.
Давайте ещё раз посмотрим на сервис аккаунта, а точнее на эту строчку.
Сейчас операция по хранению аккаунта находится в ведении AccountService, но лучше для этого использовать отдельный класс.
В DDD для классов, занимающихся постоянным хранением данных используется слово Factory, в других областях — Repository. Оставив обсуждения о терминологической чистоте мы на практике называем эти классы Repository.
Пусть вас не смущает похожее на SQL слово insert. На самом деле репозиторию совсем не обязательно использовать хоть какую-то БД вообще.
В работе мы пришли к идее об едином базовом CRUD интерфейсе для всех репозиториев.
Таким образом при разработке любой новой функциональности происходит минимум когнитивной нагрузки: просто найди в инстансе сервиса нужный репозиторий и используй.
Базовые CRUD методы довольно похожи и могут быть написаны однажды.
У нас есть базовые миксины для построения запросов,
Для нового репозитория допишите сериализацию и query методы.
Так это мы делаем для SQL репозиториев.
Похожий интерфейс но с другой реализацией используется для сохранения в файловую систему, например.
Это часть типичного миксина, у него есть ещё другие методы вроде exist_by_id и так далее.
Для сериализации и десериализации мы используем подобный миксин, большинство методов которого абстрактные.
И всё это объединяется в базовом репозитории.
Перед вами синхронный репозиторий.
…и асинхронный, использующий те же миксины для сериализации и десериализации. Для построения запросов мы используем SQLAlchemy, а для асинхронной работы с PostgreSQL — asyncpg, который принимает только raw sql, поэтому в асинхронном репозитории есть отдельный метод для перевода алхимии в постгресовский raw sql.
Это позволяет не зацикливаться на синхронном или асинхронном стиле программирования и для cpu-bound задач работать синхронно а при необходимости поддержать много RPS — асинхронно.
Что если после проверки производительности вы увидели что репозиторий слишком много времени проводит в ожидании объектов из БД? Мы можем сделать его кэширующим! При этом нет необходимости менять код приложения!
Аналогично для методов, изменяющих базу.
Вы всегда можете подправить базовые репозитории для повышения производительности или фикса багов.
Например, можно заставить некоторые репозитории ходить не в master базу а в реплику.
Можно кэшировать query и не тратить время на их составление.
Вообще, эти базовые репозитории являются удобной единой точкой оптимизации всего взаимодействия приложения со слоем хранения.
Обратите внимание, мы не называем это persistence layer, а infrastructure layer, и вот почему.
Давайте прочитаем ещё одно требование. Теперь нам нужен шедулер.
Все из них имеют свои плюсы и минусы.
Главный подход во всей разработке — откладывать решения на потом, когда информации и понимания будет больше.
Единый интерфейс репозиториев настолько привычен и прост, что в одном из проектов у нас появился SchedulerRepository, который создавал таски в celery, скрывая сложную логику обращения с ним.
Любой мог создать SchedulingTask и куда-то её сохранить.
На данном этапе мы можем ограничиться таким подходом, и продолжать работу.
Несмотря на некоторое смешение концепций, использование репозиториев в таком случае даёт мне, как разработчику, ожидание определённого интерфейса. То есть мне надо просто, как и с другими репозиториями, как-то создать объект и использовать один из привычных CRUD методов.
Надо ли говорить, что если (когда) мы захотим перестать использовать celery, код предметной области меняться не будет — главное будет сохранить интерфейс.
А в репозитории слегка изменим код.
Обратите внимание, что SchedulingTask не просочился в слой предметной области.
Теперь о приложении.
Теперь о приложении.
Слой приложения занимается только своей логикой и переиспользует бизнес-логику.
Добавим новый метод.
На данном этапе разработки мы с бизнесом думаем что достаточно просто сохранить ссылку, а потом в каком-нибудь интерфейсе рекрутёр её запросит.
Скорее всего, со временем нам поставят требования отправлять уведомление игроку, ограничивать количество ссылок, изменять время их жизни а так же делать предыдущие ссылки недействительными.
Давайте посмотрим на то, как можно сгруппировать операции по аккаунтам и ссылкам. Обратите внимание, что соблюдение GDPR и по ссылкам и по аккаунту будет отличаться.
Действия над аккаунтом формируют некий контекст аккаунта, над реферальной ссылкой же происходит в другом контексте. Давайте так и назовём эти овалы.
DDD называет это « ограниченный контекст ».
В нашем коде каждому контексту соответствует свой класс.
Вынесем код, создав новый сервис.
Обратите внимание: проверки делаются на стороне AccountService. На практике это может приводить к дополнительному коду, существующему только ради концептуальной чистоты.
DDD предписывает нам не передавать агрегат между контекстами, а использовать некоторый промежуточный слой. В данном случае он мог бы быть реализован в виде метода AccountService, вызывающего метод ReferralLinkService с определёнными параметрами.
Поэтому иногда мы делаем так, а потом рефакторим при необходимости.
Постоянный рефакторинг является неотъемлимой частью разработки. Предметная область может меняться, меняется язык. Постоянные небольшие изменения уменьшают количество техдолга, делают проект лучше и проще.
Мы стараемся не откладывать рефакторинг если видим его необходимость здесь и сейчас, поэтому можем себе позволить вот такой подход.
Какой-то внешний сервис читает бои, делает разные действия и среди прочих вызывает метод AccountService.
Смотрите, в какой-то момент аккаунт должен получить реферальную ссылку, и мы используем уже созданные методы. Эти методы, обладая ограниченным интерфейсом, позволяют вызывать себя из разных мест кода.
Про контекст использования: сервис может ответить на вопросы, в решении которых может оптимальнее быть не использовать агрегат, а сразу ходить в его внутренности.
Такое переиспользование возможно когда интерфейс является полным и ограниченным.
Мы стараемся передавать в методы полные объекты предметной области, а не их части. Это позволяет при необходимости что-то поменять в каком-то из этих методов не менять их потребителей, расширяя интерфейс.
По этой же причине методы стараются возвращать тоже полные объекты (а лучше — агрегаты).
Если из бизнес-требований нам кажется, что в какой-то uscase нужно делать транзакционно, то транзакцией управляет сервис. Здесь мы не знаем, что это за транзакция, db.transaction реализован в infrastructure layer.
Даже если кажется, что какая-то инфраструктурная работа сильно связана с агрегатом, её всё равно надо вынести в сервис, т.к. основная суть её именно инфраструктурная.
Иногда для оптимизации имеет смысл некоторые сервисы сделать синглтонами, это оправдано в случае использования ими сессий, пулов, ограничения на количество подключений с другой стороны.
Что нужно держать в голове при разработке:
надо поддерживать большую степень переиспользования кода. Для этого общаться между собой целыми объектами, выделять общие методы.
Интерфейсы должны быть чёткими и конкретными, без многочисленных параметров.
Ограниченный контекст позволяет быстро вникать, игнорируя бОльшую часть проекта.
Не должно быть повторяющегося кода, требующего специального знания об интеграции. Надо сделать сервис и пару методов с ясным ограниченным интерфейсом. Если вы где-то повторяете даже пару строчек, специфичных для какой-то технологии или сервиса — постарайтесь вынести это в отдельный метод.
Предположим такое простое действие как « пройтись по удалённым шардам и что-то оттуда забрать».
Один раз вам надо подумать как одновременно запустить несколько асинхронных запросов, как именно обработать ошибки и как вернуть.
Не факт что следующий разработчик будет это знать, и даже если будет, добавлять когнитивную нагрузку ему точно не стоит.
Мы можем находить узкие места и оптимизировать их, при этом зачастую они затрагивают ограниченную область приложения и не приходится делать всего регрессионного тестирования. В зависимости от глубины интеграции оптимизация может значительно улучшить всё приложение (тут рассказать про сжатие в redis).
Переиспользование позволило получить большую степень покрытия тестами и лёгкий способ менять требование.
Разбиение на интерфейсы позволяет легко поменять юзкейс, т.к. он выглядит как набор методов других сервисов или агрегатов. Почти как текст, написанный по-английски.
Новые разработчики поняв основную суть начинают работу в определённом контексте, не загружаясь всем приложением.
Некоторые сервисы сразу дают оттестированный код в новом проекте, сокращая bootstrap период.
А ещё, если захотим, мы сможем перейти на микросервисы, т.к. деплой сильно отделён от логики.
Укладывается ли она в существующий контекст? Или можно выделить новый? Если новый — то всё просто. Найдите основные сущности для проектирования, типа account в нашем примере, и следуйте написаному на слайде.
Если у вас сложный проект и нужно много общаться с базой (aka «программирование на SQLAlchemy»).
Пересмотрите предметную область. Как правило, после выделения контекстов многие операции сводятся к CRUD.
Не стесняйтесь доставать из базы куски значений. Но только при большой необходимости когда реально не хватает производительности. Помните, переиспользование методов сервисов и репозиториев увеличивает скорость разработки и поставки новых фичей. Три процента производительности могут не стоить денег на разработку.
Иногда может случиться что «программирование на алхимии» — вынужденная необходимость. В таком случае ограничьте контекст, где это происходит, и пусть там будет так. В остальных контекстах вы вполне можете упростить жизнь используя слои и сервисы.
Так мы можем определить правила для проектирования domain layer.
Правила проектирования Service Layer.
Правила проектирования Infrastructure Layer.
Так как после вчерашнего кому-то может быть сложно самому придумать вопрос, то я сделал это за вас)