SlideShare una empresa de Scribd logo
1 de 64
— Мой учитель
«Перед презентацией проверь, работает ли переключатель слайдов»
Борис Цема
Game Services
Wargaming
Как мы уменьшили сложность наших
проектов
3
Кадр из к/ф «Криминальное чтиво»
4
5
Команда
разработки
Проект WG
XMLRPC
Проект WG
Kafka
Проект WG
HTTP + AMQP
Проект WG
HTTP + AMQP
Проект WG
Проект WG
Проект WG
10-20 интеграций
Различные транспорты
Разные протоколы общения
Проект WG
SQL
JIRA
Как мы обычно делали
• SQLAlchemy
• HTTP API и/или MVC
• Pure Python или Django
• Deploy:
• Один WSGI App (несколько инстансов)
• БД (с репликами по необходимости)
• несколько long-run python процесов (aka workers)
6
• Просто начать.
• Подходит большинству
проектов.
• Знаком почти всем python
разработчикам.
• В большинстве случае
нормальная
производительность.
7
• Сложно добиться очень высокой
производительности.
• Поставка новых игровых фичей со
временем замедляется.
• Со временем повышается
вероятность внесения багов.
Что хорошо Что не очень
8
Как сделать лучше
https://martinfowler.com/bliki/PresentationDomainDataLayering.html
http://confluent.io
Wikipedia
Как мы теперь это делаем
Мы пишем код
Реферальная программа 2.0
13
…
14
• id
• nickname
• clan_tag
• clan_color
• is_banned
• recruits_total
• battles_count
• …
• id
• account_id
• text
• expiration_date
• …
• recruiter_id
• recruit_id
• points
• status
• …
Account Referral Link Relation
Определение предметной области
15class Account:
def __init__(
self,
id_: AccountID,
nickname: str,
clan: Clan,
exclusion_reasons: List[ExcludedAccount],
recruits_total: int,
arenas: Collection[Arena],
) -> None:
assert isinstance(clan, Clan), type(clan)
self.clan = clan
self._arenas = arenas
# More init code here...
@property
def battles_count(self) -> int:
return len(self._arenas)
@property
def clan_tag(self) -> str:
return self.clan.tag
Может ORM?
• Проектирование БД мешает на этапе выявления сущностей и их
взаимодействия.
• При первоначальном кодировании сущностей могут возникнуть
вопросы к бизнесу, которые изменят требования.
• Может оказаться что БД не нужна, или нужна в другом виде.
16
17
Account
Clan
ExcludedAccount
ArenasDomain Layer
User Story
Я как пользователь хочу зарегистрироваться в реферальной
программе чтобы иметь возможность приглашать других игроков.
18
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
22
AccountServiceService Layer
Account
Clan
ExcludedAccount
ArenasDomain Layer
23
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
24
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)
return account
25
class AccountRepository:
def get_by_id(self, instance_id: AccountID) -> Optional[Account]:
pass
def get_or_raise_by_id(self, instance_id: AccountID) -> Account:
pass
def insert(self, instance: Account) -> None:
pass
def update(self, instance: Account) -> None:
pass
def delete(self, instance: Account) -> None:
pass
26
class AccountService:
def __init__(self) -> None:
self.account_repo = AccountRepository()
27
class BaseQueryBuilderMixin:
@property
def table(self) -> Table:
raise NotImplementedError()
@property
def id_query(self) -> BinaryExpression:
return self.table.c.id == bindparam('instance_id')
@property
def get_by_id_query(self) -> Select:
return self.get_all_query().where(self.id_query)
@property
def get_by_id_for_update_query(self) -> Select:
return self.get_all_query().with_for_update(key_share=True).where(
self.id_query)
@property
def delete_by_id_query(self) -> Delete:
return self.table.delete().where(self.id_query)
28
class BaseSerializerMixin(Generic[T, T_ID]):
def get_instance_id(self, instance: T) -> T_ID:
raise NotImplementedError()
def get_instances(self, records: List[RowProxy]) -> List[T]:
return list(map(self.get_instance, records))
def get_instance(self, record: RowProxy) -> T:
raise NotImplementedError()
def instance_to_dict(self, instance: T) -> Dict:
serializer = getattr(instance, 'to_dict', None)
if callable(serializer):
return serializer()
raise NotImplementedError()
def instance_id_as_dict(self, instance_id: T_ID) -> Dict[str, Any]:
return {'instance_id': instance_id}
29
class BaseSyncRepository(Generic[T, T_ID], BaseQueryBuilderMixin,
BaseSerializerMixin[T, T_ID]):
def insert(self, instance: T) -> ResultProxy:
query = self.insert_query
params = self.instance_to_dict(instance)
return self._execute(query, **params)
def exists_by_id(self, instance_id: T_ID) -> bool:
query = exists().where(self.id_query).select()
return self._exists(query, **self.instance_id_as_dict(instance_id))
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)
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
32
AccountServiceService Layer
AccountRepositoryInfrastructure Layer
Account
Clan
ExcludedAccount
ArenasDomain Layer
Требование
Через некоторый срок неактивности аккаунт должен быть удалён из
системы, с предупреждением игрока за месяц.
33
Теперь нам нужен scheduler
• cron/at?
• celery?
• Написать свой?
34
Отложим выбор на потом
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
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
• Если взаимодействие может быть ограничено CRUD запросами.
• Если объект можно легко сконструировать.
• Если абстракция репозитория не протекает.
37
Когда можно так делать
В остальных случаях используем класс-сервис.
38
AccountServiceService Layer
AccountRepository SchedulerRepositoryInfrastructure Layer
Account
Clan
ExcludedAccount
ArenasDomain Layer
Endpoints: HTTP API
39
class AccountCreatingResource(BaseFalconResource):
def __init__(self, service: AccountService) -> None:
self.service = service
def on_post(self, req: falcon.Request, resp: falcon.Response) -> None:
account_id = req.get_param_as_int('account_id', required=True)
self.service.register_account(account_id)
resp.status_code = falcon.HTTP_201
Endpoints: CLI
40
@click.command('account')
def account_command():
pass
@account_command.command('register')
@click.option('-i', '--id', 'id_', type=int, required=True,
help='Account id to create.')
def register_account(id_: int) -> None:
service = AccountService()
existed_account = service.get_account(id_)
if existed_account is None:
service.register_account(id_)
logger.info('Account was successfully created.')
else:
logger.error('Account already exist.')
sys.exit(1)
41
AccountServiceService Layer
AccountRepository SchedulerRepositoryInfrastructure Layer
Account
Clan
ExcludedAccount
ArenasDomain Layer
Application Layer HTTP API CLI RPC MQ Listener
User Story
Я как пользователь хочу зарегистрироваться в реферальной
программе чтобы иметь возможность приглашать других игроков.
42
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)
44
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
Account Context
Referral Link Context
Соблюдение TTL ссылки
45
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
AccountService
ReferralLinkService
Соблюдение TTL ссылки
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
47
Регистрация аккаунта и CRUD операции
Регистрация ссылки
Уведомления об операциях с ссылками
Блокировка ссылок
Блокировка аккаунта
Соблюдение GDPR
Соблюдение GDPR
AccountService
ReferralLinkService
Соблюдение TTL ссылки
Некоторый маппер
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
Если что – потом отрефакторим. Система должна эволюционировать.
49
AccountServiceReferralLinkServiceService Layer
AccountRepositoryReferralLinkRepository SchedulerRepositoryInfrastructure Layer
Account
Clan
ExcludedAccount
ArenasReferralLinkDomain Layer
Application Layer HTTP API CLI RPC MQ Listener
User Story
Я как игрок хочу иметь возможность приглашать рефералов
после достижения 1000 боёв.
50
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)
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
def register_account(self, account_id: AccountID, force: bool = False,
for_link: ReferralLink = None) -> Account:
Пример: ограниченные интерфейсы
def register_account(self, account_id: AccountID) -> Account:
def register_account_by_referral_link(self, account_id: AccountID,
link: ReferralLink) -> Account:
def register_account_if_satisfied(
self, account_id: AccountID, specification: BaseSpecification
) -> Account:
54
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)
55
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)
56
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)
CachingService.instance.drop_caches_for_account(account)
if account.can_be_recruiter:
self.referral_link_service.get_or_create_referral_link(
account.id)
60
Что мы получили
• Оптимизация после performance теста проводится почти мгновенно и
без внесения багов.
• Высокая степень покрытия тестами при небольших затратах на
тестирование.
• При разработке игрового события средней сложности у нас теперь
уходит гораздо меньше ресурсов на бэкенде.
• Проще добавлять новых разработчиков.
• Проще bootstrap нового проекта.
61
62
Всё стало проще
Как внедрять
63
Если добавляете новую фичу
• Определите единый язык (может помочь составление глоссария)!
• Найдите в каком контексте появляется новая фича.
• Найдите основные сущности и создайте pure python классы.
• Выделите бизнесовые связи между ними и найдите корень агрегата.
• Создайте класс Service с методами на каждую User Story/Use Case и
выделите методы, которые можно переиспользовать в проекте.
• Постепенно мигрируйте логику, связанную с этим контекстом, из всего
кода в этот Service.
64
Если нужно много и сложно общаться с базой
• Пересмотрите предметную область (помогает разделение
контекстов).
• Не стесняйтесь доставать из базы отдельные значения
(например,get_link_text_by_id, но только при необходимости).
• Ограничьте контекст где «программирование на SQLAlchemy»
необходимо.
65
В любом случае
• Найти единый язык (может помочь составление глоссария).
• Найти контексты и ограничить их проникновение друг в друга.
• Разделить ответственности по слоям (hexagon/layered architecture).
• Уменьшать когнитивную нагрузку при любом удобном случае.
• Рефакторинг должен быть непрерывным.
• Использовать type hinting!
• Следование стратегическому дизайну DDD — инструмент а не цель.
66
Domain Layer
• Pure Python (standard library, third-party libraries)
• Интерфейс класса использует единый язык, на котором говорят все в
проекте (бизнес, дизайнеры, реклама и т.д.)
• Язык можно менять, хорошо составить глоссарий.
• Забыть об инфраструктурной части (БД, очереди, протоколы).
67
Service Layer
• Сервис соответствует не агрегату, а некоторому контексту
использования, но не более одного агрегата на сервис.
• Интерфейсы методов должны быть полны и ограничены.
• Допустимо передавать агрегат из сервиса в сервис.
• Допустимо использовать один сервис из другого.
• Сервис не всегда связан с объектом предметной области, иногда он
может лежать в инфраструктурном слое.
• Сервисы должны подвергаться постоянному рефакторингу.
68
Infrastructure Layer
• Единый интерфейс, не требующий документации (методы get_by_…,
get_or_raise_by_…).
• Типизация параметров и возвращаемых значений.
• __init__ без параметров без явной необходимости.
• Не стесняться писать дополнительные методы при необходимости.
• По возможности оперирует целыми объектами предметной области а не
их частью (не возвращает специфичные словари и списки).
• Зависит только от объектов предметной области и сторонних библиотек.
69
70
AccountServiceReferralLinkServiceService Layer
AccountRepositoryReferralLinkRepository SchedulerRepositoryInfrastructure Layer
Account
Clan
ExcludedAccount
ArenasReferralLinkDomain Layer
Application Layer HTTP API CLI RPC MQ Listener
Вопросы?
• Если кодовая база уже есть как переписать?
• Почему не микросервисы?
• Почему классы а не модули? Чем не подходит logic.py?
• Почему столько отклонений от DDD?
• Подходит ли это мне?
71
Борис Цема / Boris Tsema
boris@tsema.ru

