第三章 函数设计原则:写对人人都爱读的代码

写代码这件事,我干了十几年。回头看,最让我头疼的从来不是算法多难、框架多新,而是那些又长又臭、一个函数干八件事的代码。说白了,函数设计就是代码质量的基石。这一章,我们聊聊怎么把函数写得清爽、可靠、好维护。

核心观点:好的函数像乐高积木——每个块只做一件事,组合起来却能搭出整个世界。

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. 用字典或配置对象:把相关参数打包成一个对象
  2. 拆分成多个函数:不同场景用不同的函数
  3. 使用默认参数:只暴露必要的参数,其他用默认值
# 改进方案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个参数 无副作用,可预测 核心目标 可读性 ↑ 可测试性 ↑ 可维护性 ↑ 可复用性 ↑ 副作用隔离 → 核心逻辑纯函数 → 边界处理副作用

本章小结

函数设计这件事,说到底就是四个字:克制与分离。克制自己在一个函数里塞太多逻辑的冲动,把不同职责的代码分离到不同的函数里。做到这四点,你的代码质量会有质的提升:

  • 单一职责:一个函数只做一件事,用一句话能说清楚
  • 短小精悍:函数尽量控制在 20 行以内,超过就考虑拆分
  • 参数精简:参数不超过 3 个,多用配置对象
  • 纯函数优先:核心逻辑用纯函数,副作用隔离到边界

这些原则不是教条,而是我在无数次踩坑之后总结出来的经验。你写代码的时候,不妨多问问自己:这个函数是不是太长了?参数是不是太多了?有没有副作用混在计算逻辑里?养成这样的习惯,代码质量自然就上去了。