ストーリー
Driving Port(入力ポート)の設計
Driving Portは「アプリケーションが提供する機能」を定義します。Use Case単位でインターフェースを切ります。
設計原則: 1 Use Case = 1 Port
// domain/ports/in/CreateOrderUseCase.ts
interface CreateOrderUseCase {
execute(command: CreateOrderCommand): Promise<OrderId>;
}
interface CreateOrderCommand {
customerId: string;
items: Array<{ productId: string; quantity: number }>;
shippingAddress: string;
}
// domain/ports/in/CancelOrderUseCase.ts
interface CancelOrderUseCase {
execute(command: CancelOrderCommand): Promise<void>;
}
interface CancelOrderCommand {
orderId: string;
reason: string;
}
// domain/ports/in/GetOrderUseCase.ts
interface GetOrderUseCase {
execute(query: GetOrderQuery): Promise<OrderDto>;
}
interface GetOrderQuery {
orderId: string;
}
なぜUse Case単位なのか
// NG: 1つのPortに複数のUse Caseを詰め込む
interface OrderUseCase {
create(command: CreateOrderCommand): Promise<OrderId>;
cancel(command: CancelOrderCommand): Promise<void>;
get(query: GetOrderQuery): Promise<OrderDto>;
list(query: ListOrdersQuery): Promise<OrderDto[]>;
update(command: UpdateOrderCommand): Promise<void>;
}
// インターフェース分離の原則(ISP)に違反
// OK: Use Case単位で分離
interface CreateOrderUseCase {
execute(command: CreateOrderCommand): Promise<OrderId>;
}
// 各Adapterは必要なUse Caseだけに依存できる
Driven Port(出力ポート)の設計
Driven Portは「アプリケーションが必要とする外部機能」を定義します。
Repository Port
// domain/ports/out/OrderRepository.ts
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
save(order: Order): Promise<void>;
delete(id: OrderId): Promise<void>;
findByCustomerId(customerId: string): Promise<Order[]>;
existsById(id: OrderId): Promise<boolean>;
}
設計のポイント
// NG: ORMの都合がPortに漏れている
interface OrderRepository {
findOne(where: { id?: string; status?: string }): Promise<Order>;
createQueryBuilder(): QueryBuilder; // ORMの具体的なAPIが漏れている
}
// OK: ドメインの言葉でインターフェースを定義
interface OrderRepository {
findById(id: OrderId): Promise<Order | null>;
findPendingOrders(): Promise<Order[]>; // ドメインの概念
findByDateRange(from: Date, to: Date): Promise<Order[]>;
}
外部サービスPort
// domain/ports/out/PaymentGateway.ts
// 決済サービスへのPort -- 特定のサービス名を使わない
interface PaymentGateway {
charge(amount: Money, method: PaymentMethod): Promise<PaymentResult>;
refund(paymentId: string, amount: Money): Promise<RefundResult>;
}
// NG: 特定のサービスに依存した命名
interface StripeGateway {
createPaymentIntent(params: StripeIntentParams): Promise<StripeIntent>;
}
// domain/ports/out/InventoryChecker.ts
interface InventoryChecker {
checkAvailability(productId: string, quantity: number): Promise<boolean>;
reserve(productId: string, quantity: number): Promise<ReservationId>;
release(reservationId: ReservationId): Promise<void>;
}
// domain/ports/out/NotificationSender.ts
interface NotificationSender {
sendOrderConfirmation(order: Order): Promise<void>;
sendShippingNotification(order: Order, trackingNumber: string): Promise<void>;
}
Command / Query パターン
Driving Portの入力にはCommandオブジェクトやQueryオブジェクトを使います。
// Command: 状態を変更する操作
interface CreateOrderCommand {
readonly customerId: string;
readonly items: ReadonlyArray<{ productId: string; quantity: number }>;
readonly shippingAddress: string;
}
// Query: 状態を読み取る操作
interface GetOrderQuery {
readonly orderId: string;
}
interface ListOrdersQuery {
readonly customerId: string;
readonly status?: OrderStatus;
readonly page: number;
readonly limit: number;
}
// DTO: Use Caseが返す結果
interface OrderDto {
id: string;
customerId: string;
status: string;
totalAmount: number;
currency: string;
items: OrderItemDto[];
createdAt: string;
}
Portの配置
src/
└── domain/
└── ports/
├── in/ # Driving Ports
│ ├── CreateOrderUseCase.ts
│ ├── CancelOrderUseCase.ts
│ ├── GetOrderUseCase.ts
│ └── ListOrdersUseCase.ts
└── out/ # Driven Ports
├── OrderRepository.ts
├── PaymentGateway.ts
├── InventoryChecker.ts
└── NotificationSender.ts
まとめ
| ポイント | 内容 |
|---|---|
| Driving Port | 1 Use Case = 1 Port、ISP準拠 |
| Driven Port | ドメインの言葉で定義、技術詳細を含めない |
| Command/Query | 入力はCommandまたはQueryオブジェクト |
| DTO | 出力はドメインモデルではなくDTOで返す |
| 配置 | domain/ports/{in,out}/ に配置 |
チェックリスト
- Driving Portを1 Use Case単位で設計できる
- Driven Portをドメインの言葉で定義できる
- Command/Queryパターンを理解した
- Portに技術詳細を含めてはいけない理由を説明できる
次のステップへ
次は「Adapter(実装)の設計」を学びます。Portに対する具体的な実装を、どのように設計するかを身につけましょう。
推定読了時間: 30分