Más contenido relacionado

Similar a Как мы уменьшили сложность наших проектов

Web осень 2013 лекция 8
Web осень 2013 лекция 8Web осень 2013 лекция 8
Web осень 2013 лекция 8
Technopark
 
10 задач администрирования Active directory, решаемых с помощью power shell
10 задач администрирования Active directory, решаемых с помощью power shell10 задач администрирования Active directory, решаемых с помощью power shell
10 задач администрирования Active directory, решаемых с помощью power shell
Andrey Markin
 
Web осень 2013 лекция 6
Web осень 2013 лекция 6Web осень 2013 лекция 6
Web осень 2013 лекция 6
Technopark
 
Успешный Open Source - Андрей Светлов, PyCon RU 2014
Успешный Open Source - Андрей Светлов, PyCon RU 2014Успешный Open Source - Андрей Светлов, PyCon RU 2014
Успешный Open Source - Андрей Светлов, PyCon RU 2014
it-people
 
Moscow Python Conf 2016. Почему 100% покрытие это плохо?
Moscow Python Conf 2016. Почему 100% покрытие это плохо?Moscow Python Conf 2016. Почему 100% покрытие это плохо?
Moscow Python Conf 2016. Почему 100% покрытие это плохо?
Ivan Tsyganov
 
Easy authcache 2 кеширование для pro родионов игорь
Easy authcache 2   кеширование для pro родионов игорьEasy authcache 2   кеширование для pro родионов игорь
Easy authcache 2 кеширование для pro родионов игорь
drupalconf
 
