Context Mapping(上下文映射)

是什麼?

Context Mapping 是 DDD 戰略設計中描述多個 Bounded Context 之間關係的工具。它定義了各 BC 之間的整合模式、權力關係和溝通方式,是系統架構的宏觀藍圖。

ℹ️DDD 戰略設計

Context Map 不是技術架構圖,而是團隊協作關係圖。每種映射模式反映的不只是技術整合方式,更反映了團隊之間的權力動態和協作模式。

核心觀念

常見誤區

⚠️常見誤區

誤區一:只畫技術架構圖(API 呼叫、Message Queue),不考慮團隊關係。Context Map 的核心是反映團隊之間的協作動態。

誤區二:所有 BC 之間都用 Shared Kernel。Shared Kernel 要求雙方團隊密切協調,成本很高,只適用於關係最緊密的 BC。

誤區三:忽略 Conformist 模式的存在。當你整合第三方 API(如 Stripe、LINE Pay)時,你就是 Conformist — 你無法要求對方改模型。

映射選擇流程

1

列出所有 BC

盤點系統中的所有 Bounded Context

2

識別依賴關係

找出哪些 BC 之間有資料或功能的依賴

3

判斷權力關係

誰是上游(提供者)?誰是下游(消費者)?雙方的議價能力如何?

4

選擇映射模式

根據團隊關係和技術需求,為每對 BC 選擇合適的模式

5

繪製 Context Map

畫出完整的上下文映射圖,標注每對 BC 的關係模式

程式碼範例

C# 版本

csharp
// === Anti-Corruption Layer 範例 ===
// 上游:第三方支付 API(你無法控制它的模型)
namespace ExternalPayment
{
    // 第三方的回應格式(你不能改)
    public class PaymentGatewayResponse
    {
        public string txn_id { get; set; }
        public string status_code { get; set; }    // "00" = 成功
        public long amount_cents { get; set; }       // 金額用「分」
        public string currency_code { get; set; }
    }
}
 
// 下游:你的 Billing BC,用 ACL 隔離
namespace Billing.Infrastructure.Acl
{
    // ACL:把第三方的模型翻譯成自己的
    public class PaymentGatewayAdapter : IPaymentGateway
    {
        private readonly ExternalPaymentClient _client;
 
        public PaymentGatewayAdapter(ExternalPaymentClient client)
        {
            _client = client;
        }
 
        public async Task<PaymentResult> ProcessPayment(Money amount, PaymentMethod method)
        {
            // 轉換成第三方的格式
            var request = new ExternalPaymentRequest
            {
                AmountCents = (long)(amount.Amount * 100),
                CurrencyCode = amount.Currency,
                Method = MapPaymentMethod(method),
            };
 
            var response = await _client.Charge(request);
 
            // ACL:把第三方回應翻譯成自己的 Domain 模型
            return new PaymentResult(
                TransactionId: new TransactionId(response.txn_id),
                Status: MapStatus(response.status_code),
                Amount: new Money(response.amount_cents / 100m, response.currency_code)
            );
        }
 
        private PaymentStatus MapStatus(string code) => code switch
        {
            "00" => PaymentStatus.Succeeded,
            "01" => PaymentStatus.Declined,
            _    => PaymentStatus.Failed,
        };
    }
}
 
// === Open Host Service 範例 ===
namespace Sales.Api
{
    // 對外公開的 API — Published Language
    [ApiController]
    [Route("api/v1/orders")]
    public class OrdersController : ControllerBase
    {
        [HttpGet("{id}")]
        public async Task<OrderDto> GetOrder(Guid id)
        {
            var order = await _orderRepository.GetById(id);
            return OrderDtoMapper.ToDto(order); // 用 DTO 對外,不暴露 Domain Model
        }
    }
}

TypeScript 版本

typescript
// === Anti-Corruption Layer ===
// 第三方支付的回應(你不能改)
interface PaymentGatewayResponse {
  txn_id: string;
  status_code: string; // "00" = 成功
  amount_cents: number;
  currency_code: string;
}
 
// ACL:翻譯成你的 Domain 模型
class PaymentGatewayAdapter implements PaymentGateway {
  constructor(private client: ExternalPaymentClient) {}
 
  async processPayment(amount: Money, method: PaymentMethod): Promise<PaymentResult> {
    const response = await this.client.charge({
      amountCents: Math.round(amount.amount * 100),
      currencyCode: amount.currency,
      method: this.mapMethod(method),
    });
 
    return {
      transactionId: new TransactionId(response.txn_id),
      status: this.mapStatus(response.status_code),
      amount: Money.create(response.amount_cents / 100, response.currency_code),
    };
  }
 
