9、封装(下):以查询取代临时变量、提炼类、将类内联化、隐藏委托关系
好,咱们接着聊封装。上一章我们把封装的基础手法过了一遍,这一章我挑了几个在实际项目中特别常用的「进阶」手法来讲。说白了,这些手法都是在帮我们回答同一个问题:这段代码到底该归谁管?
我个人习惯是,只要代码里出现「不舒服」的感觉,就立刻动手重构。临时变量太多?拆。类太臃肿?拆。委托链太长?藏起来。别等代码烂到没法看才动手,那时候成本就高了。
以查询取代临时变量
先看一个最常见的坏味道:临时变量满天飞。
function getPrice() {
const basePrice = this.quantity * this.itemPrice;
const discount = Math.max(0, this.quantity - 100) * 0.05;
const shipping = Math.min(this.quantity * 0.1, 5);
return basePrice - discount + shipping;
}
这段代码有什么问题?三个临时变量,每个只在一个函数里用了一次。但如果你在其他地方也需要 basePrice 呢?你只能再算一遍,或者把这个变量传出去。这其实是在「复制知识」。
我遇到过一个项目,一个订单计算函数里有十几个临时变量,后来需求变了三次,每次都要改这些变量的计算逻辑。改到最后,没人敢动了。
解决方案很简单:把临时变量变成查询方法。
function getPrice() {
return this.basePrice - this.discount + this.shipping;
}
get basePrice() {
return this.quantity * this.itemPrice;
}
get discount() {
return Math.max(0, this.quantity - 100) * 0.05;
}
get shipping() {
return Math.min(this.quantity * 0.1, 5);
}
你看,每个计算逻辑都变成了独立的查询。以后改折扣规则?只改 discount 方法。改运费?只改 shipping。互不干扰。
提炼类
类太臃肿了怎么办?拆。
我记得有一次接手一个老系统,一个 Customer 类有 3000 多行。里面混着地址验证、订单历史、积分计算、甚至还有邮件发送的逻辑。这哪是 Customer,这分明是个「上帝类」。
判断标准很简单:如果一组数据和方法总是在一起变化,它们就应该独立成一个类。
看个例子:
class Person {
get name() { return this._name; }
set name(arg) { this._name = arg; }
get telephoneNumber() { return `(${this.officeAreaCode}) ${this.officeNumber}`; }
get officeAreaCode() { return this._officeAreaCode; }
set officeAreaCode(arg) { this._officeAreaCode = arg; }
get officeNumber() { return this._officeNumber; }
set officeNumber(arg) { this._officeNumber = arg; }
}
电话号码相关的字段和方法挤在 Person 里。如果以后要支持手机号、传真号呢?Person 会越来越胖。
提炼成 TelephoneNumber 类:
class TelephoneNumber {
get areaCode() { return this._areaCode; }
set areaCode(arg) { this._areaCode = arg; }
get number() { return this._number; }
set number(arg) { this._number = arg; }
get telephoneNumber() { return `(${this.areaCode}) ${this.number}`; }
}
class Person {
constructor() {
this._telephoneNumber = new TelephoneNumber();
}
get name() { return this._name; }
set name(arg) { this._name = arg; }
get telephoneNumber() { return this._telephoneNumber.telephoneNumber; }
get officeAreaCode() { return this._telephoneNumber.areaCode; }
set officeAreaCode(arg) { this._telephoneNumber.areaCode = arg; }
get officeNumber() { return this._telephoneNumber.number; }
set officeNumber(arg) { this._telephoneNumber.number = arg; }
}
嗯,这里要注意:提炼类之后,Person 里还保留着委托方法。这是过渡状态,后面我们会用「隐藏委托关系」来进一步简化。
将类内联化
有拆就有合。提炼类的反面,就是将类内联化。
什么时候需要内联?当一个类已经不再承担足够的职责,或者它的存在反而增加了不必要的间接层。
举个例子:
class Shipment {
get trackingInfo() {
return this._trackingInformation.display;
}
get trackingInformation() { return this._trackingInformation; }
set trackingInformation(arg) { this._trackingInformation = arg; }
}
class TrackingInformation {
get display() {
return `${this.shippingCompany}: ${this.trackingNumber}`;
}
get shippingCompany() { return this._shippingCompany; }
set shippingCompany(arg) { this._shippingCompany = arg; }
get trackingNumber() { return this._trackingNumber; }
set trackingNumber(arg) { this._trackingNumber = arg; }
}
TrackingInformation 类只包装了两个字段和一个 display 方法。Shipment 每次访问都要经过它。说白了,这个中间层已经没意义了。
内联之后:
class Shipment {
get trackingInfo() {
return `${this.shippingCompany}: ${this.trackingNumber}`;
}
get shippingCompany() { return this._shippingCompany; }
set shippingCompany(arg) { this._shippingCompany = arg; }
get trackingNumber() { return this._trackingNumber; }
set trackingNumber(arg) { this._trackingNumber = arg; }
}
代码变短了,调用方也少了一层跳转。你想想看,每次读代码都要多跳一个文件,时间长了真的很烦。
隐藏委托关系
最后一个手法,也是封装思想的精髓:不要让调用方知道你的内部结构。
看这个调用链:
const manager = person.department.manager;
这段代码有什么问题?调用方知道 Person 内部有一个 Department,Department 有一个 manager。如果以后 Department 的结构变了,所有调用方都得改。
这就是典型的「委托链暴露」。我见过最夸张的代码:a.b.c.d.e.f(),六层委托。改一个字段,全项目报错。
解决方案:在 Person 上直接提供一个方法。
class Person {
get manager() {
return this._department.manager;
}
}
// 调用方
const manager = person.manager;
调用方不再需要知道 Department 的存在。这就是封装——隐藏实现细节,只暴露意图。
我曾经在一个电商项目中,把订单、支付、物流之间的委托关系全部隐藏掉。调用方只需要说「我要取消订单」,背后的事情(退款、拦截物流、释放库存)全由内部处理。后来物流系统换了供应商,只改了内部代码,调用方一行没动。
obj.a.b.c 这种模式,就该考虑隐藏委托了。先加一个委托方法,然后逐步把调用方迁移过去。
知识体系总览
这一章的手法,其实都在围绕一个核心:职责分配。临时变量归谁管?类要不要拆?委托要不要藏?这些问题没有标准答案,但有一个判断原则——让变化的影响范围最小化。
下面这张图总结了本章的核心逻辑:
你看,这四个手法其实是一个闭环:临时变量多了就提取成查询,类大了就提炼,类小了就内联,委托暴露了就隐藏。没有绝对的对错,只有合不合适。
最后说一句:封装不是目的,可维护性才是。别为了封装而封装,多想想「这段代码三个月后我还看得懂吗?别人改的时候会踩坑吗?」想明白了,手法自然就选对了。