Інтеграція обмежених контекстів
Інтеграція обмежених контекстів (Bounded Context Integration)
Вступ
У попередній главі ми дізнались, що Обмежені контексти (Bounded Contexts) захищають узгодженість Єдиної мови всередині своїх меж і відкривають можливості до побудови моделей. Ми розуміємо, що:
- Модель не може існувати без меж
- Єдина мова діє лише всередині контексту
- Різні контексти можуть мати різні моделі одних і тих самих бізнес-сутностей
Проблема інтеграції
Коли два Обмежені контексти потребують взаємодії, виникають фундаментальні питання:
Що ми вивчимо?
У цій главі розглянемо паттерни інтеграції DDD, які визначаються характером співпраці між командами:
Крок 1: Співпраця (Cooperation)
Паттерни для команд з щільною взаємодією: Partnership та Shared Kernel.
Крок 2: Споживач-Постачальник (Customer-Supplier)
Паттерни з явним балансом сил: Conformist, ACL, OHS.
Крок 3: Різні шляхи (Separate Ways)
Коли команди вирішують НЕ інтегруватися.
Крок 4: Карта контекстів
Візуалізація інтеграцій і командних зв'язків.
Співпраця (Cooperation Patterns)
Паттерни співпраці стосуються Обмежених контекстів, над якими працюють команди з добре налагодженою взаємодією.
Характеристики співпраці
| Аспект | Опис |
|---|---|
| Спілкування | Щ density, часта синхронізація |
| Цілі | Взаємозалежні, спільний успіх |
| Відповідальність | Спільна за інтеграцію |
| Конфлікти | Вирішуються через обговорення |
| Гнучкість | Висока, обидві сторони адаптуються |
- Обидва контексти належать одній команді
- Команди працюють в одному офісі
- Цілі бізнес-команд тісно пов'язані
- Є можливість для частих зустрічей
Партнерство (Partnership Pattern)
Partnership — найпростіший паттерн співпраці. Інтеграція координується ситуативно, "на льоту".
Як працює партнерство
✅ Гнучкість: Зміни обговорюються та узґоджуються
✅ Без конфліктів: Команди співпрацюють без драм
✅ Синхронізація: Часта і безперервна
Приклад: Управління замовленнями та доставкою
Уявімо інтернет-магазин з двома контекстами:
Контекст Замовлень
Контекст Доставки
Сценарій інтеграції:
Крок 1: Зміна в контексті Замовлень
Команда Замовлень вирішує додати нове поле preferredDeliveryTime до моделі Order.
Крок 2: Уведомлення команди Доставки
Команда Замовлень повідомляє: "Ми додаємо час бажаної доставки. Вам це потрібно?"
Крок 3: Обговорення
Обидві команди обговорюють:
- Чи може Доставка використати цю інформацію?
- Як буде виглядати API контракт?
- Хто і коли внесе зміни?
Крок 4: Узгоджена імплеметація
Обидві команди адаптують свої моделі разом, у дусі партнерства.
Технічна реалізація
// Контекст Замовлень
interface OrderCreatedEvent {
orderId: string
customerId: string
items: OrderItem[]
deliveryAddress: Address
preferredDeliveryTime?: Date // Нове поле
}
// Публікація події
eventBus.publish('order.created', orderCreatedEvent)
// Контекст Доставки
eventBus.subscribe('order.created', (event: OrderCreatedEvent) => {
const shipment = новийПоле Shipment({
orderId: event.orderId,
address: event.deliveryAddress,
preferredTime: event.preferredDeliveryTime, // Використання нового поля
});
shipment Booking.schedule();
});
Переваги партнерства
Швидка адаптація
Спільна відповідальність
Узгодженість
Недоліки та обмеження
❌ Різні пріоритети: Команди конкурують за ресурси
❌ Великі команди: Координація стає складною
❌ Відсутність довіри: Команди не готові співпрацювати
Практичні поради
- Непрерывна інтеграція (CI): Автоматизуйте тести інтеграції
- Короткі цикли: Синхронізуйтесь щодня або щотижня
- Спільні зустрічі: Регулярні стендапи або планування
- Документація: Фіксуйте домовленості про контракти
- Повага до змін: Не нав'язуйте зміни без обговорення
Спільне ядро (Shared Kernel Pattern)
Shared Kernel — паттерн, за якого кілька Обмежених контекстів розділяють спільну модель або її частину.
Концепція Спільного ядра
Приклад: Корпоративна система авторизації
Розглянемо систему з власною моделлю управління правами доступу (Authorization Model).
Проблема:
Кілька конт екстів потребують:
- Перевірки прав користувача
- Управління ролями
- Аудиту доступу
Рішення: Спільне ядро AuthorizationKernel
// Спільне ядро (Shared Kernel)
namespace Company.AuthorizationKernel
{
public class User
{
public UserId Id { get; private set; }
public List<Role> Roles { get; private set; }
public List<Permission> DirectPermissions { get; private set; }
public bool HasPermission(Permission permission)
{
// Логіка перевірки: прямі дозволу + успадковані від ролей
return DirectPermissions.Contains(permission) ||
Roles.A ny(r => r.Permissions.Contains(permission));
}
}
public class Role
{
public RoleId Id { get; private set; }
public string Name { get; private set; }
public List<Permission> Permissions { get; private set; }
}
public record Permission(string Resource, string Action);
}
Використання в різних контекстах:
//Контекст Sales
namespace Company.SalesContext
{
using Company.AuthorizationKernel;
public class SalesOrderService
{
public void CreateOrder(OrderData data, User user)
{
// Використання Спільного ядра
if (!user.HasPermission(new Permission("Orders", "Create")))
throw new UnauthorizedException();
//Основни logic...
}
}
}
// Контекст Inventory
namespace Company.InventoryContext
{
using Company.AuthorizationKernel;
public class WhaleouseService
{
public void AdjustStock(StockData data, User user)
{
// Використання того самого Спільного ядра
if (!user.HasPermission(new Permission(" Stock", "Adjust")))
throw new UnauthorizedException();
// inventory logic...
}
}
}
Обмеження області дії (Shared Scope)
Чому? Модель з перекриттям зв'язує життєві цикли. Зміна в Спільному ядрі впливає на всі контексти.
Технічна реалізація
Варіант 1: Монорепозиторій
/company-monorepo
/packages
/authorization-kernel ← Спільне ядро
- User.cs
- Role.cs
- Permission.cs
/sales-context ← Контекст A
- SalesOrder.cs
- ProductCatalog.cs
/inventory-context ← Контекст B
- Warehouse.cs
- StockItem.cs
Всі контексти посилаються на однакові вихідні файли.
Варіант 2: Окрема бібліотека
# Спільне ядро як NuGet пакет
dotnet pack Authorization Kernel.csproj
dotnet nuget push AuthorizationKernel.1.0.0.nupkg
<!-- У контексті Sales -->
<PackageReference Include="Company.AuthorizationKernel" Version="1.0.0" />
<!-- У контексті Inventory -->
<PackageReference Include="Company.AuthorizationKernel" Version="1.0.0" />
Безперервна інтеграція
# GitHub Actions приклад
name: Shared Kernel CI
on:
push:
paths:
- 'packages/authorization-kernel/**'
jobs:
test-all-contexts:
runs-on: ubuntu-latest
steps:
- name: Test Sales Context
run: dotnet test packages/sales-context/tests
- name: Test Inventory Context
run: dotnet test packages/inventory-context/tests
- name: Test Reporting Context
run:ії dotnet test packages/reporting-context/tests
Коли використовувати Спільне ядро
Якщо інтеграція змін у двох окремих моделях складніша, ніж координація змін у спільній кодовій базі.
Приклад: Складна бізнес-логіка, яка часто змінюється (Core Subdomain).
::accordion Item{title="Сценарій 2: Одна команда, кілька контекстів"} Коли одна команда володіє кількома контекстами і хоче явно визначити інтеграційні контракти.
Приклад: Мікросервісна архітектура з однієї командою. ::
Тимчасове рішення під час розбиття монолітної системи на Обмежені контексти.
Приклад: Поступовий refactoring великої кодової бази.
Коли команди не можуть реалізувати Partnership через географічні або організаційні обмеження.
Приклад: Розподілені команди у різних часових поясах.
Коли НЕ використовувати
❌ Спільне ядро між багатьма (>3) контекстами — Надмірна зв'язаність
❌ Спільне ядро для різних команд без координації — Chaos!
❌ Використання дляBasic моделей (Generic Subdomains) — Краще купити готове рішення
Споживач-Постачальник (Customer-Supplier Patterns)
Друга група паттернів стосується відносин, за яких один контекст (Постачальник, Supplier) надає послуги іншому (Споживач, Customer).
Ключова різниця з Cooperation
| Аспект | Cooperation | Customer-Supplier |
|---|---|---|
| Успіх команд | Взаємоза лежний | Незалежний |
| Баланс сил | Рівний | Дисбаланс |
| Хто створює контракт | Обидві разом | Одна із сторін |
| Адаптація | Спільна | Одностороння |
Конформіст (Conformist Pattern)
Conformist — паттерн, за якого Downstream команда приймає модель Upstream команди без змін.
Концепція
Коли це виправдано?
Стандартна модель
Підходяща модель
Зовнішній постачальник
Приклад: Інтеграція з Stripe
Уявімо, що наш контекст Payments інтегрується зі Stripe для обробки платежів.
// Stripe API Model (Upstream)
interface StripeCharge {
id: string
amount: number // В центах!
currency: string
status: 'succeeded' | 'pending' | 'failed'
customer: string
created: number // Unix timestamp
}
// Наш контекст Payments (Downstream - Conformist)
class PaymentService {
async processPayment(amount: number, currency: string, customerId: string) {
// Використовуємо модель Stripe безпосередньо
const charge: StripeCharge = await stripe.charges.create({
amount: amount * 100, // Конвертуємо в центи
currency: currency,
customer: customerId,
})
// Зберігаємо заряд у форматі Stripe
await db.charges.insert({
stripeId: charge.id,
amount: charge.amount, // В центах, як у Stripe
currency: charge.currency,
status: charge.status, // Використовуємо статуси Stripe
createdAt: new Date(charge.created * 1000),
})
return charge
}
}
Переваги Conformist
✅ Швидкість розробки: Менше коду
✅ Стабільність: Використання перевіреної моделі
✅ Підтримка: Upstream забезпечує документацію
Недоліки
❌ Залежність: Зміни Upstream впливають на Downstream
❌ Незручна модель: Може не ідеально підходити для бізнес-потреб
❌ Обмеження: Неможливість адаптувати під свою Єдину мову
Предохоронний слой (Anti-Corruption Layer Pattern)
ACL (Anti-Corruption Layer) — паттерн, який ізолює Downstream контекст від моделі Upstream через проміжний шар перетворення.
Концепція
Коли використовувати ACL?
Проблема: Downstream містить Core Subdomain, який потребує ідеальної моделі.
Рішення: ACL дозволяє зберегти чистоту модели Core, незалежно від Upstream.
Приклад: Система ціноутворення як Core — не може залежати від структури даних зовнішнього постачальника.
Проблема: Модель Upstream плутана, застаріла або погано спроектована.
Рішення: ACL перетворює "безлад" у чітку модель Downstream.
Приклад: Інтеграція з Legacy-системою з громіздківеличними структурами даних.
Проблема: Upstream постійно змінює свій API.
Рішення: ACL ізолює ці зміни, модель Downstream залишається стабільною.
Приклад: Інтеграція з третьою стороною, що часто оновлює API.
Приклад: Legacy CRM Integration
Уявімо, що нашій сучасній системі потрібно інтегруватися з застарілою CRM.
Legacy CRM Model (Upstream — лиш):
<!-- Legacy XML Response -->
<Customer_Record>
< ID>12345</ID>
<FN>John</FN>
<LN>Doe</LN>
<Email_Address>john.doe@example.com</Email_Address>
<Phone_Num>+1234567890</Phone_Num>
<Tags>VIP,Premium,Enterprise</Tags>
<Created_TS>1609459200</Created_TS>
</Customer_Record>
Наша модель (Downstream — чиста):
// Наша чиста модель предметної області
class Customer {
constructor(
public readonly id: CustomerId,
public readonly name: PersonName,
public readonly email: Email,
public readonly phone: PhoneNumber,
public readonly segments: CustomerSegment[],
public readonly registeredAt: Date,
) {}
isVIP(): boolean {
return this.segments.includes(CustomerSegment.VIP)
}
}
// Value Objects
class PersonName {
constructor(
public readonly firstName: string,
public readonly lastName: string,
) {}
get fullName(): string {
return `${this.firstName} ${this.lastName}`
}
}
Предохоронний слой:
// ACL: Adapter для Legacy CRM
class LegacyCRMAdapter {
constructor(private legacyClient: Leg acyCRMClient) {}
async getCustomer(id: string): Promise<Customer> {
// Отримуємо дані з Legacy
const legacyData = await this.legacyClient.fetchCustomerXML(id);
// Трансляція в нашу модель
return this.translate(legacyData);
}
private translate(legacy: LegacyCustomerXML): Customer {
return new Customer(
new CustomerId(legacy.ID),
new PersonName(legacy.FN, legacy.LN),
new Email(legacy.Email_Address),
new PhoneNumber(legacy.Phone_Num),
this.parseSegments(legacy.Tags),
new Date(parseInt(legacy.Created_TS) * 1000),
);
}
private parseSegments(tags: string): CustomerSegment[] {
return tags.split(',').map(tag => {
switch (tag.trim()) {
case 'VIP': return CustomerSegment.VIP;
case 'Premium': return CustomerSegment.PREMIUM;
case 'Enterprise': return CustomerSegment.ENTERPRISE;
default: return CustomerSegment.REGULAR;
}
});
}
}
// Використання в додатку
class CustomerService {
constructor(private crm: LegacyCRMAdapter) {}
async getCustomerDetails(id: string): Promise<Customer> {
// Робота з чистою моделлю, незалежно від Legacy!
const customer = await this.crm.getCustomer(id);
if (customer.isVIP()) {
// Бізнес-логіка використовує чисту модель
await this.апplyVIPDiscount(customer);
}
return customer;
}
}
Структура ACL
PreдохоронПівний слой зазвичай складається з трьох компонентів:
Adapter (Адаптер)
Відповідає за підключення до Upstream API.
class UpstreamAPIAdapter {
async fetchData(): Promise<UpstreamModel> {
// HTTP, gRPC, чи інший протокол
}
}
Translator (Перекладач)
Перетворює модель Upstream у модель Downstream.
class ModelTranslator {
translate(upstream: UpstreamModel): DownstreamModel {
// Логіка перетворення
}
}
Facade (Фасад)
Надає зручний інтерфейс для Downstream контексту.
class UpstreamFacade {
constructor(
private adapter: UpstreamAPIAdapter,
private translator: ModelTranslator,
) {}
async getData(): Promise<DownstreamModel> {
const upstream = await this.adapter.fetchData()
return this.translator.translate(upstream)
}
}
Переваги ACL
Ізоляція
Чиста єдина мова
Гнучкість
Недоліки
❌ Продуктивність: Додатковий шар перетворення
❌ Вартість: Потребує більше часу на розробку
Сервіс з Відкритим Протоколом (Open-Host Service Pattern)
OHS (Open-Host Service) — паттерн, за якого Upstream захищає Downstream споживачів, надаючи стабільний, зручний публічний API.
Концепція
Published Language
Published Language (Опублікований язик) — це інтерфейс, орієнтований на інтеграцію, а не на внутрішню Єдину мову.
// Внутрішня модель Upstream (НЕ доступна зовні)
class InternalProduct {
private id: ProductId
private details: ProductDetails
private pricing: PricingModel
private inventory: InventoryTracking
// Складна internal logic
}
// Published Language (Публічний інтерфейс)
interface ProductAPI {
id: string
name: string
price: {
amount: number
currency: string
}
availability: 'in_stock' | 'out_of_stock' | 'pre_order'
}
// Upstream API Controller
class ProductController {
async getProduct(id: string): Promise<ProductAPI> {
const internal = await repo.find(id)
// Перекладання Internal Model → Published Language
return {
id: internal.id.value,
name: internal.details.name,
price: {
amount: internal.pricing.currentPrice.amount,
currency: internal.pricing.currentPrice.currency,
},
availability: this.mapAvailability(internal.inventory.status),
}
}
}
Versioning: Підтримка кількох версій
Одна з переваг OHS — можливість підтримки кількох версій API одночасно.
// API v1 (Legacy)
app.get('/api/v1/products/:id', async (req, res) => {
const product = await service.getProduct(req.params.id)
res.json({
product_id: product.id,
product_name: product.name,
price_amount: product.price.amount,
})
})
// API v2 (Новий)
app.get('/api/v2/products/:id', async (req, res) => {
const product = await service.getProduct(req.params.id)
res.json({
id: product.id,
name: product.name,
pricing: product.price,
availability: product.availability,
metadata: {
version: '2.0',
lastUpdated: new Date().toISOString(),
},
})
})
Порівняння: ACL vs OHS
| Аспект | ACL | OHS |
|---|---|---|
| Хто перекладає | Downstream (Споживач) | Upstream (Постачальник) |
| Баланс сил | Upstream сильніший | Downstream сильніший |
| Відповідальність | Споживач захищає себе | Постачальник захищає споживачів |
| Складність | У Downstream | У Upstream |
Різні Шляхи (Separate Ways Pattern)
Separate Ways — паттерн повного відмови від інтеграції. Команди вирішують працювати незалежно і дублюють функціональність.
Коли це виправдано?
Ситуація: Команди не можуть ефективно співпрацювати через:
- Географічну віддаленість
- Організаційну політику
- Культурні або мовні бар'єри
Рішення: Дублювати функціональність у кожному контексті.
Ситуація: Піддомен є Generic і доступне просте рішення.
Приклад: Логтування, моніторинг, автентифікація.
Рішення: Кожен контекст інтегрує готове рішення локально (наприклад, Serilog, Winston).
Ситуація: Моделі настільки різні, що:
- Conformist неможливий
- ACL надто складний та дорогий
Рішу: Дублювати функціональність окремо.
Приклад: Логування
// Контекст Orders
import winston from 'winston';
const logger = winston.create Logger();
class OrderService {
createOrder(data: OrderData) {
logger.info('Order created', { orderId: data.id });
// ...
}
}
// Контекст Payments
import pino from 'pino';
const logger = pino();
class PaymentService {
processPayment(data: PaymentData) {
logger.info({ paymentId: data.id }, 'Payment processed');
// ...
}
}
Обидва контексти використовують різні бібліотеки логування, кожна інтегрована локально.
Обмеження
Карта контекстів (Context Map)
Context Map — візуальне представлення Обмежених контекстів системи та їх інтеграцій.
Цінність Context Map
Високорівневе бачення
Модці комунікації
Організаційні проблеми
Приклад реальної Context Map
[Product Catalog] --OHS--> [Order Management]
|
+--Partnership--> [Payment Processing]
|
+--Partnership--> [Shipping & Delivery]
[Customer CRM] --ACL--> [Order Management]
[Analytics] --Conformist--> [Order Management]
--Conformist--> [Product Catalog]
[Inventory] --Separate Ways-- [Shipping]
Підтримка Context Map
- Як код: Використовуйте Context Mapper або PlantUML
- Living Document: Оновлюйте при змінах
- Shared Responsibility: Кожна команда оновлює свої інтеграції
- Version Control: Зберігайте в Git
Резюме та ключові думки
У цій главі ми вивчили паттерни інтеграції Обмежених контекстів, класифіковані за типом командної співпраці.
typeПаттерни співпраці (Cooperation)
- Partnership: Двостороння координація, щільна співпраця
- Shared Kernel: Спільна модель, використовувати обережно
Споживач-Постачальник (Customer-Supplier)
- Conformist: Downstream приймає модель Upstream
- ACL: Downstream захищає себе через трансляцію
- OHS: Upstream захищає Downstream через Published Language
Різні офілії (Separate Ways)
- Відмова від інтеграції, дублювання функціональності
- НЕ використовувати для Core Subdomains!
Context Map
- Візуалізація інтеграцій та командних зв'язків
- Інструмент стратегічного аналізу
Вибір паттерну
- Оцініть баланс сил: Хто диктує умови?
- Чи є автономія: Наскільки важлива незалежність моделі?
- Які ресурси: Скільки часу/грошей є на інтеграцію?
- Командна динаміка: Як добре команди співпрацюють?
- Тип піддомена: Core, Generic чи Supporting?
Зв
'язок з наступними темами
Тепер, коли ми розуміємо, як интегруються Обмежені контексти на стратегічному рівні, наступним кроком є вивчення тактичного проектування — як реалізовувати бізнес-логіку всередині контексту.
- Паттерни реалізації бізнес-логіки (Transaction Script, Domain Model)
- Architectурні паттерни (Layered, Hexagonal, CQRS)
- Технічну інтеграцію (Messaging, Sagas)