【设计模式与重构】 重构(Refactoring):代码异味与重构手法大全
重构
参考资料:refactoringguru.cn/refactoring/catalog | 《重构:改善既有代码的设计》(Martin Fowler)
一、什么是重构
重构(Refactoring):在不改变软件可观察行为的前提下,调整其内部结构,以提高可理解性、降低修改成本。
一个容易被忽视的事实:代码是写给人读的,顺便让机器执行。项目初期人人都能看懂代码,但随着功能不断叠加,结构逐渐腐化——修改一个功能要动十个文件、一个函数长到三百行、新人半个月不敢提交代码。重构就是在这些症状变成癌症之前的治疗手段。
两个关键纪律:
- 小步修改:每次重构只做一小件事,改完立刻测试提交;
- 测试保护:没有测试兜底的重构是走钢丝不系安全绳。
二、代码异味(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的消息链,加上"隐藏委托"之后,往往就是外观模式的雏形。
不必一开始就套模式——先写出能工作的简单代码,等异味出现时按手法重构,模式会在恰当的时机浮现。反过来,为了用模式而写模式,就是"夸夸其谈通用性"异味。
五、实践清单
- 闻到异味再动手:不追求一步到位的完美设计,接受"持续小步演化";
- 重构与加功能交替进行:先重构让修改变容易,再小成本加功能;
- 每一次提交只做一件事:加功能的提交不掺重构,重构的提交不改行为,方便回滚与 Review;
- 让工具干活:PyCharm/VSCode 的 Rename、Extract Method、Change Signature 比手工改安全得多;
- 测试是最硬的底气:没有测试覆盖的核心代码,第一笔"重构"应该是补测试;
- Code Review 的正确打开方式:与其口头说"这写得不好",不如直接指出对应的异味名称和重构手法,让讨论有共同语言。
与上一篇《设计模式》配套食用:模式告诉你"好的结构长什么样",重构告诉你"怎么从现在的烂摊子走过去"。

浙公网安备 33010602011771号