🔥码云GVP开源项目 12k star Uniapp+ElementUI 功能强大 支持多语言、二开方便! 广告
包含旧重构:以State/Strategy取代类型码(Replace Type Code with State/Strategy) 包含旧重构:提炼子类(Extract Subclass) 反向重构:移除子类(369) ![](https://box.kancloud.cn/9de2b9431fc432b5ca55834ad978ae7b_688x376.jpeg) ``` function createEmployee(name, type) { return new Employee(name, type); } ``` ![](https://box.kancloud.cn/a3bed334e2e1f6d1a46c5039deb25af9_91x152.jpeg) ``` function createEmployee(name, type) { switch (type) { case "engineer": return new Engineer(name); case "salesman": return new Salesman(name); case "manager": return new Manager (name); } ``` ### 动机 软件系统经常需要表现“相似但又不同的东西”,比如员工可以按职位分类(工程师、经理、销售),订单可以按优先级分类(加急、常规)。表现分类关系的第一种工具是类型码字段——根据具体的编程语言,可能实现为枚举、符号、字符串或者数字。类型码的取值经常来自给系统提供数据的外部服务。 大多数时候,有这样的类型码就够了。但也有些时候,我可以再多往前一步,引入子类。继承有两个诱人之处。首先,你可以用多态来处理条件逻辑。如果有几个函数都在根据类型码的取值采取不同的行为,多态就显得特别有用。引入子类之后,我可以用以多态取代条件表达式(272)来处理这些函数。 另外,有些字段或函数只对特定的类型码取值才有意义,例如“销售目标”只对“销售”这类员工才有意义。此时我可以创建子类,然后用字段下移(361)把这样的字段放到合适的子类中去。当然,我也可以加入验证逻辑,确保只有当类型码取值正确时才使用该字段,不过子类的形式能更明确地表达数据与类型之间的关系。 在使用以子类取代类型码时,我需要考虑一个问题:应该直接处理携带类型码的这个类,还是应该处理类型码本身呢?以前面的例子来说,我是应该让“工程师”成为“员工”的子类,还是应该在“员工”类包含“员工类别”属性、从后者继承出“工程师”和“经理”等子类型呢?直接的子类继承(前一种方案)比较简单,但职位类别就不能用在其他场合了。另外,如果员工的类别是可变的,那么也不能使用直接继承的方案。如果想在“员工类别”之下创建子类,可以运用以对象取代基本类型(174)把类型码包装成“员工类别”类,然后对其使用以子类取代类型码(362)。 ### 做法 - 自封装类型码字段。 - 任选一个类型码取值,为其创建一个子类。覆写类型码类的取值函数,令其返回该类型码的字面量值。 - 创建一个选择器逻辑,把类型码参数映射到新的子类。 > 如果选择直接继承的方案,就用以工厂函数取代构造函数(334)包装构造函数,把选择器逻辑放在工厂函数里;如果选择间接继承的方案,选择器逻辑可以保留在构造函数里。 - 测试。 - 针对每个类型码取值,重复上述“创建子类、添加选择器逻辑”的过程。每次修改后执行测试。 - 去除类型码字段。 - 测试。 - 使用函数下移(359)和以多态取代条件表达式(272)处理原本访问了类型码的函数。全部处理完后,就可以移除类型码的访问函数。 ### 范例 这个员工管理系统的例子已经被用烂了…… ##### class Employee... ``` constructor(name, type){ this.validateType(type); this._name = name; this._type = type; } validateType(arg) { if (!["engineer", "manager", "salesman"].includes(arg)) throw new Error(`Employee cannot be of type ${arg}`); } toString() {return `${this._name} (${this._type})`;} ``` 第一步是用封装变量(132)将类型码自封装起来。 ##### class Employee... ``` get type() {return this._type;} toString() {return `${this._name} (${this.type})`;} ``` 请注意,`toString`函数的实现中去掉了`this._type`的下划线,改用新建的取值函数了。 我选择从工程师("`engineer`")这个类型码开始重构。我打算采用直接继承的方案,也就是继承`Employee`类。子类很简单,只要覆写类型码的取值函数,返回适当的字面量值就行了。 ``` class Engineer extends Employee { get type() {return "engineer";} } ``` 虽然JavaScript的构造函数也可以返回其他对象,但如果把选择器逻辑放在这儿,它会与字段初始化逻辑相互纠缠,搞得一团混乱。所以我会先运用以工厂函数取代构造函数(334),新建一个工厂函数以便安放选择器逻辑。 ``` function createEmployee(name, type) { return new Employee(name, type); } ``` 然后我把选择器逻辑放在工厂函数中,从而开始使用新的子类。 ``` function createEmployee(name, type) { switch (type) { case "engineer": return new Engineer(name, type); } return new Employee(name, type); } ``` 测试,确保一切运转正常。不过由于我的偏执,我随后会修改`Engineer`类中覆写的`type`函数,让它返回另外一个值,再次执行测试,确保会有测试失败,这样我才能肯定:新建的子类真的被用到了。然后我把`type`函数的返回值改回正确的状态,继续处理别的类型。我一次处理一个类型,每次修改后都执行测试。 ``` class Salesman extends Employee {  get type() {return "salesman";} } class Manager extends Employee {  get type() {return "manager";} } function createEmployee(name, type) {  switch (type) {   case "engineer": return new Engineer(name, type);   case "salesman": return new Salesman(name, type);   case "manager": return new Manager (name, type);  }  return new Employee(name, type); } ``` 全部修改完成后,我就可以去掉类型码字段及其在超类中的取值函数(子类中的取值函数仍然保留)。 ##### class Employee... ``` constructor(name, type){  this.validateType(type);  this._name = name;  this._type = type; } get type() {return this._type;} toString() {return `${this._name} (${this.type})`;} ``` 测试,确保一切工作正常,我就可以移除验证逻辑,因为分发逻辑做的是同一回事。 ##### class Employee... ``` constructor(name, type){  this.validateType(type);  this._name = name; } function createEmployee(name, type) {  switch (type) {   case "engineer": return new Engineer(name, type);   case "salesman": return new Salesman(name, type);   case "manager": return new Manager (name, type);   default: throw new Error(`Employee cannot be of type ${type}`);  }  return new Employee(name, type); } ``` 现在,构造函数的类型参数已经没用了,用改变函数声明(124)把它干掉。 ##### class Employee... ``` constructor(name, type){  this._name = name; } function createEmployee(name, type) {  switch (type) {   case "engineer": return new Engineer(name, type);   case "salesman": return new Salesman(name, type);   case "manager": return new Manager (name, type);   default: throw new Error(`Employee cannot be of type ${type}`);  } } ``` 子类中获取类型码的访问函数——`get type`函数——仍然留着。通常我会希望把这些函数也干掉,不过可能需要多花点儿时间,因为有其他函数使用了它们。我会用以多态取代条件表达式(272)和函数下移(359)来处理这些访问函数。到某个时候,已经没有代码使用类型码的访问函数了,我再用移除死代码(237)给它们送终。 ### 范例:使用间接继承 还是前面这个例子,我们回到最起初的状态,不过这次我已经有了“全职员工”和“兼职员工”两个子类,所以不能再根据员工类别代码创建子类了。另外,我可能需要允许员工类别动态调整,这也会导致不能使用直接继承的方案。 ##### class Employee... ``` constructor(name, type){  this.validateType(type);  this._name = name;  this._type = type; } validateType(arg) {  if (!["engineer", "manager", "salesman"].includes(arg))   throw new Error(`Employee cannot be of type ${arg}`); } get type() {return this._type;} set type(arg) {this._type = arg;} get capitalizedType() {  return this._type.charAt(0).toUpperCase() + this._type.substr(1).toLowerCase(); } toString() {  return `${this._name} (${this.capitalizedType})`; } ``` 这次的`toString`函数要更复杂一点,以便稍后展示用。 首先,我用以对象取代基本类型(174)包装类型码。 ``` class EmployeeType { constructor(aString) { this._value = aString; } toString() {return this._value;} } ``` ##### class Employee... ``` constructor(name, type){  this.validateType(type);  this._name = name;  this.type = type; } validateType(arg) {  if (!["engineer", "manager", "salesman"].includes(arg))   throw new Error(`Employee cannot be of type ${arg}`); } get typeString() {return this._type.toString();} get type() {return this._type;} set type(arg) {this._type = new EmployeeType(arg);} get capitalizedType() {  return this.typeString.charAt(0).toUpperCase()   + this.typeString.substr(1).toLowerCase(); } toString() {  return `${this._name} (${this.capitalizedType})`; } ``` 然后使用以子类取代类型码(362)的老套路,把员工类别代码变成子类。 ##### class Employee... ``` set type(arg) {this._type = Employee.createEmployeeType(arg);}  static createEmployeeType(aString) {   switch(aString) {    case "engineer": return new Engineer();    case "manager": return new Manager ();    case "salesman": return new Salesman();    default: throw new Error(`Employee cannot be of type ${aString}`);   }  } class EmployeeType { } class Engineer extends EmployeeType {  toString() {return "engineer";} } class Manager extends EmployeeType {  toString() {return "manager";} } class Salesman extends EmployeeType {  toString() {return "salesman";} } ``` 如果重构到此为止的话,空的`EmployeeType`类可以去掉。但我更愿意留着它,用来明确表达各个子类之间的关系。并且有一个超类,也方便把其他行为搬移进去,例如我专门放在`toString`函数里的“名字大写”逻辑,就可以搬到超类。 ##### class Employee... ``` toString() { return `${this._name} (${this.type.capitalizedName})`; } ``` ##### class EmployeeType... ``` get capitalizedName() { return this.toString().charAt(0).toUpperCase() + this.toString().substr(1).toLowerCase(); } ``` 熟悉本书第1版的读者大概能看出,这个例子来自第1版的以State/Strategy取代类型码重构手法。现在我认为这是以间接继承的方式使用以子类取代类型码,所以就不再将其作为一个单独的重构手法了。(而且我也一直不喜欢那个老重构手法的名字。)