封装与模块化重构:让代码像乐高一样清晰

大家好,我是你们的老朋友。今天我们来聊聊封装与模块化重构。说实话,这是我在项目中最常用的一组重构手法。为什么?因为大部分技术债务,说白了就是数据散落、逻辑混乱、类型滥用。你想想看,一个项目跑了两三年,代码里到处都是魔法数字、裸集合、散装记录……嗯,我见过太多这样的烂摊子了。

封装与模块化重构的核心思想很简单:把零散的东西收拢,把混乱的东西归类。就像整理你的工具箱——螺丝刀不能扔在抽屉里,得分类放好。代码也一样。

核心原则: 数据和行为应该待在一起。类型码应该被真正的类型取代。集合应该被封装,不能裸奔。
封装与模块化重构 · 五大手法 封装与模块化重构 封装记录 封装集合 以对象取代基本类型 以子类取代类型码 以策略取代类型码 从「数据散装」到「类型驱动」的渐进式重构路径 封装记录 → 封装集合 → 对象取代基本类型 → 子类/策略取代类型码

1. 封装记录:别再让数据裸奔

什么是封装记录?说白了,就是把散落在各处的相关数据收拢到一个类里。我见过太多项目,一个用户的姓名、年龄、邮箱散落在三个不同的数组里,每次操作都要同步维护三个索引——这不是给自己挖坑吗?

重构前:

// 散装数据,维护噩梦
let userName = "张三";
let userAge = 28;
let userEmail = "zhangsan@example.com";

function printUser() {
  console.log(`${userName}, ${userAge}, ${userEmail}`);
}

重构后:

class User {
  constructor(name, age, email) {
    this.name = name;
    this.age = age;
    this.email = email;
  }
  
  print() {
    console.log(`${this.name}, ${this.age}, ${this.email}`);
  }
}

const user = new User("张三", 28, "zhangsan@example.com");
user.print();
我的习惯: 只要发现三个以上散落的变量经常一起出现,我就会考虑封装成记录。别等到五个、六个才动手——那时候改起来就疼了。

2. 封装集合:别让数组裸奔

封装集合,比封装记录更进一层。你想想看,一个团队列表,直接暴露在外面,谁都能往里塞人、删人、改人。我曾经在一个项目中看到这样的代码:

// 裸集合,谁都能改
let teamMembers = ["张三", "李四", "王五"];

// 某天有人这么写
teamMembers.push(null);
teamMembers[1] = undefined;
// 然后整个系统崩了

重构后:

class Team {
  #members = [];
  
  addMember(name) {
    if (!name || typeof name !== 'string') {
      throw new Error('成员姓名必须是非空字符串');
    }
    this.#members.push(name);
  }
  
  removeMember(name) {
    const index = this.#members.indexOf(name);
    if (index !== -1) this.#members.splice(index, 1);
  }
  
  getMembers() {
    return [...this.#members]; // 返回副本,保护原始数据
  }
  
  get count() {
    return this.#members.length;
  }
}

const team = new Team();
team.addMember("张三");
team.addMember("李四");
console.log(team.getMembers()); // ["张三", "李四"]
避坑指南: 我曾经在返回集合时直接返回了内部数组引用,结果调用方偷偷改了数据,排查了整整两天。记住:永远返回副本,或者用只读包装。

3. 以对象取代基本类型:告别魔法数字

这个重构手法,说白了就是给基本类型穿上衣服。你想想看,一个订单状态用数字 0、1、2 表示,一个用户角色用字符串 "admin"、"user" 表示——这些基本类型本身没有任何语义,全靠程序员脑补。

重构前:

// 魔法数字满天飞
function getOrderStatusText(status) {
  if (status === 0) return "待支付";
  if (status === 1) return "已支付";
  if (status === 2) return "已发货";
  if (status === 3) return "已完成";
  return "未知";
}

// 调用时全靠猜
getOrderStatusText(1); // 这是啥意思?

重构后:

class OrderStatus {
  static PENDING = new OrderStatus("待支付", 0);
  static PAID = new OrderStatus("已支付", 1);
  static SHIPPED = new OrderStatus("已发货", 2);
  static COMPLETED = new OrderStatus("已完成", 3);
  
  constructor(label, code) {
    this.label = label;
    this.code = code;
  }
  
