第7章:高质量的子程序

第7章:高质量的子程序:用调用意图、输入输出合同和可处理的失败边界压缩调用者的推理成本。

学习目标

  • 能为一个子程序写出调用意图、输入约束、单一职责、结果合同和失败处理方式。
  • 能用正常输入、恰好边界和一个故障样本检查参数与返回值,并定位合同首个偏离。
  • 能在重命名、拆分或改写子程序后,以同一输入重放并证明调用者仍可独立使用它。

为什么需要高质量的子程序

“把一段代码包进函数”只是语法动作。真正的工程收益来自调用边界:调用者能根据名称和参数知道何时可以调用,能根据结果知道下一步做什么,还能在失败时判断是修正输入、重试还是转交错误。这里的核心是一个 ,而不是某个固定的行数上限。

把合同写成推理链:

routine=preconditionresponsibilitypostcondition(+ failure meaning)routine = precondition \rightarrow responsibility \rightarrow postcondition \quad (+\ failure\ meaning)

先预测:如果只改变一个输入,哪一段证据会先变化?如果调用者必须阅读实现才能回答,说明合同仍然泄漏了实现细节。高质量子程序也不等于“越短越好”:一个有明确职责、边界和结果的长流程,可能比几个互相修改隐式状态的小函数更容易验收。

先找出合同会在哪里破裂

7.1 创建子程序的正当理由

创建子程序的第一个理由是给一个可命名的意图建立边界:它可以隐藏重复的操作、降低复杂度、隔离变化,或让一段行为拥有独立的验证入口。7.1 创建子程序的正当理由 不要求所有短操作都被包装,也不把“以后可能复用”当作无条件投资。判断标准是:抽取之后,调用点是否更接近问题域,调用者是否少记住一组实现步骤。

似乎过于简单而没必要写成子程序的操作

似乎过于简单而没必要写成子程序的操作 也可能值得命名,例如“把摄氏温度转为协议要求的开尔文值”或“判断订单是否允许重试”。代码很短,但单位、业务语义或变化来源不短。相反,如果一个名字只重复了运算符本身,却没有降低认知负担,内联表达往往更诚实。

总结:创建子程序的理由

总结:创建子程序的理由 可以收束为一个问题:这个边界是否让调用者更容易说清意图、验证输入和处理结果?若答案是肯定的,再考虑复用、测试隔离和替换实现;若只是为了让文件看起来更碎,就不要把拆分当作质量本身。

7.2 在子程序层上设计

7.2 在子程序层上设计 时,先把流程分成调用者可以讨论的动作,再决定每个动作的内部步骤。这里的 约束入口, 约束出口;两者之间才是实现可以自由替换的空间。

一个边界若同时改变数据库、发送消息和更新界面,就算函数只有 20 行,也可能包含三种变化理由。设计时可画出“入口—职责—出口”的轨迹,并问每一段失败由谁拥有。如果错误需要跨越很多层才被解释,说明子程序层次没有把因果放在合适的位置。

7.3 好的子程序名字

7.3 好的子程序名字 应该让调用者读出动作、对象和必要单位。convertMetersToFeetconvert 更能阻止同类型数值被误换;tryLoadProfileloadProfile 更诚实地表达了可能失败。名字不要承诺实现细节,也不要把历史原因藏在缩写里。若名称无法写成一句“对什么做什么并得到什么”,通常是职责还没有稳定。

7.4 子程序可以写多长

7.4 子程序可以写多长 没有脱离上下文的行数答案。更有用的测量是:读者需要同时保留多少状态,异常路径是否仍能沿一条原因链解释,测试是否可以为每个出口写出独立的预期。长但连续的算法可以保持一个边界;短小但混合权限、隐式全局变量和多个布尔开关的函数,反而更难可靠使用。拆分的目标是降低推理跨度,不是制造更多跳转。

7.5 如何使用子程序参数

参数是合同的一部分,而不是把局部变量搬到括号里的容器。一个清楚的 会说明输入是否只读、集合是否会被修改、空值是否有特殊含义,以及失败时输出是否仍然有效。参数顺序也应有稳定语义;两个同类型的数量如果单位不同,名字或类型至少要有一道防错边界。

优先传入完成一个职责所需的最小数据。把十几个字段塞进一个“选项对象”并不会自动改善接口;如果其中一半只在某个分支使用,应该重新检查是否存在两个更小的子程序。试一试:交换两个同类型参数,再看测试是否会立即失败;如果不会,接口需要更明显的语义约束。

7.6 使用函数时要特别考虑的问题

7.6 使用函数时要特别考虑的问题 包括副作用、异常路径、资源所有权、返回值是否可忽略,以及调用者能否在不同语言环境下保持同样理解。调用函数之前,先写一个正常样本、一个恰好边界和一个非法样本的预期;运行后记录返回、状态变化和错误文本,不能只看最终页面是否“看起来成功”。

