重构之多态消除条件分支
最近翻看之前写过一个项目,其中用到了大量的 switch-case 分支语句,大致的代码结构如下
代码演示
class A {
  run() {
    console.log(A.name, 'run1!');
  }
}
class B {
  run() {
    console.log(B.name, 'run2!');
  }
}
class C {
  run() {
    console.log(C.name, 'run3!');
  }
}
function fun1(type) {
  let temp;
  switch (type) {
    case 1:
      temp = new A();
      break;
    case 2:
      temp = new B();
      break;
    case 3:
      temp = new C();
      break;
    default:
      throw new Error('Unsupported types');
      break;
  }
  temp.run();
}
fun1(1);
可以发现每个分支都有个共同的特点,通过构造函数实例化对象 a,调用a.task方法
temp = new A()
但现在有一个需求 给 A,B,C 三个类分别加上getUser方法,用来获取传入的 user,代码如下
class A {
  constructor(user) {
    this.user = user;
  }
  getUser() {
    return this.user;
  }
  run() {
    console.log(A.name, 'run1!');
  }
}
class B {
  constructor(user) {
    this.user = user;
  }
  getUser() {
    return this.user;
  }
  run() {
    console.log(B.name, 'run2!');
  }
}
class C {
  constructor(user) {
    this.user = user;
  }
  getUser() {
    return this.user;
  }
  run() {
    console.log(C.name, 'run3!');
  }
}
function fun2(type) {
  let temp;
  switch (type) {
    case 1:
      temp = new A({ username: 'aaa' });
      break;
    case 2:
      temp = new B({ username: 'bbb' });
      break;
    case 3:
      temp = new C({ username: 'ccc' });
      break;
    default:
      throw new Error('Unsupported types');
      break;
  }
  let user = temp.getUser();
  return user;
}
let user = fun2(1);
console.log(user);
从这你也能看的出来,如果我要在添加一个需求的话,我就要分别给这三个类添加方法(当然这个是避免不了的),同时又要定义一个fun3来指定功能,没错,需求和写着代码的都是我。于是我决定重构一些这一部分的代码,为以后方便我后续的修改操作。
重构代码
首先 我们也能到两个部分都有 switch 分支,并且都夹杂着 break 语句,说实话,看的不是很入眼。同 时定义了一个 temp 临时参数用于调用,不妨封装成一个函数,专门用来获取该类的实例对象。
function getClass(type) {
  switch (type) {
    case 1:
      return new A({ username: 'aaa' });
    case 2:
      return new B({ username: 'bbb' });
    case 3:
      return new C({ username: 'ccc' });
    default:
      throw new Error('Unsupported types');
  }
}
function fun2(type) {
  let temp = getClass(type);
  let user = temp.getUser();
  return user;
}
这样 就能把那个共有的switch-case代码封装成一个工厂函数用于获取。
利用多态来消除分支
但是上面这么写的话,其实是会一个很隐患的问题,比如 C 类忘记写getUser方法了,但是我却调用了temp.getUser(),那么 js 运行到该代码的时候就会报错temp.getUser is not a function,这是 js 特性,并不能通过文本编辑器找到该 bug。但如果是 TypeScript,上面的代码就会提示
类型“A | B | C”上不存在属性“getUser”。
  类型“C”上不存在属性“getUser”。ts(2339)
虽说 ES6 可以通过继承,实现对象的多态性,但 ES6 并没有抽象类的。加上我的项目是基于 TypeScript 的,所以用到的也是 TypeScript 的类(也强烈建议使用 TypeScript),故以下的代码也都是基于 TypeScript 所编写的。
interface User {
  username: string;
}
abstract class S {
  protected user: User;
  constructor(user) {
    this.user = user;
  }
  abstract getUser();
}
class A extends S {
  getUser(): User {
    console.log('A');
    return this.user;
  }
}
class B extends S {
  getUser(): User {
    console.log('B');
    return this.user;
  }
}
class C extends S {
  getUser(): User {
    console.log('C');
    return this.user;
  }
}
function getClass(type): S {
  switch (type) {
    case 1:
      return new A({ username: 'aaa' });
    case 2:
      return new B({ username: 'bbb' });
    case 3:
      return new C({ username: 'ccc' });
    default:
      throw new Error('Unsupported types');
  }
}
function fun3(type) {
  let temp = getClass(type);
  let user = temp.getUser();
  console.log(user);
  return user;
}
fun3(3);
这样,ABC 都是继承与 S 类,并且必须复写 getUser 方法,否则编辑器将会报错,编译出来的 js 代码如下(ES2020 标准,其实也就正常的 JS 继承)
var __extends =
  (this && this.__extends) ||
  (function () {
    var extendStatics = function (d, b) {
      extendStatics =
        Object.setPrototypeOf ||
        ({ __proto__: [] } instanceof Array &&
          function (d, b) {
            d.__proto__ = b;
          }) ||
        function (d, b) {
          for (var p in b) if (Object.prototype.hasOwnProperty.call(b, p)) d[p] = b[p];
        };
      return extendStatics(d, b);
    };
    return function (d, b) {
      if (typeof b !== 'function' && b !== null) throw new TypeError('Class extends value ' + String(b) + ' is not a constructor or null');
      extendStatics(d, b);
      function __() {
        this.constructor = d;
      }
      d.prototype = b === null ? Object.create(b) : ((__.prototype = b.prototype), new __());
    };
  })();
var S = /** @class */ (function () {
  function S(user) {
    this.user = user;
  }
  return S;
})();
var A = /** @class */ (function (_super) {
  __extends(A, _super);
  function A() {
    return (_super !== null && _super.apply(this, arguments)) || this;
  }
  A.prototype.getUser = function () {
    console.log('A');
    return this.user;
  };
  return A;
})(S);
var B = /** @class */ (function (_super) {
  __extends(B, _super);
  function B() {
    return (_super !== null && _super.apply(this, arguments)) || this;
  }
  B.prototype.getUser = function () {
    console.log('B');
    return this.user;
  };
  return B;
})(S);
var C = /** @class */ (function (_super) {
  __extends(C, _super);
  function C() {
    return (_super !== null && _super.apply(this, arguments)) || this;
  }
  C.prototype.getUser = function () {
    console.log('C');
    return this.user;
  };
  return C;
})(S);
function getClass(type) {
  switch (type) {
    case 1:
      return new A({ username: 'aaa' });
    case 2:
      return new B({ username: 'bbb' });
    case 3:
      return new C({ username: 'ccc' });
    default:
      throw new Error('Unsupported types');
  }
}
function fun2(type) {
  var temp = getClass(type);
  var user = temp.getUser();
  console.log(user);
  return user;
}
fun2(3);