1 分

バーバラ・リスコフのデータ抽象 — 信頼できるAPIの設計

バーバラ・リスコフのデータ抽象原則を学び、明確で安定したAPI設計、破壊的変更の削減、保守しやすいシステム構築の方法を身につけましょう。

バーバラ・リスコフのデータ抽象 — 信頼できるAPIの設計

なぜバーバラ・リスコフは今もAPI設計で重要か

バーバラ・リスコフは、現代のソフトウェアチームが“壊れない”ものを作る方法に静かに影響を与えた計算機科学者です。彼女のデータ抽象情報隠蔽、そして後の**リスコフの置換原則(LSP)**に関する研究は、プログラミング言語から日常的なAPI設計に至るまで幅広く影響を与えました:振る舞いを明確に定義し、内部を保護し、他者がそのインターフェースに安全に依存できるようにすることです。

プロダクトの文脈での「信頼できるインターフェース」

信頼できるAPIは単に理論的に「正しい」だけではありません。それはプロダクトの速度を高めるインターフェースです:

  • 新機能を出しても既存の顧客が壊れない。
  • 統合がバージョン間で動き続ける。
  • オンコールのインシデントが減る(障害が予測可能になるため)。
  • チームは内部を調整するために大掛かりな調整をしなくて済む。

その信頼性は体験です:APIを呼ぶ開発者にとって、保守するチームにとって、間接的にそれに依存するユーザーにとっての体験です。

データ抽象がバグ(とミーティング)を減らす仕組み

データ抽象とは、呼び出し側が保存や計算の細かい実装ではなく、概念(アカウント、キュー、サブスクリプション)に対して小さな操作セットでやり取りするという考え方です。

表現の詳細を隠すと、誤りのカテゴリをまるごと取り除けます:誰も公開されるべきでないデータベースのフィールドに「偶然」依存したり、システムが扱えない方法で共有状態を変更したりできません。同じく重要なのは、抽象化は調整のコストを下げることです:公開振る舞いが一貫していれば、チームは内部をリファクタリングするための許可を求める必要がありません。

この記事を読み終えるとできること

この記事の終わりまでに、実践的に以下ができるようになります:

  • APIの振る舞いを明確な約束として書く(エッジケース含む)。
  • システムが進化してもインターフェースを小さく安定的に保つ方法。
  • 呼び出し側が扱える予測可能な障害モードの設計。

後で手早く要点を見たい場合は /blog/a-practical-checklist-for-designing-reliable-apis にジャンプしてください。

ジャーゴンなしで説明するデータ抽象

データ抽象は単純な考え方です:何かをどう「作られているか」ではなく、それが何をするかで扱います。

自動販売機を思い浮かべてください。モーターの回り方や硬貨の数え方を知らなくても良い。必要なのは操作(“商品を選ぶ”、“支払う”、“商品を受け取る”)とルール(“十分に支払えば商品が出る。売り切れなら返金される”)だけです。これが抽象化です。

「何をするか」と「どう動くか」

ソフトウェアでは、インターフェースが「何をするか」です:操作名、受け取る入力、出力、期待されるエラー。実装は「どう動くか」です:データベースのテーブル、キャッシュ戦略、内部クラス、パフォーマンスの工夫。

これらを切り離すことで、システムが進化してもAPIが安定します。内部を書き直したり、ライブラリを差し替えたり、ストレージを最適化しても、ユーザー向けのインターフェースは変わりません。

1分でわかる抽象データ型(ADT)

抽象データ型は「コンテナ + 許される操作 + ルール」を、特定の内部構造にコミットせずに記述したものです。

例: Stack(後入れ先出し)

  • push(item): アイテムを追加する
  • pop(): 最後に追加したアイテムを取り出して返す
  • peek(): 取り出さずにトップのアイテムを見る

重要なのは約束です: pop() は最新の push() を返す。内部が配列であれリンクリストであれ、これは公開された契約です。

これが実際のAPIにどう当てはまるか

