mcpField
A
api-design

api-design

github.com/wshaddix開発8676 回インストール36 週間前に更新
リポジトリ

api-design について

ライブラリの開発者やレビュー担当者向けに、公開 API とワイヤ形式の設計・変更を扱うスキルです。互換性を保つ拡張型の変更、検証、バージョン管理の指針を示します。

主な機能

  1. 01ソース・バイナリ・ワイヤ互換性
  2. 02拡張型 API の進化
  3. 03命名と引数の規約
  4. 04エラーと戻り値の指針
  5. 05非推奨化とバージョン管理
  6. 06API 承認テストの指針

活用シーン

  1. 01公開ライブラリ API の設計
  2. 02プルリクエストの破壊的変更レビュー
  3. 03分散システムのワイヤ形式更新計画
  4. 04非推奨化とリリース方針の選択

インストール

コマンドをコピーしてターミナルで実行します。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill api-design

インストール後、Claude Code で/api-designを入力して読み込まれたか確認します。

詳細

パブリック API の設計と互換性

このスキルを使用する場面

次のような場合にこのスキルを使用します。

  • NuGet パッケージやライブラリのパブリック API を設計するとき
  • 既存のパブリック API に変更を加えるとき
  • 分散システムのワイヤ形式の変更を計画するとき
  • バージョン管理戦略を実装するとき
  • 破壊的変更がないかプルリクエストをレビューするとき

3 種類の互換性

種類定義範囲
API/ソース新しいバージョンに対してコードをコンパイルできるパブリックメソッドのシグネチャ、型
バイナリコンパイル済みコードが新しいバージョンで実行できるアセンブリのレイアウト、メソッドトークン
ワイヤシリアル化されたデータを他のバージョンで読み取れるネットワークプロトコル、永続化形式

これらのいずれかを破ると、ユーザーのアップグレードに支障が生じます。


拡張のみの設計

安定した API の基盤は、削除も変更もせず、拡張のみを行うことです。

3 つの柱
  1. 以前の機能は不変です - リリース後は、動作とシグネチャを変更しません
  2. 新しい構成要素で機能を追加します - オーバーロード、新しい型、オプトイン機能を追加します
  3. 非推奨期間を経てから削除します - リリース単位ではなく、年単位で設けます
メリット
  • 古いコードは新しいバージョンでも引き続き動作します
  • 新旧の方法が共存します
  • 既定では、破壊的変更なしにアップグレードできます
  • ユーザーは自分の都合に合わせてアップグレードできます

API サーフェスの命名規則

型の命名
型の種類接尾辞のパターン例
基底クラス抽象基底型に限り Base を付けるValidatorBase
インターフェイスI を先頭に付けるIWidgetFactory
例外Exception を末尾に付けるWidgetNotFoundException
属性Attribute を末尾に付けるRequiredPermissionAttribute
イベント引数EventArgs を末尾に付けるWidgetCreatedEventArgs
オプション/構成Options を末尾に付けるWidgetServiceOptions
ビルダーBuilder を末尾に付けるWidgetBuilder
メソッドの命名
パターン規則例
同期動詞または動詞句Calculate()、GetWidget()
非同期Async を末尾に付けるCalculateAsync()、GetWidgetAsync()
ブール値の問い合わせIs/Has/Can を先頭に付けるIsValid()、HasPermission()
Try パターンTry を先頭に付け、out パラメーターを使用するTryGetWidget(int id, out Widget widget)
ファクトリーCreate を先頭に付けるCreateWidget()、CreateWidgetAsync()
変換To/From を先頭に付けるToDto()、FromEntity()
パブリック API では略語を避ける
// WRONG -- abbreviations in public surface
public IReadOnlyList<TxnResult> GetRecentTxns(int cnt);

// CORRECT -- spelled out for clarity
public IReadOnlyList<TransactionResult> GetRecentTransactions(int count);

パラメーターの順序

パラメーターの順序を統一すると、認知負荷を軽減できます。

標準的な順序
  1. 対象/主題 -- 操作の対象となる主要なエンティティ
  2. 必須パラメーター -- 既定値のない必須の入力
  3. オプションのパラメーター -- 適切な既定値を持つ入力
  4. キャンセルトークン -- 常に最後に配置します(CA1068 によって規則が適用されます)
public Task<Widget> GetWidgetAsync(
    int widgetId,                              // 1. Target
    WidgetOptions options,                     // 2. Required
    bool includeHistory = false,               // 3. Optional
    CancellationToken cancellationToken = default); // 4. Always last
オーバーロードの展開順序
// Simple -- sensible defaults
public Task<Widget> GetWidgetAsync(int widgetId,
    CancellationToken cancellationToken = default)
    => GetWidgetAsync(widgetId, WidgetOptions.Default, cancellationToken);

// Detailed -- full control
public Task<Widget> GetWidgetAsync(int widgetId,
    WidgetOptions options,
    CancellationToken cancellationToken = default);

戻り値の型の選択

