提高代码质量:如何编写高质量函数

函数是代码的基本组成单元,是实现业务逻辑的核心载体。一段优秀的代码,必然由一个个职责清晰、易于理解的函数构成;而混乱的函数(如几百行的“上帝函数”、命名模糊的doSomething、参数堆砌的调用)则会让代码可读性骤降,维护成本指数级上升,甚至成为Bug的温床。

编写高质量函数,不仅仅是个人编码风格的体现,更是团队协作效率、项目可维护性的关键保障。本文将从函数设计原则、命名规范、参数/返回值设计、副作用管理、错误处理等多个维度,结合实际代码示例,系统讲解如何编写清晰、鲁棒、易于维护的函数,帮助你显著提升代码质量。

目录#

  1. 函数设计的核心原则 1.1 单一职责原则(SRP) 1.2 短小精悍:控制函数长度 1.3 纯函数优先,副作用可控
  2. 见名知意:函数命名规范 2.1 采用动词短语,避免模糊命名 2.2 遵循语言与团队的命名惯例 2.3 慎用缩写,优先可读性
  3. 简洁易用:参数设计最佳实践 3.1 控制参数数量,超过3个则封装 3.2 避免布尔参数,拆分多分支逻辑 3.3 合理使用默认参数,避开陷阱 3.4 明确参数类型,提升代码清晰度
  4. 清晰无歧义:返回值处理规范 4.1 保持返回类型一致性 4.2 返回空集合而非None/Null 4.3 用复合结构封装多返回值 4.4 用异常替代错误码
  5. 副作用管理:减少隐含依赖 5.1 识别与分类副作用 5.2 副作用逻辑与纯逻辑分离 5.3 副作用显式化:在命名中体现
  6. 鲁棒性保障:错误处理策略 6.1 捕获具体异常,避免“一网打尽” 6.2 自定义业务异常,提升可维护性 6.3 避免沉默失败,记录关键错误信息
  7. 可维护与可测试:注释、文档与测试友好性 7.1 注释:只写“为什么”,不写“是什么” 7.2 文档字符串:标准化函数说明 7.3 依赖注入:提升函数可测试性
  8. 实战:从“糟糕函数”到“优质函数”的重构
  9. 总结与持续优化
  10. 参考文献

1. 函数设计的核心原则#

1.1 单一职责原则(SRP)#

定义:一个函数应该只负责完成一个明确的任务,避免“一函数多职责”。

反例:一个函数同时承担验证输入、计算逻辑、数据库操作和邮件发送:

def process_order(order):
    # 1. 验证订单合法性
    if not order.get("user_id"):
        print("用户ID不能为空")
        return None
    # 2. 计算订单总价
    total = sum(item["price"] * item["quantity"] for item in order["items"])
    # 3. 保存订单到数据库
    db = MySQLdb.connect(...)
    cursor.execute("INSERT INTO orders ...")
    # 4. 发送订单通知邮件
    smtplib.SMTP(...).sendmail(...)
    return total

正例:拆分为4个单一职责的函数:

def validate_order(order) -> bool: ...  # 仅负责验证
def calculate_order_total(order) -> float: ...  # 仅负责计算总价
def save_order_to_db(order, total) -> bool: ...  # 仅负责数据库存储
def send_order_notification(order, total) -> bool: ...  # 仅负责发送邮件
 
# 主流程函数协调各子函数
def process_order(order):
    if not validate_order(order):
        return None
    total = calculate_order_total(order)
    if not save_order_to_db(order, total):
        return None
    send_order_notification(order, total)
    return total

1.2 短小精悍:控制函数长度#

最佳实践:函数长度不超过20-30行(不含注释和空行),超过则考虑拆分。

原因:过长的函数往往隐含多个职责,阅读者需要花费大量精力梳理逻辑分支;短小的函数更易于理解、测试和重构。

1.3 纯函数优先,副作用可控#

纯函数定义:相同输入始终得到相同输出,且不产生任何副作用(如修改全局变量、修改入参、IO操作、网络请求)。

纯函数优势

  • 易于测试:无需依赖外部环境,只需验证输入输出
  • 可缓存:因为输入确定输出确定,可以用缓存(如functools.lru_cache)提升性能
  • 无隐含依赖:逻辑完全透明

示例:纯函数与有副作用函数对比

# 纯函数:计算订单总价,无任何副作用
def calculate_order_total(items: list[dict]) -> float:
    return sum(item["price"] * item["quantity"] for item in items)
 
# 有副作用函数:记录日志到文件(IO操作)
def log_order_processing(order_id: str, total: float):
    with open("order_logs.txt", "a") as f:
        f.write(f"订单{order_id}总价:{total}\n")

2. 见名知意:函数命名规范#

2.1 采用动词短语,避免模糊命名#