同じ分離はあらゆるところに当てはまります:

  • REST エンドポイント: POST /payments がインターフェースで、詐欺チェック、リトライ、DB書き込みが実装です。
  • SDK メソッド: client.upload(file) がインターフェースで、チャンク分割、圧縮、並列リクエストが実装です。
  • UI コンポーネント: “DatePicker” は props/イベントを公開し、DOM構造やアクセシビリティの配線は実装です。

抽象化で設計すると、ユーザーが頼る契約に注力でき、裏側を自由に変えても壊さない自由を得られます。

不変条件(インバリアント):システムを正しく保つ隠れたルール

インバリアントは抽象化の内部で常に真であるべきルールです。APIを設計しているなら、インバリアントはデータが不可能な状態に陥らないようにするガードレールです—例えば二通貨が混在した口座や、アイテムがないのに「完了」となった注文など。

インバリアントはどう見えるか(数学抜きで)

インバリアントはタイプの「現実の形」です:

  • Cart に負の数量を含めてはいけない。
  • UserEmail は常に有効なメールアドレスである(「後で検証」ではない)。
  • Reservationstart < end で、両方とも同じタイムゾーンである。

これらが成り立たなくなると、システムは予測不可能になります。というのも、各機能が「壊れた」データをどう扱うか推測しなければならなくなるからです。

検証とエラーハンドリングを導くインバリアント

良いAPIは境界でインバリアントを強制します:

  • 作成時: 無効な入力を早期に拒否して明確なエラーを返す。
  • 更新時: インバリアントを保つ変更だけを許可する。
  • パース/入出力: 外部データは信用せず、保存前に検証する。

これによりエラーハンドリングが自然に改善します。「何かが壊れた」という曖昧な失敗ではなく、APIがどのルールに違反したかを説明できるようになります(例: “end must be after start”)。

インバリアントをインターフェース越しに漏らさない

呼び出し側が「まず normalize() を呼ばないと動かない」など内部ルールを覚えなければならないのは避けるべきです。もしインバリアントが特別な手順に依存しているなら、それはインバリアントではなく“足を撃つ銃”です。

インターフェースは次のように設計しましょう:

  • 無効な状態を表現しづらくする(あるいは表現不能にする)
  • メソッドは自動的にインバリアントを保持する

ドキュメントの実践チェックリスト

APIの型を文書化するときは次を書き出してください:

  1. インバリアントの記述(英語でわかりやすく、テスト可能に)
  2. どこで強制するか(コンストラクタ、セッター、エンドポイント)
  3. 違反時の挙動(エラー種別/メッセージ、ステータスコード)
  4. どのメソッドが保持するか(例外があれば明記)
  5. 有効/無効入力の例(短く具体的に)

契約(Contracts):呼び出し側と保守者に振る舞いを明確にする

良いAPIは単なる関数群ではなく、約束です。契約はその約束を明確にし、呼び出し側が振る舞いを信頼でき、保守者が内部を変えても誰も驚かないようにします。

契約で明示すべきこと

最低限、次を文書化してください:

  • 事前条件: 呼び出し前に真であるべきこと(有効域、必要な権限、スレッド安全性)。
  • 事後条件: 成功後に真となること(返り値の意味、状態の変化)。
  • 副作用: 他に何が変わるか(ディスク書き込み、ネットワーク送信、渡されたオブジェクトの変更)。

この明確さにより振る舞いは予測可能になります:呼び出し側はどの入力が安全か、どの結果を扱うべきかを知り、テストは意図をチェックできます。

契約は“部族知識”を減らす

契約がないとチームは記憶や暗黙の慣習に頼ります:「ここには null を渡すな」「その呼び出しは時々リトライする」「エラー時は空を返すことがある」など。これらはオンボーディングやリファクタで失われがちです。

文書化された契約はそれらを共有知識に変えます。コードレビューの対象も「この変更は契約を満たすか?」という議論に変わり、挙動の推測ではなく契約に基づく議論になります。

良い表現と曖昧な表現の比較

曖昧: “ユーザーを作成する。”

