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 里还保留着委托方法。这是过渡状态,后面我们会用「隐藏委托关系」来进一步简化。

避坑:不要为了拆而拆。如果一个类只有两三个字段,硬拆成两个类反而增加复杂度。我见过有人把 50 行的类拆成 5 个文件,结果找逻辑要翻半天。适可而止。

将类内联化

有拆就有合。提炼类的反面,就是将类内联化。

什么时候需要内联?当一个类已经不再承担足够的职责,或者它的存在反而增加了不必要的间接层

举个例子:

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 这种模式,就该考虑隐藏委托了。先加一个委托方法,然后逐步把调用方迁移过去。

知识体系总览

这一章的手法,其实都在围绕一个核心:职责分配。临时变量归谁管?类要不要拆?委托要不要藏?这些问题没有标准答案,但有一个判断原则——让变化的影响范围最小化

下面这张图总结了本章的核心逻辑:

封装(下)核心手法与决策逻辑 封装手法 以查询取代临时变量 提炼类 将类内联化 隐藏委托关系 核心原则:让变化的影响范围最小化,隐藏实现细节,只暴露意图 拆与合都是手段,目的是让代码更容易理解和修改

你看,这四个手法其实是一个闭环:临时变量多了就提取成查询,类大了就提炼,类小了就内联,委托暴露了就隐藏。没有绝对的对错,只有合不合适。

最后说一句:封装不是目的,可维护性才是。别为了封装而封装,多想想「这段代码三个月后我还看得懂吗?别人改的时候会踩坑吗?」想明白了,手法自然就选对了。


公众号:蓝海资料掘金营,微信deep3321