1. 前言

学习JavaScript也有一段时间了,在学习过程中遇到了各种各样的问题,但是很多问题也都会的到解决,JavaScript作为一门语言来讲,也就会存在设计模式,所谓的设计模式无非是程序设计的一种编程思想。

2. 简单工厂

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

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

工厂模式,你只需要修改工厂代码。其他地方引用工厂,可以做到只修改一个地方,其他代码都不动,就是解耦了。

如果你有很多地方都需要A的实例,那编写一个工厂专门生成A的实例(如果生成逻辑改变了,直接修改工厂)。那么这些需要A的实例的地方只需要从工厂中getObject()就可以了,完全不用管我的实例是咋来的。

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

使用ES6重写简单工厂模式时,我们不再使用构造函数创建对象,而是使用class的新语法。

看下面的例子:

class BicycleShop {
    sellBicycle( model ){
        var bicycle;
        switch( model ){
            case "The Speedster":
                bicycle = new Speedster();
                break;
            case "The Lowrider":
                bicycle = new Lowrider();
                break;
            case "The Cruiser":
            default:
                bicycle = new Cruiser();
                break;
        }
        return bicycle;
    }
}
class Speedster {
    go(){
        console.log("Speedster")
    }
}
class Lowrider {
    go(){
        console.log("Lowrider")
    }
}
class Cruiser {
    go(){
        console.log("Cruiser")
    }
}
class Bicycle {
    run(type){
        let bicycleShop = new BicycleShop();
        let bicycle = bicycleShop.sellBicycle(type);
        bicycle.go();
    }
}
let bicycle = new Bicycle();
bicycle.run("The Speedster"); //  Speedster

在上面的代码中创建了一个BicycleShop类,里面有一个sellBicycle方法,这个方法接收一个参数,为了返回不同的类,来创建不同得方法。

我们可以思考一下,如果上面的方法不使用工厂方法的话,则是把其他类中的所有的所有的方法写在Bicycle方法里面,若要满足若干的需求,就需要写好多好多方法,使代码变得臃肿、混乱。

使用工厂方法的好处针对不同的方法,都用自己的业务逻辑,不需要担心他们到底是怎么实现的,方便后期的维护与拓展。

3. 工厂方法

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

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

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

class Bicycle {
    constructor(){
        if(new.target === Bicycle){
            throw new Error('抽象类不能实例化!');
        }
    }
    
}
class BicycleShop extends Bicycle{
    sellBicycle( model ){
        var bicycle;
        switch( model ){
            case "The Speedster":
                bicycle = new Speedster();
                break;
            case "The Lowrider":
                bicycle = new Lowrider();
                break;
            case "The Cruiser":
            default:
                bicycle = new Cruiser();
                break;
        }
        return bicycle;
    }
}
class Speedster {
    go(){
        console.log("Speedster")
    }
}
class Lowrider {
    go(){
        console.log("Lowrider")
    }
}
class Cruiser {
    go(){
        console.log("Cruiser")
    }
}
const bicycleShop = new BicycleShop();
let speedster = bicycleShop.sellBicycle("The Speedster");
let lowrider = bicycleShop.sellBicycle("The Lowrider");
let cruiser = bicycleShop.sellBicycle("The Cruiser");
speedster.go();  //  Speedster
lowrider.go();   //  Lowrider
cruiser.go();    //  Cruiser

4. 抽象工厂

JS中是没有直接的抽象类的,abstract是个保留字,但是还没有实现,因此我们需要在类的方法中抛出错误来模拟抽象类,如果继承的子类中没有覆写该方法而调用,就会抛出错误。

多个抽象产品类,每个抽象产品类可以派生出多个具体产品类。一个抽象工厂类,可以派生出多个具体工厂类。每个具体工厂类可以创建多个具体产品类的实例。

class Bicycle {
    constructor(){
        if(new.target === Bicycle){
            throw new Error('抽象类不能实例化!');
        }
    }
    
}
class Speedster extends Bicycle{
    go(){
        console.log("Speedster")
    }
}
class Lowrider extends Bicycle{
    go(){
        console.log("Lowrider")
    }
}
class Cruiser extends Bicycle{
    go(){
        console.log("Cruiser")
    }
}
function getAbstractUserFactory(type) {
  switch (type) {
    case 'The Speedster':
      return Speedster;
      break;
    case 'The Lowrider':
      return Lowrider;
      break;
    case 'The Cruiser':
      return Cruiser;
      break;
    default:
      throw new Error('参数错误, 可选参数:"The Speedster"、"The Lowrider"、"The Cruiser"')
  }
}
let speedsterClass = getAbstractUserFactory('The Speedster');
let lowriderClass = getAbstractUserFactory('The Lowrider');
let CruiserClass = getAbstractUserFactory('The Cruiser');

let speedsterBicycle = new speedsterClass('高速自行车');
let lowriderBicycle = new lowriderClass('低速自行车');
let cruiserBicycle = new CruiserClass('巡洋舰自行车');

speedsterBicycle.go();  //  Speedster
lowriderBicycle.go();   //  Lowrider
cruiserBicycle.go();    //  Cruiser

5. 实战

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

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

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: '/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方法添加的路由刷新后不能保存,所以会导致路由无法访问。通常的做法是本地加密保存用户信息,在刷新后获取本地权限并解密,根据权限重新添加路由。这里因为和工厂模式没有太大的关系就不再赘述。

6. 总结

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