本节摘要:函数是代码组织的基本单元,函数设计规范的核心是单一职责、短小精悍、参数克制与卫语句前置。本节给出函数长度与参数数量的经验上限、动词命名的搭配要求、类型提示的用法、一致返回值的原则,并演示用卫语句把异常条件拦截在函数开头、让主逻辑贴左侧的完整过程。
阅读完本节,你应当能够:
接手过一个两百行的下单函数:开头校验参数,中间查库存、算价格、凑优惠券,尾部落库发消息,六层嵌套,八个参数。它的三宗罪在维护中逐一兑现。其一,没人能一次读懂:读者的工作记忆大约能维持四个左右的活跃变量与条件,两百行意味着读到后面忘了前面,只能反复回滚重读。其二,测试写不动:想单独测"价格计算"这段逻辑?不行,它和数据库查询、消息发送焊死在同一个函数体里,单测被迫连带启动一堆依赖。其三,修改影响面模糊:改价格逻辑可能碰坏发消息的分支,因为共享着同一个作用域里的十几个中间变量。
三宗罪同源于一个设计缺陷:函数承担了太多职责。函数的本质是命名过的计算单元——名字承诺一件事,函数体兑现这件事。承诺两件的函数,名字说不清,测试测不全,修改必误伤。
每个函数只做一件事。判断标准很朴素:能用一句"做什么"的短句概括吗?概括不出来或者要写"并且",就该拆。长度上的经验上限是二十到五十行(不含注释与空行)——不是铁律,而是预警线:超过它的函数几乎总在偷偷承担第二职责。第 2.1 节的拆分信号在这里同样适用:需要用注释给函数内部分段时,就让段落升级为函数。
回顾订单函数拆分后的样子(概念代码):
def place_order(order): validate_order(order) total = calculate_total(order) deduct_inventory(order) save_order(order) send_confirmation(order)
主函数五行,读起来是流程目录;每个子函数独立可测、独立可改。价格计算想加新规则?只动 calculate_total,其他环节毫发无伤。
参数数量上限建议三到五个。原因有二:参数越多,调用方的组合成本与出错概率越高(顺序传参时类型相近的参数极易互换);参数列表本身也在暗示函数职责过多——要那么多输入才能干的事,多半不止一件事。超限的解法是参数对象:把相关参数聚合成一个结构体传入。八个散参数变成一个"下单请求"对象,调用代码可读性立刻改善。布尔参数是另一个坑:place_order(order, True) 里的 True 是什么意思?调用处无从知晓,应改为具名参数或拆成两个意图明确的函数。
类型提示为参数与返回值标注类型(概念示例):
def calculate_area(length: float, width: float) -> float: return length * width
收益是三重的:调用处 IDE 即时提示类型、静态检查提前抓出类型不匹配、读者不必读函数体就知道输入输出契约。返回值的一致性原则同样重要:同型返回——函数要么总返回同类型,要么在非法输入时抛异常;切忌有时返回数据、有时返回空值、有时返回错误码,调用方被迫三路判断,遗漏一路就是缺陷。空值返回是最常见的隐患来源,更安全的策略是用异常表达"给不出结果",或用明确的"结果对象"同时携带成功数据与失败原因。
卫语句指函数开头集中处理异常条件与前置条件的模式:条件不满足就立刻抛异常或提前返回,让主逻辑不被嵌套包裹。对照(概念代码):
# 嵌套版 主逻辑被推到右侧 def place_order(order): if order is not None: if order.is_valid: if order.items: # 真正的主逻辑从这里开始 ... # 卫语句版 主逻辑贴左 def place_order(order): if order is None: raise InvalidOrderError("订单不能为空") if not order.is_valid: raise InvalidOrderError("订单校验未通过") if not order.items: raise InvalidOrderError("订单无商品") # 主逻辑从这行开始 一路向右不回头 ...
卫语句版每行条件独立成立、独立可读,主流程零缩进。它与"尽早失败"原则(第 4.1 节展开)天然配套:错误条件在入口即被拦截,不会带着脏状态流入深层逻辑。
| 维度 | 规则 | 预警信号 | 修复手段 |
|---|---|---|---|
| 职责 | 一件事一个函数 | 名字里要写并且 | 拆子函数 |
| 长度 | 二十到五十行预警 | 注释分段 | 段落升级为函数 |
| 参数 | 三到五个以内 | 布尔裸参数 | 参数对象封装 |
| 返回 | 同型返回 | 用空值表示失败 | 异常或结果对象 |
| 嵌套 | 开头卫语句拦截 | 三层以上嵌套 | 提前返回 |
| 命名 | 动词开头可预期 | 名字说不清做什么 | 对照动词词汇表 |
💡 关键直觉:函数的调用处是函数设计的镜子。调用代码读起来别扭(参数记不住顺序、返回值要三路判断、布尔实参含义不明),说明设计有问题——修函数,别让每个调用点各自忍耐。
函数与方法的可见性表达因语言而异:有的语言用单下划线前缀表示"约定私有"、双下划线触发名称改写;有的用关键字显式声明;新一代脚本语言用未导出即私有、类成员加井号前缀的机制。团队的统一约定应是:私有命名只用一种记号,公开接口必须配文档注释(第 3.1 节),让"能调用什么"一目了然。
理想函数是纯函数:同样输入永远同样输出,不修改任何外部状态。纯函数可测性极佳(不需要准备环境、不需要清理现场)。现实中不可能全部纯化,但方向要坚定:函数需要外部状态时通过参数注入而非直接读写全局变量——全局变量让函数行为依赖"此刻全局是什么",测试时要先摆好全局、并发时互相踩踏。这也是依赖注入思想的微观形态(第 5.2 节展开)。
⚠️ 常见坑:把"函数要短"执行成机械的行数竞赛,拆出几十个两行小函数、主流程要跳转七八层才能读完。拆分的单位是职责不是行数——两个步骤强耦合且必须按序阅读时,强行拆开反而增加理解成本。判断标准始终是读者的总理解成本。
存量长函数的治理不是推倒重写。惯用手法是提取函数:找到内聚的代码段(比如那十五行价格计算),原样提取为新函数,参数与返回值按数据流向梳理,调用点替换,测试护航,提交。一次一个提取,每步可验证。这种小步重构累计起来,能在不冻结开发的情况下把两百行函数逐步驯化。
下一节缩小到最小单元:变量与常量——作用域怎么压、魔法数字怎么灭。