# 简洁与显式:删掉一行是否删掉了契约
学习目标:区分源代码行数、需要掌握的概念和行为可见性,判断哪些简化保持语义。前置阅读:库存判断、字符串转端口号;只需理解分支与失败调用方。本篇是基于现有代码的设计分析,不新增程序,也不把简洁程度作为语言排名。
# 案例与变化
原库存规则接收已经校验的非负整数:0 显示 sold out,1~5 显示 low stock,其余显示 in stock。现在产品将低库存阈值从 5 调整为 10,但 0 的提示保持不变。
这个变化只有一个业务维度,不需要引入策略对象。必须保持三条互斥路径;新的边界观察是 0、1、10、11。把三段代码压成“库存有值吗”的判断并不等价:非零库存仍然有两个区间。把阈值判断放到零判断之前,也会把 0 错归为低库存。
另一个常见的“简化”是把端口解析的格式检查删掉,只保留数字转换。这里需求并未放宽:"80x"、" 80" 和 "80" 仍须失败。代码更短但接受集合变了,不属于保持契约的重构。
# 实现需要改变什么
八种语言的库存完整程序都只需调整 stock <= 5 的阈值与边界用例,第一分支和输出文本不变。Rust 与 Kotlin 能用 if 产值,并不意味着必须把三个打印分支改成嵌套表达式;Go 没有三元表达式,也不值得为这个变化创建映射或闭包。
机制事实:Rust 的 if 可以作为表达式,分支结果需要类型兼容;Kotlin 的 if 用作表达式时需要完整分支。分别见 Rust 控制流与 Kotlin 条件表达式。这些规则描述可用机制,不规定哪种布局最易读。
端口例子则有不同层次的显式性:
| 现有位置 | 看得见的规则 | 删除后的代价 |
|---|---|---|
JS/TS 的长度与 /[^0-9]/ 检查 |
1~5 位 ASCII 数字 | Number 的接受范围不再受协议限制 |
Kotlin 的两次 require |
格式与业务范围分别成立 | toInt() 只承担解析,不承担端口业务规则 |
C++ 的 ptr != end |
必须消费完整输入 | 一般解析逻辑可能误收前缀 |
Go 调用方的 if err != nil |
错误值先于成功值消费 | 返回的 0 可能被误当端口 |
机制事实:ECMAScript 的 parseInt允许读取数字前缀;因此把现有 JS/TS 函数改成单独调用它,会令 "80x" 变为 80。C++ 示例已经预先限制字符,后续停止位置检查在本契约内不是新的可达失败原因;保留它是沿用完整解析结果的消费方式,不应夸大为本例唯一安全屏障。
# 保证与责任
工具反馈:把库存阈值写成 10 通常仍是类型正确的程序,编译器不会知道产品究竟要 5 还是 10。Rust、Go、Java、Kotlin 对条件的布尔约束能排除部分语法或类型误用;JS/TS、Python、C++ 的条件可采用相应真值转换。但这些机制都不证明分支顺序符合库存业务,参见原例关键差异。
运行契约:库存需要观察新边界,而不是只保留 stock = 3 的成功输出。端口需要保留拒绝输入表,而不是只验证 "8080"。类型检查与少量中间值都无法发现接受集合被偷偷放宽。
调用方责任:库存调用方仍负责拒绝负数;端口调用方仍须在失败分支停止使用该结果。现有 JS 驱动只消费 RangeError,其他异常继续抛出;把它简化成无条件打印“invalid port”会隐藏程序错误。异常路径虽然增加行数,却使失败归属可见。
再看报告输出:Python 的 with、显式 UTF-8、newline="" 和关闭后字节断言各自表达不同责任。删掉它们不能只记为“减少样板”;必须先说明资源时序、编码和行尾由谁继续保证。这正是保证由谁承担要求追问的边界。
# 取舍与适用条件
设计分析:当分支只是从两个值中选择日志级别,表达式可让“只初始化一次”更明显;当每条分支包含写入、失败处理或资源操作,语句块更容易暴露时序。简洁应减少读者必须同时记住的规则,而不是把规则移到隐式转换、默认值或远处文档。
本次阈值变化不值得引入“库存状态策略工厂”。若阈值由配置提供、多个界面共享且需要统一更新,再把阈值和展示规则集中到函数或配置边界才有真实收益。代价是新增参数或共享依赖,而不是免费减少复杂度。
不要从单个短函数推导性能结论。Kotlin 原例避免每次构造正则,表达的是具体实现选择;八门语言的运行时间、分配或编译成本并未在此测量。
# 验证与相关阅读
本文未修改原程序,阈值变化及反例是设计推导,没有新增可运行代码。语言基线见主题首页,已有程序执行证据与未覆盖范围见验证记录;不能把原阈值用例的通过状态扩大到新需求。