第7章:高质量的子程序
第7章:高质量的子程序:用调用意图、输入输出合同和可处理的失败边界压缩调用者的推理成本。
学习目标
- 能为一个子程序写出调用意图、输入约束、单一职责、结果合同和失败处理方式。
- 能用正常输入、恰好边界和一个故障样本检查参数与返回值,并定位合同首个偏离。
- 能在重命名、拆分或改写子程序后,以同一输入重放并证明调用者仍可独立使用它。
为什么需要高质量的子程序
“把一段代码包进函数”只是语法动作。真正的工程收益来自调用边界:调用者能根据名称和参数知道何时可以调用,能根据结果知道下一步做什么,还能在失败时判断是修正输入、重试还是转交错误。这里的核心是一个 ↡把调用者需要知道的输入、职责、结果和失败处理压缩到调用边界的可检查协议,而不是某个固定的行数上限。
把合同写成推理链:
先预测:如果只改变一个输入,哪一段证据会先变化?如果调用者必须阅读实现才能回答,说明合同仍然泄漏了实现细节。高质量子程序也不等于“越短越好”:一个有明确职责、边界和结果的长流程,可能比几个互相修改隐式状态的小函数更容易验收。
先找出合同会在哪里破裂
7.1 创建子程序的正当理由
创建子程序的第一个理由是给一个可命名的意图建立边界:它可以隐藏重复的操作、降低复杂度、隔离变化,或让一段行为拥有独立的验证入口。7.1 创建子程序的正当理由 不要求所有短操作都被包装,也不把“以后可能复用”当作无条件投资。判断标准是:抽取之后,调用点是否更接近问题域,调用者是否少记住一组实现步骤。
似乎过于简单而没必要写成子程序的操作
似乎过于简单而没必要写成子程序的操作 也可能值得命名,例如“把摄氏温度转为协议要求的开尔文值”或“判断订单是否允许重试”。代码很短,但单位、业务语义或变化来源不短。相反,如果一个名字只重复了运算符本身,却没有降低认知负担,内联表达往往更诚实。
总结:创建子程序的理由
总结:创建子程序的理由 可以收束为一个问题:这个边界是否让调用者更容易说清意图、验证输入和处理结果?若答案是肯定的,再考虑复用、测试隔离和替换实现;若只是为了让文件看起来更碎,就不要把拆分当作质量本身。
7.2 在子程序层上设计
在 7.2 在子程序层上设计 时,先把流程分成调用者可以讨论的动作,再决定每个动作的内部步骤。这里的 ↡调用开始前必须成立、能够被程序或调用者检查的输入与状态条件 约束入口,↡成功返回时调用者可以依赖的结果、状态变化和不变量 约束出口;两者之间才是实现可以自由替换的空间。
一个边界若同时改变数据库、发送消息和更新界面,就算函数只有 20 行,也可能包含三种变化理由。设计时可画出“入口—职责—出口”的轨迹,并问每一段失败由谁拥有。如果错误需要跨越很多层才被解释,说明子程序层次没有把因果放在合适的位置。
7.3 好的子程序名字
7.3 好的子程序名字 应该让调用者读出动作、对象和必要单位。convertMetersToFeet 比 convert 更能阻止同类型数值被误换;tryLoadProfile 比 loadProfile 更诚实地表达了可能失败。名字不要承诺实现细节,也不要把历史原因藏在缩写里。若名称无法写成一句“对什么做什么并得到什么”,通常是职责还没有稳定。
7.4 子程序可以写多长
7.4 子程序可以写多长 没有脱离上下文的行数答案。更有用的测量是:读者需要同时保留多少状态,异常路径是否仍能沿一条原因链解释,测试是否可以为每个出口写出独立的预期。长但连续的算法可以保持一个边界;短小但混合权限、隐式全局变量和多个布尔开关的函数,反而更难可靠使用。拆分的目标是降低推理跨度,不是制造更多跳转。
7.5 如何使用子程序参数
参数是合同的一部分,而不是把局部变量搬到括号里的容器。一个清楚的 ↡把每个参数的角色、单位、允许范围、所有权和返回关系写清楚的接口约定 会说明输入是否只读、集合是否会被修改、空值是否有特殊含义,以及失败时输出是否仍然有效。参数顺序也应有稳定语义;两个同类型的数量如果单位不同,名字或类型至少要有一道防错边界。
优先传入完成一个职责所需的最小数据。把十几个字段塞进一个“选项对象”并不会自动改善接口;如果其中一半只在某个分支使用,应该重新检查是否存在两个更小的子程序。试一试:交换两个同类型参数,再看测试是否会立即失败;如果不会,接口需要更明显的语义约束。
7.6 使用函数时要特别考虑的问题
7.6 使用函数时要特别考虑的问题 包括副作用、异常路径、资源所有权、返回值是否可忽略,以及调用者能否在不同语言环境下保持同样理解。调用函数之前,先写一个正常样本、一个恰好边界和一个非法样本的预期;运行后记录返回、状态变化和错误文本,不能只看最终页面是否“看起来成功”。
什么时候使用函数,什么时候使用过程
什么时候使用函数,什么时候使用过程 取决于结果是否是调用者推理所需的值。查询、转换和判断通常适合返回值明确的函数;改变外部状态的动作可以使用过程式接口,但必须把副作用、失败和幂等性写进合同。不要用一个看似返回值的函数偷偷完成写入,也不要让过程通过隐式全局状态传回关键结果。
设置函数的返回值
设置函数的返回值 时,返回值要帮助调用者做下一步,而不是仅仅让实现内部“有东西可回传”。如果失败原因、部分完成状态或重试建议会影响调用者决策,就用结构化结果表达它们;如果使用异常或错误码,则要保持边界和生命周期一致。返回空值不是默认的错误策略,除非空值本身就是合同中的一种可区分结果。
7.7 宏子程序和内联子程序
7.7 宏子程序和内联子程序 提醒我们:抽象边界和编译替换是两件事。内联可能减少调用开销,但不应改变名称、参数和错误语义;宏则可能重复求值、污染作用域或绕开类型检查。除非有测量证据,先保持普通子程序的可读合同,再讨论优化。
宏子程序在使用上的限制
宏子程序在使用上的限制 主要来自语法替换的不可见性:参数可能被求值多次,调用处的运算符优先级可能改变表达式含义,局部名称也可能与调用环境冲突。若必须使用宏,应让每个参数只求值一次、保护表达式边界,并提供等价的测试样本;否则用带类型的函数承载行为。
内联子程序
内联子程序 仍然应该遵守普通子程序的职责、输入和结果合同。内联建议不是性能证明;性能判断要依赖代表性输入、版本和观察窗口。只要内联导致调试、覆盖或错误定位变差,就应优先撤销它,而不是为了一个未经测量的猜想牺牲边界清晰度。
先预测,再操作专属合同实验
下面的三步将目录知识点收束到一条可重放的因果链。先预测哪一个节点会在故障输入下最先变红,再点击节点、注入故障,最后按“重置实验”回到同一个 focus。三步中的每个视觉组件都显示入口、职责、结果和失败边界,避免把动画当成装饰。
1. 先从调用意图建立边界
第7章 · 调用边界的可复核合同
子程序合同压缩器
选择合同节点,再注入“错误语义不清”的故障,观察调用者为什么必须停下。
展开本章目录检查点
- 第7章 高质量的子程序
- 7.1 创建子程序的正当理由
- 似乎过于简单而没必要写成子程序的操作
- 总结:创建子程序的理由
- 7.2 在子程序层上设计
- 7.3 好的子程序名字
- 7.4 子程序可以写多长
- 7.5 如何使用子程序参数
- 7.6 使用函数时要特别考虑的问题
- 什么时候使用函数,什么时候使用过程
- 设置函数的返回值
- 7.7 宏子程序和内联子程序
- 宏子程序在使用上的限制
- 内联子程序
- 关键点
最小可重放实现
把子程序当作合同,可以先在测试草图中写出不变量,再决定具体语言的语法:
baseline = callRoutine(validInput)
assert baseline.result == expectedResult
assert baseline.sideEffects == expectedEffects
rejected = callRoutine(boundaryFault)
assert rejected.errorMeaning == "invalid-input"
assert rejected.sideEffects == baseline.sideEffects
resetEnvironment()
assert callRoutine(validInput) == baseline这段草图不规定异常、错误码或结果对象的唯一形式;它规定的是观察顺序。先有基线,才知道故障改变了什么;先保存副作用,才不会把“返回失败但已写入一半”误判成安全拒绝;复位后用同一输入重跑,才能排除环境残留造成的假成功。
关键点
关键点 是把子程序质量从“风格偏好”变成可观察合同:创建理由必须对应稳定意图,层次要把变化理由放在正确边界,名字要表达动作和对象,长度要服务于认知而非行数,参数要携带单位与所有权,返回值和失败语义要能指导调用者,宏与内联不能抹掉这些证据。先预测,再动手试;如果故障后的首个偏离无法解释,就退回接口设计而不是继续堆条件分支。
练习与答案
练习
问题 1:写出一个子程序合同
选择“计算订单折扣”或“读取用户配置”,写出调用意图、两个前置条件、一个后置条件和一个可处理的失败结果。先预测:输入刚好位于边界时,返回值和状态应该分别是什么?
问题 2:找出参数开关造成的职责泄漏
给出一个带 validateOnly、sendNotice 和 saveResult 三个开关的子程序调用,说明它承担了哪些变化理由。请拆成更小的接口,并为每个接口写一个最小测试输入。
问题 3:用故障重放验证返回值
固定同一版本和输入,比较“统一返回 false”的实现与能区分坏输入、暂时不可用、部分完成的实现。说明你会保存哪三份证据,以及重置后怎样判断修复没有掩盖问题。
术语复核
名词解释
本章出现的专业名词,用大白话再讲一遍。
- 子程序合同
写在调用边界上的使用说明:什么输入可以进来、要完成什么、成功和失败分别留下什么结果。
- 单一职责
一个子程序主要因为一种原因改变;它不是要求只能有一行代码,而是要求责任能被一句话说清。
- 前置条件
调用开始前必须成立的事实,例如输入范围、权限或对象状态。
- 后置条件
调用成功后可以检查并依赖的结果与状态变化。
- 参数契约
对每个参数的角色、单位、范围、可变性和所有权做出的接口约定。
- 错误语义
让调用者知道失败原因、状态后果以及下一步处理方式的明确含义。
来源与改编边界
本章只用公开目录核对单元范围和节点顺序,不把试读页当作原书全文。章节坐标来自 《代码大全(第2版)》2006 年中文公开试读目录,英文版作者、出版信息和官方样章范围参考 Microsoft Press 官方书页。关于接口、输入验证和可复核工程实践的迁移解释,参照 C++ Core Guidelines;这些资料用于事实和范围核对,不代替原书未公开的具体正文。
本页的合同图、故障实验、代码草图、练习和答案均为围绕官方目录的独立教学重写;它们不宣称复现原书段落,也不把现代语言规则倒写成 2006 年版的原作结论。