Easy authcache 2 кэширование для pro. Родионов Игорь
Easy authcache 2   кэширование для pro. Родионов ИгорьEasy authcache 2   кэширование для pro. Родионов Игорь
Easy authcache 2 кэширование для pro. Родионов Игорь
PVasili
 
Система обработки бизнес-логики server-side приложения на Groovy
Система обработки бизнес-логики server-side приложения на GroovyСистема обработки бизнес-логики server-side приложения на Groovy
Система обработки бизнес-логики server-side приложения на Groovy
Regn
 

Similar a Как мы уменьшили сложность наших проектов (20)

Производительность в Django
Производительность в DjangoПроизводительность в Django
Производительность в Django
 
Web осень 2013 лекция 8
Web осень 2013 лекция 8Web осень 2013 лекция 8
Web осень 2013 лекция 8
 
10 задач администрирования Active directory, решаемых с помощью power shell
10 задач администрирования Active directory, решаемых с помощью power shell10 задач администрирования Active directory, решаемых с помощью power shell
10 задач администрирования Active directory, решаемых с помощью power shell
 
Grail: шаги для ваших Python-тестов
Grail: шаги для ваших Python-тестовGrail: шаги для ваших Python-тестов
Grail: шаги для ваших Python-тестов
 
Grail - CodeFest'2015
Grail - CodeFest'2015Grail - CodeFest'2015
Grail - CodeFest'2015
 
Лекция 6. Классы 1.
Лекция 6. Классы 1.Лекция 6. Классы 1.
Лекция 6. Классы 1.
 
Web осень 2013 лекция 6
Web осень 2013 лекция 6Web осень 2013 лекция 6
Web осень 2013 лекция 6
 
Writing Open Source Library
Writing Open Source LibraryWriting Open Source Library
Writing Open Source Library
 
Успешный Open Source - Андрей Светлов, PyCon RU 2014
Успешный Open Source - Андрей Светлов, PyCon RU 2014Успешный Open Source - Андрей Светлов, PyCon RU 2014
Успешный Open Source - Андрей Светлов, PyCon RU 2014
 
Moscow Python Conf 2016. Почему 100% покрытие это плохо?
Moscow Python Conf 2016. Почему 100% покрытие это плохо?Moscow Python Conf 2016. Почему 100% покрытие это плохо?
Moscow Python Conf 2016. Почему 100% покрытие это плохо?
 
Easy authcache 2 кеширование для pro родионов игорь
Easy authcache 2   кеширование для pro родионов игорьEasy authcache 2   кеширование для pro родионов игорь
Easy authcache 2 кеширование для pro родионов игорь
 
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
PVS-Studio. Статический анализатор кода. Windows/Linux, C/C++/C#
 
