Loading

【设计模式与重构】 重构(Refactoring):代码异味与重构手法大全

重构

参考资料:refactoringguru.cn/refactoring/catalog | 《重构:改善既有代码的设计》(Martin Fowler)

一、什么是重构

重构(Refactoring):在不改变软件可观察行为的前提下,调整其内部结构,以提高可理解性、降低修改成本。

一个容易被忽视的事实:代码是写给人读的,顺便让机器执行。项目初期人人都能看懂代码,但随着功能不断叠加,结构逐渐腐化——修改一个功能要动十个文件、一个函数长到三百行、新人半个月不敢提交代码。重构就是在这些症状变成癌症之前的治疗手段。

两个关键纪律:

  1. 小步修改:每次重构只做一小件事,改完立刻测试提交;
  2. 测试保护:没有测试兜底的重构是走钢丝不系安全绳。

二、代码异味(Code Smells)

异味不是 Bug,而是"这段代码可能有问题"的信号。RefactoringGuru 把常见异味分为五组。

2.1 臃肿的类(Bloaters)

代码、函数、类膨胀得难以操作。它们不是一夜之间出现的,而是随程序演化逐渐累积。

过长函数(Long Method) 与 过大的类(Large Class):

# 坏味道:一个函数干了四件事,无法复用、无法单独测试
def process_order(order):
    # 校验
    if not order.items: raise ValueError("空订单")
    if order.total <= 0: raise ValueError("金额异常")
    # 计算折扣
    if order.user.is_vip: order.total *= 0.9
    if order.total > 1000: order.total -= 50
    # 生成物流单
    ...
    # 发送通知
    ...

基本类型偏执(Primitive Obsession):用字符串/数字硬扛本该是对象的概念。

# 坏味道:电话号码、经纬度全是裸字符串,校验逻辑散落各处
def ship(user_phone: str, city: str, lat: str, lng: str): ...

# 重构后:概念有了自己的家,校验内聚
from dataclasses import dataclass

@dataclass(frozen=True)
class Phone:
    number: str
    def __post_init__(self):
        if not self.number.isdigit() or len(self.number) != 11:
            raise ValueError("手机号不合法")

@dataclass(frozen=True)
class GeoPoint:
    lat: float
    lng: float

def ship(phone: Phone, dest: GeoPoint): ...

过长参数列(Long Parameter List) 与 数据泥团(Data Clumps):总是成群出现的参数,暗示它们该组成一个类。

# 坏味道
def create_user(name, age, street, city, zip, company_street, company_city, company_zip): ...

# 重构后
def create_user(profile: Profile, home: Address, company: Address): ...

2.2 面向对象滥用者(OO Abusers)

switch 惊悚现身(Switch Statements):同样的 switch 散落在多个地方,新增类型就要改遍所有分支——这正是策略模式、状态模式的用武之地(见上一篇《设计模式》)。

临时字段(Temporary Field):字段只在特定情况下才有意义,其余时间为 None,阅读者根本猜不到它的生命周期。

被拒绝的遗赠(Refused Bequest):子类继承了父类,却对一半的方法和字段不感冒——说明继承关系设计错了。

2.3 变更防碍者(Change Preventers)

发散式变化(Divergent Change):一个类因为多种不同的原因被反复修改(既因为加数据库字段改它,又因为改报表格式改它)——应拆分成多个类(提炼类)。

霰弹式修改(Shotgun Surgery):与发散式相反,一个小改动要在十几个类里各改一行——应把散落的行为聚拢到一处(搬移函数/内联类)。

# 坏味道:改一次"客户"的定义,要同时动三个类
class Order:   def get_customer_name(self): return self.customer.name
class Invoice: def print_customer(self): print(self.customer.name)
class Report:  def customer_column(self): return self.customer.name

# 重构后:职责聚拢到 Customer 自己身上
class Customer:
    @property
    def display_name(self): return self.name

平行继承体系(Parallel Inheritance Hierarchies):每给 A 类加一个子类,就得同步给 B 类加一个——用一次搬移把两个体系合并。

2.4 冗余物(Dispensables)

  • 重复代码(Duplicate Code):异味之王。同一个坑摔两次就该提炼了;
  • 死代码(Dead Code):永远不会执行的分支,直接删(有 Git 历史兜底);
  • 夸夸其谈通用性(Speculative Generality):"将来可能用得上"的抽象类和参数——将来真的需要时再加;
  • 纯数据类(Data Class):只有字段和 getter/setter 的类,行为全在别人那里;
  • 令人误解的注释(Comments):当代码烂到需要一大段注释解释时,先重构代码,让注释变得多余。

2.5 耦合者(Couplers)

