抽象度の選択 ─ Pit of Success と Sharp Tools
責務の境界を決めたら、次に問われるのが どの抽象度で API を提供するか。低すぎれば利用者が苦労し、高すぎれば自由が奪われる。この章では設計哲学として Pit of Success を導入する。
“Pit of Success” の起源
Microsoft Brad Abrams のブログ “The Pit of Success” で広まった概念。Rico Mariani の発言が元:
We strive to design our APIs and frameworks so that you simply fall into the pit of success.
Jeff Atwood も「Falling Into The Pit of Success」 で論じた。要点:
正しい使い方が “自然” になる API を設計する。 利用者が考えなくても、デフォルトの選択肢で正しく動く。
逆を考えると分かりやすい。Pit of Despair(失望の穴) API は、
- デフォルトが「間違った設定」になっている
- 詳細を理解しないと正しく使えない
- 一見動いているが、後で破綻する
Pit of Success の API は、何も考えずに使うとすでに正しく動いている。
設計の 3 原則
原則 1:デフォルトが「正しい」
// ❌ Pit of Despair:デフォルトが retry 無し
const client = new HttpClient();
client.get('/api'); // 失敗したら例外、retry なし
// ✅ Pit of Success:デフォルトが retry あり
const client = new HttpClient(); // 内部でデフォルト retry 付き
client.get('/api'); // 一時的失敗は自動 retry
// 必要なら .withRetry({ maxAttempts: 0 }) で disable
デフォルトを 慎重に決める 必要がある。「何も指定しなければこうなる」が、ほとんどの利用者にとって正しいようにする。
原則 2:間違いが目立つ
// ❌ Pit of Despair:黙って動いてしまう
notify.send({ to: '[email protected]' }); // → API リクエストせず黙ってログだけ書いて成功扱い
// ✅ Pit of Success:間違いは即エラー
notify.send({ to: '[email protected]' }); // configuration が無いと例外で起動時に失敗
動いているように見えて実は動いていない のが最も悪い。起動時 / 設定時に失敗する 設計が望ましい。
原則 3:危険な操作は明示的
// ❌ Pit of Despair:destructive な操作が submit() に紛れる
api.submit({ deleteAfter: true }); // ← 引数 1 つで delete してしまう
// ✅ Pit of Success:destructive は明示的に
api.submit({ data });
api.submitAndDelete({ data }); // ← 名前から意図が明確
型 / メソッド名 / 引数名で意図を表す。読み書き両方を見て分かる API。
Sharp Tools の必要性
ただし、Pit of Success だけでは expert user が困る。
「私は分かっている。デフォルトじゃなくて自分で制御させて」
これに応える概念が Sharp Tools。鋭利だが、上級者が望むときだけ手に取れる:
// 普通の使い方(Pit of Success、デフォルト動作)
await db.transaction(async (tx) => {
await tx.update(...);
});
// Expert 用(Sharp tool、明示的な isolation level)
await db.transaction({ isolationLevel: 'Serializable' }, async (tx) => {
await tx.update(...);
});
両方が同じ API に共存する設計が望ましい:
- デフォルトは Pit of Success(初級者が落ちる)
- Sharp tool は明示的に取り出せる(上級者が必要なときだけ)
抽象度の階層
API の抽象度は階段状に提供できる:
graph TB
L4[L4: Declarative<br/>「メールを送って」とだけ宣言]
L3[L3: High-level SDK<br/>retry / 認証 / log を内蔵]
L2[L2: Low-level Client<br/>HTTP 直接、retry 自分で]
L1[L1: Raw API<br/>HTTP リクエストを手書き]
L4 --> L3 --> L2 --> L1
Note1[多くの利用者は L3-L4 を使う]
Note2[Expert は L2 に降りる]
style L4 fill:#e1ffe1
style L3 fill:#e1ffe1
style L2 fill:#fff4e1
style L1 fill:#ffe1e1
例:通知基盤
// L4: Declarative(最も抽象的)
@OnUserSignUp()
sendWelcomeEmail(user: User);
// → デコレータで宣言、基盤が呼び出しを自動生成
// L3: High-level SDK
await notify.sendEmail({ to, subject, body });
// → SDK が retry / 認証 / log を全部内蔵
// L2: Low-level Client
const client = createNotifyClient({ retries: 3 });
await client.post('/notifications', { ... });
// → 利用者が retry を制御
// L1: Raw API
await fetch('https://notify.internal/api/v1', { method: 'POST', ... });
// → 認証 token、retry、log を全部手書き
全レベルを提供すると、利用者は自分のレベルを選べる。
“Magic” の罠
Pit of Success の極端な形は「全部 magic にする」── 利用者が何も書かなくても動く。だがこれは罠になる:
| 問題 | 例 |
|---|---|
| デバッグできない | エラーが起きたとき、何が走っているか分からない |
| 挙動が予測できない | 同じコードが環境で違う動きをする |
| 学習曲線が高い | 「どう動いているか」を理解するのに時間がかかる |
| 拡張できない | カスタマイズしたいとき、抜け道がない |
Boring API(前章)を思い出す。Magic は短期的に楽だが、長期的にコストが高い。透明性のある抽象が望ましい:
- 何が起きているかをログ / トレース で見られる
- デフォルトを上書きできる “抜け道” がある
- マニュアルで一読すれば全体像が掴める
段階的に深めていける設計
良い API は オニオン構造:
利用者の理解度
↓
表面: SDK の関数呼び出し(ほとんどの人がここ)
↓
中層: SDK の内部構造、設定オプション
↓
深層: HTTP API、認証プロトコル、エラーモデル
↓
最深: 内部実装、運用ルール
最初は表面で動く、必要に応じて中層・深層を学べる。学習曲線が緩やか になる。
この章の要点
- Pit of Success = 正しい使い方が自然になる API 設計
- 3 原則:デフォルトが正しい / 間違いが目立つ / 危険な操作は明示的
- Sharp Tools は上級者向けに明示的な引数で残す
- 抽象度は L4 (Declarative) → L1 (Raw API) の階段で提供
- Magic は短期的に楽だが長期的にコスト高。透明性のある抽象を選ぶ
- API は オニオン構造で、利用者が必要なだけ深く潜れる設計
次章への問いかけ
ここまでは「API の表面の設計」。だが API が呼ばれる時、制御の所在 が問題になる。
利用者が API を呼ぶのか、基盤が利用者の callback を呼ぶのか。これは Library と Framework の本質的違い ── Inversion of Control の話。