Easy authcache 2 кэширование для pro. Родионов Игорь
Easy authcache 2   кэширование для pro. Родионов ИгорьEasy authcache 2   кэширование для pro. Родионов Игорь
Easy authcache 2 кэширование для pro. Родионов Игорь
 
Securing Rails Applications
Securing Rails ApplicationsSecuring Rails Applications
Securing Rails Applications
 
Суперсилы Chrome DevTools — Роман Сальников, 2ГИС
Суперсилы Chrome DevTools — Роман Сальников, 2ГИССуперсилы Chrome DevTools — Роман Сальников, 2ГИС
Суперсилы Chrome DevTools — Роман Сальников, 2ГИС
 
Java 9: what is there beyond modularization
Java 9: what is there beyond modularizationJava 9: what is there beyond modularization
Java 9: what is there beyond modularization
 
Оптимизация UI потока / Дмитрий Куркин (Mail.Ru)
Оптимизация UI потока / Дмитрий Куркин (Mail.Ru)Оптимизация UI потока / Дмитрий Куркин (Mail.Ru)
Оптимизация UI потока / Дмитрий Куркин (Mail.Ru)
 
Система обработки бизнес-логики server-side приложения на Groovy
Система обработки бизнес-логики server-side приложения на GroovyСистема обработки бизнес-логики server-side приложения на Groovy
Система обработки бизнес-логики server-side приложения на Groovy
 
Сергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и Javascript
Сергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и JavascriptСергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и Javascript
Сергей Шамбир, Адаптация Promise/A+ для взаимодействия между C++ и Javascript
 
Спецификация WSGI (PEP-333)
Спецификация WSGI (PEP-333)Спецификация WSGI (PEP-333)
Спецификация WSGI (PEP-333)
 