  toString() {
    return this.label;
  }
}

// 调用时一目了然
const status = OrderStatus.PAID;
console.log(status.toString()); // "已支付"
核心收益: 类型安全 + 语义清晰 + 行为内聚。你不再需要到处写 if-else 判断,所有逻辑都封装在对象里。

4. 以子类取代类型码:用继承解决分支

当类型码决定了对象的行为时,子类化是最好的选择。我记得在一个项目中,员工类型码决定了薪资计算方式——经理、工程师、销售,每种类型的计算逻辑完全不同。用 if-else 写了一大坨,每次新增类型都要改好几个地方。

重构前:

class Employee {
  constructor(type) {
    this.type = type; // "manager" | "engineer" | "sales"
  }
  
  calculateSalary(base) {
    if (this.type === "manager") return base * 1.5;
    if (this.type === "engineer") return base * 1.2;
    if (this.type === "sales") return base + base * 0.1;
    throw new Error("未知类型");
  }
}

重构后:

class Employee {
  // 工厂方法
  static create(type) {
    switch(type) {
      case "manager": return new Manager();
      case "engineer": return new Engineer();
      case "sales": return new Sales();
      default: throw new Error("未知类型");
    }
  }
  
  calculateSalary(base) {
    throw new Error("子类必须实现");
  }
}

class Manager extends Employee {
  calculateSalary(base) { return base * 1.5; }
}

class Engineer extends Employee {
  calculateSalary(base) { return base * 1.2; }
}

class Sales extends Employee {
  calculateSalary(base) { return base + base * 0.1; }
}

// 使用
const emp = Employee.create("engineer");
console.log(emp.calculateSalary(10000)); // 12000
我的建议: 子类化适合类型码稳定且行为差异大的场景。如果类型码经常变化,或者行为差异不大,策略模式可能更合适。

5. 以策略取代类型码:用组合代替继承

策略模式,说白了就是把算法抽出来,让它们可以互相替换。子类化是「是什么」,策略模式是「做什么」。当类型码对应的行为可以独立变化时,策略模式更灵活。

重构前:

class Order {
  constructor(type) {
    this.type = type; // "normal" | "discount" | "vip"
  }
  
  calculatePrice(amount) {
    if (this.type === "normal") return amount;
    if (this.type === "discount") return amount * 0.9;
    if (this.type === "vip") return amount * 0.8;
    throw new Error("未知类型");
  }
}

重构后:

// 策略接口
class PricingStrategy {
  calculate(amount) {
    throw new Error("子类必须实现");
  }
}

class NormalPricing extends PricingStrategy {
  calculate(amount) { return amount; }
}

class DiscountPricing extends PricingStrategy {
  calculate(amount) { return amount * 0.9; }
}

class VipPricing extends PricingStrategy {
  calculate(amount) { return amount * 0.8; }
}

// 订单类
class Order {
  constructor(strategy) {
    this.strategy = strategy;
  }
  
  calculatePrice(amount) {
    return this.strategy.calculate(amount);
  }
}

// 使用
const order = new Order(new VipPricing());
console.log(order.calculatePrice(100)); // 80
策略 vs 子类:
维度子类化策略模式
关系is-a(是一个)has-a(有一个)
灵活性较低(继承树固定)较高(可动态切换)
适用场景类型码决定对象本质类型码决定算法行为
代码量较少略多(需要策略接口)
避坑指南: 我曾经在一个项目中同时用了子类和策略,结果把类型码和策略混在一起,搞得代码比原来还乱。记住:子类解决「是什么」,策略解决「怎么做」,别混为一谈。

小结:封装与模块化的核心心法

好了,今天的内容就到这里。总结一下这五种重构手法的选择思路:

  • 数据散落 → 封装记录
  • 集合裸奔 → 封装集合
  • 基本类型滥用 → 以对象取代基本类型
  • 类型码决定本质 → 以子类取代类型码
  • 类型码决定行为 → 以策略取代类型码

我个人习惯是:先封装数据,再封装行为。数据稳了,行为自然就好组织了。你想想看,如果数据都是散装的,你怎么可能写出干净的逻辑?

嗯,下一章我们会聊更进阶的重构手法。今天就到这里,大家回去可以看看自己的项目里,有没有裸奔的集合和魔法数字——动手改一改,你会感受到代码变清爽的快乐。

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