良い例: “一意な email でユーザーを作成する。\n\n- 事前条件: email は有効なアドレスであること。呼び出し側は users:create 権限を持つこと。\n- 事後条件: 新しい userId を返す。ユーザーは永続化され直ちに取得可能である。\n- 失敗モード: email が既に存在する場合は 409、無効フィールドは 400。部分的にユーザーが作られることはない。”

曖昧: “素早くアイテムを取得する。”

良い例:limit 件までを createdAt 降順で返す。\n\n- 副作用: なし。\n- 一貫性: 最大で 60 秒古い可能性がある。\n- ページング: 次ページは nextCursor を使う。カーソルは 15 分で失効する。”

情報隠蔽:内部を非公開にし、APIを安定させる

情報隠蔽はデータ抽象の実践面です:呼び出し側は APIが何をするか を頼りにし、どのようにするか を頼りにしないでください。内部が見えなければ、自由に変更しても破壊的なリリースになりません。

表現ではなく操作を公開する

良いインターフェースは作成・取得・更新・一覧・検証のような操作の小さな集合を公開し、表現(テーブル、キャッシュ、キュー、ファイルレイアウト、サービス境界)は非公開にします。

例: 「カートにアイテムを追加する」は操作です。「CartRowId」は実装の詳細です。実装の詳細を公開すると、利用者がそれに基づくロジックを組んでしまい、変更が難しくなります。

内部を隠すとリファクタが安全になる理由

クライアントが安定した振る舞いにだけ依存すると、あなたは自由に:

  • データベースやストレージ形式を切り替えられる
  • モノリスをサービスに分割できる
  • キャッシュやインデックスを追加/変更できる
  • 内部モデルを再編成できる

…それでもAPIは互換性を保ちます。これが本当の利得です:ユーザーには安定性、保守者には自由が与えられます。

流出しがちなパターンに注意

内部が意図せず漏れる例:

  • 内部ストレージでのみ意味を持つ内部ID(自動増分整数、シャードキー)を返す。
  • ミュータブルな構造を公開する(生のオブジェクトを返してクライアントが修正・再送できる)。
  • クライアントに内部状態を構築させる(status=3 のような数値を受け入れる)。

安定するレスポンス形の設計

意味を返す形を優先してください:

  • 公開IDは安定で不透明に(例: "userId": "usr_…")し、DB行番号を避ける。
  • コレクションはコピーや読み取り専用ビューを返し、順序や内部フィールドに偶然依存されないようにする。
  • フィールドは後方互換的に追加し、既存フィールドの意味を変えない。

変更する可能性がある詳細は公開しないでください。もし利用者が本当に必要なら、それを意図的にインターフェースの一部として昇格させ、文書化します。

リスコフの置換原則(LSP)をインターフェースの約束として扱う

まずコントラクトを定義する
Koder.aiでAPIコントラクトを動くReactアプリやGoサービスに変換する。

LSPを一言で言えば:「あるコードがインターフェースで動作するなら、そのインターフェースの有効な実装に差し替えても、特別なケースを要せず動作を保つべきだ」ということです。

LSPは継承の話だけではなく、信頼 の話です。インターフェースを公開するとき、あなたは振る舞いについて約束をしています。LSPはすべての実装がその約束を守るべきだと言っています。

LSPは「呼び出し側を驚かせるな」

呼び出し側はAPIの記述を信頼します—今日の偶然の振る舞いではありません。もしインターフェースが「有効なレコードなら save() に渡せる」と言うなら、すべての実装はその有効なレコードを受け入れなければなりません。インターフェースが「get() は値か明確な ‘not found’ を返す」と言うなら、実装はランダムに新しいエラーを投げたり部分的なデータを返したりできません。

安全な拡張は、新しい実装やプロバイダを追加してもユーザーがコードを書き換える必要を生じさせません。これがLSPの実際的な利点です:インターフェースを差し替え可能に保つこと。

APIでの一般的なLSP違反

