关于 api-design
面向库作者和评审者的公共 API 与线格式设计技能,涵盖兼容性、扩展式演进及版本管理实践。
主要功能
- 01源码、二进制与线格式兼容性
- 02扩展式 API 演进
- 03命名与参数约定
- 04错误及返回类型指导
- 05弃用与版本管理实践
- 06API 审批测试指导
适用场景
- 01设计公共库 API
- 02评审拉取请求中的破坏性变更
- 03规划分布式线格式更新
- 04选择弃用与发布策略
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ wshaddix/ dotnet-skills --skill api-design安装后在 Claude Code 中输入/api-design确认已加载。
详细介绍
公共 API 设计与兼容性
何时使用此技能
在以下情况使用此技能:
- 为 NuGet 包或库设计公共 API
- 更改现有公共 API
- 规划分布式系统的线路格式更改
- 实施版本控制策略
- 审查可能造成重大变更的拉取请求
三种兼容性类型
| 类型 | 定义 | 范围 |
|---|---|---|
| API/源代码 | 代码可针对较新版本编译 | 公共方法签名、类型 |
| 二进制 | 编译后的代码可在较新版本上运行 | 程序集布局、方法令牌 |
| 线路 | 序列化数据可由其他版本读取 | 网络协议、持久化格式 |
破坏其中任何一种兼容性,都会增加用户升级的阻力。
仅扩展式设计
稳定 API 的基础:绝不移除或修改,只进行扩展。
三大支柱
- 既有功能保持不变 - 发布后,行为和签名即固定
- 通过新构造添加新功能 - 添加重载、新类型和可选择启用的功能
- 仅在弃用期结束后移除 - 以年为单位,而不是以发布次数为单位
优势
- 旧代码在新版本中继续正常工作
- 新旧实现路径可以共存
- 默认情况下,升级不会造成破坏性变更
- 用户可以按照自己的计划升级
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);
参数顺序
统一的参数顺序可以减轻认知负担。
标准顺序
- 目标/主体 -- 要操作的主要实体
- 必需参数 -- 没有默认值的必要输入
- 可选参数 -- 具有合理默认值的输入
- 取消令牌 -- 始终放在最后(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? | 可空类型表示可选性 |
| 集合,可能为空 | IReadOnlyList<Widget> | 不可变、可通过索引访问,并表示不可修改 |
| 流式结果 | IAsyncEnumerable<Widget> | 避免缓冲整个结果集 |
| 包含详细信息的操作结果 | Result<Widget> / 判别联合 | 无需异常即可提供丰富的错误信息 |
| 异步空返回值 | Task | 除事件处理程序外,绝不使用 async void |
| 经常同步完成 | ValueTask<Widget> | 缓存命中时避免分配 Task |
优先使用 IReadOnlyList,而非 IEnumerable
// 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) | 解析或验证失败很常见且操作同步的场景 |
| 结果对象 | 存在多种调用方需要区分的失败类型 |
参数验证
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; }
}
枚举序列化策略
// 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 审查流程
- PR 包含对
*.verified.txt文件的更改 - 审查者可以在差异中看到精确的 API 表面变更
- 破坏性变更会立即显现
- 必须经过审慎决策才能批准
版本控制策略
语义化版本控制(实践指南)
| 版本 | 允许的变更 |
|---|---|
| 补丁(1.0.x) | Bug 修复、安全补丁 |
| 次版本(1.x.0) | 新功能、弃用、移除过时内容 |
| 主版本(x.0.0) | 破坏性变更、移除旧 API |
核心原则
- 避免意外破坏——即使是主版本,也应提前公告
- 随时扩展——新 API 可以随任何版本发布
- 先弃用,再移除——至少提前一个次版本使用
[Obsolete] - 说明时间安排——用户需要规划升级
Pull Request 检查清单
- 没有移除公共成员(改用
[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 }
Agent 注意事项
- 公共 API 名称中不要使用缩写——应拼写完整单词。
- 不要将 CancellationToken 放在可选参数之前——CA1068 要求它位于最后。
- 不要从公共 API 返回可变集合——应返回
IReadOnlyList<T>。 - 更改序列化属性名称时,必须添加
[JsonPropertyName]注解。 - 不要向现有公共方法添加必需参数——应添加重载或使用默认值。
- 不要在 API 表面中使用
async void——应返回Task或ValueTask。 - 设计异常层次结构时,必须包含基础库异常作为基类。
- 不要将扩展方法放在
System命名空间中。
资源
评论
支持 Markdown