Clean Architecture 的同心圓是語言無關的——Entity、Use Case、Interface Adapter、Framework & Driver 這四層的分工,在任何語言裡都成立。差別只是每個語言用什麼方式表達「介面」和「依賴注入」。

這篇用同一個題目(建立訂單 use case)在三種語言裡實作,讓你看到跨語言的共通骨架。


題目:建立訂單 Use Case

業務規則:

  • 訂單金額必須 > 0
  • 庫存足夠才能建立
  • 建立成功後通知用戶

這個 Use Case 需要:OrderRepository(查 / 存訂單)、InventoryRepository(查庫存)、NotificationService(發通知)——三個 port,都是介面,實作在外層。


Node.js / TypeScript

TypeScript 用 interface 直接表達 port,class implements 實作 adapter:

// domain/entities/Order.ts
export class Order {
  constructor(
    public readonly id: string,
    public readonly amount: number,
    public readonly userId: string,
  ) {
    if (amount <= 0) throw new Error('Amount must be positive')
  }
}
 
// domain/ports/OrderRepository.ts
export interface OrderRepository {
  save(order: Order): Promise<void>
}
 
// domain/ports/InventoryRepository.ts
export interface InventoryRepository {
  checkAvailability(productId: string, qty: number): Promise<boolean>
}
 
// application/use-cases/CreateOrder.ts
export class CreateOrderUseCase {
  constructor(
    private orders: OrderRepository,
    private inventory: InventoryRepository,
    private notifications: NotificationService,
  ) {}
 
  async execute(cmd: CreateOrderCommand): Promise<void> {
    const available = await this.inventory.checkAvailability(cmd.productId, cmd.qty)
    if (!available) throw new Error('Insufficient inventory')
 
    const order = new Order(generateId(), cmd.amount, cmd.userId)
    await this.orders.save(order)
    await this.notifications.notify(cmd.userId, `訂單 ${order.id} 已建立`)
  }
}
 
// infrastructure/PostgresOrderRepository.ts
export class PostgresOrderRepository implements OrderRepository {
  constructor(private db: Database) {}
  async save(order: Order) {
    await this.db.query('INSERT INTO orders ...', [order.id, order.amount])
  }
}

依賴注入在 composition root(main.ts / DI container)組裝:

const useCase = new CreateOrderUseCase(
  new PostgresOrderRepository(db),
  new PostgresInventoryRepository(db),
  new EmailNotificationService(smtp),
)

Go

Go 沒有 class 繼承,用 implicit interface(只要有對應的 method 就算實作了 interface)表達 port:

// domain/order.go
type Order struct {
    ID     string
    Amount float64
    UserID string
}
 
func NewOrder(id string, amount float64, userID string) (*Order, error) {
    if amount <= 0 {
        return nil, errors.New("amount must be positive")
    }
    return &Order{ID: id, Amount: amount, UserID: userID}, nil
}
 
// domain/ports.go
type OrderRepository interface {
    Save(ctx context.Context, order *Order) error
}
 
type InventoryRepository interface {
    CheckAvailability(ctx context.Context, productID string, qty int) (bool, error)
}
 
// application/create_order.go
type CreateOrderUseCase struct {
    orders        OrderRepository
    inventory     InventoryRepository
    notifications NotificationService
}
 
func (uc *CreateOrderUseCase) Execute(ctx context.Context, cmd CreateOrderCommand) error {
    ok, err := uc.inventory.CheckAvailability(ctx, cmd.ProductID, cmd.Qty)
    if err != nil || !ok {
        return errors.New("insufficient inventory")
    }
    order, err := NewOrder(newID(), cmd.Amount, cmd.UserID)
    if err != nil {
        return err
    }
    if err := uc.orders.Save(ctx, order); err != nil {
        return err
    }
    return uc.notifications.Notify(ctx, cmd.UserID, "訂單已建立")
}
 
// infrastructure/postgres_order_repository.go
type PostgresOrderRepository struct { db *sql.DB }
 
func (r *PostgresOrderRepository) Save(ctx context.Context, order *Order) error {
    _, err := r.db.ExecContext(ctx, "INSERT INTO orders ...", order.ID, order.Amount)
    return err
}

Go 的 implicit interface 讓 infrastructure 層不需要 implements 關鍵字——只要有對應的 method signature,就自動滿足 interface。這讓 domain port 和 infrastructure adapter 可以完全獨立定義,互不 import。


Python

Python 用 ABC(Abstract Base Class)或 Protocol(Python 3.8+)表達 port:

# domain/entities.py
from dataclasses import dataclass
 
@dataclass
class Order:
    id: str
    amount: float
    user_id: str
 
    def __post_init__(self):
        if self.amount <= 0:
            raise ValueError("Amount must be positive")
 
# domain/ports.py
from abc import ABC, abstractmethod
 
class OrderRepository(ABC):
    @abstractmethod
    async def save(self, order: Order) -> None: ...
 
class InventoryRepository(ABC):
    @abstractmethod
    async def check_availability(self, product_id: str, qty: int) -> bool: ...
 
# application/create_order.py
class CreateOrderUseCase:
    def __init__(
        self,
        orders: OrderRepository,
        inventory: InventoryRepository,
        notifications: NotificationService,
    ):
        self._orders = orders
        self._inventory = inventory
        self._notifications = notifications
 
    async def execute(self, cmd: CreateOrderCommand) -> None:
        available = await self._inventory.check_availability(cmd.product_id, cmd.qty)
        if not available:
            raise ValueError("Insufficient inventory")
        order = Order(id=new_id(), amount=cmd.amount, user_id=cmd.user_id)
        await self._orders.save(order)
        await self._notifications.notify(cmd.user_id, "訂單已建立")
 
# infrastructure/postgres_order_repository.py
class PostgresOrderRepository(OrderRepository):
    def __init__(self, pool):
        self._pool = pool
 
    async def save(self, order: Order) -> None:
        async with self._pool.acquire() as conn:
            await conn.execute("INSERT INTO orders ...", order.id, order.amount)

三種語言的共通骨架

語法不同,但以下幾件事在三種語言裡完全一樣:

Port 定義在 domain 層,不 import 任何 infrastructureOrderRepository 介面只描述「我需要什麼操作」,不知道是 PostgreSQL 還是 MongoDB。

Use Case 只依賴 portCreateOrderUseCase 接收介面,不知道具體實作是什麼——測試時可以傳入 in-memory mock,生產時傳入 Postgres 實作。

Adapter 在最外層,知道所有技術細節PostgresOrderRepository 知道 SQL、連線池、transaction——這些細節不應該出現在 domain 或 use case 裡。

組裝在 composition root:只有程式進入點(main / app factory)知道「用哪個 adapter」,其他所有地方只看到介面。

這個骨架在任何有介面概念的語言裡都能實現,語法是表面差異。