戻り値の選び方
シナリオ戻り値の型理由
単一のエンティティが常に存在するWidget見つからない場合は例外をスローします
単一のエンティティが存在しない可能性があるWidget?Nullable によって省略可能であることを示します
空の場合もあるコレクションIReadOnlyList<Widget>不変でインデックスアクセスが可能であり、変更不可であることを示します
ストリーミング結果IAsyncEnumerable<Widget>結果セット全体のバッファリングを回避します
詳細を伴う操作結果Result<Widget>/判別可能なユニオン例外を使わず、詳細なエラー情報を提供します
非同期処理で値を返さないTaskイベントハンドラーを除き、async void は使用しません
同期的に完了することが多いValueTask<Widget>キャッシュヒット時の Task の割り当てを回避します
IEnumerable より IReadOnlyList を優先する
// WRONG -- caller does not know if result is materialized or lazy
public IEnumerable<Widget> GetWidgets();

// CORRECT -- signals materialized, indexable collection
public IReadOnlyList<Widget> GetWidgets();

// CORRECT -- signals streaming/lazy evaluation explicitly
public IAsyncEnumerable<Widget> GetWidgetsStreamAsync(
    CancellationToken cancellationToken = default);
Try パターン
public bool TryGetWidget(int widgetId, [NotNullWhen(true)] out Widget? widget);

public Task<Widget?> TryGetWidgetAsync(int widgetId,
    CancellationToken cancellationToken = default);

エラー報告の戦略

例外の階層
public class WidgetServiceException : Exception
{
    public WidgetServiceException(string message) : base(message) { }
    public WidgetServiceException(string message, Exception inner) : base(message, inner) { }
}

public class WidgetNotFoundException : WidgetServiceException
{
    public int WidgetId { get; }
    public WidgetNotFoundException(int widgetId)
        : base($"Widget {widgetId} not found.") => WidgetId = widgetId;
}

public class WidgetValidationException : WidgetServiceException
{
    public IReadOnlyList<string> Errors { get; }
    public WidgetValidationException(IReadOnlyList<string> errors)
        : base("Widget validation failed.") => Errors = errors;
}
例外と戻り値を使い分ける場面
アプローチ使用する場面
例外をスローする想定外の失敗、プログラミングエラー、インフラストラクチャの障害
null / default を返す「見つからない」が通常かつ想定される結果である場合
Try パターン(bool + out)失敗がよく発生し、同期処理である解析や検証
Result オブジェクト呼び出し側が区別する必要のある複数の失敗ケース
引数の検証
public Widget CreateWidget(string name, decimal price)
{
    ArgumentException.ThrowIfNullOrWhiteSpace(name);
    ArgumentOutOfRangeException.ThrowIfNegativeOrZero(price);

    return new Widget(name, price);
}

API 変更のガイドライン

安全な変更(すべてのリリースで可能)
// ADD new overloads with default parameters
public void Process(Order order, CancellationToken ct = default);

// ADD new optional parameters to existing methods
public void Send(Message msg, Priority priority = Priority.Normal);

// ADD new types, interfaces, enums
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }

// ADD new members to existing types
public class Order
{
    public DateTimeOffset? ShippedAt { get; init; }  // NEW
}
安全でない変更(原則禁止、またはメジャーバージョンのみ)
// REMOVE or RENAME public members
public void ProcessOrder(Order order);  // Was: Process()

// CHANGE parameter types or order
public void Process(int orderId);  // Was: Process(Order order)

// CHANGE return types
public Order? GetOrder(string id);  // Was: public Order GetOrder()

// CHANGE access modifiers
internal class OrderProcessor { }  // Was: public

// ADD required parameters without defaults
public void Process(Order order, ILogger logger);  // Breaks callers!
非推奨化のパターン
// Step 1: Mark as obsolete with version
[Obsolete("Obsolete since v1.5.0. Use ProcessAsync instead.")]
public void Process(Order order) { }

// Step 2: Add new recommended API
public Task ProcessAsync(Order order, CancellationToken ct = default);

// Step 3: Remove in next major version

拡張ポイント

インターフェイスベースの拡張
// GOOD -- interface-based extension point
public interface IWidgetValidator
{
    ValueTask<bool> ValidateAsync(Widget widget, CancellationToken ct = default);
}

// GOOD -- delegate-based extension for simple hooks
public class WidgetServiceOptions
{
    public Func<Widget, CancellationToken, ValueTask>? OnWidgetCreated { get; set; }
}
拡張メソッドのガイドライン
ガイドライン理由
型と同じ名前空間に拡張メソッドを配置する追加の using ステートメントなしで見つけやすくなります
拡張メソッドを System または System.Linq に配置しない名前空間の汚染を防ぎます
型を所有している場合は、拡張メソッドよりインスタンスメソッドを優先する拡張メソッドは最後の手段です
this パラメーターには、使用可能な中で最も具体的な型を指定するIntelliSense の汚染を防ぎます

ワイヤー互換性