函数名应直接描述其行为,使用动词+名词的短语结构,避免使用doSomethinghandleData这类模糊命名。

糟糕命名优质命名
get()get_user_by_id()
compute()calculate_shipping_cost()
process()validate_order_params()
data()generate_invoice_pdf()

2.2 遵循语言与团队的命名惯例#

不同语言有不同的命名规范,需严格遵循:

  • Python/TypeScript/Java:小驼峰命名法(如getUserInfo
  • Java:类名大驼峰,函数名小驼峰
  • Python:私有函数以下划线开头(如_calculate_subtotal
  • Go:大驼峰表示导出函数,小驼峰表示私有函数

2.3 慎用缩写,优先可读性#

仅使用行业通用缩写(如HTTPIDURL),避免自定义缩写。例如:

  • 糟糕:getUsrByID()Usr属于非通用缩写)
  • 优质:get_user_by_id()

3. 简洁易用:参数设计最佳实践#

3.1 控制参数数量,超过3个则封装#

最佳实践:函数参数数量不超过3-4个。若参数过多,用对象、数据类或结构体封装。

反例:参数堆砌,调用时难以记忆顺序

def create_order(user_id: str, items: list, coupon: str, shipping_address: str, payment_method: str):
    pass

正例:用dataclass封装参数

from dataclasses import dataclass
 
@dataclass
class CreateOrderParams:
    user_id: str
    items: list
    coupon: str = ""
    shipping_address: str = ""
    payment_method: str = "wechat"
 
def create_order(params: CreateOrderParams):
    pass
 
# 调用方式更清晰
create_order(CreateOrderParams(user_id="123", items=[]))

3.2 避免布尔参数,拆分多分支逻辑#

布尔参数会让函数内部产生逻辑分支,且调用方难以直接理解True/False的含义。

反例

def send_notification(user: dict, is_urgent: bool):
    if is_urgent:
        # 发送紧急通知逻辑
    else:
        # 发送普通通知逻辑

正例:拆分为两个函数

def send_normal_notification(user: dict): ...
def send_urgent_notification(user: dict): ...

3.3 合理使用默认参数,避开陷阱#

默认参数可以简化调用,但需注意语言特定陷阱:

  • Python:避免使用可变默认参数(如listdict),因为默认参数在函数定义时初始化,而非每次调用时
    # 错误示例:可变默认参数导致的陷阱
    def add_item(item, items=[]):
        items.append(item)
        return items  # 多次调用会复用同一个列表
     
    # 正确示例
    def add_item(item, items=None):
        if items is None:
            items = []
        items.append(item)
        return items

3.4 明确参数类型,提升代码清晰度#

使用类型提示(Python)或类型注解(TypeScript/Java),让调用方无需查看函数实现即可知晓参数要求。

# Python类型提示
def get_user_by_id(user_id: str) -> dict | None: ...
 
# TypeScript类型注解
function getUserById(userId: string): User | null { ... }

4. 清晰无歧义:返回值处理规范#

4.1 保持返回类型一致性#

函数的返回类型应始终一致,避免有时返回None,有时返回列表/字典,导致调用方需额外判断,增加Bug风险。

反例

def search_users(keyword: str):
    if not keyword:
        return None  # 无关键词返回None
    return db.query("SELECT * FROM users WHERE name LIKE %s", f"%{keyword}%")  # 有结果返回列表

正例:始终返回列表,无结果返回空列表

def search_users(keyword: str) -> list[dict]:
    if not keyword:
        return []
    return db.query(...) or []

4.2 返回空集合而非None/Null#

返回空列表、空字典而非None,调用方无需额外判断是否为None,可直接遍历。

正例

def get_user_orders(user_id: str) -> list[dict]:
    orders = db.query("SELECT * FROM orders WHERE user_id = %s", user_id)
    return orders or []  # 无订单返回空列表

4.3 用复合结构封装多返回值#

避免返回多个零散值,改用字典、数据类或元组(元组需保证顺序固定)封装。

反例

def calculate_stats(numbers: list[int]) -> tuple:
    return sum(numbers), len(numbers), sum(numbers)/len(numbers)  # 调用时需记忆顺序

正例

from dataclasses import dataclass
 
@dataclass
class StatsResult:
    total: int
    count: int
    average: float
 
def calculate_stats(numbers: list[int]) -> StatsResult:
    total = sum(numbers)
    count = len(numbers)
    return StatsResult(total, count, total/count if count else 0.0)

4.4 用异常替代错误码#

错误码需要调用方手动判断,代码冗余且易遗漏;异常可以中断流程并携带详细错误信息,更符合业务逻辑的异常处理方式。

反例

def create_order(order) -> int:
    if not order.get("user_id"):
        return -1  # 错误码表示用户ID缺失
    if not order.get("items"):
        return -2  # 错误码表示无商品
    return 0  # 成功

正例

class OrderValidationError(Exception):
    pass
 
def create_order(order):
    if not order.get("user_id"):
        raise OrderValidationError("用户ID不能为空")
    if not order.get("items"):
        raise OrderValidationError("订单中无商品")
    # 业务逻辑...

5. 副作用管理:减少隐含依赖#

5.1 识别与分类副作用#

常见副作用包括:

  • 修改全局变量或外部状态
  • 修改函数输入参数
  • IO操作:读写文件、数据库操作、网络请求
  • 系统调用:修改系统时间、启动进程

5.2 副作用逻辑与纯逻辑分离#

将有副作用的代码与纯计算逻辑拆分开,纯逻辑负责核心业务计算,副作用代码负责与外部交互。

示例

# 纯逻辑:计算折扣后价格
def apply_discount(price: float, discount_rate: float) -> float:
    return price * (1 - discount_rate)
 
# 有副作用:保存价格到数据库
def save_discounted_price(product_id: str, discounted_price: float):
    db.execute("UPDATE products SET price = %s WHERE id = %s", (discounted_price, product_id))

5.3 副作用显式化:在命名中体现#

如果函数包含副作用,应在命名中明确体现,避免调用方误判。

糟糕命名优质命名
process_data()save_processed_data_to_db()
update_user()update_user_and_notify()

6. 鲁棒性保障:错误处理策略#

6.1 捕获具体异常,避免“一网打尽”#

避免捕获Exception这类通用异常,否则会隐藏未知错误(如语法错误、系统错误),导致调试困难。

反例

def save_order():
    try:
        db.execute(...)
    except Exception as e:
        print("出错了")  # 无法定位具体错误原因

正例

def save_order():
    try:
        db.execute(...)
    except MySQLdb.IntegrityError as e:
        logging.error("订单数据违反唯一性约束:%s", str(e))
    except MySQLdb.OperationalError as e:
        logging.error("数据库连接失败:%s", str(e))

6.2 自定义业务异常,提升可维护性#

针对业务场景定义专属异常,让错误信息更具可读性,调用方可针对性处理。

# 自定义业务异常
class InventoryNotEnoughError(Exception):
    def __init__(self, product_id: str, required: int, available: int):
        super().__init__(f"商品{product_id}库存不足:需要{required},现有{available}")
        self.product_id = product_id
 
# 使用异常
def create_order_item(product_id: str, quantity: int):
    available = get_inventory(product_id)
    if available < quantity:
        raise InventoryNotEnoughError(product_id, quantity, available)

6.3 避免沉默失败,记录关键错误信息#

不要在try-except块中什么都不做,至少记录错误日志,便于后续排查问题。

反例

def send_notification():
    try:
        smtplib.SMTP(...).sendmail(...)
    except Exception:
        pass  # 沉默失败,问题无法被发现

正例

import logging
 
def send_notification():
    try:
        smtplib.SMTP(...).sendmail(...)
    except smtplib.SMTPException as e:
        logging.error("发送通知邮件失败:%s", str(e))
        # 可选:触发告警或降级处理

7. 可维护与可测试:注释、文档与测试友好性#

7.1 注释:只写“为什么”,不写“是什么”#

注释应解释代码的意图而非实现,因为实现可以通过阅读代码直接理解,但意图往往需要额外说明。

反例

# 遍历订单商品
for item in order["items"]:
    total += item["price"] * item["quantity"]  # 计算商品总价

正例

# 计算订单总价:包含商品单价×数量,不包含运费和优惠券
total = sum(item["price"] * item["quantity"] for item in order["items"])

7.2 文档字符串:标准化函数说明#

使用标准化的文档字符串(如Google风格、NumPy风格)描述函数的功能、参数、返回值、异常和示例。

示例(Google风格)

def calculate_order_total(items: list[dict]) -> float:
    """计算订单商品的税前总价
    
    Args:
        items: 订单项列表,每个元素需包含'price'(float)和'quantity'(int)字段
        
    Returns:
        订单商品的税前总价,保留两位小数
        
    Raises:
        ValueError: 若订单项缺少price或quantity字段,或值为负数
    """
    total = 0.0
    for item in items:
        if "price" not in item or "quantity" not in item:
            raise ValueError("订单项必须包含price和quantity字段")
        price = item["price"]
        quantity = item["quantity"]
        if price < 0 or quantity < 0:
            raise ValueError("价格和数量不能为负数")
        total += price * quantity
    return round(total, 2)

7.3 依赖注入:提升函数可测试性#

避免在函数内部硬编码依赖(如数据库连接、第三方服务实例),而是通过参数注入依赖,方便测试时替换为Mock对象。

反例

def get_user_orders(user_id: str):
    db = MySQLdb.connect(host="localhost", user="root")  # 硬编码数据库连接
    return db.query("SELECT * FROM orders WHERE user_id = %s", user_id)

正例

def get_user_orders(user_id: str, db_connection=None):
    if db_connection is None:
        db_connection = MySQLdb.connect(host="localhost", user="root")
    return db_connection.query("SELECT * FROM orders WHERE user_id = %s", user_id)
 
# 测试时传入Mock数据库连接
def test_get_user_orders():
    mock_db = MockDB()
    mock_db.add_mock_data("orders", [{"id": "1", "user_id": "123"}])
    result = get_user_orders("123", mock_db)
    assert len(result) == 1

8. 实战:从“糟糕函数”到“优质函数”的重构#

初始糟糕函数#

def handle_request(request):
    # 解析请求参数
    user_id = request.get("user_id")
    if not user_id:
        return {"code": 400, "msg": "user_id required"}
    # 查询用户
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if not user:
        return {"code": 404, "msg": "user not found"}
    # 生成用户报告
    report = {
        "user_id": user["id"],
        "name": user["name"],
        "order_count": db.query("SELECT COUNT(*) FROM orders WHERE user_id = %s", user_id)[0][0]
    }
    # 保存报告到Redis
    redis.set(f"report:{user_id}", json.dumps(report))
    return {"code": 200, "data": report}

重构后优质函数#

from typing import Dict, Optional
import json
import logging
from my_db import get_db_connection, get_redis_client  # 封装的依赖
 
# 自定义异常
class InvalidRequestError(Exception):
    pass
 
class UserNotFoundError(Exception):
    pass
 
# 解析请求参数
def parse_request_params(request: Dict) -> str:
    user_id = request.get("user_id")
    if not user_id:
        raise InvalidRequestError("user_id 是必填参数")
    return user_id
 
# 查询用户信息
def get_user(user_id: str, db=None) -> Dict:
    db = db or get_db_connection()
    user = db.query("SELECT * FROM users WHERE id = %s", user_id)
    if not user:
        raise UserNotFoundError(f"用户{user_id}不存在")
    return user[0]
 
# 计算用户订单数量
def get_user_order_count(user_id: str, db=None) -> int:
    db = db or get_db_connection()
    count = db.query("SELECT COUNT(*) FROM orders WHERE user_id = %s", user_id)[0][0]
    return count
 
# 生成用户报告
def generate_user_report(user: Dict, order_count: int) -> Dict:
    return {
        "user_id": user["id"],
        "name": user["name"],
        "order_count": order_count
    }
 
# 保存报告到Redis
def save_report_to_redis(report: Dict, redis=None) -> None:
    redis = redis or get_redis_client()
    redis.set(f"report:{report['user_id']}", json.dumps(report))
    logging.info("用户报告已保存到Redis:%s", report["user_id"])
 
# 主处理函数
def handle_request(request: Dict) -> Dict:
    try:
        user_id = parse_request_params(request)
        user = get_user(user_id)
        order_count = get_user_order_count(user_id)
        report = generate_user_report(user, order_count)
        save_report_to_redis(report)
        return {"code": 200, "data": report}
    except InvalidRequestError as e:
        return {"code": 400, "msg": str(e)}
    except UserNotFoundError as e:
        return {"code": 404, "msg": str(e)}
    except Exception as e:
        logging.error("处理请求失败:%s", str(e))
        return {"code": 500, "msg": "服务器内部错误"}

重构亮点

  • 每个函数职责单一,可单独测试
  • 使用自定义异常处理业务错误,逻辑清晰
  • 依赖通过参数注入,易于Mock测试
  • 错误日志完善,便于排查问题
  • 代码可读性显著提升,维护成本降低

9. 总结与持续优化#

编写高质量函数是提升代码质量的基础,核心可以总结为以下几点:

  1. 职责单一:一个函数只做一件事
  2. 见名知意:命名清晰,无需注释即可理解功能
  3. 简洁易用:参数数量少,类型明确,调用方无歧义
  4. 逻辑透明:副作用可控,返回值一致
  5. 鲁棒可靠:错误处理完善,避免沉默失败
  6. 易于维护:注释得当,文档清晰,可测试性强

提升函数质量不是一次性的工作,需要在日常编码中不断实践、重构,结合代码评审和团队规范,逐步形成良好的编码习惯。


10. 参考文献#

  1. 《代码整洁之道》(Clean Code: A Handbook of Agile Software Craftsmanship)- Robert C. Martin
  2. 《重构:改善既有代码的设计》(Refactoring: Improving the Design of Existing Code)- Martin Fowler
  3. PEP 8 -- Style Guide for Python Code
  4. Google Java Style Guide
  5. TypeScript Documentation - Functions