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 任何 infrastructure:OrderRepository 介面只描述「我需要什麼操作」,不知道是 PostgreSQL 還是 MongoDB。
Use Case 只依賴 port:CreateOrderUseCase 接收介面,不知道具體實作是什麼——測試時可以傳入 in-memory mock,生產時傳入 Postgres 實作。
Adapter 在最外層,知道所有技術細節:PostgresOrderRepository 知道 SQL、連線池、transaction——這些細節不應該出現在 domain 或 use case 裡。
組裝在 composition root:只有程式進入點(main / app factory)知道「用哪個 adapter」,其他所有地方只看到介面。
這個骨架在任何有介面概念的語言裡都能實現,語法是表面差異。