# 类型与业务建模:让合法端口保持合法
学习目标:理解业务不变量在哪里建立、怎样跨调用保持,以及封装为什么有成本。前置阅读:端口解析与保证由谁承担,知道普通整数和成功结果的区别即可。本文提出设计演进,不新增未经验证的领域类型实作。
# 案例与变化
原解析器在边界拒绝非法文本,返回 u16、int、number 等普通数值。现在端口要经过配置、连接计划和启动三个模块;有人把重试次数的整数传给连接参数,或者对合法端口做算术后传入 65536。
新目标不是“每一层再解析一次”,而是让内部 API 能区分已经校验的连接端口与任意整数。外部格式仍为 1~5 个 ASCII 数字、数值 1~65535,失败仍不得静默使用默认值。这里不新增监听端口 0 的含义,也不保证端口当前可用。
# 实现需要改变什么
现有各语言解析函数中的字符、长度与范围检查是建立不变量的地方,不能因为新类型名叫 Port 就删除它们。变化应集中在成功返回值、构造权限和内部消费接口:
- 让成功路径构造受控的领域值,失败通道仍保留。
- 配置与连接计划传递领域值,不随手解包成普通整数再重新包装。
- 仅在实际调用底层网络 API 的边界取得原始数值。
- 把原来直接传整数的调用点逐一迁移;配置反序列化也必须走同一个有效性边界。
| 机制选择 | 可能阻止的误用 | 仍须承担的封装成本 |
|---|---|---|
| Rust 私有字段的新类型 | 外部模块直接构造任意字段值 | 构造函数、取值方法、模块边界 |
| Go 定义类型与受控字段 | 不同定义类型的意外混用 | 可见性、显式转换与包内约束 |
| Java/Kotlin 封装类 | 普通整数直接充当领域参数 | 构造入口与必要的互操作 |
| TypeScript 品牌化类型 | 已检查代码中普通数值的误传 | 品牌边界与禁止滥用断言的约定 |
| Python 包装对象 | 提供独立接口及集中校验位置 | 运行时校验与可选静态检查 |
| C++ 私有构造的值类型 | 不受控隐式构造与类型混淆 | 构造策略、访问控制、转换规则 |
机制事实:Rust 的可见性规则允许控制字段访问;Go 的类型定义与类型别名不同,定义类型建立新的类型身份,但显式转换仍需按规范分析。把整数简单重命名,不能自动成为不可伪造的有效值。
机制事实:TypeScript 的类型断言不执行校验;Python 的 NewType主要提供静态区分,在运行时调用返回原对象。因此二者都不能单靠“新的类型名”保证端口范围。
# 保证与责任
静态工具可以发现受检查调用点仍在传普通整数,帮助列出迁移范围;不能确认校验工厂中写的是 <= 65535 而不是 <= 65536。Rust 原来的 u16 排除了大于 65535 的可表示值,却仍包含 0;参见 u16::MIN。领域规则比机器表示范围更窄。
运行契约包含两部分:工厂拒绝非法输入;构造后没有公开可变字段或绕过入口的正常路径破坏不变量。内部可信模块仍可能写错,反序列化框架也可能采取不同构造方式;不能只测试一个公开构造函数就宣称全部入口封闭。
调用方责任是先消费失败,再保存成功领域值。Rust 的 Result、Go 的 error、C++ 的 optional 与异常分支仍然必要。新类型不提供网络连接成功保证,也不改变端口解析失败后停止启动的策略。需要向用户解释格式错误还是范围错误时,还要单独扩展错误模型,不能期待 Port 名字携带失败信息。
库存例子提供相同警告:非负库存值可排除负数,却不能保证显示提示时库存没有被其他请求修改。类型约束一个值,不自动约束并发时序。报告输出中的 Writer 同样只表明写入能力,不代表报告已发布。
# 取舍与适用条件
设计分析:三层传递、同类型参数容易混用、反复校验造成分歧时,领域值值得引入。收益是把不变量建立集中起来,并让一部分错误调用在工具阶段暴露;代价是公共 API 迁移、构造入口维护以及与标准库整数 API 的转换。
只有一个解析后立即打印的命令行例子时,现有普通返回值和明确调用分支已经足够。为每个临时整数包一层类型会增加概念数量,却没有消除真实重复。对 Java、Kotlin、Python 的包装可能有对象成本,对 Rust、C++ 等值类型也不能不经编译与测量就承诺任意封装都免费。
未来若监听配置允许 0 表示自动分配,应重新确认领域:连接端口与监听请求未必应共用同一个类型。直接放宽原类型,会让所有内部调用的含义一起变化;拆分概念的成本可能小于让每个消费者重新记住“这个 0 是哪一种”。这是需求分化,不是当前实现已经支持的新功能。
# 验证与相关阅读
本文没有新增领域类型或运行程序。沿用的是现有端口契约和调用代码的证据,不把封装建议视为实现通过。语言基线见主题首页,原边界输入及未验证范围见验证记录。