  private mapStatus(code: string): PaymentStatus {
    switch (code) {
      case "00": return PaymentStatus.Succeeded;
      case "01": return PaymentStatus.Declined;
      default:   return PaymentStatus.Failed;
    }
  }
}

Python 版本

python
# === Anti-Corruption Layer ===
from dataclasses import dataclass
 
@dataclass
class PaymentGatewayResponse:
    """第三方的回應(不能改)"""
    txn_id: str
    status_code: str
    amount_cents: int
    currency_code: str
 
class PaymentGatewayAdapter:
    """ACL:翻譯成 Domain 模型"""
    def __init__(self, client: ExternalPaymentClient):
        self._client = client
 
    async def process_payment(self, amount: Money, method: PaymentMethod) -> PaymentResult:
        response = await self._client.charge(
            amount_cents=int(amount.amount * 100),
            currency_code=amount.currency,
            method=self._map_method(method),
        )
        return PaymentResult(
            transaction_id=TransactionId(response.txn_id),
            status=self._map_status(response.status_code),
            amount=Money(response.amount_cents / 100, response.currency_code),
        )
 
    def _map_status(self, code: str) -> PaymentStatus:
        mapping = {"00": PaymentStatus.SUCCEEDED, "01": PaymentStatus.DECLINED}
        return mapping.get(code, PaymentStatus.FAILED)

關係圖

Sales BC
Customer-Supplier
Shipping BC
共享
Billing BC
ACL (Conformist)
第三方支付 (External)
Inventory BC
Shared Kernel (共用型別)

此圖展示一個典型電商系統的 Context Map:Sales BC 是核心,與 Shipping、Billing 是 Customer-Supplier 關係。Billing 整合第三方支付時用 ACL 保護自己。Sales 和 Inventory 透過 Domain Event 解耦。Sales 和 Shipping 共享少量型別(Shared Kernel)。每條連線的標籤代表一種映射模式。

映射模式選擇決策表

| 你和上游的關係 | 推薦模式 | 理由 | |---------------|---------|------| | 同一個團隊、緊密協作 | Shared Kernel | 溝通成本低,共享少量模型可以減少重複 | | 不同團隊、上游願意配合 | Customer-Supplier | 上游會考慮你的需求,但決定權在上游 | | 上游完全無法控制(第三方 API) | Conformist + ACL | 遵從對方格式,但用 ACL 保護自己的 Domain Model | | 你是平台,有很多消費者 | Open Host Service + Published Language | 提供標準化、版本化的 API | | 兩個 BC 完全獨立、無互動 | Separate Ways(各走各的) | 不需要任何映射 |

實戰補充

💡資深開發者筆記

在實務中,最常用到的三種模式是 Anti-Corruption LayerCustomer-Supplier、和 Open Host Service

ACL 是必修課:只要你整合第三方系統(支付、物流、ERP),就需要 ACL。把第三方的 SDK 或 API Client 封裝在 Infrastructure 層,用你自己的 Domain Interface 隔離。

在 .NET 專案中,ACL 的實作通常是一個 Adapter 類別,實作 Domain 層定義的介面,放在 Infrastructure 專案中。DI Container 負責把 Adapter 注入到 Application Service。

畫 Context Map 的最好時機是系統設計初期和每次大型重構之前。用 Miro 或 draw.io 畫一張圖,貼在團隊 Wiki 上,所有人都能看到系統的宏觀架構。

理解測驗

🤔 當你整合一個你無法控制的第三方 API 時,最適合用哪種模式?

🤔 Shared Kernel 的主要風險是什麼?

🤔 Context Map 反映的是什麼?

重點整理

💡一句話記住

口訣:「Context Map 是外交地圖——畫出誰聽誰的、誰保護自己、誰共享資源」。

| 模式 | 說明 | |------|------| | Shared Kernel | 共享一小部分模型,修改需雙方同意 | | Customer-Supplier | 上游提供、下游消費,上游有決定權 | | Conformist | 下游完全採用上游模型,不做轉換 | | ACL | 下游建轉換層,隔離上游模型 | | Open Host Service | 上游提供公開 API 讓多方整合 | | Published Language | 用標準格式定義交換的資料結構 |

你可能也想看

Bounded ContextRepository Pattern

按 ← → 鍵切換課程