第三章 函数设计原则:写对人人都爱读的代码
写代码这件事,我干了十几年。回头看,最让我头疼的从来不是算法多难、框架多新,而是那些又长又臭、一个函数干八件事的代码。说白了,函数设计就是代码质量的基石。这一章,我们聊聊怎么把函数写得清爽、可靠、好维护。
核心观点:好的函数像乐高积木——每个块只做一件事,组合起来却能搭出整个世界。
3.1 单一职责原则:一个函数只做一件事
单一职责原则(SRP)听起来简单,做起来却不容易。我见过太多函数,名字叫 process_data,里面却塞了数据清洗、格式转换、写入数据库、发邮件通知……你想想看,这样的函数谁敢改?
判断一个函数是否违反 SRP,有个很实用的方法:用一句话描述这个函数在做什么。如果这句话里出现了“并且”、“然后”、“同时”这些词,那大概率是职责过多了。
来看个反面例子:
# 反面教材:一个函数干了三件事
def handle_user_registration(user_data):
# 1. 验证数据
if not user_data.get('email') or not user_data.get('password'):
raise ValueError("缺少必要字段")
# 2. 保存到数据库
db.session.add(User(**user_data))
db.session.commit()
# 3. 发送欢迎邮件
send_email(user_data['email'], "欢迎注册!")
return {"status": "ok"}
这个函数的问题很明显:验证、持久化、通知,三个职责混在一起。哪天要改邮件模板,你得从注册逻辑里翻出来;哪天要加个短信通知,又得往里塞。改着改着,函数就失控了。
正确的做法是拆开:
# 好代码:每个函数只做一件事
def validate_registration_data(user_data):
"""只负责数据验证"""
if not user_data.get('email') or not user_data.get('password'):
raise ValueError("缺少必要字段")
# 还可以加更多验证规则
def save_user_to_db(user_data):
"""只负责持久化"""
user = User(**user_data)
db.session.add(user)
db.session.commit()
return user
def notify_user_registration(email):
"""只负责通知"""
send_email(email, "欢迎注册!")
# 组合使用
def register_user(user_data):
"""协调者:只负责编排,不负责具体实现"""
validate_registration_data(user_data)
user = save_user_to_db(user_data)
notify_user_registration(user.email)
return {"status": "ok", "user_id": user.id}
我的习惯:写函数时先问自己三个问题——这个函数的名字能准确描述它的行为吗?如果别人只看函数名不看代码,能猜出它做了什么吗?如果下个月要改其中一个逻辑,我需要动整个函数吗?
3.2 函数长度控制:短小精悍才是王道
函数多长算长?没有绝对的数字,但我个人有个经验法则:一个函数如果超过 20 行,就要开始警惕了。如果超过 50 行,那基本可以断定它做了太多事。
为什么函数要短?原因很简单:
- 可读性:短函数一眼就能看完,理解成本低
- 可测试性:短函数的分支路径少,容易覆盖所有场景
- 可复用性:短函数往往职责单一,更容易被其他地方调用
- 可维护性:改短函数的风险小,影响范围可控
我记得有一次接手一个遗留系统,有个函数 300 多行,里面嵌套了 5 层 if-else。我花了整整两天才搞明白它在干什么。后来我把它拆成 12 个小函数,每个 10-15 行,测试覆盖率从 20% 直接拉到 90%。
来看一个典型的“长函数”问题:
# 长函数:一个函数处理订单的完整生命周期
def process_order(order_id):
order = get_order(order_id)
# 验证订单状态
if order.status != 'pending':
raise Exception("订单状态不正确")
# 检查库存
for item in order.items:
if item.quantity > item.product.stock:
raise Exception(f"{item.product.name} 库存不足")
# 计算总价
total = 0
for item in order.items:
total += item.product.price * item.quantity
if order.coupon:
total = total * (1 - order.coupon.discount)
total = round(total, 2)
# 扣减库存
for item in order.items:
item.product.stock -= item.quantity
db.session.add(item.product)
# 更新订单
order.status = 'paid'
order.total = total
db.session.add(order)
db.session.commit()
# 发送通知
send_notification(order.user_id, f"订单 {order_id} 支付成功")
return order
这个函数 30 多行,看起来还行?但仔细看,它包含了验证、计算、库存操作、状态更新、通知五个职责。拆开之后:
# 拆分成短函数
def validate_order(order):
"""验证订单状态"""
if order.status != 'pending':
raise Exception("订单状态不正确")
def check_inventory(order):
"""检查库存是否充足"""
for item in order.items:
if item.quantity > item.product.stock:
raise Exception(f"{item.product.name} 库存不足")
def calculate_total(order):
"""计算订单总价"""
total = sum(item.product.price * item.quantity for item in order.items)
if order.coupon:
total *= (1 - order.coupon.discount)
return round(total, 2)
def deduct_inventory(order):
"""扣减库存"""
for item in order.items:
item.product.stock -= item.quantity
def update_order_status(order, total):
"""更新订单状态和金额"""
order.status = 'paid'
order.total = total
def notify_payment(order):
"""发送支付成功通知"""
send_notification(order.user_id, f"订单 {order.id} 支付成功")
def process_order(order_id):
"""编排订单处理流程"""
order = get_order(order_id)
validate_order(order)
check_inventory(order)
total = calculate_total(order)
deduct_inventory(order)
update_order_status(order, total)
db.session.commit()
notify_payment(order)
return order
避坑指南:我曾经犯过一个错误——为了追求函数短小,把逻辑拆得太碎,导致函数之间互相调用、依赖关系复杂。拆函数的目的是让代码更清晰,不是制造新的混乱。每个函数都应该有独立存在的价值。
3.3 参数数量控制:少即是多
函数的参数数量,我建议控制在 3 个以内。超过 3 个参数,调用者就得翻文档才能搞明白每个参数是什么意思。
参数过多的坏处很明显:
- 调用困难:调用者需要记住参数的顺序和含义
- 测试爆炸:每个参数都可能有多组取值,组合起来测试用例数量暴增
- 耦合度高:参数越多,函数和调用方的依赖关系越复杂
来看个反面例子:
# 参数过多:6个参数,调用者很难记住顺序
def create_report(title, data, format='pdf', page_size='A4', orientation='portrait', watermark=False):
# 生成报告的逻辑
pass
# 调用时:哪个参数是format?哪个是orientation?
create_report("销售报表", sales_data, 'pdf', 'A4', 'portrait', True)
解决参数过多的方法:
- 用字典或配置对象:把相关参数打包成一个对象
- 拆分成多个函数:不同场景用不同的函数
- 使用默认参数:只暴露必要的参数,其他用默认值
# 改进方案1:使用配置对象
class ReportConfig:
def __init__(self, format='pdf', page_size='A4', orientation='portrait', watermark=False):
self.format = format
self.page_size = page_size
self.orientation = orientation
self.watermark = watermark
def create_report(title, data, config=None):
config = config or ReportConfig()
# 使用 config 生成报告
pass
# 调用时更清晰
config = ReportConfig(format='pdf', watermark=True)
create_report("销售报表", sales_data, config)
# 改进方案2:拆分成多个函数
def create_pdf_report(title, data, watermark=False):
"""专门生成PDF报告"""
pass
def create_excel_report(title, data):
"""专门生成Excel报告"""
pass
我的建议:如果函数参数超过 3 个,先停下来想想——是不是这个函数做了太多事?是不是可以把参数打包成对象?是不是应该拆分成多个函数?
3.4 纯函数与副作用:可预测的代码才是好代码
纯函数这个概念,说白了就是:同样的输入,永远得到同样的输出,并且不改变外部状态。纯函数的好处太多了——容易测试、容易推理、容易并行执行。
来看对比:
| 特性 | 纯函数 | 非纯函数(有副作用) |
|---|---|---|
| 输入输出关系 | 确定:相同输入 → 相同输出 | 不确定:相同输入 → 可能不同输出 |
| 外部状态 | 不读取也不修改 | 可能读取或修改全局变量、文件、数据库等 |
| 测试难度 | 简单:只需验证输入输出 | 复杂:需要 mock 外部依赖 |
| 并发安全 | 天然安全 | 需要加锁或同步机制 |
| 可缓存性 | 可以缓存结果 | 不能缓存 |
来看代码示例:
# 纯函数:不依赖外部状态,不修改外部状态
def calculate_discount(price, discount_rate):
"""计算折扣后的价格"""
return price * (1 - discount_rate)
# 非纯函数:依赖全局变量
discount_rate = 0.1
def calculate_discount_impure(price):
"""依赖全局变量 discount_rate"""
return price * (1 - discount_rate)
# 非纯函数:修改外部状态
total_discount = 0
def apply_discount(price, discount_rate):
"""修改全局变量 total_discount"""
global total_discount
discount = price * discount_rate
total_discount += discount
return price - discount
在实际项目中,完全避免副作用是不可能的——你总要读写数据库、发 HTTP 请求、写日志。但我们可以做的是:把有副作用的代码隔离到边界,核心业务逻辑尽量用纯函数实现。
核心策略:纯函数做计算,副作用函数做操作。计算和操作分离,测试和调试都轻松。
来看一个实际例子:
# 不好的做法:计算和副作用混在一起
def process_payment(user_id, amount):
user = get_user_from_db(user_id) # 副作用:读数据库
if user.balance < amount:
raise Exception("余额不足")
new_balance = user.balance - amount # 计算
update_user_balance(user_id, new_balance) # 副作用:写数据库
send_receipt(user.email, amount) # 副作用:发邮件
return new_balance
# 好的做法:分离计算和副作用
def calculate_new_balance(balance, amount):
"""纯函数:只做计算"""
if balance < amount:
raise Exception("余额不足")
return balance - amount
def process_payment(user_id, amount):
"""有副作用的函数:只做编排和操作"""
user = get_user_from_db(user_id)
new_balance = calculate_new_balance(user.balance, amount)
update_user_balance(user_id, new_balance)
send_receipt(user.email, amount)
return new_balance
避坑指南:我曾经在一个项目中,把日志记录写在业务逻辑函数里。结果有一天日志系统挂了,整个业务也跟着崩了。从那以后,我坚持把日志、监控这些副作用操作放到装饰器或中间件里,绝不污染核心逻辑。
知识体系总览
下面这张图总结了函数设计的四个核心原则,以及它们之间的关系:
本章小结
函数设计这件事,说到底就是四个字:克制与分离。克制自己在一个函数里塞太多逻辑的冲动,把不同职责的代码分离到不同的函数里。做到这四点,你的代码质量会有质的提升:
- 单一职责:一个函数只做一件事,用一句话能说清楚
- 短小精悍:函数尽量控制在 20 行以内,超过就考虑拆分
- 参数精简:参数不超过 3 个,多用配置对象
- 纯函数优先:核心逻辑用纯函数,副作用隔离到边界
这些原则不是教条,而是我在无数次踩坑之后总结出来的经验。你写代码的时候,不妨多问问自己:这个函数是不是太长了?参数是不是太多了?有没有副作用混在计算逻辑里?养成这样的习惯,代码质量自然就上去了。