name: api-design description: 使用扩展优先设计原则设计稳定、兼容的公共API。管理NuGet包和分布式系统的API兼容性、线缆兼容性和版本控制。 invocable: false
公共API设计与兼容性
何时使用此技能
在以下情况下使用此技能:
- 为NuGet包或库设计公共API
- 对现有公共API进行更改
- 为分布式系统规划线缆格式更改
- 实施版本控制策略
- 审查包含破坏性更改的拉取请求
三种兼容性类型
| 类型 | 定义 | 范围 |
|---|---|---|
| API/源代码 | 代码能针对新版本编译 | 公共方法签名、类型 |
| 二进制 | 已编译代码能针对新版本运行 | 程序集布局、方法标记 |
| 线缆 | 序列化数据能被其他版本读取 | 网络协议、持久化格式 |
破坏其中任何一种都会给用户带来升级摩擦。
扩展优先设计
稳定API的基础:永不删除或修改,只扩展。
三大支柱
- 先前功能不可变 - 一旦发布,行为和签名即被锁定
- 通过新构造添加新功能 - 添加重载、新类型、可选功能
- 仅在弃用期后移除 - 以年为单位,而非发布周期
优势
- 旧代码在新版本中继续工作
- 新旧路径共存
- 升级默认非破坏性
- 用户按自己的时间表升级
资源:
API变更指南
安全变更(任何版本)
// 添加带有默认参数的新重载
public void Process(Order order, CancellationToken ct = default);
// 向现有方法添加新的可选参数
public void Send(Message msg, Priority priority = Priority.Normal);
// 添加新类型、接口、枚举
public interface IOrderValidator { }
public enum OrderStatus { Pending, Complete, Cancelled }
// 向现有类型添加新成员
public class Order
{
public DateTimeOffset? ShippedAt { get; init; } // 新增
}
不安全变更(仅主版本或永不)
// 移除或重命名公共成员
public void ProcessOrder(Order order); // 原为:Process()
// 更改参数类型或顺序
public void Process(int orderId); // 原为:Process(Order order)
// 更改返回类型
public Order? GetOrder(string id); // 原为:public Order GetOrder()
// 更改访问修饰符
internal class OrderProcessor { } // 原为:public
// 添加没有默认值的必需参数
public void Process(Order order, ILogger logger); // 破坏调用者!
弃用模式
// 步骤1:标记为过时并注明版本(任何版本)
[Obsolete("自v1.5.0起已过时。请使用ProcessAsync。")]
public void Process(Order order) { }
// 步骤2:添加新的推荐API(同一版本)
public Task ProcessAsync(Order order, CancellationToken ct = default);
// 步骤3:在下一个主版本中移除(v2.0+)
// 仅在用户有时间迁移后
API批准测试
通过自动化API表面测试防止意外的破坏性更改。
使用ApiApprover + Verify
dotnet add package PublicApiGenerator
dotnet add package Verify.Xunit
[Fact]
public Task ApprovePublicApi()
{
var api = typeof(MyLibrary.PublicClass).Assembly.GeneratePublicApi();
return Verify(api);
}
创建 ApprovePublicApi.verified.txt:
namespace MyLibrary
{
public class OrderProcessor
{
public OrderProcessor() { }
public void Process(Order order) { }
public Task ProcessAsync(Order order, CancellationToken ct = default) { }
}
}
任何API更改都会导致测试失败 - 审查者必须明确批准更改。
PR审查流程
- PR包含对
*.verified.txt文件的更改 - 审查者在差异中看到确切的API表面更改
- 破坏性更改立即可见
- 需要做出有意识的决定来批准
线缆兼容性
对于分布式系统,序列化数据必须能在不同版本间读取。
要求
| 方向 | 要求 |
|---|---|
| 向后兼容 | 旧写入者 → 新读取者(当前版本读取旧数据) |
| 向前兼容 | 新写入者 → 旧读取者(旧版本读取新数据) |
两者都是零停机滚动升级所必需的。
安全演进线缆格式
阶段1:添加读取端支持(可选)
// 新消息类型 - 读取器先部署
public sealed record HeartbeatV2(
Address From,
long SequenceNr,
long CreationTimeMs); // 新字段
// 反序列化器处理新旧两种格式
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
阶段3:设为默认(未来版本)
在安装基础已吸收读取端代码之后。
基于模式的序列化
优先选择基于模式的格式而非基于反射的格式:
| 格式 | 类型 | 线缆兼容性 |
|---|---|---|
| Protocol Buffers | 基于模式 | 优秀 - 明确的字段编号 |
| MessagePack | 基于模式 | 良好 - 使用合约 |
| System.Text.Json | 基于模式(使用源生成) | 良好 - 明确的属性 |
| Newtonsoft.Json | 基于反射 | 差 - 类型名称在负载中 |
| BinaryFormatter | 基于反射 | 极差 - 切勿使用 |
详情请参阅 dotnet/serialization 技能。
封装模式
内部API
明确标记非公共API:
// 用于文档的属性
[InternalApi]
public class ActorSystemImpl { }
// 命名空间约定
namespace MyLibrary.Internal
{
public class InternalHelper { } // 为可扩展性而公开,非为用户
}
清晰记录:
位于
.Internal命名空间中或标记有[InternalApi]的类型可能在任意版本间更改,恕不另行通知。
密封类
// 应做:密封非为继承设计的类
public sealed class OrderProcessor { }
// 不应做:意外留下未密封的类
public class OrderProcessor { } // 用户可能继承,阻碍更改
接口隔离
// 应做:小巧、专注的接口
public interface IOrderReader
{
Order? GetById(OrderId id);
}
public interface IOrderWriter
{
Task SaveAsync(Order order);
}
// 不应做:庞大的接口(无法在不破坏的情况下添加方法)
public interface IOrderRepository
{
Order? GetById(OrderId id);
Task SaveAsync(Order order);
// 添加新方法会破坏所有实现!
}
版本控制策略
语义化版本控制(实用版)
| 版本 | 允许的更改 |
|---|---|
| 补丁 (1.0.x) | 错误修复、安全补丁 |
| 次要 (1.x.0) | 新功能、弃用、过时项移除 |
| 主版本 (x.0.0) | 破坏性更改、旧API移除 |
关键原则
- 无意外破坏 - 即使是主版本也应提前宣布和规划
- 随时可扩展 - 新API可在任何版本中发布
- 先弃用后移除 -
[Obsolete]至少持续一个次要版本 - 沟通时间表 - 用户需要规划升级
切斯特顿的栅栏
在移除或更改某物之前,先理解它为何存在。
假设每个公共API都有人在使用。如果你想更改它:
- 在GitHub上讨论提案
- 记录迁移路径
- 提供弃用期
- 在计划版本中发布
拉取请求检查清单
审查涉及公共API的PR时:
- [ ] 未移除公共成员(改用
[Obsolete]) - [ ] 未更改签名(改用添加重载)
- [ ] 未添加新的必需参数(使用默认值)
- [ ] API批准测试已更新(已审查
.verified.txt更改) - [ ] 线缆格式更改是可选的(读取端优先)
- [ ] 破坏性更改已记录(发布说明、迁移指南)
反模式
伪装成修复的破坏性更改
// 破坏用户的“错误修复”
public async Task<Order> GetOrderAsync(OrderId id) // 原为同步!
{
// “修复”为异步 - 但破坏了所有调用者
}
// 正确做法:添加新方法,弃用旧方法
[Obsolete("请使用GetOrderAsync")]
public Order GetOrder(OrderId id) => GetOrderAsync(id).Result;
public async Task<Order> GetOrderAsync(OrderId id) { }
静默行为更改
// 更改默认值会破坏依赖旧行为的用户
public void Configure(bool enableCaching = true) // 原为:false!
// 正确做法:使用新名称的新参数
public void Configure(
bool enableCaching = false, // 保留原始默认值
bool enableNewCaching = true) // 新行为可选
多态序列化
// 避免:线缆格式中的类型名称
{ "$type": "MyApp.Order, MyApp", "Id": 123 }
// 重命名Order类 = 线缆中断!
// 首选:明确的鉴别器
{ "type": "order", "id": 123 }