什么时候使用函数,什么时候使用过程

什么时候使用函数,什么时候使用过程 取决于结果是否是调用者推理所需的值。查询、转换和判断通常适合返回值明确的函数;改变外部状态的动作可以使用过程式接口,但必须把副作用、失败和幂等性写进合同。不要用一个看似返回值的函数偷偷完成写入,也不要让过程通过隐式全局状态传回关键结果。

设置函数的返回值

设置函数的返回值 时,返回值要帮助调用者做下一步,而不是仅仅让实现内部“有东西可回传”。如果失败原因、部分完成状态或重试建议会影响调用者决策,就用结构化结果表达它们;如果使用异常或错误码,则要保持边界和生命周期一致。返回空值不是默认的错误策略,除非空值本身就是合同中的一种可区分结果。

7.7 宏子程序和内联子程序

7.7 宏子程序和内联子程序 提醒我们:抽象边界和编译替换是两件事。内联可能减少调用开销,但不应改变名称、参数和错误语义;宏则可能重复求值、污染作用域或绕开类型检查。除非有测量证据,先保持普通子程序的可读合同,再讨论优化。

宏子程序在使用上的限制

宏子程序在使用上的限制 主要来自语法替换的不可见性:参数可能被求值多次,调用处的运算符优先级可能改变表达式含义,局部名称也可能与调用环境冲突。若必须使用宏,应让每个参数只求值一次、保护表达式边界,并提供等价的测试样本;否则用带类型的函数承载行为。

内联子程序

内联子程序 仍然应该遵守普通子程序的职责、输入和结果合同。内联建议不是性能证明;性能判断要依赖代表性输入、版本和观察窗口。只要内联导致调试、覆盖或错误定位变差,就应优先撤销它,而不是为了一个未经测量的猜想牺牲边界清晰度。

先预测,再操作专属合同实验

下面的三步将目录知识点收束到一条可重放的因果链。先预测哪一个节点会在故障输入下最先变红,再点击节点、注入故障,最后按“重置实验”回到同一个 focus。三步中的每个视觉组件都显示入口、职责、结果和失败边界,避免把动画当成装饰。

分步1 / 3

1. 先从调用意图建立边界

第7章 · 调用边界的可复核合同

子程序合同压缩器

选择合同节点,再注入“错误语义不清”的故障,观察调用者为什么必须停下。

子程序合同:意图 → 入口 → 职责 → 结果 → 失败高质量不是“短函数”标签,而是让调用者能在边界外完成可靠推理1调用意图动作、对象与单位2前置条件输入和状态可检查3单一职责变化理由不混合4后置条件结果与副作用可复核5失败边界错误语义可处理当前检查点:调用意图选择节点查看它应留下的证据;实现细节留在合同边界之后。证据:名称、输入、职责与结果可以分别复核可交接:调用者不需要阅读实现即可决定下一步
合同闭合:调用意图的证据已可交给调用者。
目录节点 15/15
展开本章目录检查点
  • 第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:找出参数开关造成的职责泄漏

给出一个带 validateOnlysendNoticesaveResult 三个开关的子程序调用,说明它承担了哪些变化理由。请拆成更小的接口,并为每个接口写一个最小测试输入。

问题 3:用故障重放验证返回值

固定同一版本和输入,比较“统一返回 false”的实现与能区分坏输入、暂时不可用、部分完成的实现。说明你会保存哪三份证据,以及重置后怎样判断修复没有掩盖问题。

术语复核

名词解释

本章出现的专业名词,用大白话再讲一遍。

子程序合同

写在调用边界上的使用说明:什么输入可以进来、要完成什么、成功和失败分别留下什么结果。

单一职责

一个子程序主要因为一种原因改变;它不是要求只能有一行代码,而是要求责任能被一句话说清。

前置条件

调用开始前必须成立的事实,例如输入范围、权限或对象状态。

后置条件

调用成功后可以检查并依赖的结果与状态变化。

参数契约

对每个参数的角色、单位、范围、可变性和所有权做出的接口约定。

错误语义

让调用者知道失败原因、状态后果以及下一步处理方式的明确含义。

来源与改编边界

本章只用公开目录核对单元范围和节点顺序,不把试读页当作原书全文。章节坐标来自 《代码大全(第2版)》2006 年中文公开试读目录,英文版作者、出版信息和官方样章范围参考 Microsoft Press 官方书页。关于接口、输入验证和可复核工程实践的迁移解释,参照 C++ Core Guidelines;这些资料用于事实和范围核对,不代替原书未公开的具体正文。

本页的合同图、故障实验、代码草图、练习和答案均为围绕官方目录的独立教学重写;它们不宣称复现原书段落,也不把现代语言规则倒写成 2006 年版的原作结论。

讨论

评论区加载中…