APIが約束を破るよくある例は:

  • 入力の狭窄(厳しい事前条件): 新実装がインターフェースで許容されていた入力を拒否する。例: インターフェースは UTF-8 全文字列を ID として受け入れるのに、新実装は数値のみ受け入れる。

  • 出力の弱化(事後条件の緩み): 新実装が約束したより少ない情報を返す。例: インターフェースで結果はソート・一意・完全とされていたが、ある実装は未ソート・重複・抜け落ちがある。

微妙な違反は障害時の振る舞いを変えることです:ある実装は「not found」を返し別の実装は例外を投げると、呼び出し側は差し替えられなくなります。

プラグイン的振る舞いを驚きなく設計する

“プラグイン”をサポートするには、インターフェースを契約のように書きます:

  • 有効な入力を明記し、実装間で一貫させる。
  • 出力の意味(順序、デフォルト、エッジケース)を定義する。
  • 失敗モードを標準化する:どのエラーが起こり得るかとその意味。

もし実装が本当に厳しいルールを必要とするなら、同じインターフェースに隠さず別のインターフェースを作るか、機能として明示(例: supportsNumericIds())してクライアントが自発的に選べるようにします。

良いインターフェースは小さく、凝集的で、読みやすい

良い設計のインターフェースは“使いやすそう”に感じられます。なぜなら、呼び出し側に必要なものだけを公開して、それ以上は公開しないからです。リスコフのデータ抽象の観点は、狭く安定して読みやすいインターフェースへ導きます。

「何でもやる」より凝集を優先する

大きなAPIは無関係な責務を混ぜがちです:設定、状態変更、集計、トラブルシューティングが一箇所にあると、安全に呼べる操作がわかりにくくなります。

凝集したインターフェースは同じ抽象に属する操作だけをグループ化します。キューなら enqueue/dequeue/peek/size にフォーカスする、という具合です。概念が少ないほど誤用の道は減ります。

あいまいなパラメータを避ける

「柔軟性」はしばしば「不明瞭さ」を生みます。options: anymode: string、多数のブール値(forceskipCachesilent)は定義されていない組み合わせを作ります。

代替策:

  • 異なる振る舞いには明確なメソッドを用意する(例: publish()publishDraft())。
  • 小さく型付けされたオプションオブジェクトを使い、デフォルトと無効な組み合わせを文書化する。

パラメータのためにソースを読まないと何が起きるか分からないなら、それは良い抽象の一部ではありません。

名前付けもインターフェースの一部

名前は契約を伝えます。観測可能な振る舞いを表す動詞を選んでください:reservereleasevalidatelistget。比喩や意味のあいまいな語は避けてください。同じように聞こえる2つのメソッドがあれば、呼び出し側は類似した振る舞いを期待します—だからそれを真にすべきです。

モジュール/リソースを分けるべきとき

次に気づいたら分割を検討してください:

  • 異なるユーザーロール(例: “admin” と “consumer”)で必要な機能が違う。
  • 変更頻度が異なる部分がある(ある部分は頻繁に進化し、別の部分は安定が求められる)。

分割でコアの約束を保ちながら内部を進化させやすくなります。成長を見越すなら、スリムな“コア”パッケージとアドオンの構成を検討してください。詳しくは /blog/evolving-apis-without-breaking-users を参照してください。

ユーザーを壊さずAPIを進化させる

実装を自分で管理する
プロジェクトの元となるソースコードをエクスポートして、完全にコントロールを維持する。

APIはめったに停滞しません。新機能、エッジケース、そして「小さな改善」が実は実アプリケーションを壊すことがあります。目標はインターフェースを凍結することではなく、既存のユーザーが依存する約束を壊さずに進化させることです。

セマンティックバージョニング(実務的に、限界あり)

セマンティックバージョニングはコミュニケーションの手段です:

  • MAJOR: 互換性を壊す変更をした。
  • MINOR: 後方互換的に機能を追加した。
  • PATCH: 意図した振る舞いを変えずにバグを修正した。

しかし判断力が必要です。ある「バグ修正」が呼び出し側が依存していた挙動を変えるなら、実際には破壊的変更です—たとえ元の挙動が偶発的だったとしても。

