14

1 什么是工厂模式?

工厂模式是用来创建对象的一种最常用的设计模式。我们不暴露创建对象的具体逻辑,而是将将逻辑封装在一个函数中,那么这个函数就可以被视为一个工厂。工厂模式根据抽象程度的不同可以分为:简单工厂,工厂方法和抽象工厂。

如果只接触过JavaScript这门语言的的人可能会对抽象这个词的概念有点模糊,因为JavaScript一直将abstract作为保留字而没有去实现它。如果不能很好的理解抽象的概念,那么就很难理解工厂模式中的三种方法的异同。所以,我们先以一个场景去简单的讲述一下抽象和工厂的概念。

想象一下你的女朋友生日要到了,你想知道她想要什么,于是你问她:“亲爱的,生日要到了你想要什么生日礼物啊?”

正巧你女朋友是个猫奴,最经迷上了抖音上的一只超级可爱的苏格兰折耳猫,她也很想要一只网红同款猫。

于是她回答你说:“亲爱的,我想要一只动物。”

你心平气和的问她:“想要什么动物啊?”

你女友说:“我想要猫科动物。”

这时你内心就纳闷了,猫科动物有老虎,狮子,豹子,猞猁,还有各种小猫,我哪里知道你要什么?

于是你问女友:“你要哪种猫科动物啊?”

“笨死了,还要哪种,肯定是小猫咪啊,难道我们家还能像迪拜土豪那样养老虎啊!”你女朋友答道。

“好好, 那你想要哪个品种的猫呢?”你问道

“我想要外国的品种, 不要中国的土猫” 你女友傲娇的回答到。

这时你已经快奔溃了,作为程序员的你再也受不了这种挤牙膏式的提问,于是你哀求到:“亲爱的,你就直接告诉我你到底想要哪个品种,哪个颜色,多大的猫?”

你女友想了想抖音的那只猫,回答道:“我想要一只灰色的,不超过1岁的苏格兰短耳猫!”

于是,你在女友生日当天到全国最大的宠物批发市场里面去,挑了一只“灰色的,不超过1岁的苏格兰短耳猫”回家送给了你女友, 圆了你女友拥有网红同款猫的梦想!

上面中你最终买到并送给女友那只猫可以被看作是一个实例对象宠物批发市场可以看作是一个工厂,我们可以认为它是一个函数,这个工厂函数里面有着各种各样的动物,那么你是如何获取到实例的呢?因为你给宠物批发市场传递了正确的参数, “color: 灰色”“age: 不超过1岁”"breed:苏格兰短耳",**“category:
猫"。前面的对话中, 你女朋友回答“动物”,“猫科动物”,“国外的品种”让你不明白她到底想要什么,就是因为她说得太抽象了。她回答的是一大类动物的共有特征而不是具体动物,这种将复杂事物的一个或多个共有特征抽取出来的思维过程就是抽象**。

既然已经明白了抽象的概念,下面我们来看一下之前提到的工厂模式的三种实现方法: 简单工厂模式、工厂方法模式、抽象工厂模式。

1.1 简单工厂模式

简单工厂模式又叫静态工厂模式,由一个工厂对象决定创建某一种产品对象类的实例。主要用来创建同一类对象。

在实际的项目中,我们常常需要根据用户的权限来渲染不同的页面,高级权限的用户所拥有的页面有些是无法被低级权限的用户所查看。所以我们可以在不同权限等级用户的构造函数中,保存该用户能够看到的页面。在根据权限实例化用户。代码如下:

let UserFactory = function (role) {
  function SuperAdmin() {
    this.name = "超级管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
  }
  function Admin() {
    this.name = "管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
  }
  function NormalUser() {
    this.name = '普通用户',
    this.viewPage = ['首页', '通讯录', '发现页']
  }

  switch (role) {
    case 'superAdmin':
      return new SuperAdmin();
      break;
    case 'admin':
      return new Admin();
      break;
    case 'user':
      return new NormalUser();
      break;
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user');
  }
}

//调用
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin') 
let normalUser = UserFactory('user')

UserFactory就是一个简单工厂,在该函数中有3个构造函数分别对应不同的权限的用户。当我们调用工厂函数时,只需要传递superAdmin, admin, user这三个可选参数中的一个获取对应的实例对象。你也许发现,我们的这三类用户的构造函数内部很相识,我们还可以对其进行优化。

