11、组织数据(一):自封装字段、以对象取代数组、以子类取代类型码
数据组织,说白了就是让代码里的数据更听话。
我见过太多项目,一开始图省事,把数据随便往数组里一塞,或者用个数字代表类型。结果项目一膨胀,改个需求得翻遍整个代码库。嗯,今天咱们就来聊聊三个最基础、也最实用的数据重构手法。
11.1 自封装字段:别直接碰字段
先问个问题:你在类里访问自己的字段,是直接 this.field,还是通过 getter/setter?
很多人觉得,自己类内部访问自己字段,没必要绕一圈。我以前也这么想。直到有一次,我需要给一个字段加个校验逻辑——所有赋值的地方都得改。那叫一个痛苦。
核心思想:即使是类内部,也通过方法访问字段。
看个例子。重构前:
class Employee {
constructor(name) {
this._name = name;
}
getInfo() {
return `员工:${this._name}`;
}
setName(name) {
this._name = name;
}
}
重构后:
class Employee {
constructor(name) {
this._name = name;
}
get name() {
return this._name;
}
set name(value) {
this._name = value;
}
getInfo() {
return `员工:${this.name}`; // 通过 getter 访问
}
setName(name) {
this.name = name; // 通过 setter 赋值
}
}
你可能会说:「这不就是加了一层包装吗?有啥区别?」
区别大了。假设后来需求变了,名字前面要加个前缀。你只需要改 getter 一处:
get name() {
return `EMP_${this._name}`;
}
所有调用 this.name 的地方自动生效。如果是直接访问 this._name,你得满世界找。
我的习惯:新写的类,一律用 getter/setter 访问内部字段。老代码重构时,我会先加 getter/setter,然后把内部引用一个个替换掉。这叫「先铺路,再开车」。
11.2 以对象取代数组:别把数据塞进数组里
这个坑我踩过。早期写一个报表系统,为了省事,用数组存一行数据:
const row = ['张三', 28, '开发部', 15000];
然后通过下标访问:
console.log(row[0]); // 姓名
console.log(row[2]); // 部门
你想想看,三个月后你再看这段代码,你知道 row[2] 是啥吗?还得去翻上下文。更可怕的是,如果中间插了一列,所有下标都得改。
我曾经接手过一个项目,一个数组里塞了 12 个字段。为了加一个字段,我花了整整两天去排查所有 data[7]、data[9] 这种魔法数字。那之后我立了个规矩:数组长度超过 3,必须换成对象。
重构后:
class EmployeeRow {
constructor(name, age, department, salary) {
this.name = name;
this.age = age;
this.department = department;
this.salary = salary;
}
}
const row = new EmployeeRow('张三', 28, '开发部', 15000);
console.log(row.name); // 清晰
console.log(row.department); // 明确
说白了,数组适合存「同一类东西」,比如一组数字、一组名字。但如果数组里的每个元素含义不同,那就是个「伪数组」,赶紧换成对象。
判断标准:如果你需要写注释来解释 data[3] 是什么,那就该重构了。
11.3 以子类取代类型码:别用数字代表类型
这个手法,我建议每个程序员都刻在脑子里。
先看个反面教材:
class Employee {
constructor(type) {
this.type = type; // 1=全职, 2=兼职, 3=实习生
}
getPay() {
switch (this.type) {
case 1: return 10000;
case 2: return 5000;
case 3: return 3000;
default: throw new Error('未知类型');
}
}
getWorkHours() {
switch (this.type) {
case 1: return 40;
case 2: return 20;
case 3: return 30;
default: throw new Error('未知类型');
}
}
}
问题在哪?
- 每个方法里都有 switch,重复代码满天飞
- 加一个新类型,得改所有 switch
- 类型码是魔法数字,可读性差
重构方案:用子类代替类型码。
class Employee {
// 基类,公共逻辑放这里
}
class FullTimeEmployee extends Employee {
getPay() { return 10000; }
getWorkHours() { return 40; }
}
class PartTimeEmployee extends Employee {
getPay() { return 5000; }
getWorkHours() { return 20; }
}
class InternEmployee extends Employee {
getPay() { return 3000; }
getWorkHours() { return 30; }
}
然后通过工厂方法创建:
function createEmployee(type) {
switch (type) {
case 'fulltime': return new FullTimeEmployee();
case 'parttime': return new PartTimeEmployee();
case 'intern': return new InternEmployee();
default: throw new Error('未知类型');
}
}
你看,switch 被压缩到一处了。加新类型时,只需要加一个新类,改工厂方法一处即可。
我个人习惯:如果类型码导致 switch 出现两次以上,我就开始考虑用子类。别等到代码烂成一锅粥再动手。
11.4 知识体系图
这三个手法其实是一条主线:让数据更「有名字」、更「有结构」、更「有行为」。
11.5 避坑指南
- 自封装别过度:如果字段只是简单存取,没有额外逻辑,直接暴露也无妨。我一般等需要加逻辑时再封装。
- 数组转对象别硬改:如果数组被很多地方引用,先加一个包装类,逐步迁移。别一次性改完,容易出事。
- 子类别滥用:如果类型码只有两种,且行为差异很小,用 if 就够了。子类适合「行为差异大」的场景。
我曾经在一个项目里,把只有两种类型的代码强行拆成子类,结果多了十几个文件,反而增加了复杂度。记住:重构是为了让代码更简单,不是更复杂。
好了,这三个手法你可以在下一个重构任务里立刻用起来。数据组织好了,后面的逻辑重构才能站得住脚。