Введение

В современном Domain-Driven Design (DDD) при построении сложных систем часто возникает необходимость интеграции между разными подсистемами или Bounded Context-ами. Цель данной главы – изучить ключевые паттерны интеграции и способы взаимодействия между контекстами. Мы рассмотрим, как трансформировать модели данных при интеграции, как надёжно публиковать события между сервисами, и как координировать бизнес-процессы, охватывающие несколько контекстов. В частности, мы обсудим преобразование моделей (антикоррупционный слой и открытый протокол), шаблон надёжной публикации событий (Outbox), а также паттерны координации процессов, такие как сага и диспетчер процессов. Все темы будут сопровождаться понятными примерами на Python с использованием Django – в качестве бизнес-домена возьмём сервис бронирования авиабилетов.

Преобразование моделей для интеграции

Интеграция между контекстами требует трансформации данных – чтобы различные части системы могли обмениваться информацией, не искажая модель друг друга. DDD предлагает несколько стратегий для этого. Рассмотрим два важных паттерна: антикоррупционный слой и сервис с открытым протоколом. Оба подходят для разных ситуаций интеграции, но оба используют преобразование моделей, чтобы связать контексты и при этом защитить целостность внутренних доменных моделей.

Антикоррупционный слой (Anticorruption Layer)

Когда наш контекст должен взаимодействовать с внешней системой или 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)

Другой стратегией интеграции является предоставление самим контекстом понятного интерфейса для других. Сервис с открытым протоколом (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.

Публикация событий: паттерн исходящих сообщений (Outbox)