Где же ты, DDD? 
Tags: ddd, python
Published: 2021-05-09 22:53:36

..Так получилось, что меня зацепила мысль об использовании DDD. Более того, я даже проинвестировал львиную долю своего и чужого времени в эту тему. 

Но что-то идет не так. Написав с десяток репозиториев, сотню сущностей - ощущения ясности не приходит, каменный цветок не выходит. 

Одна из причин - перерабатывать творчество Эванса и Вернона сложно, курсы Хорикова на pluralsight хоть и классные, но как-то про .NET... Вообще, создается впечатление ~~что в СССР секса нет~~ что за пределами .NET и Java - DDD не возможен. 

И разные [awesome листы](https://github.com/valignatev/ddd-dynamic) как раз это подтверждают. Создается стойкое впечатление, что идея классная, книжки крутые по теме написаны, а взять и использовать - невозможно! 

Давайте внезапно возьмем статью про [Стратегический DDD](https://vaadin.com/learn/tutorials/ddd/strategic_domain_driven_design) и помедитируем на нее. 

Слово **домен**, само по себе переводится очень плохо и русскому уху скорее напоминает про интернет серфинг. К коему неминуемо хочется прибегнуть, каждый раз встречая это слово. Переводить его как **предмет** - лишний раз вспоминать про школу. **Предметная область** - отличное словосочетание для того, чтобы вставить его в заголовок скачанного из интернета реферата. Есть понятное слово - **тема**. 

## Как моделировать тему? 

*Если перевести domain еще можно, то subdomain - совсем жесть*. 

Для начала тему надо подробить на ~~БДСМ-~~практики. Целиком ее объять невозможно. Только Йода постиг тему полностью и теперь жмет это знание и передает его ученикам по частям. 

Понять, что у нас практикует клиент - сложно. Он обычно говорит на своем языке. Но все же, выделить из речи одно - два предложения и перевести с непонятного на технический - можно. А еще в ходе бесед с клиентом нужно найти **общий язык**. Язык в ходе беседы будет расширяться, в него будут добавляться новые понятия. Из него будут выкидываться уже устоявшиеся и пустившие корни в виде адского-хард-кода термины. 

В любом случае, прежде чем моделировать ту или иную практику - потребуется понять, какого она типа будет. Типов у практик может быть несколько: 
- Основная 
- Поддерживающая 
- Общая 

На примере сервиса для сдачи анализов можно определить практики как следующие: 

![Иллюстрация](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/0asb0vj46h83fwd24kl5.png)
 

{% spoiler Исходник PlantUML %}
```plantuml
@startuml
("основное" \n Записи Пациента) as P 
("поддержка" \n Расписание) as S 
("поддержка" \n Лаборатория) as L 
("общее" \n Авторизация Пользователей) as A 
("общее" \n Загрузка Файлов) as F 

P -u-> F
P -d-> S
P -d-> L 
L -> F
P -d-> A
L -d-> A 
S -d-> A
@enduml
```
{% endspoiler %}

Основная - практика с которой организация получает основной доход. Пациент к нам приходит и сдает анализы - ему важен результат. 

Поддерживающая - необходима для осуществления основной. Нам материал, который пациент сдал - нужно прогнать через лабораторию и получить этот самый результат. А еще прикольно избегать очередей - для этого потребуется ~~вести журнал усилиями бабушки-вахтерши~~ создать сервис для планирования временных слотов. 

Общая - то что делают все. Загрузка файлов, сервис с пользователями и прочее. 

## Отделяем мух от котлет - от проблем к решениям 

Знание темы и практик в лучшем случае даст нам понимание проблем бизнеса. Но при этом для решения этих проблем - нужно общаться дальше. 

Решения проблем нам дадут эксперты - но прежде необходимо выработать общий с ними язык. С этого момента, все технические решения должны легко переводиться на язык экспертов. 

Естественно в организации существуют разные эксперты с разными знаниями. Эти знания нужно ограничить. Ограничение знаний эксперта и перевод их с экспертного языка на общий - как раз и дает нам ограниченный контекст. 

Теперь немного совы. Контексты между собой взаимодействуют. Поэтому хорошо создать карту их взаимодействия. [Примеры - тут](https://miro.com/app/board/o9J_lSxQSq4=/). Главное - не надо кидаться эту карту рисовать. Создаем сначала контексты, потом их объединяем, когда их станет больше трех :) 

### Промежуточный итог 

Моделировать тему важно. Это позволит: 
1. Найти ограничения в контекстах. 
2. Избежать построения супер-моделей, которые превращают любую систему в кошмар. 
3. Понять, что мы ~~спасаем мир~~ решаем проблемы конкретной организации, а не просто пишем код. 

## Тактика 

*Мы берем и непринужденно заходим в медитацию над [следующей статьей](https://vaadin.com/learn/tutorials/ddd/tactical_domain_driven_design).. Река течет, вода льется - давайте ловить рыбу*

С экспертами нужно общаться и от общения с ними неизбежно будут появляться артефакты. 

## Какие артефакты можно поймать от эксперта? 

Ну во-первых эксперт вряд ли начнет говорить о своей проблеме с объектов-значений. Скорее всего это будут общие фразы из серии:
- Мне тут нужно все назначения пациентов посмотреть и предложить им аналогичные препараты. Чтоб у них был выбор.. 

Данная фраза звучит уже внутри темы "Аптека", задав контекст "Назначение препаратов". При этом "Назначение" - это главная фишка, которая интересует эксперта. Ее в DDD называют "Корень Агрегата" - абракадабра, над которой нужно медитировать пару лет, если честно. И хоть и [появляются статьи](https://habr.com/ru/company/dododev/blog/532628/), которые делают эту штуку понятнее - но по классическим трудам эту штука не очень интуитивна. 

Когда встречаются эти мысли про агрегаты, сущность, объект-значение и прочее - хочется выть. Можно проработать с десяток  лет в индустрии и писать всю карьеру обычные модели, да заворачивать их в CRUD. Осознание новых понятий приходит не сразу. Но зато потом - "Я не понимал, не понимал, и вдруг как понял!".. 

Итак, давайте сразу попробуем записать агрегат. Да еще и не на Java, а на Python. Зацепив pydantic. 

```python 
from pydantic import BaseModel


PatientId = NewType('PatientId', int)
PrescriptionId = NewType('PrescriptionId', int)


class Patient(BaseModel):
    """
    Данные о пациенте:
    - имя
    - фамилия
    - телефон для связи
    """
    patient_id: PatientId
    first_name: str 
    last_name: str 
    phone: str


class ActiveComponent(BaseModel):
    """
    Действующее вещество: 
    - Название 
    - Единицы измерения 
    - Количество
    """
    name: str 
    unit: str 
    amount: float


class Medicine(BaseModel):
    """
    Коммерческий препарат. 

    - Название 
    - Действующее вещество
    """
    name: str 
    active_component: ActiveComponent


class Prescription(BaseModel): 
    """
    Назначение

    - Пациент 
    - Список препаратов
    """
    prescription_id: PrescriptionId
    assign_date: Date
    patient: Patient
    medicine: List[Medicine]
``` 

Является ли "Назначение" агрегатом? И да, и нет. Обратите внимание, в рамках моделирования - нам не обязательно прикреплять id ко всем сущностям. Достаточно только к ключевым, а ими тут являются "Назначение" и "Пациент". Сами препараты - мы должны жестко зафиксировать в "Назначении". 

Назначение пока что представляет собой анемичную модель. Видно, что без внешнего воздействия - ничего с ним не сделать. 

Давайте его оживим: 
```python 
class Prescription(BaseModel): 
    """
    Назначение

    - Пациент 
    - Список препаратов
    """
    prescription_id: PrescriptionId
    assign_date: Date
    patient_id: PatientId
    medicines: List[Medicine]

    def add_medicine(self, medicine): 
        self.medicines.append(medicine)

    def get_patient(self, patient_repository: IPatientRepository):
        return patient_repository.get(self.patient_id)

    def get_alternative_medicines(self, medicine_repository: IMedicineRepository):
        for medicine in self.medicines: 
            yield medicine_repository.get_by_active_component_name(medicine.active_component.name)
```

Стало поживее, одновременно передача репозиториев внутрь агрегата - спорная тема. А получение списка альтернативных препаратов - выглядит совсем кошмарно. Но это лишь для того, чтобы подумать о [трилемме DDD](https://enterprisecraftsmanship.com/posts/domain-model-purity-completeness/). 

Что на самом деле получаем? 
- Агрегат можно обсуждать с экспертом, его код деревянный. 
- Агрегат закрывает бизнес-логику. Ее также можно закрыть сервисом, но на практике мы используем сервисы чаще, чем это
 требуется. Чаще всего причина создания сервиса - криво сделанный агрегат. 

Остаются открытыми вопросы: 
- А пациент точно должен фигурировать в назначении? Он не из другого контекста? 
- А если мы склеим пациентов с назначениями через сервис - это DDD или уже нет? 

И я их не закрою :) 

### Полезные правила при дизайне агрегатов: 

1. Агрегаты должны быть небольшими. Это объект, который можно выгрузить в память целиком и система при этом не пострадает. 
2. К другим агрегатам и сущностям из агрегата нужно ссылаться через Id. 
3. В рамках одной транзакции в БД - желательно изменять только один агрегат. 
4. Оптимизм. Сохранение агрегата не должно ломать консистентность данных. Условно говоря, агрегат самодостаточен и если в нем произвели изменения - они произведены по правилам. 

## События в Теме  

*Все помнят анекдот - "Чем круче джип - тем дальше идти за трактором". Не многие скорбят о тех, кто трактор не нашел.*

С агрегатами все хорошо, до тех пор пока не требуется взаимодействие между ними и их не становится 100500. 

Как только пациенту становится интересно о том, что в назначение добавлен новый препарат - карточный домик рушится. 

Давайте попробуем напрямую сказать пациенту о том, что новые супер-таблетки появились в назначении: 

```python 
class Prescription(BaseModel): 
    """
    Назначение

    - Пациент 
    - Список препаратов
    """
    prescription_id: PrescriptionId
    assign_date: Date
    patient_id: PatientId
    medicines: List[Medicine]

    def add_medicine(self, medicine, patient_repository): 
        self.medicines.append(medicine)
        patient = self.get_patient(patient_repository)
        patient.notify(medicine)

    def get_patient(self, patient_repository: IPatientRepository):
        return patient_repository.get(self.patient_id)
``` 

Метод **add_medicine** стал выглядеть совсем больно, не так ли? Давайте уберем боль: 


```python 
class PrescriptionEventType(enum.Enum):
    MEDICINE_ADDED = 1


class PrescrtionEvent(BaseModel):
    type: PrescriptionEventType
    data: dict



class Prescription(BaseModel): 
    """
    Назначение

    - Пациент 
    - Список препаратов
    """
    prescription_id: PrescriptionId
    assign_date: Date
    patient_id: PatientId
    medicines: List[Medicine]
    log: List[PrescrtionEvent]

    def add_medicine(self, medicine): 
        self.medicines.append(medicine)
        self.log.append(PrescrtionEvent(
            type=PrescriptionEventType.MEDICINE_ADDED,
            data={ 
                'patient': self.patient_id,
                'medicine': medicine.dict(),
            }
        ))
```

Итак, события вынесены в лог. А кто его будет читать? По хорошему - любая штука, которая использует агрегат. В python есть множество имплементаций в эту сторону, от развесистого [event sourcing](https://eventsourcing.readthedocs.io/) до плотно завязанных на иные паттерны решений. В книге cosmic python - целая половина книги про [события](https://www.cosmicpython.com/book/part2.html). 

Интуитивно подскажу следующее: 
- Посмотрите как события [реализованы в .NET](https://docs.microsoft.com/en-us/dotnet/architecture/microservices/microservice-ddd-cqrs-patterns/domain-events-design-implementation) и возможно это поможет. 
- В 2021м году - искать готовые удобные библиотеки в python для DDD не надо. Их нет. 
- Решение вида: 

```python 
from fastapi import BackgroundTasks, FastAPI

app = FastAPI()


def handle_prescription_event(prescription_event: PrescriptionEvent):
    if prescription_event.type == PrescriptionEventType.MEDICINE_ADDED: 
        ...

@app.post("/add_medicine/{prescription_id}")
async def add_medicine(medicine: Medicine, prescription_id: PrescriptionId, background_tasks: BackgroundTasks):
    prescription = prescription_service.get_prescription(prescription_id)
    prescription.add_medicine(medicine)
    prescription_service.update(prescription)
    for e in prescription.log: 
        background_tasks.add_task(handle_prescription_event, e)
    return {'medicine_added': 1}
```

вполне себе работает. 
- Используя свойство оптимистичности агрегатов - можно немного расслабиться по поводу проблем с конкурентностью событий. 

Я не уверен, на каком именно параграфе данной статьи убежал эксперт по теме. Но давайте заглянем дальше, туда, куда ему точно смотреть не стоит. 

Итак, есть вещи которые при беседе с экспертом можно упоминать, но в детали лучше не вдаваться, а именно: 

- Репозитории. Это слово можно использовать вместо базы данных. А можно использовать "база данных" вместо "репозиторий" - и это будет продуктивнее. Еще можно попробовать слово "хранилище". Цель этого понятия простая - мы достаем агрегат в помощью хранилища и после работы с ним - кладем его обратно в хранилище. 

Выглядит репозиторий примитивно: 
```python
class IPatientRepository(object):
    def get_patient_by_phone(self, phone: str): 
        raise NotADirectoryError()

    def get_patients(self): 
        raise NotADirectoryError()


class PatientRepository(IPatientRepository): 
    def __init__(self): 
        self.db = {} 

    def get_patient_by_phone(self, phone: str): 
        return self.db.get('patients_by_phone', {}).get(phone)

    def get_patients(self): 
        for patient in self.db['patients'].values():
            yield patient
```

Суть в том, что вместо вызовов ORM или прямых обращений в БД - мы работаем с ней через репозиторий. Тестируемость кода при таком подходе растет. Репозиторий - один из наиболее часто применяемых дизайн паттернов. 

- Сервисы. Конструкция которая содержит в себе всю логику, которая не вошла в агрегат. Может использовать несколько репозиториев, вызывать сторонние системы и т.д.

- Фабрика позволяет создавать агрегаты принимая на вход простые инструкции. Эдакий синтаксический сахар для агрегатов. 

- Модули. В случае java, python и go - это packages. В случае с .NET - namespaces. Суть также простая - не надо класть все репозитории, сущности и т.д. в один модуль. 

Очень часто DDD идет бок о бок с гексагональной архитектурой. Об этом есть [хорошие доклады](https://rannts.ru/meetups/7/hexagonal/). Я же избегу воспеваний гексагональной архитектуры - уверен, что находясь в этой точке мы уже достаточно запутаны и упоминание о том, что DDD позволяет использовать 10 фреймворков одновременно на одном тематическом ядре - нас запутает еще больше :) 

P.S.: Я пишу эту статью в специально сумбурном стиле, чтобы приблизительно показать архитекторам ~~душ~~, какая ерунда творится в голове среднего разработчика от прочитанного о DDD. До сих пор не существует мягкого входа в тему, в то время как разработка приложений в традиционном стиле ведет к лапше разного качества. 

Спасибо.