Как мы уменьшили сложность наших проектов

  • 1. — Мой учитель «Перед презентацией проверь, работает ли переключатель слайдов»
  • 2. Борис Цема Game Services Wargaming Как мы уменьшили сложность наших проектов
  • 3. 3 Кадр из к/ф «Криминальное чтиво»
  • 4. 4
  • 5. 5 Команда разработки Проект WG XMLRPC Проект WG Kafka Проект WG HTTP + AMQP Проект WG HTTP + AMQP Проект WG Проект WG Проект WG 10-20 интеграций Различные транспорты Разные протоколы общения Проект WG SQL JIRA
  • 6. Как мы обычно делали • SQLAlchemy • HTTP API и/или MVC • Pure Python или Django • Deploy: • Один WSGI App (несколько инстансов) • БД (с репликами по необходимости) • несколько long-run python процесов (aka workers) 6
  • 7. • Просто начать. • Подходит большинству проектов. • Знаком почти всем python разработчикам. • В большинстве случае нормальная производительность. 7 • Сложно добиться очень высокой производительности. • Поставка новых игровых фичей со временем замедляется. • Со временем повышается вероятность внесения багов. Что хорошо Что не очень
  • 9. Как мы теперь это делаем Мы пишем код
  • 11. 14 • id • nickname • clan_tag • clan_color • is_banned • recruits_total • battles_count • … • id • account_id • text • expiration_date • … • recruiter_id • recruit_id • points • status • … Account Referral Link Relation Определение предметной области
  • 12. 15class Account: def __init__( self, id_: AccountID, nickname: str, clan: Clan, exclusion_reasons: List[ExcludedAccount], recruits_total: int, arenas: Collection[Arena], ) -> None: assert isinstance(clan, Clan), type(clan) self.clan = clan self._arenas = arenas # More init code here... @property def battles_count(self) -> int: return len(self._arenas) @property def clan_tag(self) -> str: return self.clan.tag
  • 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
  • 18. 23 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
  • 19. 24 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) return account
  • 20. 25 class AccountRepository: def get_by_id(self, instance_id: AccountID) -> Optional[Account]: pass def get_or_raise_by_id(self, instance_id: AccountID) -> Account: pass def insert(self, instance: Account) -> None: pass def update(self, instance: Account) -> None: pass def delete(self, instance: Account) -> None: pass
  • 21. 26 class AccountService: def __init__(self) -> None: self.account_repo = AccountRepository()
  • 22. 27 class BaseQueryBuilderMixin: @property def table(self) -> Table: raise NotImplementedError() @property def id_query(self) -> BinaryExpression: return self.table.c.id == bindparam('instance_id') @property def get_by_id_query(self) -> Select: return self.get_all_query().where(self.id_query) @property def get_by_id_for_update_query(self) -> Select: return self.get_all_query().with_for_update(key_share=True).where( self.id_query) @property def delete_by_id_query(self) -> Delete: return self.table.delete().where(self.id_query)
  • 23. 28 class BaseSerializerMixin(Generic[T, T_ID]): def get_instance_id(self, instance: T) -> T_ID: raise NotImplementedError() def get_instances(self, records: List[RowProxy]) -> List[T]: return list(map(self.get_instance, records)) def get_instance(self, record: RowProxy) -> T: raise NotImplementedError() def instance_to_dict(self, instance: T) -> Dict: serializer = getattr(instance, 'to_dict', None) if callable(serializer): return serializer() raise NotImplementedError() def instance_id_as_dict(self, instance_id: T_ID) -> Dict[str, Any]: return {'instance_id': instance_id}
  • 24. 29 class BaseSyncRepository(Generic[T, T_ID], BaseQueryBuilderMixin, BaseSerializerMixin[T, T_ID]): def insert(self, instance: T) -> ResultProxy: query = self.insert_query params = self.instance_to_dict(instance) return self._execute(query, **params) def exists_by_id(self, instance_id: T_ID) -> bool: query = exists().where(self.id_query).select() return self._exists(query, **self.instance_id_as_dict(instance_id))
  • 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
  • 28. Требование Через некоторый срок неактивности аккаунт должен быть удалён из системы, с предупреждением игрока за месяц. 33
  • 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 Когда можно так делать В остальных случаях используем класс-сервис.
  • 33. 38 AccountServiceService Layer AccountRepository SchedulerRepositoryInfrastructure Layer Account Clan ExcludedAccount ArenasDomain Layer
  • 34. Endpoints: HTTP API 39 class AccountCreatingResource(BaseFalconResource): def __init__(self, service: AccountService) -> None: self.service = service def on_post(self, req: falcon.Request, resp: falcon.Response) -> None: account_id = req.get_param_as_int('account_id', required=True) self.service.register_account(account_id) resp.status_code = falcon.HTTP_201
  • 35. Endpoints: CLI 40 @click.command('account') def account_command(): pass @account_command.command('register') @click.option('-i', '--id', 'id_', type=int, required=True, help='Account id to create.') def register_account(id_: int) -> None: service = AccountService() existed_account = service.get_account(id_) if existed_account is None: service.register_account(id_) logger.info('Account was successfully created.') else: logger.error('Account already exist.') sys.exit(1)
  • 36. 41 AccountServiceService Layer AccountRepository SchedulerRepositoryInfrastructure Layer Account Clan ExcludedAccount ArenasDomain Layer Application Layer HTTP API CLI RPC MQ Listener
  • 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 Если что – потом отрефакторим. Система должна эволюционировать.
  • 44. 49 AccountServiceReferralLinkServiceService Layer AccountRepositoryReferralLinkRepository SchedulerRepositoryInfrastructure Layer Account Clan ExcludedAccount ArenasReferralLinkDomain Layer Application Layer HTTP API CLI RPC MQ Listener
  • 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 • Требуется бОльшая производительность. • Логика не сложная и покрыта тестами. Когда использовать:
  • 48. 53 def register_account(self, account_id: AccountID, force: bool = False, for_link: ReferralLink = None) -> Account: Пример: ограниченные интерфейсы def register_account(self, account_id: AccountID) -> Account: def register_account_by_referral_link(self, account_id: AccountID, link: ReferralLink) -> Account: def register_account_if_satisfied( self, account_id: AccountID, specification: BaseSpecification ) -> Account:
  • 49. 54 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)
  • 50. 55 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)
  • 51. 56 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) CachingService.instance.drop_caches_for_account(account) if account.can_be_recruiter: self.referral_link_service.get_or_create_referral_link( account.id)
  • 52. 60
  • 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
  • 62. 70 AccountServiceReferralLinkServiceService Layer AccountRepositoryReferralLinkRepository SchedulerRepositoryInfrastructure Layer Account Clan ExcludedAccount ArenasReferralLinkDomain Layer Application Layer HTTP API CLI RPC MQ Listener
  • 63. Вопросы? • Если кодовая база уже есть как переписать? • Почему не микросервисы? • Почему классы а не модули? Чем не подходит logic.py? • Почему столько отклонений от DDD? • Подходит ли это мне? 71
  • 64. Борис Цема / Boris Tsema boris@tsema.ru