破壊的変更は型だけの問題ではない

多くの破壊的変更はコンパイラでは検出されません:

  • 受け入れる値の厳格化(以前受け入れていた値を拒否する)。
  • 意味の変更(同じフィールド名だが解釈が変わる)。
  • タイミングの変更(以前は速かった呼び出しが遅くなる/ブロックする)。
  • エラー挙動の変更(新しいエラーコード、異なるリトライ挙動、部分結果)。

事前条件と事後条件の観点で考えることが重要です:呼び出し側が提供すべきもの、戻り値として期待できるもの。

実際に使える非推奨(deprecation)パス

非推奨は明示的で期限付きであると有効です:

  • ドキュメントやレスポンスで古い振る舞いを非推奨としてマークする(警告、ヘッダ、ログ)。
  • 二重サポート期間(旧・新を並行して動かす)を提供する。
  • 明確なタイムラインを公開する(例: “60日で新しい既定、180日で削除”)。

抽象化が進化を楽にする理由

リスコフ流のデータ抽象は、ユーザーが依存できるものを狭めるので進化を楽にします。呼び出し側がインターフェース契約にしか依存しなければ、ストレージ形式やアルゴリズム、最適化を自由に変えられます。

実務では強力なツールも助けになります。例えば、内部APIを素早く反復しながら React フロントや Go + PostgreSQL バックエンドを作るとき、vibe-coding のようなワークフロー(記事では Koder.ai を例示)は実装を加速します。ただし基本は変わりません:明確な契約、安定した識別子、後方互換的な進化が必要です。スピードは乗数なので、正しいインターフェース習慣に乗数をかける価値があります。

エラーハンドリングと障害モード:予測可能性のために設計する

信頼できるAPIは「決して失敗しない」ものではなく、呼び出し側が理解し、処理し、テストできる方法で失敗するものです。エラーハンドリングは抽象化の一部であり、「正しい使い方」と「世界(ネットワーク、ディスク、権限、時間)が異なるときどうなるか」を定義します。

プログラマエラー vs ランタイム障害

まず2つのカテゴリを分けます:

  • プログラマエラー: 呼び出し側が契約を破った場合(無効なID形式、メソッドの呼び順ミス、必須フィールドの欠落)。これらは早期かつ明確に検出されるべきで、検証エラーで指摘します。
  • ランタイム障害: 呼び出し側が契約を守ったが外部要因で失敗した場合(タイムアウト、依存先の停止、クォータ超過、競合)。これらは表現可能で回復可能にするべきです。

この区分でインターフェースは正直になります:呼び出し側がコードで直せるものとランタイムで対処すべきものが分かります。

契約で失敗の形を決める

契約は失敗の扱いを暗示します:

  • エラー(検証応答) は契約違反向け。
  • 例外 はライブラリ内の真に例外的な非局所失敗向け、あるいはすべての呼び出し箇所で分岐させるのが非現実的な場合。
  • Result型Ok | Error)は失敗が予期され、呼び出し側に明示的に扱わせたい場合に有効。

選択する仕組みは何でも構いませんが、API全体で一貫させて、利用者が推測しないようにしてください。

障害モードを明示かつテスト可能にする

各操作について発生し得る失敗を意味で列挙してください:"バージョンが古くて衝突した"、"見つからない"、"権限がない"、"レート制限"。安定したエラーコードと構造化フィールドを提供し、テストが文字列に依存しなくて済むようにします。

リトライ、冪等性、部分成功

操作がリトライして安全か、どんな条件で安全か、冪等性をどう達成するか(冪等キー、自然なリクエストID)を文書化します。部分成功が起こりうる(バッチ処理)場合、成功と失敗の報告方法、タイムアウト後に呼び出し側が想定すべき状態を定義してください。

抽象のテスト:インターフェースが約束に合致することを証明する

抽象は約束です:「有効な入力でこれらの操作を呼べば、これらの結果が得られ、これらのルールが常に成り立つ」。テストはコード変更に対してその約束を守らせる手段です。

