Class modifiers for API maintainers(面向 API 维护者的类修饰符)
面向 API 维护者时,类修饰符的核心作用不是“让类看起来更严格”,而是明确用户可以怎样依赖你的公开类型。Dart 3.0 以后,除 abstract 外的类修饰符都可以帮助库作者控制外部库能否构造、继承、实现或混入某个类型。
如果一个类型已经作为公开 API 发布,给它新增 interface、base、final 或 sealed 通常会减少用户能力,可能是破坏性变更。设计新 API 时应尽早声明边界;维护旧 API 时要先判断用户是否已经依赖了继承、实现或 with 混入。
先判断 API 使用者需要什么能力
选择修饰符前,先把公开类型的使用方式拆开看:
| 问题 | 如果答案是“是” | 常见选择 |
|---|---|---|
| 用户需要直接创建实例吗 | 保留可构造类 | class、base class、interface class、final class |
| 用户需要继承你的实现吗 | 允许 extends | class、abstract class、base class |
| 用户只需要替换实现吗 | 允许 implements | abstract interface class、interface class |
| 用户需要把实现作为 Mixin 复用吗 | 允许 with | mixin、mixin class、base mixin |
| 用户需要穷尽处理所有分支吗 | 固定直接子类型集合 | sealed class |
这里的“用户”指声明该类型的库之外的代码。Dart 的限制以库为边界,而不是以包或目录为边界;同一个库内部仍然可以做更多操作。
迁移 Dart 3.0 前可作为 Mixin 的类
Dart 3.0 之前,满足条件的普通类可以被其他类放在 with 子句中使用。Dart 3.0 之后,类默认不能再作为 Mixin 使用;如果要继续支持这种 API,需要显式声明为 mixin class。
mixin class Timestamped {
DateTime createdAt = DateTime.now();
}
import 'timestamps.dart';
class Note with Timestamped {
Note(this.content);
final String content;
}
void main() {
final note = Note('release notes');
print(note.createdAt);
}
mixin class 同时保留“可以当类继承”和“可以当 Mixin 混入”的能力。若你只是想提供混入能力,不希望用户构造或继承它,使用 mixin 更清晰:
mixin Auditable {
void audit(String action) {
print('audit: $action');
}
}
迁移时可以用两个问题判断:用户是否应该构造这个类型;用户是否应该通过 with 复用它。两个答案都是“是”时使用 mixin class;只需要混入时使用 mixin;不再支持混入时保持普通 class,但这可能破坏旧用户代码。
用 interface 发布可替换契约
当你希望用户实现同一组能力,但不希望他们继承你的内部实现时,使用 interface。更常见的纯接口形式是 abstract interface class。
abstract interface class Cache {
String? read(String key);
void write(String key, String value);
}
import 'cache.dart';
final class MemoryCache implements Cache {
final Map<String, String> _values = {};
String? read(String key) => _values[key];
void write(String key, String value) {
_values[key] = value;
}
}
interface 的好处是 API 维护者可以避免跨库继承带来的脆弱基类问题。用户只能实现接口,不能复用并覆盖你的方法体;因此你的类内部方法调用 this 上的其他方法时,更容易保持可预期行为。
TypeScript 对比:TypeScript 的 interface 主要是静态类型结构;Dart 的 abstract interface class 是 Dart 类型声明,也会参与 implements、is 等类型关系。两者都能表达契约,但 Dart 还通过类修饰符控制外部库能否继承实现。
用 base 要求用户继承实现
当你的公开类型依赖自身实现或私有成员,并且希望所有子类型都继承这些实现时,使用 base。外部库可以 extends,但不能 implements。
base class Connection {
bool _open = false;
void open() {
_open = true;
}
void send(String message) {
if (!_open) {
throw StateError('connection is not open');
}
print(message);
}
}
import 'connection.dart';
final class LoggingConnection extends Connection {
void send(String message) {
print('sending: $message');
super.send(message);
}
}
如果外部库可以 implements Connection,它就不会继承 _open 和 open() 的状态逻辑,维护者新增依赖私有成员的方法时更容易破坏实现类。base 通过禁止外部 implements,让所有外部子类型都经过真实继承链。
直接继承或实现 base 类型的类必须继续声明为 base、final 或 sealed。这叫传递性约束,用来避免子类重新打开实现边界。
用 final 关闭外部子类型
如果用户只应该构造和使用你的类型,不应该通过继承或实现来创建新的子类型,使用 final class。
final class PackageVersion {
const PackageVersion(this.major, this.minor, this.patch);
final int major;
final int minor;
final int patch;
String toString() => '$major.$minor.$patch';
}
final class 对 API 维护者最宽松:你可以更安全地新增实例方法、调整内部实现,用户不会有第三方子类或实现类需要同步适配。它适合值对象、配置对象、结果对象等不希望外部扩展的公开类型。
如果类型只作为静态成员命名空间使用,可以组合成 abstract final class,同时禁止实例化和外部子类型:
abstract final class PackageNames {
static const core = 'core';
static const cli = 'cli';
}
用 sealed 发布固定分支集合
当一个公开类型代表一组由库作者维护的固定分支,并且你希望用户在 switch 中穷尽处理这些分支时,使用 sealed class。
sealed class LoadState {}
final class Loading extends LoadState {}
final class Loaded extends LoadState {
Loaded(this.data);
final String data;
}
final class Failed extends LoadState {
Failed(this.message);
final String message;
}
String label(LoadState state) {
return switch (state) {
Loading() => 'loading',
Loaded(:final data) => data,
Failed(:final message) => 'error: $message',
};
}
sealed 会让分析器知道所有直接子类型,因此上面的 switch 不需要兜底分支。代价是:以后给 LoadState 新增直接子类型,会让用户已有的穷尽 switch 变成非穷尽,这通常也是破坏性变更。
如果你希望未来可以非破坏性地增加新的子类型,不要用 sealed 固定分支集合;可以考虑 final,让用户不能创建外部子类型,同时仍需要在模式匹配中保留兜底处理。
维护公开 API 的选择顺序
维护库时可以按下面顺序判断:
- 先处理 Dart 3.0 的 Mixin 行为变化:旧类如果确实要继续支持
with,显式改成mixin class。 - 如果类型是纯契约,优先用
abstract interface class。 - 如果类型必须继承实现才能正确工作,用
base class或abstract base class。 - 如果类型不应该有外部子类型,用
final class。 - 如果类型代表固定分支并希望支持穷尽检查,用
sealed class。
新增限制前要检查公开 API 的兼容性。对包作者来说,收紧外部继承、实现或混入能力通常应配合主版本升级和迁移说明。