Notas del editor

  1. Здравствуйте, меня зовут Борис Цема, и, как говорит любимый персонаж моего любимого фильма, я решаю проблемы. В основном используя для этого язык Python.
  2. Всем известный продукт Wargaming это World Of Tanks: многопользовательская онлайн игра, технически представляющая собой толстый клиент и сервер. Сражения на танках — это основной геймплей. Но кроме них игроку нужны ещё социализация, игровые события и мета-геймплей. Всё это в Wargaming создаётся отдельными проектами, которые делаются отдельными департаментами. Так проекты выглядят для игрока
  3. Так — для нас изнутри. Проект в WG отвечает какой-то определённой цели: это может быть какая-нибудь долгоживущая внутренняя система (например поддержки пользователя или сбора и хранения бизнес-информации), это может быть некоторый новый мета-геймплей (УРы или Глобалка) или временное событие (Ивент для футбола в 2018 году, Хэллоуин). Каждый проект делается некоторой командой разработки (как правило, она собирается заново) и интегрируется с десятком-другим сервисов WG.
  4. После того как собрали требования, мы обычно берём SQLAlchemy, последнюю из используемых в компании версий языка, определяем взаимодействие с фронтендом (HTTP API или MVC) и с интеграциями, и дальше делаем как-то так.
  5. Есть много известных и опробованных десятилетиями способов, о которых вам расскажут коучи и консультанты.
  6. После оценки производительности оказалось, что в худшем сценарии использования нам не хватит IOPS даже на запись, не говоря про изменения. Было не ясно справятся ли известные нам SQL решения (соседний проект решал похожую проблему в in-memory БД). Проект имел сложную предметную область, изначально не было уверенности в том, что её понимание останется прежним. Высокий RPS, большой поток входящих на обработку сообщений.
  7. Мы попытались охватить всё это у доски, но не получилось. core, которое мы будем использовать — что туда класть? А если предметная область поменяется и это перестанет быть тем, что подходит в core? ORM? Было очевидно что понадобится шардинг, было не очевидно что не ошибёмся с ним изначально. Нам точно нужен был asyncio, но весь проект не должен был быть асинхронным.
  8. В результате performance в тестах превзошёл прогнозируемый в несколько раз. Когда потребовалось расширить проект для следующего игрового события мы сделали это не напрягаясь в рекордные сроки. После нескольких небольших ошибок в проде (где их нет) на новом проекте их действительно не было. Много кода получилось не проектно-специфично, в результате в следующих новых проектах смогли переиспользовать большую часть кода.
  9. Но я не консультант, я работал с этим какое-то время, и попробовав и адаптировав часть этих методик я сейчас расскажу как мы это делаем.
  10. Для примера я буду использовать недавно вышедшую Реферальную программу 2.0. Суть её в следующем: игрок, соответствующий определённым критериям, может создать реферальную ссылку по которой пригласить других игроков, соответствующих другим критериям. За игру вдвоём игроки получают бонусы, реферальная программа разбита на этапы, за завершение этапов — награды, за завершение всей программы — награды.
  11. Первым делом мы пытаемся понять предметную область и выделить основные её сущности. В этом процессе часто участвуют та часть команды которая непосредственно не программирует. Её могут называть « бизнес-парни », « владельцы продукта » и ещё как-то. Там могут быть бизнес-аналитики, гейм-дизайнеры, UI/UX дизайнеры. Мы собираемся, смотрим требования, задаём вопросы, пытаемся понять что и как должно работать, внимательно слушаем. В процессе таких встреч (их может быть несколько), в кабинетах всё чаще начинают повторяться специфичные для проекта слова: провинция, бой, очки реферальной программы, реферальная ссылка. Это очень важные слова. Их надо обязательно запомнить, осмыслить, и перевести в код. Если в коде что-то не смотрится, всегда можно настоять на использовании других слов для какой-то части. Существование на проекте единого языка между всеми его участниками — один из важнейших пунктов для успеха. Так обеспечивается скорость и однозначность понимания между бизнесом, тестировщиками и разработкой. Ещё важный пункт: если вы в коде смоделировали классы и объекты максимально близко к реально произносимой предметной области, то и при изменении последней будет легче менять код. Поэтому важно следить чтобы основные свойства в коде выглядели так же, как их произносят « бизнес-люди ».
  12. В коде это может выглядеть так. Здесь мы забываем про всё лишнее и начинаем танцевать от предметной области. Обратите внимание: в кабинете звучит слово « количество боёв », в коде мы по необходимости храним какое-то представление этих боёв, но у класса Account есть свойство battles_count. Когда вы в требованиях встретите « у игрока должно быть 1000 боёв а ещё » и тут 20 пунктов, в коде вам будет проще видеть if account.battles_count > 1000 and … чем if len(account.arenas) > 1000.
  13. Обычно на этом этапе сразу хочется определить таблицы в БД и создать модели в какой-нибудь SQLAlchemy. Однако основная задача при выявлении предметной области — найти основные сущности, их составляющие и их взаимодействие. Вы ещё не очень в контексте проекта, он не очень удерживается в голове, причём не только у вас. Моделируя классы, выставляя ограничения вы выявляете граничные случаи и задаёте вопросы бизнесу, который, на самом деле, довольно крупными мазками рисует проект. На этом этапе могут вскрыться ситуации: а если игрок ещё не успел подтвердить процесс регистрации но уже пришёл к нам? Сколько максимально может быть рекрутов и нужно ли отображать какую-то информацию о них? Какое время жизни ссылки? От чего зависит? и так далее. Если вы начинаете отвлекаться на « хранить в JSON или в Text», « как разбить на таблицы » то пропускаете этот важный этап. Когда дойдёте до очередного вопроса, бизнес скажет « а, точно, давай-ка лучше сделаем так », и вам опять надо перекраивать. Поэтому проектировать БД на этом этапе не нужно. Потом вы начнёте реализовывать юзкейсы, взаимодействие между сущностями и фиксация на БД будет мешать, тем более что может оказаться что СУБД и не нужна а достаточно строчки в redis или статичного json файла. В крайнем случае в SQLAlchemy можно сделать класс а потом смаппить. Основная идея этого этапа — отложить максимум технических решений на потом. Это выглядит как прототип, который не будет выбрасываться.
  14. В результате у нас появляется слой предметной области, реализующий основные сущности. Эти основные сущности где-то внутри себя содержат какие-то другие данные, нужные только им. Они их агрегируют. В терминах DDD такие сущности называют агрегатами, а весь этот слой мы называем domain layer.
  15. После выделения существительных, давайте перейдём к глаголам. Прочитаем требования.
  16. Действия над предметной областью будем реализовывать в классах-сервисах. Что такое Сервис? Класс, публичные методы которого полностью или частично реализуют описанные человеческим языком требования. Ответственность класса — предметная логика, описанная в требовании, а не внутреннее состояние Account. AccountService оркестрирует работу над Account. Даже при таком простом действии начинают возникать вопросы. Эти вопросы начинают менять требования, и нам легко это сделать т.к. ещё немного написано и мы не потратились на создание схемы в БД.
  17. Что у нас получилось? Класс, публичные методы которого реализуют user story. В нём мы держим всю логику, а вызывать этот метод можно через HTTP API, RPC (XMLRPC, zeromq), CLI и как угодно. Этот вызов не есть ответственность Service класса. Класс Service оркестрирует работу над аккаунтом: куда сходить, когда сохранить.
  18. При внимательном взгляде на получившийся код из собственного опыта можно предположить что какие-то методы точно ещё не раз понадобятся. В данном случае вот этот метод может понадобится саппорту для разбора проблем у пользователей. При необходимости мы можем сделать его публичным и переиспользовать, как только где-то ещё в требовании появится проверка на забаненость. Вообще, переиспользование — сильная сторона такого подхода.
  19. Давайте ещё раз посмотрим на сервис аккаунта, а точнее на эту строчку. Сейчас операция по хранению аккаунта находится в ведении AccountService, но лучше для этого использовать отдельный класс. В DDD для классов, занимающихся постоянным хранением данных используется слово Factory, в других областях — Repository. Оставив обсуждения о терминологической чистоте мы на практике называем эти классы Repository.
  20. Пусть вас не смущает похожее на SQL слово insert. На самом деле репозиторию совсем не обязательно использовать хоть какую-то БД вообще.
  21. В работе мы пришли к идее об едином базовом CRUD интерфейсе для всех репозиториев.
  22. Таким образом при разработке любой новой функциональности происходит минимум когнитивной нагрузки: просто найди в инстансе сервиса нужный репозиторий и используй.
  23. Базовые CRUD методы довольно похожи и могут быть написаны однажды. У нас есть базовые миксины для построения запросов, Для нового репозитория допишите сериализацию и query методы. Так это мы делаем для SQL репозиториев. Похожий интерфейс но с другой реализацией используется для сохранения в файловую систему, например. Это часть типичного миксина, у него есть ещё другие методы вроде exist_by_id и так далее.
  24. Для сериализации и десериализации мы используем подобный миксин, большинство методов которого абстрактные.
  25. И всё это объединяется в базовом репозитории. Перед вами синхронный репозиторий.
  26. …и асинхронный, использующий те же миксины для сериализации и десериализации. Для построения запросов мы используем SQLAlchemy, а для асинхронной работы с PostgreSQL — asyncpg, который принимает только raw sql, поэтому в асинхронном репозитории есть отдельный метод для перевода алхимии в постгресовский raw sql. Это позволяет не зацикливаться на синхронном или асинхронном стиле программирования и для cpu-bound задач работать синхронно а при необходимости поддержать много RPS — асинхронно.
  27. Что если после проверки производительности вы увидели что репозиторий слишком много времени проводит в ожидании объектов из БД? Мы можем сделать его кэширующим! При этом нет необходимости менять код приложения! Аналогично для методов, изменяющих базу. Вы всегда можете подправить базовые репозитории для повышения производительности или фикса багов. Например, можно заставить некоторые репозитории ходить не в master базу а в реплику. Можно кэшировать query и не тратить время на их составление. Вообще, эти базовые репозитории являются удобной единой точкой оптимизации всего взаимодействия приложения со слоем хранения.
  28. Обратите внимание, мы не называем это persistence layer, а infrastructure layer, и вот почему.
  29. Давайте прочитаем ещё одно требование. Теперь нам нужен шедулер.
  30. Все из них имеют свои плюсы и минусы. Главный подход во всей разработке — откладывать решения на потом, когда информации и понимания будет больше.
  31. Единый интерфейс репозиториев настолько привычен и прост, что в одном из проектов у нас появился SchedulerRepository, который создавал таски в celery, скрывая сложную логику обращения с ним. Любой мог создать SchedulingTask и куда-то её сохранить. На данном этапе мы можем ограничиться таким подходом, и продолжать работу. Несмотря на некоторое смешение концепций, использование репозиториев в таком случае даёт мне, как разработчику, ожидание определённого интерфейса. То есть мне надо просто, как и с другими репозиториями, как-то создать объект и использовать один из привычных CRUD методов.
  32. Надо ли говорить, что если (когда) мы захотим перестать использовать celery, код предметной области меняться не будет — главное будет сохранить интерфейс. А в репозитории слегка изменим код.
  33. Обратите внимание, что SchedulingTask не просочился в слой предметной области.
  34. Теперь о приложении.
  35. Теперь о приложении. Слой приложения занимается только своей логикой и переиспользует бизнес-логику.
  36. Добавим новый метод. На данном этапе разработки мы с бизнесом думаем что достаточно просто сохранить ссылку, а потом в каком-нибудь интерфейсе рекрутёр её запросит. Скорее всего, со временем нам поставят требования отправлять уведомление игроку, ограничивать количество ссылок, изменять время их жизни а так же делать предыдущие ссылки недействительными.
  37. Давайте посмотрим на то, как можно сгруппировать операции по аккаунтам и ссылкам. Обратите внимание, что соблюдение GDPR и по ссылкам и по аккаунту будет отличаться. Действия над аккаунтом формируют некий контекст аккаунта, над реферальной ссылкой же происходит в другом контексте. Давайте так и назовём эти овалы. DDD называет это « ограниченный контекст ».
  38. В нашем коде каждому контексту соответствует свой класс.
  39. Вынесем код, создав новый сервис. Обратите внимание: проверки делаются на стороне AccountService. На практике это может приводить к дополнительному коду, существующему только ради концептуальной чистоты.
  40. DDD предписывает нам не передавать агрегат между контекстами, а использовать некоторый промежуточный слой. В данном случае он мог бы быть реализован в виде метода AccountService, вызывающего метод ReferralLinkService с определёнными параметрами.
  41. Поэтому иногда мы делаем так, а потом рефакторим при необходимости. Постоянный рефакторинг является неотъемлимой частью разработки. Предметная область может меняться, меняется язык. Постоянные небольшие изменения уменьшают количество техдолга, делают проект лучше и проще. Мы стараемся не откладывать рефакторинг если видим его необходимость здесь и сейчас, поэтому можем себе позволить вот такой подход.
  42. Какой-то внешний сервис читает бои, делает разные действия и среди прочих вызывает метод AccountService. Смотрите, в какой-то момент аккаунт должен получить реферальную ссылку, и мы используем уже созданные методы. Эти методы, обладая ограниченным интерфейсом, позволяют вызывать себя из разных мест кода.
  43. Про контекст использования: сервис может ответить на вопросы, в решении которых может оптимальнее быть не использовать агрегат, а сразу ходить в его внутренности.
  44. Такое переиспользование возможно когда интерфейс является полным и ограниченным. Мы стараемся передавать в методы полные объекты предметной области, а не их части. Это позволяет при необходимости что-то поменять в каком-то из этих методов не менять их потребителей, расширяя интерфейс. По этой же причине методы стараются возвращать тоже полные объекты (а лучше — агрегаты).
  45. Если из бизнес-требований нам кажется, что в какой-то uscase нужно делать транзакционно, то транзакцией управляет сервис. Здесь мы не знаем, что это за транзакция, db.transaction реализован в infrastructure layer.
  46. Даже если кажется, что какая-то инфраструктурная работа сильно связана с агрегатом, её всё равно надо вынести в сервис, т.к. основная суть её именно инфраструктурная.
  47. Иногда для оптимизации имеет смысл некоторые сервисы сделать синглтонами, это оправдано в случае использования ими сессий, пулов, ограничения на количество подключений с другой стороны.
  48. Что нужно держать в голове при разработке: надо поддерживать большую степень переиспользования кода. Для этого общаться между собой целыми объектами, выделять общие методы. Интерфейсы должны быть чёткими и конкретными, без многочисленных параметров. Ограниченный контекст позволяет быстро вникать, игнорируя бОльшую часть проекта. Не должно быть повторяющегося кода, требующего специального знания об интеграции. Надо сделать сервис и пару методов с ясным ограниченным интерфейсом. Если вы где-то повторяете даже пару строчек, специфичных для какой-то технологии или сервиса — постарайтесь вынести это в отдельный метод.
  49. Предположим такое простое действие как « пройтись по удалённым шардам и что-то оттуда забрать». Один раз вам надо подумать как одновременно запустить несколько асинхронных запросов, как именно обработать ошибки и как вернуть. Не факт что следующий разработчик будет это знать, и даже если будет, добавлять когнитивную нагрузку ему точно не стоит.
  50. Мы можем находить узкие места и оптимизировать их, при этом зачастую они затрагивают ограниченную область приложения и не приходится делать всего регрессионного тестирования. В зависимости от глубины интеграции оптимизация может значительно улучшить всё приложение (тут рассказать про сжатие в redis). Переиспользование позволило получить большую степень покрытия тестами и лёгкий способ менять требование. Разбиение на интерфейсы позволяет легко поменять юзкейс, т.к. он выглядит как набор методов других сервисов или агрегатов. Почти как текст, написанный по-английски. Новые разработчики поняв основную суть начинают работу в определённом контексте, не загружаясь всем приложением. Некоторые сервисы сразу дают оттестированный код в новом проекте, сокращая bootstrap период.
  51. А ещё, если захотим, мы сможем перейти на микросервисы, т.к. деплой сильно отделён от логики.
  52. Укладывается ли она в существующий контекст? Или можно выделить новый? Если новый — то всё просто. Найдите основные сущности для проектирования, типа account в нашем примере, и следуйте написаному на слайде.
  53. Если у вас сложный проект и нужно много общаться с базой (aka «программирование на SQLAlchemy»). Пересмотрите предметную область. Как правило, после выделения контекстов многие операции сводятся к CRUD. Не стесняйтесь доставать из базы куски значений. Но только при большой необходимости когда реально не хватает производительности. Помните, переиспользование методов сервисов и репозиториев увеличивает скорость разработки и поставки новых фичей. Три процента производительности могут не стоить денег на разработку. Иногда может случиться что «программирование на алхимии» — вынужденная необходимость. В таком случае ограничьте контекст, где это происходит, и пусть там будет так. В остальных контекстах вы вполне можете упростить жизнь используя слои и сервисы.
  54. Так мы можем определить правила для проектирования domain layer.
  55. Правила проектирования Service Layer.
  56. Правила проектирования Infrastructure Layer.
  57. Так как после вчерашнего кому-то может быть сложно самому придумать вопрос, то я сделал это за вас)