公共API设计与兼容性 api-design

本技能专注于指导开发者如何设计稳定、向后兼容的公共API,适用于NuGet包、类库和分布式系统。核心内容包括扩展优先设计原则、API兼容性管理(源代码、二进制、线缆)、版本控制策略、自动化API批准测试以及安全演进线缆格式。关键词:API设计、兼容性、版本控制、NuGet包、分布式系统、扩展优先、语义化版本、API测试、线缆格式、破坏性更改。

架构设计 0 次安装 0 次浏览 更新于 2/26/2026

name: api-design description: 使用扩展优先设计原则设计稳定、兼容的公共API。管理NuGet包和分布式系统的API兼容性、线缆兼容性和版本控制。 invocable: false

公共API设计与兼容性

何时使用此技能

在以下情况下使用此技能:

  • 为NuGet包或库设计公共API
  • 对现有公共API进行更改
  • 为分布式系统规划线缆格式更改
  • 实施版本控制策略
  • 审查包含破坏性更改的拉取请求

三种兼容性类型

类型 定义 范围
API/源代码 代码能针对新版本编译 公共方法签名、类型
二进制 已编译代码能针对新版本运行 程序集布局、方法标记
线缆 序列化数据能被其他版本读取 网络协议、持久化格式

破坏其中任何一种都会给用户带来升级摩擦。


扩展优先设计

稳定API的基础:永不删除或修改,只扩展

三大支柱

  1. 先前功能不可变 - 一旦发布,行为和签名即被锁定
  2. 通过新构造添加新功能 - 添加重载、新类型、可选功能
  3. 仅在弃用期后移除 - 以年为单位,而非发布周期

优势

  • 旧代码在新版本中继续工作
  • 新旧路径共存
  • 升级默认非破坏性
  • 用户按自己的时间表升级

资源:


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审查流程

  1. PR包含对 *.verified.txt 文件的更改
  2. 审查者在差异中看到确切的API表面更改
  3. 破坏性更改立即可见
  4. 需要做出有意识的决定来批准

线缆兼容性

对于分布式系统,序列化数据必须能在不同版本间读取。

要求

方向 要求
向后兼容 旧写入者 → 新读取者(当前版本读取旧数据)
向前兼容 新写入者 → 旧读取者(旧版本读取新数据)

两者都是零停机滚动升级所必需的。

安全演进线缆格式

阶段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移除

关键原则

  1. 无意外破坏 - 即使是主版本也应提前宣布和规划
  2. 随时可扩展 - 新API可在任何版本中发布
  3. 先弃用后移除 - [Obsolete] 至少持续一个次要版本
  4. 沟通时间表 - 用户需要规划升级

切斯特顿的栅栏

在移除或更改某物之前,先理解它为何存在。

假设每个公共API都有人在使用。如果你想更改它:

  1. 在GitHub上讨论提案
  2. 记录迁移路径
  3. 提供弃用期
  4. 在计划版本中发布

拉取请求检查清单

审查涉及公共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 }

资源