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 知识体系图

这三个手法其实是一条主线:让数据更「有名字」、更「有结构」、更「有行为」。

数据组织三大手法 数据重构 自封装字段 通过 getter/setter 访问字段 以对象取代数组 用命名属性代替魔法下标 以子类取代类型码 用多态消除 switch 语句 共同目标 让数据有名字、有结构、有行为 → 代码更易读、易改、易扩展

11.5 避坑指南

  • 自封装别过度:如果字段只是简单存取,没有额外逻辑,直接暴露也无妨。我一般等需要加逻辑时再封装。
  • 数组转对象别硬改:如果数组被很多地方引用,先加一个包装类,逐步迁移。别一次性改完,容易出事。
  • 子类别滥用:如果类型码只有两种,且行为差异很小,用 if 就够了。子类适合「行为差异大」的场景。

我曾经在一个项目里,把只有两种类型的代码强行拆成子类,结果多了十几个文件,反而增加了复杂度。记住:重构是为了让代码更简单,不是更复杂。

好了,这三个手法你可以在下一个重构任务里立刻用起来。数据组织好了,后面的逻辑重构才能站得住脚。


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