契約をユニット/結合テストにする

契約を自動化可能なチェックに翻訳して始めてください。

ユニットテストは各操作の事後条件とエッジケースを検証します:返り値、状態変化、エラー挙動。インターフェースが「存在しないアイテムの削除は false を返し何も変えない」と言うなら、そのテストを書きます。

結合テストは実際の境界(DB、ネットワーク、シリアライズ、認可)をまたいで契約を検証します。多くの契約違反は型のエンコード/デコードやリトライ/タイムアウトでのみ現れます。

不変条件のためのプロパティベーステスト

不変条件は任意の有効な操作列に対して成立しなければなりません(例: "残高は負にならない"、"IDは一意である"、"list() で返したアイテムは get(id) で取得できる")。

プロパティベーステストは大量のランダムだが有効な入力と操作列を生成して反例を探します。人がテストに書かないような奇妙なコーナーケースを見つけるのに有効です。

公開API向けのコンシューマ駆動契約テスト

公開または共有APIでは、消費者が実際に行うリクエストと依存するレスポンスの例を公開させます。プロバイダはCIでこれらの契約を実行し、実際の利用を壊さないことを確認します—プロバイダ側が想定していなかった利用でも検出できます。

本番での契約ドリフトを監視する

すべてをテストで網羅することはできないので、本番で契約が変わっていないかを示すシグナルを監視してください:レスポンス形状の変化、4xx/5xx の増加、新しいエラーコード、レイテンシの急増、未知フィールドやデシリアライズ失敗。エンドポイントとバージョンごとに追跡すると早期にドリフトを検出できます。

スナップショットやロールバックを配信パイプラインでサポートしているなら、これらはこの考え方と相性が良いです:早期にドリフトを検出し、クライアントに適応を強いることなくロールバックできます。(例えば Koder.ai はワークフローの一部としてスナップショットとロールバックを含めており、"契約を最初に、変更はその後" のアプローチと整合します。)

よくあるアンチパターンと回避方法

GoとPostgresで始める
GoとPostgreSQLのバックエンドを作り、境界での不変条件に集中する。

抽象を重視するチームでも、目先の“実用的”なやり方に流されて徐々にAPIが特例の寄せ集めになることがあります。繰り返し出る罠とその対策をいくつか紹介します。

永続的なフィーチャーフラグをAPIのノブにする

フィーチャーフラグはローアウトに有用ですが、?useNewPricing=truemode=legacyv2=true のようにフラグが公開・長期化すると、利用者はそれらを組み合わせて予期しない方法で使い、結果的に複数の振る舞いを永続的にサポートする羽目になります。

安全なアプローチ:

  • ロールアウト用フラグは可能なら内部に留める。
  • 振る舞いを分ける必要があるなら、明確な名前とライフサイクルを持つ機能として表現し、古いものを取り除く計画を立てる。
  • 有効な組み合わせを文書化し、残りは明示的に拒否する。

DB概念をインターフェースに漏らす

テーブルIDやジョインキー、"SQL 風" フィルタ(例: where=...)を公開すると、クライアントは保存モデルを学ばされ、リファクタが難しくなります。

代わりにドメイン概念と安定した識別子でモデリングしてください。クライアントには「どう格納されているか」ではなく「何を求めているか」を聞かせるべきです(例: "ある顧客の指定期間の注文")。

「とりあえずフィールドを追加する」反射行動

フィールドを追加するのは一見無害ですが、繰り返すと責務が曖昧になり、不変条件が弱まり、クライアントが偶発的な詳細に依存するようになります。

長期コストを避けるには:

  • 新しい概念なら新しいフォーカスされたタイプを導入する。
  • 関連フィールドは意味の明確なネストオブジェクトにまとめる。
  • 追加はすべて契約変更として扱い、その意味と常に成り立たせるべきことを検討する。

抽象化が強すぎる場合

過度な抽象化は本当のニーズを阻むことがあります—例: "このカーソル後から始める" を表現できないページング、"完全一致" を指定できない検索。クライアントは回避策を取って複数回呼び出したりローカルでフィルタしたりし、結果的に性能やエラーが悪化します。