依恋情结(Feature Envy):一个函数对别的类的数据比对自家数据更感兴趣——把函数搬过去。

# 坏味道:订单函数满眼都是 customer 的字段
class Order:
    def discount_rate(self):
        c = self.customer
        if c.is_vip and c.years > 5: return 0.2
        if c.is_vip: return 0.1
        return 0

# 重构后:逻辑搬到数据所在的地方
class Customer:
    def discount_rate(self):
        if self.is_vip and self.years > 5: return 0.2
        if self.is_vip: return 0.1
        return 0.0

class Order:
    def discount_rate(self):
        return self.customer.discount_rate()

消息链(Message Chains):order.customer.address.city——客户端耦合了整条导航路径,中间任何一环变了都要跟着改(迪米特法则)。

# 重构后:隐藏委托
class Order:
    @property
    def shipping_city(self):
        return self.customer.address.city

中间人(Middle Man):一个类一半的方法都在转发给另一个类——和"隐藏委托"是反向拉锯,平衡点是一半以上在转发就删掉中间人。

三、重构手法(Refactoring Techniques)

RefactoringGuru 把几十种手法归为六组,下面挑最常用的、每种附 Python 前后对比。

3.1 重新组织函数(Composing Methods)

提炼函数(Extract Method)——使用频率最高的一招:

# 重构前
def print_owing(invoice):
    print("******* 客户账单 *******")
    outstanding = 0
    for o in invoice.orders:
        outstanding += o.amount
    due = datetime.now() + timedelta(days=30)
    print(f"客户:{invoice.customer}")
    print(f"欠款:{outstanding}")
    print(f"到期:{due:%Y-%m-%d}")

# 重构后:每个函数只做一件事,主流程一目了然
def print_owing(invoice):
    print_banner()
    outstanding = calculate_outstanding(invoice)
    record_due_date(invoice)
    print_details(invoice, outstanding)

以查询取代临时变量(Replace Temp with Query):

# 重构前
base = quantity * price
if base > 1000: discount = base * 0.1   # base 在下面还要用,不敢乱动

# 重构后:临时变量变成随时可调的函数
def base_price(self):
    return self.quantity * self.price

以函数对象取代函数(Replace Method with Method Object):长函数里局部变量纠缠不清时,把整个函数搬进一个类,局部变量变字段,然后放心地提炼函数。

3.2 在对象之间搬移特性(Moving Features between Objects)

提炼类(Extract Class):

# 重构前:Person 什么都管
class Person:
    def __init__(self, name, area_code, number):
        self.name = name
        self.area_code = area_code
        self.number = number

# 重构后:电话职责独立
class TelephoneNumber:
    def __init__(self, area_code, number):
        self.area_code = area_code
        self.number = number

class Person:
    def __init__(self, name, phone: TelephoneNumber):
        self.name = name
        self.phone = phone

隐藏委托关系(Hide Delegate) / 移除中间人(Remove Middle Man):见 2.5,两个手法方向相反,按耦合程度动态取舍。

搬移函数(Move Method):行为和数据分家时,函数搬到数据所在类,原处留一个委托(或直接删掉)。

3.3 重新组织数据(Organizing Data)

以字面常量取代魔法数(Replace Magic Number with Symbolic Constant):

# 重构前
if speed > 0.7 * max_speed: warn()

# 重构后
SAFETY_RATIO = 0.7
if speed > SAFETY_RATIO * max_speed: warn()

以对象取代数据值(Replace Data Value with Object):见 2.1 的 Phone/GeoPoint。

封装集合(Encapsulate Collection):绝不要把内部集合的原引用暴露出去。

# 坏味道:外部可以随意改课程列表,绕过所有业务规则
student.courses.append(course)

# 重构后:变更必须走方法,规则才有处安放
class Student:
    def __init__(self):
        self._courses = []

    def enroll(self, course):
        if course in self._courses:
            raise ValueError("重复选课")
        self._courses.append(course)

以类取代类型码(Replace Type Code with Class):

# 坏味道:1、2、3 的含义全靠猜
def create_vehicle(kind: int): ...

# 重构后:枚举让非法值在入口就被拦截
from enum import Enum

class VehicleKind(Enum):
    CAR = auto()
    TRUCK = auto()
    MOTORCYCLE = auto()

3.4 简化条件表达式(Simplifying Conditional Expressions)

分解条件表达式(Decompose Conditional):

# 重构前:条件本身是谜语
if user.is_vip and (order.total > 1000 or datetime.now().day == 1):
    charge = order.total * 0.85
else:
    charge = order.total

