🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
曾用名:以字段取代子类(Replace Subclass with Fields) 反向重构:以子类取代类型码(362) ![](https://box.kancloud.cn/a246072830e6dd2dde96f67569905427_417x382.jpeg) ``` class Person {  get genderCode() {return "X";} } class Male extends Person {  get genderCode() {return "M";} } class Female extends Person {  get genderCode() {return "F";} } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` class Person { get genderCode() {return this._genderCode;} } ``` ### 动机 子类很有用,它们为数据结构的多样和行为的多态提供支持,它们是针对差异编程的好工具。但随着软件的演化,子类所支持的变化可能会被搬移到别处,甚至完全去除,这时子类就失去了价值。有时添加子类是为了应对未来的功能,结果构想中的功能压根没被构造出来,或者用了另一种方式构造,使该子类不再被需要了。 子类存在着就有成本,阅读者要花心思去理解它的用意,所以如果子类的用处太少,就不值得存在了。此时,最好的选择就是移除子类,将其替换为超类中的一个字段。 ### 做法 - 使用以工厂函数取代构造函数(334),把子类的构造函数包装到超类的工厂函数中。 > 如果构造函数的客户端用一个数组字段来决定实例化哪个子类,可以把这个判断逻辑放到超类的工厂函数中。 - 如果有任何代码检查子类的类型,先用提炼函数(106)把类型检查逻辑包装起来,然后用搬移函数(198)将其搬到超类。每次修改后执行测试。 - 新建一个字段,用于代表子类的类型。 - 将原本针对子类的类型做判断的函数改为使用新建的类型字段。 - 删除子类。 - 测试。 本重构手法常用于一次移除多个子类,此时需要先把这些子类都封装起来(添加工厂函数、搬移类型检查),然后再逐个将它们折叠到超类中。 ### 范例 一开始,代码中遗留了两个子类。 ##### class Person... ``` constructor(name) {  this._name = name; } get name() {return this._name;} get genderCode() {return "X";} // snip class Male extends Person {  get genderCode() {return "M";} } class Female extends Person {  get genderCode() {return "F";} } ``` 如果子类就干这点儿事,那真的没必要存在。不过,在移除子类之前,通常有必要检查使用方代码是否有依赖于特定子类的行为,这样的行为需要被搬移到子类中。在这个例子里,我找到一些客户端代码基于子类的类型做判断,不过这也不足以成为保留子类的理由。 ##### 客户端... `const numberOfMales = people.filter(p => p instanceof Male).length;`每当想要改变某个东西的表现形式时,我会先将当下的表现形式封装起来,从而尽量减小对客户端代码的影响。对于“创建子类对象”而言,封装的方式就是以工厂函数取代构造函数(334)。在这里,实现工厂有两种方式。 最直接的方式是为每个构造函数分别创建一个工厂函数。 ``` function createPerson(name) {  return new Person(name); } function createMale(name) {  return new Male(name); } function createFemale(name) {  return new Female(name); } ``` 虽然这是最直接的选择,但这样的对象经常是从输入源加载出来,直接根据性别代码创建对象。 ``` function loadFromInput(data) {  const result = [];  data.forEach(aRecord => {   let p;   switch (aRecord.gender) {    case 'M': p = new Male(aRecord.name); break;    case 'F': p = new Female(aRecord.name); break;    default: p = new Person(aRecord.name);   }   result.push(p);  });  return result; } ``` 有鉴于此,我觉得更好的办法是先用提炼函数(106)把“选择哪个类来实例化”的逻辑提炼成工厂函数。 ``` function createPerson(aRecord) {  let p;  switch (aRecord.gender) {   case 'M': p = new Male(aRecord.name); break;   case 'F': p = new Female(aRecord.name); break;   default: p = new Person(aRecord.name);  }  return p; } function loadFromInput(data) {  const result = [];  data.forEach(aRecord => {   result.push(createPerson(aRecord));  });  return result; } ``` 提炼完工厂函数后,我会对这两个函数做些清理。先用内联变量(123)简化`createPerson`函数: ``` function createPerson(aRecord) {  switch (aRecord.gender) {   case 'M': return new Male (aRecord.name);   case 'F': return new Female(aRecord.name);   default: return new Person(aRecord.name);  } } ``` 再用以管道取代循环(231)简化`loadFromInput`函数: ``` function loadFromInput(data) { return data.map(aRecord => createPerson(aRecord)); } ``` 工厂函数封装了子类的创建逻辑,但代码中还有一处用到`instanceof`运算符——这从来不会是什么好味道。我用提炼函数(106)把这个类型检查逻辑提炼出来。 ##### 客户端... ``` const numberOfMales = people.filter(p => isMale(p)).length; function isMale(aPerson) {return aPerson instanceof Male;} ``` 然后用搬移函数(198)将其移到`Person`类。 ##### class Person... `get isMale() {return this instanceof Male;}`##### 客户端... `const numberOfMales = people.filter(p => p.isMale).length;`重构到这一步,所有与子类相关的知识都已经安全地包装在超类和工厂函数中。(对于“超类引用子类”这种情况,通常我会很警惕,不过这段代码用不了一杯茶的工夫就会被干掉,所以也不用太担心。) 现在,添加一个字段来表示子类之间的差异。既然有来自别处的一个类型代码,直接用它也无妨。 ##### class Person... ``` constructor(name, genderCode) { this._name = name; this._genderCode = genderCode || "X"; } get genderCode() {return this._genderCode;} ``` 在初始化时先将其设置为默认值。(顺便说一句,虽然大多数人可以归类为男性或女性,但确实有些人不是这两种性别中的任何一种。忽视这些人的存在,是一个常见的建模错误。) 首先从“男性”的情况开始,将相关逻辑折叠到超类中。为此,首先要修改工厂函数,令其返回一个`Person`对象,然后修改所有`instanceof`检查逻辑,改为使用性别代码字段。 ``` function createPerson(aRecord) { switch (aRecord.gender) { case 'M': return new Person(aRecord.name, "M"); case 'F': return new Female(aRecord.name); default: return new Person(aRecord.name); } } ``` ##### class Person... `get isMale() {return "M" === this._genderCode;}`此时我可以测试,删除`Male`子类,再次测试,然后对`Female`子类也如法炮制。 ``` function createPerson(aRecord) { switch (aRecord.gender) { case 'M': return new Person(aRecord.name, "M"); case 'F': return new Person(aRecord.name, "F"); default: return new Person(aRecord.name); } } ``` 类型代码的分配有点儿失衡,默认情况没有类型代码,这种情况让我很烦心。未来阅读代码的人会一直好奇背后的原因。所以我更愿意现在做点儿修改,给所有情况都平等地分配类型代码——只要不会引入额外的复杂性就好。 ``` function createPerson(aRecord) { switch (aRecord.gender) { case 'M': return new Person(aRecord.name, "M"); case 'F': return new Person(aRecord.name, "F"); default: return new Person(aRecord.name, "X"); } } ``` ##### class Person... ``` constructor(name, genderCode) { this._name = name; this._genderCode = genderCode || "X"; } ```