対策は制御された柔軟性を用意することです:サポートするフィルタ演算子の小さな集合のような明確な拡張点を提供し、オープンエンドな逃げ道を避けます。

能力を奪わずに単純化する

単純化は能力を奪うことを意味しません。混乱するオプションを非推奨にしても、基礎の能力はより明確な形で残します:重複する複数のパラメータを1つの構造化リクエストオブジェクトに置き換える、あるいは1つの"何でもやる"エンドポイントを2つの凝集したエンドポイントに分ける。その後、バージョン付きドキュメントと明確な非推奨タイムラインで移行を導きます(/blog/evolving-apis-without-breaking-users を参照)。

信頼できるAPIを設計するための実践チェックリスト

リスコフのデータ抽象の考えを適用するためのシンプルで繰り返せるチェックリストです。目標は完璧さではなく、APIの約束を明示し、テスト可能にし、進化させやすくすることです。

短いチェックリスト

  • 不変条件: データやリソースについて常に真であるべきことは何か?(例: "残高は負にならない"、"IDは一意"、"アイテムは安定した順序で返る")。
  • 契約: 各操作について 事前条件事後条件副作用(変わらないものも含む)を書く。
  • 隠された表現: 意図的に非公開にする詳細(ストレージ形式、キャッシュ、内部ID)を列挙し、呼び出し側が依存できないようにする。
  • 進化計画: 機能を追加する方法を決める:バージョニング戦略、非推奨ポリシー、古い振る舞いをどれくらいサポートするか。

繰り返せるAPIレビューワークフロー

  1. 実装を見ずにインターフェースだけ読む。 新しいメンバーが挙動を予測できるか?
  2. 5つの "ストーリーテスト" を通す: 通常ケース、空のケース、境界ケース、無効入力ケース、障害ケース。
  3. 代替可能性をチェックする: 複数実装があるなら、入れ替えて呼び出し側を驚かせないか?
  4. 隠れた結合を探す: クライアントが内部状態、タイミング、保存の詳細を知る必要があるか?
  5. 導入しようとしている破壊的変更を書き出す。 リストが空になるまで(あるいは意図的に受け入れるまで)再設計する。

ドキュメントテンプレート(コピペ可)

短い一貫したブロックを使います:

  • Operation: transfer(from, to, amount)
  • Requires: amount > 0 と口座が存在すること
  • Ensures: 残高は原子更新される。総和は保存される。
  • Errors: InsufficientFunds, AccountNotFound, Timeout
  • Notes: 冪等性、順序、性能期待値

さらに読みたいなら

深掘りするなら 抽象データ型(ADT), 契約による設計(Design by Contract), リスコフの置換原則(LSP) を調べてください。

チームが内部ノートを保っているなら /docs/api-guidelines のようなページにリンクしてレビューワークフローを再利用しやすくしてください。そして、手で作るにせよチャット駆動のビルダー(Koder.ai のような)で高速にサービスを作るにせよ、これらのガイドラインは「速く出す」ための非妥協の一部として扱ってください。信頼できるインターフェースは速度が複利的に効くための基盤です。

よくある質問

なぜバーバラ・リスコフの研究は今日のAPI設計でも重要なのですか?

彼女はデータ抽象情報隠蔽を広め、これらは現代のAPI設計に直接結びつきます:小さく安定した契約を公開し、実装は柔軟に保つ。実利としては、破壊的変更が減り、安全なリファクタリングが可能になり、統合が予測しやすくなります。

「信頼できるインターフェース」はプロダクトやエンジニアリングでどういう意味ですか?

呼び出し側が時間を超えて依存できるAPIのことです:

  • 新しいバージョンでも既存のクライアントが壊れない。
  • 障害モードが一貫して文書化されている。
  • 内部を変更しても公開振る舞いは変わらない。

信頼性は「決して失敗しない」ことではなく、むしろ予測可能に失敗することと契約を守ることにあります。

