封装与模块化重构:让代码像乐高一样清晰
大家好,我是你们的老朋友。今天我们来聊聊封装与模块化重构。说实话,这是我在项目中最常用的一组重构手法。为什么?因为大部分技术债务,说白了就是数据散落、逻辑混乱、类型滥用。你想想看,一个项目跑了两三年,代码里到处都是魔法数字、裸集合、散装记录……嗯,我见过太多这样的烂摊子了。
封装与模块化重构的核心思想很简单:把零散的东西收拢,把混乱的东西归类。就像整理你的工具箱——螺丝刀不能扔在抽屉里,得分类放好。代码也一样。
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()); // "已支付"
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
| 维度 | 子类化 | 策略模式 |
|---|---|---|
| 关系 | is-a(是一个) | has-a(有一个) |
| 灵活性 | 较低(继承树固定) | 较高(可动态切换) |
| 适用场景 | 类型码决定对象本质 | 类型码决定算法行为 |
| 代码量 | 较少 | 略多(需要策略接口) |
小结:封装与模块化的核心心法
好了,今天的内容就到这里。总结一下这五种重构手法的选择思路:
- 数据散落 → 封装记录
- 集合裸奔 → 封装集合
- 基本类型滥用 → 以对象取代基本类型
- 类型码决定本质 → 以子类取代类型码
- 类型码决定行为 → 以策略取代类型码
我个人习惯是:先封装数据,再封装行为。数据稳了,行为自然就好组织了。你想想看,如果数据都是散装的,你怎么可能写出干净的逻辑?
嗯,下一章我们会聊更进阶的重构手法。今天就到这里,大家回去可以看看自己的项目里,有没有裸奔的集合和魔法数字——动手改一改,你会感受到代码变清爽的快乐。