第5章 Portってなに?(差し込み口=約束)🔌😊
![hex_ts_study_005[(./picture/hex_ts_study_005_ports_adapters_metaphor.png)
この章は「Port=中心(ユースケース/ドメイン)が外側にお願いする“約束”」を、ちゃんと体でわかるようにする回だよ〜🧠💕 Ports & Adapters(ヘキサゴナル)は、中心を外側(UI/DB/外部API/ファイル)から独立させるのが主目的で、その“接続点”が Port なのです🔌🛡️ (Alistair Cockburn)
1) まず結論:Portは「中心が決める、やってほしいことリスト」📜✨
Port はざっくり言うと、
- 中心が外側に対して「こうやって呼んでね」「これを返してね」って決める“約束”
- そして外側は、その約束に合わせて実装(=Adapter)する
っていう関係だよ🔌🧩
ポイントはここ👇 ✅ Portは“中心側が持つ”(中心が主導権を握る) ✅ 外側の都合(DBの形、HTTPの形、フレームワークの型)を中心に持ち込まない この思想が Ports & Adapters の核だよ〜🛡️ (Alistair Cockburn)
2) 「Port=interface」って思ってOK?🤔➡️ だいたいOK!🙆♀️
TypeScriptだと Port はたいてい interface(または type)で表現するよ✨ 理由はシンプルで、
- 約束=「メソッド名」「引数」「戻り値」
- 実装は外側に置く(Adapterがやる)
って形にぴったりだから😊
3) Portには2種類あるよ(超重要)🚪➡️⬅️
🟦 Inbound Port(外→中)🚪🔌
外側(CLI/HTTP/UI)が、中心のユースケースを呼ぶための入口。 例:AddTodo, CompleteTodo, ListTodos みたいな「アプリの操作」🎮
🟧 Outbound Port(中→外)💾🔌
中心が、外側(DB/ファイル/外部API)にお願いするための出口。 例:Todo を保存する、取得する、通知する、時間を取る…など⏰📨
この分類は Ports & Adapters の定番整理だよ📌 (ウィキペディア)
4) Port設計のコツ:「最小の約束」にする✂️✨
Portって、作り方を間違えると一気にしんどくなるの…😵💫 だから合言葉はこれ👇
✅ “最小の約束”にするコツ3つ🌱
- ユースケースが本当に必要な操作だけに絞る
- 外側の事情が透ける名前/型にしない(HTTPっぽい、DBっぽい、ファイルっぽいのNG🙅♀️)
- 「なんでもできるポート」を作らない(巨大化の原因🐘🍔)
5) 例で体感:ToDoミニの Port を作ってみる📝💖
ここでは「中心が欲しい約束」を、まず書いちゃうよ😊 (Adapterは次章で実装するイメージ🧩)
5-1) Inbound Port(ユースケース入口)🚪🔌
// app/ports/inbound/AddTodoPort.ts
export interface AddTodoPort {
execute(input: { title: string }): Promise<{ id: string }>;
}
// app/ports/inbound/ListTodosPort.ts
export interface ListTodosPort {
execute(): Promise<Array<{ id: string; title: string; completed: boolean }>>;
}
- execute だけで十分なことが多いよ(まずは迷子防止🧭✨)
- input/output は「外に見せる形」なので、ここではシンプルにしてOK📮
5-2) Outbound Port(保存のお願い)💾🔌
// app/ports/outbound/TodoRepositoryPort.ts
export interface TodoRepositoryPort {
save(todo: { id: string; title: string; completed: boolean }): Promise<void>;
findById(id: string): Promise<{ id: string; title: string; completed: boolean } | null>;
list(): Promise<Array<{ id: string; title: string; completed: boolean }>>;
}
ここでの気持ちはね👇 中心「ToDoを保存したい。どう保存するかは知らん!」🧠🛡️ 外側「了解!配列でもファイルでもDBでも、任せて!」🧩✨
この“中心は外側の具体を知らない”が、依存逆転の美味しさだよ〜🔁🧪 (martinfowler.com)
6) やりがち事故:「Portが太る」😱🐘
❌ ダメ寄り例:巨大Port(なんでも屋)🧹😵
export interface TodoRepositoryPort {
save(todo: any): Promise<void>;
update(todo: any): Promise<void>;
delete(id: string): Promise<void>;
findById(id: string): Promise<any>;
findByTitle(title: string): Promise<any[]>;
search(query: string, page: number): Promise<any>;
exportCsv(path: string): Promise<void>;
importCsv(path: string): Promise<void>;
// ...増殖
}
これ、最初は便利そうに見えるけど… 中心が「外側の都合(CSVとか検索ページングとか)」に引っ張られて崩れていくやつ😵💫💥
✅ 太り判定チェックリスト🥗✅
- Portのメソッド名が 技術の言葉 になってる(CSV/SQL/HTTP/ORM など)❌
- Portの引数/戻り値が 外部ライブラリの型 になってる❌
- 1つのPortが 複数の目的 を背負ってる(保存+エクスポート+検索UI事情)❌
Ports & Adapters は「中心を汚さない」が正義だよ🛡️✨ (Thoughtworks)
7) Portが増えすぎて死ぬ問題💀➡️ 予防線を張ろう⚠️
Wikiでも「ポートの数や粒度は固定じゃない(極端にユースケースごとでもOK)」って言われるけど、実務では“増えすぎ”が普通に事故るの🥹 (ウィキペディア)
だからおすすめはこれ👇
✅ ちょうどいい粒度の目安📏✨
- Inbound Port:1ユースケース = 1 Port から始める(迷子にならない)🧭
- Outbound Port:外部の“種類”ごとに1 Port(保存、通知、時間、外部API など)🧩
- そして「必要になったら分割」する(先に作りすぎない)🌱
8) AI拡張の使いどころ(Port編)🤖💖
AIは Port 設計でやりがちな事故があるの👇 「便利そうだから何でも入れちゃう」🐘🍔
なので、使い方はこうがおすすめ✨
✅ AIに頼ってOK(安全)🧰
- Portの命名案を複数出させる(言葉選び)🗣️
- 既にある Port を見せて「大きすぎない?」ってレビューさせる🔍
- input/output を「もっと小さくできる?」って削らせる✂️
⚠️ AIに任せすぎNG(危険)🧨
- 「Portの責務を決める」そのものを丸投げ
- 「将来必要そう」を理由にメソッド増やしまくる提案を採用
コピペで使える質問テンプレ📝🤖
- 「この Port は“最小の約束”になってる?不要な操作が混ざってない?」
- 「技術都合(DB/HTTP/ファイル)っぽいメソッド名が混ざってない?」
- 「この Port の変更理由は1つに絞れてる?」
9) この章のまとめ🎁💖
- Port は 中心が外側に出す“約束” 🔌
- Port は 中心が持つ(主導権は中心)🛡️
- Port は 最小の約束 にする(太ったら負け🥗)
- Inbound/Outbound を分けると整理が楽になるよ🚪➡️⬅️
次章は、いよいよ Adapter(実装) に入って「この約束を外側でどう叶えるか」を作っていくよ〜🧩🚀✨