分散システムでは、シリアル化されたデータをバージョン間で読み取れる必要があります。

要件
方向要件
後方互換性古い書き込み側 → 新しい読み取り側
前方互換性新しい書き込み側 → 古い読み取り側

ダウンタイムなしのローリングアップグレードには、両方が必要です。

ワイヤーフォーマットを安全に進化させる

フェーズ 1:読み取り側のサポートを追加する

public sealed record HeartbeatV2(
    Address From,
    long SequenceNr,
    long CreationTimeMs);  // NEW field

public object Deserialize(byte[] data, string manifest) => manifest switch
{
    "Heartbeat" => DeserializeHeartbeatV1(data),
    "HeartbeatV2" => DeserializeHeartbeatV2(data),
    _ => throw new NotSupportedException()
};

フェーズ 2:書き込み側を有効にする(次のマイナーバージョン)

akka.cluster.use-heartbeat-v2 = on
防御的なシリアル化設計
public sealed class WidgetDto
{
    [JsonPropertyName("id")]
    public int Id { get; init; }

    [JsonPropertyName("name")]
    public required string Name { get; init; }

    [JsonPropertyName("category")]
    public string? Category { get; init; }

    [JsonPropertyName("priority")]
    [JsonIgnore(Condition = JsonIgnoreCondition.WhenWritingDefault)]
    public int Priority { get; init; }
}
Enum のシリアル化戦略
// GOOD -- string serialization is rename-safe
[JsonConverter(typeof(JsonStringEnumConverter))]
public enum WidgetStatus
{
    Draft,
    Active,
    Archived
}

// RISKY -- integer serialization breaks when members are reordered
public enum WidgetPriority
{
    Low = 0,
    Medium = 1,
    High = 2
}

API 承認テスト

自動化された API サーフェステストで、意図しない破壊的変更を防ぎます。

[Fact]
public Task ApprovePublicApi()
{
    var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
    return Verify(api);
}
PR レビュープロセス
  1. PR に *.verified.txt ファイルの変更を含める
  2. レビュアーは差分で API サーフェスの変更を正確に確認できる
  3. 破壊的変更がすぐにわかる
  4. 承認には意識的な判断が必要

バージョン管理戦略

セマンティックバージョニング(実践的な運用)
バージョン許容される変更
パッチ(1.0.x)バグ修正、セキュリティパッチ
マイナー(1.x.0)新機能、非推奨化、obsolete の削除
メジャー(x.0.0)破壊的変更、古い API の削除
主な原則
  1. 予告なしの破壊的変更を避ける - メジャーバージョンであっても告知が必要です
  2. 拡張はいつでも可能 - 新しい API はどのリリースでも提供できます
  3. 削除前に非推奨化する - 少なくとも 1 つのマイナーバージョンで [Obsolete] を使用します
  4. スケジュールを伝える - ユーザーがアップグレードを計画できるようにします

プルリクエストのチェックリスト

  • 公開メンバーを削除していない(代わりに [Obsolete] を使用する)
  • シグネチャを変更していない(代わりにオーバーロードを追加する)
  • 新しい必須パラメーターを追加していない(デフォルト値を使用する)
  • API 承認テストを更新した(.verified.txt の変更をレビュー済み)
  • ワイヤーフォーマットの変更はオプトイン方式である(読み取り側を先に対応する)
  • 破壊的変更を文書化した(リリースノート、移行ガイド)

アンチパターン

修正を装った破壊的変更
// "Bug fix" that breaks users
public async Task<Order> GetOrderAsync(OrderId id)  // Was sync!
{
}

// Correct: Add new method, deprecate old
[Obsolete("Use GetOrderAsync instead")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;

public async Task<Order> GetOrderAsync(OrderId id) { }
サイレントな動作変更
// Changing defaults breaks users
public void Configure(bool enableCaching = true)  // Was: false!

// Correct: New parameter with new name
public void Configure(
    bool enableCaching = false,
    bool enableNewCaching = true)
ポリモーフィックなシリアル化
// AVOID: Type names in wire format
{ "$type": "MyApp.Order, MyApp", "Id": 123 }

// PREFER: Explicit discriminators
{ "type": "order", "id": 123 }

エージェントが注意すべき点

  1. 公開 API 名に略語を使用しないでください -- 単語を省略せずに記述します。
  2. CancellationToken を省略可能なパラメーターより前に配置しないでください -- CA1068 により最後に配置する必要があります。
  3. 変更可能なコレクションを公開 API から返さないでください -- IReadOnlyList<T> を返します。
  4. [JsonPropertyName] 属性を付けずに、シリアル化されたプロパティ名を変更しないでください。
  5. 既存の公開メソッドに必須パラメーターを追加しないでください -- オーバーロードを追加するか、デフォルト値を使用します。
  6. API サーフェスで async void を使用しないでください -- Task または ValueTask を返します。
  7. 基底となるライブラリ例外なしに例外階層を設計しないでください。
  8. 拡張メソッドを System 名前空間に配置しないでください。

リソース

コメント