В современном Domain-Driven Design (DDD) при построении сложных систем часто возникает необходимость интеграции между разными подсистемами или Bounded Context-ами. Цель данной главы – изучить ключевые паттерны интеграции и способы взаимодействия между контекстами. Мы рассмотрим, как трансформировать модели данных при интеграции, как надёжно публиковать события между сервисами, и как координировать бизнес-процессы, охватывающие несколько контекстов. В частности, мы обсудим преобразование моделей (антикоррупционный слой и открытый протокол), шаблон надёжной публикации событий (Outbox), а также паттерны координации процессов, такие как сага и диспетчер процессов. Все темы будут сопровождаться понятными примерами на Python с использованием Django – в качестве бизнес-домена возьмём сервис бронирования авиабилетов.
Интеграция между контекстами требует трансформации данных – чтобы различные части системы могли обмениваться информацией, не искажая модель друг друга. DDD предлагает несколько стратегий для этого. Рассмотрим два важных паттерна: антикоррупционный слой и сервис с открытым протоколом. Оба подходят для разных ситуаций интеграции, но оба используют преобразование моделей, чтобы связать контексты и при этом защитить целостность внутренних доменных моделей.
Когда наш контекст должен взаимодействовать с внешней системой или legacy-приложением, возникает риск «загрязнения» нашей доменной модели внешними концепциями. Антикоррупционный слой (ACL) – это прослойка, которая действует как переводчик между внешней системой и нашим доменомdev.to. Идея в том, чтобы изолировать наш внутренний код от чужих моделей данных: внешний ввод сначала проходит через ACL, который преобразует его в понятные нашему домену объекты, и наоборот. Таким образом, дизайн нашего приложения не ограничивается семантикой или ограничениями внешних системlearn.microsoft.com. Например, если внешний сервис предоставляет данные о рейсах в формате SOAP с определённой схемой, а у нас современный REST API и своя структура моделей, ACL возьмёт на себя преобразование: получит SOAP-сообщение, извлечёт данные рейса и создаст объекты нашей модели (или DTO) в нужном форматеdev.to. Благодаря этому мы избегаем «коррупции» доменной модели – внешние изменения не приводят к прямым изменениям в нашем коде, всё сопоставление сосредоточено в одном месте.
В Django антикоррупционный слой можно реализовать в виде сервиса или модуля-прослойки. Это может быть класс-адаптер, функция-конвертер или даже слой сериализации. Ниже приведён упрощённый пример. Предположим, наш сервис бронирования авиабилетов получает данные о рейсе от внешнего API партнёра. Мы определим DTO для внешних данных и функцию-конвертер, которая на лету создаёт экземпляр нашей Django-модели Flight
на основе этих данных:
# Внешняя структура данных (например, ответ от стороннего API)
class ExternalFlightData:
def __init__(self, origin_code, dest_code, departure_time):
self.origin_code = origin_code
self.dest_code = dest_code
self.departure_time = departure_time
# Наша модель домена в Django (упрощённо)
from django.db import models
class Flight(models.Model):
origin_code = models.CharField(max_length=10)
dest_code = models.CharField(max_length=10)
departure_time = models.DateTimeField()
# Антикоррупционный слой – функция преобразования внешних данных в нашу модель
def import_flight(ext_data: ExternalFlightData) -> Flight:
flight = Flight(
origin_code=ext_data.origin_code,
dest_code=ext_data.dest_code,
departure_time=parse_datetime(ext_data.departure_time)
)
flight.save() # Сохраняем новый рейс в нашей БД
return flight
В этом примере функция import_flight
изолирует остальную систему от формата внешних данных: внешний JSON/XML парсится в ExternalFlightData
, а затем мы получаем привычный объект Flight
. Такой простой трансформации "на лету" часто достаточно, когда структуры данных похожи и преобразование прямолинейное.
Однако, преобразования могут быть и более сложными с отслеживанием состояний. Например, если внешняя система оперирует сущностями, для которых у нас нет прямых аналогов, либо требуется сопоставлять идентификаторы. В антикоррупционном слое можно вести таблицу соответствия (mapping) между внешними ID и нашими моделями. Или, к примеру, если данные должны агрегироваться или проходить несколько этапов обработки, ACL может хранить временное состояние. В Django это можно реализовать через дополнительную модель или кеш. Рассмотрим ситуацию: внешний сервис возвращает список рейсов с обновлениями, и мы хотим избежать дублирования при импортировании. Мы можем завести модель ExternalFlightMap
с полями external_id
и ссылкой на Flight
. При каждом новом импорте сначала проверяем, есть ли запись с таким external_id
. Если есть – обновляем существующую запись Flight
, если нет – создаём новую и сохраняем mapping. Такой подход иллюстрирует сложное преобразование с отслеживанием состояния – прослойка помнит, что уже было импортировано, и обеспечивает идемпотентность интеграции.
Другой стратегией интеграции является предоставление самим контекстом понятного интерфейса для других. Сервис с открытым протоколом (Open-host Service) означает, что наш контекст экспонирует определённый набор операций или API, которыми могут пользоваться другие команды или системыdev.to. В таком случае мы становимся поставщиком сервиса, а внешние системы – его потребителями. В отличие от антикоррупционного слоя, который мы создаём у себя для использования чужого контекста, открытый хост-сервис – это когда мы предлагаем внешний контракт.
Практически это реализуется как хорошо задокументированный API, библиотека или протокол. Например, сервис бронирования авиабилетов может предоставить REST API для получения доступных рейсов, бронирования билета и т.д. – таким образом другие контексты (например, сервис оплаты или внешние агенты по продаже билетов) интегрируются с нами через этот открытый интерфейс. Хост-сервис сам выполняет необходимые преобразования внутри, чтобы внешним клиентам было удобно: говорят, что используется опубликованный язык – общий формат, понятный всем потребителям. Наш сервис может возвращать данные о рейсах в общепринятом формате (например, коды аэропортов IATA, время в ISO8601 и пр.), даже если внутри у нас используются другие обозначения. Важно, что открытый протокол снижает дублирование функций – один контекст предоставляет функциональность многим сразуddd-practitioners.comdev.to.
В Django создать открытый сервис можно с помощью Django REST Framework или GraphQL. Например, определим эндпоинт /api/v1/flights/
для получения рейсов. Внутри в представлении или ViewSet мы получаем объекты Flight
(нашу модель) и сериализуем их в JSON по контракту, принятому внешне. Преобразование модели здесь тоже есть – но оно встроено в слой представления. Ниже приведён упрощённый пример сериализатора:
# Сериализатор для открытого API (Django REST Framework)
from rest_framework import serializers
class FlightSerializer(serializers.ModelSerializer):
class Meta:
model = Flight
fields = ['id', 'origin_code', 'dest_code', 'departure_time']
# Представление, возвращающее список рейсов через API
from rest_framework.viewsets import ReadOnlyModelViewSet
class FlightViewSet(ReadOnlyModelViewSet):
queryset = Flight.objects.all()
serializer_class = FlightSerializer
Здесь FlightSerializer
определяет, как наша модель представляется во внешнем формате (JSON). Клиент, вызывающий API, не знает ничего о нашей внутренней реализации – для него сервис с открытым протоколом выглядит как чёрный ящик с понятным интерфейсом. Такой подход упрощает интеграцию для потребителей, но важно помнить: мы обязуемся поддерживать этот открытый контракт. Изменения в нашем внутреннем сервисе должны либо быть обратно совместимыми, либо сопровождаться версионированием API, иначе потребители придётся менять свой код. Это и есть обратная сторона – некоторое увеличение связности между контекстами за счёт общего интерфейсаddd-practitioners.com.
Итого, антикоррупционный слой и открытый протокол – это два противоположных направления интеграции. В первом случае инициатор – наш контекст, и мы ставим щит перед влиянием чужой модели. Во втором – инициатор интеграции внешняя сторона, а мы предлагаем ей удобную «дверь» для взаимодействия с нашим доменом. Оба паттерна активно используются на практике для поддержки чистоты доменной модели и снижения когезии между bounded context.
В распределённых системах на основе микросервисов или контекстов, основанных на DDD, очень важно надёжно обмениваться событиями. Допустим, в нашем сервисе бронирования после успешного создания брони мы хотим отправить событие «Бронь создана» другим контекстам – например, чтобы сервис оплаты выставил счёт, а сервис уведомлений отправил письмо клиенту. Как гарантировать, что событие точно будет опубликовано при сохранении изменений, и, наоборот, что не уйдёт дубликат или «фантомное» событие, если операция не состоялась? Здесь на помощь приходит паттерн Outbox (паттерн исходящих сообщений).