# 重构后:条件读起来像句子
def is_eligible_for_discount(user, order):
    return user.is_vip and (order.total > 1000 or datetime.now().day == 1)

def vip_charge(total):
    return total * 0.85

charge = vip_charge(order.total) if is_eligible_for_discount(user, order) else order.total

以卫语句取代嵌套条件(Replace Nested Conditional with Guard Clauses):

# 重构前:子弹式嵌套,正常流程被淹没
def pay(employee):
    if employee:
        if employee.is_active:
            if not employee.is_on_leave:
                return employee.salary

# 重构后:异常情况提前拦截,主干保持水平
def pay(employee):
    if employee is None: raise ValueError("员工不存在")
    if not employee.is_active: raise ValueError("已离职")
    if employee.is_on_leave: raise ValueError("休假中")
    return employee.salary

以多态取代条件表达式(Replace Conditional with Polymorphism):把散落各处的类型判断收编进子类(对应策略/状态模式)。

引入 Null 对象(Introduce Null Object):

# 重构前:每个调用点都要防 None
name = customer.name if customer else "游客"

# 重构后:空值也是一个对象,一次定义,处处安心
class NullCustomer:
    name = "游客"
    def is_null(self): return True

class Customer:
    def is_null(self): return False

3.5 简化函数调用(Simplifying Method Calls)

将查询函数和修改函数分离(Separate Query from Modifier):

# 坏味道:叫 get 的函数居然会写日志、改状态
def get_alerted_users(self): ...

# 重构后
def get_users_with_pending_alert(self): ...   # 纯查询
def send_alerts_to(self, users): ...          # 纯修改

引入参数对象(Introduce Parameter Object)、保持对象完整(Preserve Whole Object):

# 重构前:参数越加越多
def order(customer_name, customer_email, customer_vip): ...

# 重构后:整块传递
def order(customer: Customer): ...

以异常取代错误码(Replace Error Code with Exception):

# 重构前:调用方经常忘记检查返回值
def withdraw(amount):
    if amount > balance: return -1

# 重构后:错误无法被忽略
def withdraw(amount):
    if amount > self.balance:
        raise InsufficientFunds(f"余额 {self.balance},请求 {amount}")

以工厂函数取代构造函数(Replace Constructor with Factory Method):

@staticmethod
def from_config(cfg: dict) -> "Database":
    return Database(host=cfg["host"], port=int(cfg.get("port", 5432)))

3.6 处理继承关系(Dealing with Generalization)

函数上移(Pull Up Method)/ 字段上移(Pull Up Field):两个子类有相同的代码,搬到超类。

函数下移(Push Down Method):超类的方法只对某个子类有意义,搬下去。

提炼超类(Extract Superclass):两个类有相似的代码,提炼共同父类(或改为共享一个委托类)。

折叠继承体系(Collapse Hierarchy):子类没增加任何行为,合并掉。

以委托取代继承(Replace Inheritance with Delegation):想复用工具类,但不该"is-a"只能是"has-a"时,继承换成组合——这也是《设计模式》里"优先使用组合"的由来。

四、重构与设计模式的关系

RefactoringGuru 有一个精辟的观点:设计模式不是起点,而是重构的终点。

  • 一个不断膨胀的 if/elif 分支,经过"以多态取代条件表达式"的重构,终点就是策略模式;
  • 一串日益复杂的对象创建参数,经过"以对象取代数据值 + 提炼类",最终会自然长成建造者模式;
  • 到处都是 obj.a.b.c 的消息链,加上"隐藏委托"之后,往往就是外观模式的雏形。

不必一开始就套模式——先写出能工作的简单代码,等异味出现时按手法重构,模式会在恰当的时机浮现。反过来,为了用模式而写模式,就是"夸夸其谈通用性"异味。

五、实践清单

  1. 闻到异味再动手:不追求一步到位的完美设计,接受"持续小步演化";
  2. 重构与加功能交替进行:先重构让修改变容易,再小成本加功能;
  3. 每一次提交只做一件事:加功能的提交不掺重构,重构的提交不改行为,方便回滚与 Review;
  4. 让工具干活:PyCharm/VSCode 的 Rename、Extract Method、Change Signature 比手工改安全得多;
  5. 测试是最硬的底气:没有测试覆盖的核心代码,第一笔"重构"应该是补测试;
  6. Code Review 的正确打开方式:与其口头说"这写得不好",不如直接指出对应的异味名称和重构手法,让讨论有共同语言。

与上一篇《设计模式》配套食用:模式告诉你"好的结构长什么样",重构告诉你"怎么从现在的烂摊子走过去"。

posted @ 2026-09-19 05:45  Stellan7  阅读(17)  评论(0)    收藏  举报