常见的JavaScript二十三种设计模式 —— 浅谈JavaScript工厂模式

144 阅读6分钟

1. 故事背景

// 文件A
class Basketball {
  constructor() {
    this.introduction = "篮球起源于美国";
  }
  getMember = function () {
    console.log("篮球一队需要五个人");
  };
}
// 文件B
let basketball = new Basketball();
console.log(basketball.introduction);

// 文件C
let basketball = new Basketball();
basketball.getMember();

在文件A中定义一个Basketball类,然后在文件B中实例化这个类的对象,并输出它的introduction,在文件C中也实例化这个类并执行getMember方法。

某一期需求中,需要对这个Basketball进行改造,根据此时的语义,Basketball不再适合这个需求了,于是换了一个类名:室内篮球(InDoorBasketball),那么此时就需要同时更改文件B和文件C中实例化的部分,一旦类名发生修改,我的实例化的代码也得随之修改。如下:

// 文件A
class InDoorBasketball {
  // ...
}
// 文件B
let basketball = new InDoorBasketball();
console.log(basketball.introduction);
// 文件C
let basketball = new InDoorBasketball();
basketball.getMember();

又有一起需求中,需要对这个类进行废置处理,用一个新的类进行管理,功能不变,但是代码执行效率有所提高,于是又更换了类名:InDoorBasketBallV2,那么又发生了和上面一样的更改操作。

一旦类名发生修改,与之依赖的对象都需要随之更改。有了这种大背景下,使用工厂模式可以有效的解耦,将多个类封装在一个工厂类中,通过这个工厂类就可以创建我们需要的对象,这样开发者就不需要再去关注这些对象依赖哪个基类。这个工厂类我们一般称为简单工厂模式。

2. 简单工厂模式

简单工厂模式又称静态工厂模式,由工厂对象来创建对象类的实例。实例化的对象与其基类进行解耦,依照一个协议进行实例化对象。这就好比我们去了体育商品店,与售货员说需要一个篮球,那么你说的话就是协议。

// 文件A
class Basketball {
  constructor() {
    this.introduction = "篮球起源于美国";
  }
  getMember = function () {
    console.log("篮球一队需要五个人");
  };
}
class Football {
  constructor() {
    this.introduction = "足球全世界都很流行";
  }
  getMember = function () {
    console.log("足球一队需要十一个人");
  };
}
class SimpleFactory {
  get = function (name) {
    switch (name) {
      case "Basketball":
        return new Basketball();
      case "Football":
        return new Football();
    }
  };
}
// 文件B
let simpleFactoryObj = new SimpleFactory();
let footballer = simpleFactoryObj.get("Football");
console.log(footballer.introduction);
let basketballer = simpleFactoryObj.get("Basketball");
console.log(basketballer.introduction);
// 文件C
let simpleFactoryObj = new SimpleFactory();
let footballer = simpleFactoryObj.get("Football");
footballer.getMember();
let basketballer = simpleFactoryObj.get("Basketball");
basketballer.getMember();

使用这种方法进行改造,降低了实例对象与依赖基类之间的耦合。简单工厂模式的理念是创建对象,上面这种方式是对不同的类进行实例化操作。虽然减低了一定的耦合,但是依旧存在弊端,每添加一个类,就需要修改两个地方(添加类、修改工厂函数),那么这个时候就得对简单工厂模式进行改造,改造后的名字叫做工厂方法模式。

3. 抽象工厂模式

通过抽象使工厂用于创建多种类型的实例。简单来说就是将实例创建对象的工作推迟到子类当中,这样核心类就成为了抽象类。

抽象工厂模式避免了简单工厂模式的弊端,做到了每添加一个类,只需要关注这个添加的类,而不需关注工厂类。

在这之前,我们需要谈一个比较冷门的模式(安全模式类)

我们在创建一个对象时有时候会忘记写new而直接调用,如下:

let d = new Demo(); // 正确创建实例
d.show(); // 成功获取
let d = Demo(); // 错误创建实例
d.show(); // 获取失败

解决方案是在构造函数开始的时候先判断当前对象的this指向的是不是本类(Demo),如果是则通过new关键字创建对象,如果不是则说明类是在全局作用域中执行(一般情况下),在全局作用域下,this指向window,这时我们就要重新返回新创建的对象了。

let Demo = function () {
  if (!(this instanceof Foo)) {
    return new Foo();
  }
};
Demo.prototype = {
  show: function () {
    console.log("show");
  },
};
let foo = new Demo();
foo.show();
let foo2 = Demo();
foo2.show();

JavaScript本身是不支持继承的,常用的继承是使用原型链模拟继承功能,虽然实现的机制很巧妙,不仅去掉了类的概念,同时学习起来也非常直观,非常适合解释类语言。但是并不符合OOP的抽象观念,所以基于原型链的继承常不被认为是OOP。

在最基本的定义中,基于原型的继承实际上只需要两个语言要素,一个是对象,另一个就是clone。基于原型的继承,光看字面上的意思就是要先有一个对象作为要扩展的“原型”,我们继承了这个原型的所有功能后再写那些新对象特有的功能。也就是说,一个正常的原型继承的编程方式大概要类似于这样:

var obj1 = { ... }; // obj1是最初的原型

var obj2 = clone(obj1); // obj2克隆了obj1对象,也就是以obj1作为原型,有obj1的所有功能
obj2[...] = ...;
obj2[...] = ...;

var obj3 = clone(obj2); // obj3克隆了obj2对象,也就是以obj2作为原型,有obj2的所有功能
obj3[...] = ...;
obj3[...] = ...;

上面的这种写法就是最简单直接的基于原型的继承了,比起C++/Java,使用起来一样简单,而且学习成本更低,需要知道的前置概念更少。但是JavaScript并没有天然支持clone功能。JavaScript的思路是先创建一个子类并把新功能都写好,然后再对这个子类指明它的原型是谁。如下:

// 这是父类
function base() {
this.name = 'base';
}

// 这是子类
function sub1() {
this.name = 'sub1';
} 

sub1.prototype = new base();  // 让子类的原型指向父类

面向对象的语言里有抽象工厂模式,首先声明一个抽象类作为父类,以概括某一类产品所需要的特征,继承该父类的子类需要实现父类中声明的方法而实现父类中所声明的功能

/**
* 实现subType类对工厂类中的superType类型的抽象类的继承
* @param subType 要继承的类
* @param superType 工厂类中的抽象类type
*/
const VehicleFactory = function(subType, superType) {
 if (typeof VehicleFactory[superType] === 'function') {
  function F() {
   this.type = '车辆'
  } 
  F.prototype = new VehicleFactory[superType]()
  subType.constructor = subType
  subType.prototype = new F()        // 因为子类subType不仅需要继承superType对应的类的原型方法,还要继承其对象属性
 } else throw new Error('不存在该抽象类')
}
// 模拟抽象类
VehicleFactory.Car = function() {
 this.type = 'car'
}
VehicleFactory.Car.prototype = {
 getPrice: function() {
  return new Error('抽象方法不可使用')
 },
 getSpeed: function() {
  return new Error('抽象方法不可使用')
 }
}

const BMW = function(price, speed) {
 this.price = price
 this.speed = speed
}

VehicleFactory(BMW, 'Car')    // 继承Car抽象类
BMW.prototype.getPrice = function() {    // 覆写getPrice方法
 console.log(`BWM price is ${this.price}`)
}

BMW.prototype.getSpeed = function() {
 console.log(`BWM speed is ${this.speed}`)
}

const baomai5 = new BMW(30, 99)
baomai5.getPrice()             // BWM price is 30
baomai5 instanceof VehicleFactory.Car    // true