4、基本重构手法(下):移动函数、移动字段、封装字段、引入参数对象
好,咱们接着聊。上一节讲了提炼函数和变量,算是重构的「基本功」。这一节要聊的四个手法,更偏向于「搬家」和「整理」。
说白了,代码写久了,东西放错地方是常有的事。函数不该待在这个类里,字段跑到了别人的地盘上,参数列表长得像购物清单……这些我都遇到过。别急,有办法治。
4.1 移动函数(Move Function)
一个函数,如果它跟另一个类的交互比跟自己的类还频繁,那它就该搬家。
什么时候该搬?
- 函数里频繁调用另一个类的 getter 或 setter
- 函数依赖另一个类的内部数据比依赖自己的还多
- 函数在代码里被调用的位置,明显更适合放在目标类里
我记得有一次接手一个订单系统,有个 calculateTotal 函数写在 Order 类里,结果它里面全是 order.items 的循环计算。你想想看,这逻辑明显该属于 OrderItem 或者 ItemCollection。搬过去之后,Order 类清爽多了。
操作步骤:
- 检查源函数在当前上下文中的所有引用
- 检查目标类是否已经有类似功能
- 将函数复制到目标类,调整参数
- 在源函数中委托调用目标函数
- 逐步替换所有调用方
- 删除源函数
// 重构前:函数在 Order 里,却疯狂操作 items
class Order {
constructor(items) {
this.items = items;
}
getTotalPrice() {
let total = 0;
for (let item of this.items) {
total += item.price * item.quantity;
}
return total;
}
}
// 重构后:把计算逻辑移到 Item 里
class Item {
constructor(price, quantity) {
this.price = price;
this.quantity = quantity;
}
getSubtotal() {
return this.price * this.quantity;
}
}
class Order {
constructor(items) {
this.items = items;
}
getTotalPrice() {
return this.items.reduce((sum, item) => sum + item.getSubtotal(), 0);
}
}
4.2 移动字段(Move Field)
字段放错地方,比函数放错地方更隐蔽。因为字段不报错,它就在那儿,你甚至可能意识不到它碍事。
典型症状:
- 某个字段在另一个类中被频繁访问
- 字段的值只在特定场景下才有意义
- 字段的更新逻辑散落在多个类中
我曾经在一个项目中看到 Customer 类里有个 discountRate 字段。结果呢?这个字段只在 Order 类里被用到。你说它待在那儿干嘛?搬过去之后,Order 自己就能算折扣,不用每次去问 Customer 要。
// 重构前:discountRate 在 Customer 里,但只有 Order 用
class Customer {
constructor(name, discountRate) {
this.name = name;
this.discountRate = discountRate;
}
}
class Order {
constructor(customer, amount) {
this.customer = customer;
this.amount = amount;
}
getDiscountedAmount() {
return this.amount * (1 - this.customer.discountRate);
}
}
// 重构后:字段搬到真正使用它的地方
class Order {
constructor(customer, amount, discountRate) {
this.customer = customer;
this.amount = amount;
this.discountRate = discountRate;
}
getDiscountedAmount() {
return this.amount * (1 - this.discountRate);
}
}
4.3 封装字段(Encapsulate Field)
这个手法最简单,但也最容易被忽视。说白了,就是给字段加上 getter 和 setter,不让外部直接访问。
为什么非要封装?
- 直接暴露字段,后续想加校验逻辑就得改所有调用方
- 字段类型或名称变了,影响面不可控
- 面向对象的基本原则:数据应该由自己管理
嗯,这里要注意。很多人觉得「反正就一个字段,没必要封装」。我刚开始也这么想,直到有一次一个 public 的 age 字段被到处赋值,有人传了负数,有人传了字符串……从那以后,我再也不敢偷懒了。
// 重构前:直接暴露字段
class Person {
constructor(name, age) {
this.name = name;
this.age = age;
}
}
// 重构后:封装字段,加校验
class Person {
constructor(name, age) {
this.name = name;
this._age = age;
}
get age() {
return this._age;
}
set age(value) {
if (value < 0 || value > 150) {
throw new Error('年龄不合法');
}
this._age = value;
}
}
4.4 引入参数对象(Introduce Parameter Object)
这个手法,专门对付「参数爆炸」。一个函数有六七个参数,调用的时候自己都记不清顺序。你想想看,这种代码谁维护谁头疼。
什么时候该用?
- 多个参数经常一起出现
- 参数之间存在逻辑关联
- 参数列表超过 3 个,且还在增长
我记得有个报表生成函数,参数有 startDate、endDate、region、category、minAmount、maxAmount……每次调用都得翻文档。后来我把它们打包成一个 ReportFilter 对象,整个世界清净了。
// 重构前:参数列表太长
function generateReport(startDate, endDate, region, category, minAmount, maxAmount) {
// 生成报表逻辑
}
// 重构后:引入参数对象
class ReportFilter {
constructor(startDate, endDate, region, category, minAmount, maxAmount) {
this.startDate = startDate;
this.endDate = endDate;
this.region = region;
this.category = category;
this.minAmount = minAmount;
this.maxAmount = maxAmount;
}
}
function generateReport(filter) {
// 使用 filter 对象
}
filter.isValid() 或 filter.toQueryString()。
4.5 四种手法的对比
| 手法 | 解决的问题 | 适用场景 | 风险等级 |
|---|---|---|---|
| 移动函数 | 函数放错类 | 函数依赖另一个类的数据更多 | 中 |
| 移动字段 | 字段放错类 | 字段只在另一个类中被使用 | 中 |
| 封装字段 | 数据暴露在外 | 需要加校验或后续可能变化 | 低 |
| 引入参数对象 | 参数列表过长 | 多个参数经常一起出现 | 低 |
这四种手法,说白了都是在做一件事:让代码的归属更清晰。函数、字段、参数,它们都有自己的「家」。别让它们流浪。
下一节,我们会聊到更高级的重构手法——继承体系的重构。到时候你会发现,今天这些「搬家」手法,其实都是基础。