let UserFactory = function (role) {
  function User(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  switch (role) {
    case 'superAdmin':
      return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
      break;
    case 'admin':
      return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
      break;
    case 'user':
      return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
      break;
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user')
  }
}

//调用
let superAdmin = UserFactory('superAdmin');
let admin = UserFactory('admin') 
let normalUser = UserFactory('user')

简单工厂的优点在于,你只需要一个正确的参数,就可以获取到你所需要的对象,而无需知道其创建的具体细节。但是在函数内包含了所有对象的创建逻辑(构造函数)和判断逻辑的代码,每增加新的构造函数还需要修改判断逻辑代码。当我们的对象不是上面的3个而是30个或更多时,这个函数会成为一个庞大的超级函数,便得难以维护。所以,简单工厂只能作用于创建的对象数量较少,对象的创建逻辑不复杂时使用

1.2 工厂方法模式

工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。但是在JavaScript中很难像传统面向对象那样去实现创建抽象类。所以在JavaScript中我们只需要参考它的核心思想即可。我们可以将工厂方法看作是一个实例化对象的工厂类。

在简单工厂模式中,我们每添加一个构造函数需要修改两处代码。现在我们使用工厂方法模式改造上面的代码,刚才提到,工厂方法我们只把它看作是一个实例化对象的工厂,它只做实例化对象这一件事情! 我们采用安全模式创建对象。

//安全模式创建的工厂方法函数
let UserFactory = function(role) {
  if(this instanceof UserFactory) {
    var s = new this[role]();
    return s;
  } else {
    return new UserFactory(role);
  }
}

//工厂方法函数的原型中设置所有对象的构造函数
UserFactory.prototype = {
  SuperAdmin: function() {
    this.name = "超级管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据', '权限管理']
  },
  Admin: function() {
    this.name = "管理员",
    this.viewPage = ['首页', '通讯录', '发现页', '应用数据']
  },
  NormalUser: function() {
    this.name = '普通用户',
    this.viewPage = ['首页', '通讯录', '发现页']
  }
}

//调用
let superAdmin = UserFactory('SuperAdmin');
let admin = UserFactory('Admin') 
let normalUser = UserFactory('NormalUser')

上面的这段代码就很好的解决了每添加一个构造函数就需要修改两处代码的问题,如果我们需要添加新的角色,只需要在UserFactory.prototype中添加。例如,我们需要添加一个VipUser:

UserFactory.prototype = {
  //....
  VipUser: function() {
    this.name = '付费用户',    
    this.viewPage = ['首页', '通讯录', '发现页', 'VIP页']
  }
}

//调用
let vipUser = UserFactory('VipUser');

上面的这段代码中,使用到的安全模式可能很难一次就能理解。

let UserFactory = function(role) {
  if(this instanceof UserFactory) {
    var s = new this[role]();
    return s;
  } else {
    return new UserFactory(role);
  }
}

因为我们将SuperAdminAdminNormalUser等构造函数保存到了UserFactory.prototype中,也就意味着我们必须实例化UserFactory函数才能够进行以上对象的实例化。如下面代码所示

let UserFactory = function() {}

UserFactory.prototype = {
 //...
}

//调用
let factory = new UserFactory();
let superAdmin = new factory.SuperAdmin();

在上面的调用函数的过程中, 一旦我们在任何阶段忘记使用new, 那么就无法正确获取到superAdmin这个对象。但是一旦使用安全模式去进行实例化,就能很好解决上面的问题。

1.3 抽象工厂模式

上面介绍了简单工厂模式和工厂方法模式都是直接生成实例,但是抽象工厂模式不同,抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。

上面例子中的superAdminadminuser三种用户角色,其中user可能是使用不同的社交媒体账户进行注册的,例如:wechatqqweibo。那么这三类社交媒体账户就是对应的类簇。在抽象工厂中,类簇一般用父类定义,并在父类中定义一些抽象方法,再通过抽象工厂让子类继承父类。所以,抽象工厂其实是实现子类继承父类的方法

上面提到的抽象方法是指声明但不能使用的方法。在其他传统面向对象的语言中常用abstract进行声明,但是在JavaScript中,abstract是属于保留字,但是我们可以通过在类的方法中抛出错误来模拟抽象类。

let WechatUser = function() {}
WechatUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

上述代码中的getPrice就是抽象方法,我们定义它但是却没有去实现。如果子类继承WechatUser但是并没有去重写getName,那么子类的实例化对象就会调用父类的getName方法并抛出错误提示。

下面我们分别来实现账号管理的抽象工厂方法:

let AccountAbstractFactory = function(subType, superType) {
  //判断抽象工厂中是否有该抽象类
  if(typeof AccountAbstractFactory[superType] === 'function') {
    //缓存类
    function F() {};
    //继承父类属性和方法
    F.prototype = new AccountAbstractFactory[superType] ();
    //将子类的constructor指向子类
    subType.constructor = subType;
    //子类原型继承父类
    subType.prototype = new F();

  } else {
    throw new Error('抽象类不存在!')
  }
}

//微信用户抽象类
AccountAbstractFactory.WechatUser = function() {
  this.type = 'wechat';
}
AccountAbstractFactory.WechatUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//qq用户抽象类
AccountAbstractFactory.QqUser = function() {
  this.type = 'qq';
}
AccountAbstractFactory.QqUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

//新浪微博用户抽象类
AccountAbstractFactory.WeiboUser = function() {
  this.type = 'weibo';
}
AccountAbstractFactory.WeiboUser.prototype = {
  getName: function() {
    return new Error('抽象方法不能调用');
  }
}

AccountAbstractFactory 就是一个抽象工厂方法,该方法在参数中传递子类和父类,在方法体内部实现了子类对父类的继承。对抽象工厂方法添加抽象类的方法我们是通过点语法进行添加的。

下面我们来定义普通用户的子类:

//普通微信用户子类
function UserOfWechat(name) {
  this.name = name;
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现WechatUser类的继承
AccountAbstractFactory(UserOfWechat, 'WechatUser');
//子类中重写抽象方法
UserOfWechat.prototype.getName = function() {
  return this.name;
}

//普通qq用户子类
function UserOfQq(name) {
  this.name = name;
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现QqUser类的继承
AccountAbstractFactory(UserOfQq, 'QqUser');
//子类中重写抽象方法
UserOfQq.prototype.getName = function() {
  return this.name;
}

//普通微博用户子类
function UserOfWeibo(name) {
  this.name = name;
  this.viewPage = ['首页', '通讯录', '发现页']
}
//抽象工厂实现WeiboUser类的继承
AccountAbstractFactory(UserOfWeibo, 'WeiboUser');
//子类中重写抽象方法
UserOfWeibo.prototype.getName = function() {
  return this.name;
}

上述代码我们分别定义了UserOfWechatUserOfQqUserOfWeibo三种类。这三个类作为子类通过抽象工厂方法实现继承。特别需要注意的是,调用抽象工厂方法后不要忘记重写抽象方法,否则在子类的实例中调用抽象方法会报错。

我们来分别对这三种类进行实例化,检测抽象工厂方法是实现了类簇的管理。

//实例化微信用户
let wechatUserA = new UserOfWechat('微信小李');
console.log(wechatUserA.getName(), wechatUserA.type); //微信小李 wechat
let wechatUserB = new UserOfWechat('微信小王');
console.log(wechatUserB.getName(), wechatUserB.type); //微信小王 wechat

//实例化qq用户
let qqUserA = new UserOfQq('QQ小李');
console.log(qqUserA.getName(), qqUserA.type); //QQ小李 qq
let qqUserB = new UserOfQq('QQ小王');
console.log(qqUserB.getName(), qqUserB.type); //QQ小王 qq

//实例化微博用户
let weiboUserA =new UserOfWeibo('微博小李');
console.log(weiboUserA.getName(), weiboUserA.type); //微博小李 weibo
let weiboUserB =new UserOfWeibo('微博小王');
console.log(weiboUserB.getName(), weiboUserB.type); //微博小王 weibo

从打印结果上看,AccountAbstractFactory这个抽象工厂很好的实现了它的作用,将不同用户账户按照社交媒体这一个类簇进行了分类。这就是抽象工厂的作用,它不直接创建实例,而是通过类的继承进行类簇的管理。抽象工厂模式一般用在多人协作的超大型项目中,并且严格的要求项目以面向对象的思想进行完成。

2 ES6中的工厂模式

ES6中给我们提供了class新语法,虽然class本质上是一颗语法糖,并也没有改变JavaScript是使用原型继承的语言,但是确实让对象的创建和继承的过程变得更加的清晰和易读。下面我们使用ES6的新语法来重写上面的例子。

2.1 ES6重写简单工厂模式

使用ES6重写简单工厂模式时,我们不再使用构造函数创建对象,而是使用class的新语法,并使用static关键字将简单工厂封装到User类的静态方法中:

//User类
class User {
  //构造器
  constructor(opt) {
    this.name = opt.name;
    this.viewPage = opt.viewPage;
  }

  //静态方法
  static getInstance(role) {
    switch (role) {
      case 'superAdmin':
        return new User({ name: '超级管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据', '权限管理'] });
        break;
      case 'admin':
        return new User({ name: '管理员', viewPage: ['首页', '通讯录', '发现页', '应用数据'] });
        break;
      case 'user':
        return new User({ name: '普通用户', viewPage: ['首页', '通讯录', '发现页'] });
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }
}

//调用
let superAdmin = User.getInstance('superAdmin');
let admin = User.getInstance('admin');
let normalUser = User.getInstance('user');

2.2 ES6重写工厂方法模式

在上文中我们提到,工厂方法模式的本意是将实际创建对象的工作推迟到子类中,这样核心类就变成了抽象类。但是JavaScript的abstract是一个保留字,并没有提供抽象类,所以之前我们只是借鉴了工厂方法模式的核心思想。

虽然ES6也没有实现abstract,但是我们可以使用new.target来模拟出抽象类。new.target指向直接被new执行的构造函数,我们对new.target进行判断,如果指向了该类则抛出错误来使得该类成为抽象类。下面我们来改造代码。

class User {
  constructor(name = '', viewPage = []) {
    if(new.target === User) {
      throw new Error('抽象类不能实例化!');
    }
    this.name = name;
    this.viewPage = viewPage;
  }
}

class UserFactory extends User {
  constructor(name, viewPage) {
    super(name, viewPage)
  }
  create(role) {
    switch (role) {
      case 'superAdmin': 
        return new UserFactory( '超级管理员', ['首页', '通讯录', '发现页', '应用数据', '权限管理'] );
        break;
      case 'admin':
        return new UserFactory( '普通用户', ['首页', '通讯录', '发现页'] );
        break;
      case 'user':
        return new UserFactory( '普通用户', ['首页', '通讯录', '发现页'] );
        break;
      default:
        throw new Error('参数错误, 可选参数:superAdmin、admin、user')
    }
  }
}

let userFactory = new UserFactory();
let superAdmin = userFactory.create('superAdmin');
let admin = userFactory.create('admin');
let user = userFactory.create('user');

2.3 ES6重写抽象工厂模式

抽象工厂模式并不直接生成实例, 而是用于对产品类簇的创建。我们同样使用new.target语法来模拟抽象类,并通过继承的方式创建出UserOfWechat, UserOfQq, UserOfWeibo这一系列子类类簇。使用getAbstractUserFactor来返回指定的类簇。

class User {
  constructor(type) {
    if (new.target === User) {
      throw new Error('抽象类不能实例化!')
    }
    this.type = type;
  }
}

class UserOfWechat extends User {
  constructor(name) {
    super('wechat');
    this.name = name;
    this.viewPage = ['首页', '通讯录', '发现页']
  }
}

class UserOfQq extends User {
  constructor(name) {
    super('qq');
    this.name = name;
    this.viewPage = ['首页', '通讯录', '发现页']
  }
}

class UserOfWeibo extends User {
  constructor(name) {
    super('weibo');
    this.name = name;
    this.viewPage = ['首页', '通讯录', '发现页']
  }
}

function getAbstractUserFactory(type) {
  switch (type) {
    case 'wechat':
      return UserOfWechat;
      break;
    case 'qq':
      return UserOfQq;
      break;
    case 'weibo':
      return UserOfWeibo;
      break;
    default:
      throw new Error('参数错误, 可选参数:superAdmin、admin、user')
  }
}

let WechatUserClass = getAbstractUserFactory('wechat');
let QqUserClass = getAbstractUserFactory('qq');
let WeiboUserClass = getAbstractUserFactory('weibo');

let wechatUser = new WechatUserClass('微信小李');
let qqUser = new QqUserClass('QQ小李');
let weiboUser = new WeiboUserClass('微博小李');

3 工厂模式的项目实战应用

在实际的前端业务中,最常用的简单工厂模式。如果不是超大型的项目,是很难有机会使用到工厂方法模式和抽象工厂方法模式的。下面我介绍在Vue项目中实际使用到的简单工厂模式的应用。

在普通的vue + vue-router的项目中,我们通常将所有的路由写入到router/index.js这个文件中。下面的代码我相信vue的开发者会非常熟悉,总共有5个页面的路由:

// index.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login.vue'
import SuperAdmin from '../components/SuperAdmin.vue'
import NormalAdmin from '../components/Admin.vue'
import User from '../components/User.vue'
import NotFound404 from '../components/404.vue'

Vue.use(Router)

export default new Router({
  routes: [
    //重定向到登录页
    {
      path: '/',
      redirect: '/login'
    },
    //登陆页
    {
      path: '/login',
      name: 'Login',
      component: Login
    },
    //超级管理员页面
    {
      path: '/super-admin',
      name: 'SuperAdmin',
      component: SuperAdmin
    },
    //普通管理员页面
    {
      path: '/normal-admin',
      name: 'NormalAdmin',
      component: NormalAdmin
    },
    //普通用户页面
    {
      path: '/user',
      name: 'User',
      component: User
    },
    //404页面
    {
      path: '*',
      name: 'NotFound404',
      component: NotFound404
    }
  ]
})

当涉及权限管理页面的时候,通常需要在用户登陆根据权限开放固定的访问页面并进行相应权限的页面跳转。但是如果我们还是按照老办法将所有的路由写入到router/index.js这个文件中,那么低权限的用户如果知道高权限路由时,可以通过在浏览器上输入url跳转到高权限的页面。所以我们必须在登陆的时候根据权限使用vue-router提供的addRoutes方法给予用户相对应的路由权限。这个时候就可以使用简单工厂方法来改造上面的代码。

router/index.js文件中,我们只提供/login这一个路由页面。

//index.js

import Vue from 'vue'
import Router from 'vue-router'
import Login from '../components/Login.vue'

Vue.use(Router)

export default new Router({
  routes: [
    //重定向到登录页
    {
      path: '/',
      redirect: '/login'
    },
    //登陆页
    {
      path: '/login',
      name: 'Login',
      component: Login
    }
  ]
})

我们在router/文件夹下新建一个routerFactory.js文件,导出routerFactory简单工厂函数,用于根据用户权限提供路由权限,代码如下

//routerFactory.js

import SuperAdmin from '../components/SuperAdmin.vue'
import NormalAdmin from '../components/Admin.vue'
import User from '../components/User.vue'
import NotFound404 from '../components/404.vue'

let AllRoute = [
  //超级管理员页面
  {
    path: '/super-admin',
    name: 'SuperAdmin',
    component: SuperAdmin
  },
  //普通管理员页面
  {
    path: '/normal-admin',
    name: 'NormalAdmin',
    component: NormalAdmin
  },
  //普通用户页面
  {
    path: '/user',
    name: 'User',
    component: User
  },
  //404页面
  {
    path: '*',
    name: 'NotFound404',
    component: NotFound404
  }
]

let routerFactory = (role) => {
  switch (role) {
    case 'superAdmin':
      return {
        name: 'SuperAdmin',
        route: AllRoute
      };
      break;
    case 'normalAdmin':
      return {
        name: 'NormalAdmin',
        route: AllRoute.splice(1)
      }
      break;
    case 'user':
      return {
        name: 'User',
        route:  AllRoute.splice(2)
      }
      break;
    default: 
      throw new Error('参数错误! 可选参数: superAdmin, normalAdmin, user')
  }
}

export { routerFactory }

在登陆页导入该方法,请求登陆接口后根据权限添加路由:

//Login.vue

import {routerFactory} from '../router/routerFactory.js'
export default {
  //... 
  methods: {
    userLogin() {
      //请求登陆接口, 获取用户权限, 根据权限调用this.getRoute方法
      //..
    },
    
    getRoute(role) {
      //根据权限调用routerFactory方法
      let routerObj = routerFactory(role);
      
      //给vue-router添加该权限所拥有的路由页面
      this.$router.addRoutes(routerObj.route);
      
      //跳转到相应页面
      this.$router.push({name: routerObj.name})
    }
  }
};

在实际项目中,因为使用this.$router.addRoutes方法添加的路由刷新后不能保存,所以会导致路由无法访问。通常的做法是本地加密保存用户信息,在刷新后获取本地权限并解密,根据权限重新添加路由。这里因为和工厂模式没有太大的关系就不再赘述。

总结

上面说到的三种工厂模式和上文的单例模式一样,都是属于创建型的设计模式。简单工厂模式又叫静态工厂方法,用来创建某一种产品对象的实例,用来创建单一对象;工厂方法模式是将创建实例推迟到子类中进行;抽象工厂模式是对类的工厂抽象用来创建产品类簇,不负责创建某一类产品的实例。在实际的业务中,需要根据实际的业务复杂度来选择合适的模式。对于非大型的前端应用来说,灵活使用简单工厂其实就能解决大部分问题。


yihui_indie
2.5k 声望938 粉丝

前端拼装工程师