APIエンドポイントやメソッドをどうやって明確な振る舞いの約束に変えればいいですか?

振る舞いを契約として書き出します:

  • 事前条件: 呼び出し前に満たすべきこと(有効域、権限)。
  • 事後条件: 成功後に成り立つこと(返り値、状態変化)。
  • 副作用: 他に何が変わるか(書き込み、ネットワーク呼び出し、キャッシュ更新)。

空結果・重複・順序などエッジケースも含めて明記すると、呼び出し側は契約に基づいて実装・テストできます。

不変条件とは何で、APIはどこでそれらを強制すべきですか?

不変条件とは、抽象化の内部で常に成立しているべきルールです(例:「数量は負にならない」)。不変条件は境界で強制します:

  • 作成/更新時に検証する。
  • 無効な入力は早期に特定のエラーで拒否する。
  • normalize() のような儀式を呼び出し側に要求しない。

こうすることでシステムの下流で「起こり得ない状態」を扱う必要が減ります。

情報隠蔽とは何か、レスポンスの形やIDにどう適用すればよいですか?

情報隠蔽は操作や意味を公開し、内部表現を公開しないことです。将来変更する可能性のあるもの(テーブル、キャッシュ、シャードキー、内部ステータス)に消費者を結びつけないでください。

実践的な方法:

  • 公開IDは安定で不透明に(例: usr_...)して、データベースの行IDは出さない。
  • クライアントに内部状態を組み立てさせない(例: status=3 を避ける)。
  • フィールドを追加する際は後方互換性を保ち、既存フィールドの意味を変えない。
なぜデータベースの概念をAPIに漏らすことが長期的に問題になるのですか?

それは実装を固定してしまうからです。クライアントがテーブルIDやジョインキー、SQLライクなフィルタに依存すると、スキーマを変えるたびにAPI互換性が壊れます。

「保存方法」ではなくドメインの問いをモデル化してください(例: 「ある顧客の指定期間の注文」)。契約の背後で実装は隠すべきです。

実務的なAPI用語でのリスコフの置換原則(LSP)とは何ですか?

LSPは「あるコードがインターフェースで動くなら、そのインターフェースのどの実装に入れ替えても動き続けるべきだ」という原則です。APIでは「呼び出し側を驚かせない」ことを意味します。

代替可能な実装をサポートするには:

  • 有効な入力を標準化する(実装が追加の事前条件を課さない)。
  • 出力の保証(順序、一意性、完全性)を示す。
  • エラーや「見つからない」の扱いを統一する。
複数の実装やプロバイダが存在する場合、よくあるLSP違反は何ですか?

注意する違反例:

  • 入力の狭窄: 新実装がインターフェースで許容されていた入力を拒否する。
  • 出力の弱化: アイテムを落とす、順序を変える、部分的なデータを返す。
  • 異なる障害セマンティクス: ある実装は「not found」を返し別の実装は例外を投げる、など。

実装に追加の制約が必要なら、別インターフェースを定義するか、supportsNumericIds() のような明示的機能として提示して、クライアントが能動的に選べるようにします。

小さく凝集した理解しやすいAPIをどう設計すればいいですか?

インターフェースは小さく一貫性があり読みやすいべきです:

  • 単一の抽象に属する操作だけを公開する。
  • options: any や多数のブール値を避け、曖昧さを減らす。
  • reservereleaselistvalidate のように観測可能な振る舞いを表す名前を使う。

役割や変更頻度が異なる部分があればモジュールを分けます(進化に関しては /blog/evolving-apis-without-breaking-users を参照)。

失敗を予測可能でテスト可能にするには、エラー処理をどう設計すべきですか?

エラー処理を契約の一部として設計します:

  • プログラマエラー(契約違反)とランタイム障害を分ける。
  • テストが文字列マッチに頼らないよう、安定したエラーコードや構造化フィールドを文書化する。
  • リトライの安全性や冪等性(idempotency key、リクエストID)を明記し、バッチの部分成功時の扱いを定義する。

重要なのは一貫性で、呼び出し側が予測して処理できることです。

Related posts