JavaScript設計模式與實踐--工廠模式

1 什麼是工廠模式?

工廠模式是用來創建對象的一種最常用的設計模式。我們不暴露創建對象的具體邏輯,而是將將邏輯封裝在一個函數中,那麼這個函數就可以被視爲一個工廠。工廠模式根據抽象程度的不同可以分爲:簡單工廠工廠方法抽象工廠

如果只接觸過JavaScript這門語言的的人可能會對抽象這個詞的概念有點模糊,因爲JavaScript一直將abstract作爲保留字而沒有去實現它。如果不能很好的理解抽象的概念,那麼就很難理解工廠模式中的三種方法的異同。所以,我們先以一個場景去簡單的講述一下抽象和工廠的概念。

想象一下你的女朋友生日要到了,你想知道她想要什麼,於是你問她:“親愛的,生日要到了你想要什麼生日禮物啊?” 正巧你女朋友是個貓奴,最經迷上了抖音上的一隻超級可愛的蘇格蘭摺耳貓,她也很想要一隻網紅同款貓。

於是她回答你說:“親愛的,我想要一隻動物。”

你心平氣和的問她:“想要什麼動物啊?”

你女友說:“我想要貓科動物。”

這時你內心就納悶了,貓科動物有老虎,獅子,豹子,猞猁,還有各種小貓,我哪裏知道你要什麼?

於是你問女友:“你要哪種貓科動物啊?”

“笨死了,還要哪種,肯定是小貓咪啊,難道我們家還能像迪拜土豪那樣養老虎啊!”你女朋友答道。

“好好, 那你想要哪個品種的貓呢?”你問道

“我想要外國的品種, 不要中國的土貓” 你女友傲嬌的回答到。

這時你已經快奔潰了,作爲程序員的你再也受不了這種擠牙膏式的提問,於是你哀求到:“親愛的,你就直接告訴我你到底想要哪個品種,哪個顏色,多大的貓?”

你女友想了想抖音的那隻貓,回答道:“我想要一隻灰色的,不超過1歲的蘇格蘭短耳貓!”

於是,你在女友生日當天到全國最大的寵物批發市場裏面去,挑了一隻“灰色的,不超過1歲的蘇格蘭短耳貓”回家送給了你女友, 圓了你女友擁有網紅同款貓的夢想!

上面中你最終買到並送給女友那隻貓可以被看作是一個實例對象,寵物批發市場可以看作是一個工廠,我們可以認爲它是一個函數,這個工廠函數裏面有着各種各樣的動物,那麼你是如何獲取到實例的呢?因爲你給寵物批發市場傳遞了正確的參數: “color: 灰色”,“age: 不超過1歲”,"breed:蘇格蘭短耳",“category: 貓"。前面的對話中, 你女朋友回答“動物”,“貓科動物”,“國外的品種”讓你不明白她到底想要什麼,就是因爲她說得太抽象了。

她回答的是一大類動物的共有特徵而不是具體動物,這種將複雜事物的一個或多個共有特徵抽取出來的思維過程就是抽象

既然已經明白了抽象的概念,下面我們來看一下之前提到的工廠模式的三種實現方法: 簡單工廠模式工廠方法模式抽象工廠模式

1.1 簡單工廠模式

簡單工廠模式又叫靜態工廠模式,由一個工廠對象決定創建某一種產品對象類的實例。主要用來創建同一類對象。

在實際的項目中,我們常常需要根據用戶的權限來渲染不同的頁面,高級權限的用戶所擁有的頁面有些是無法被低級權限的用戶所查看。所以我們可以在不同權限等級用戶的構造函數中,保存該用戶能夠看到的頁面。在根據權限實例化用戶。使用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');
複製代碼

User就是一個簡單工廠,在該函數中有3個實例中分別對應不同的權限的用戶。當我們調用工廠函數時,只需要傳遞superAdmin, admin, user這三個可選參數中的一個獲取對應的實例對象。

簡單工廠的優點在於,你只需要一個正確的參數,就可以獲取到你所需要的對象,而無需知道其創建的具體細節。但是在函數內包含了所有對象的創建邏輯(構造函數)和判斷邏輯的代碼,每增加新的構造函數還需要修改判斷邏輯代碼。當我們的對象不是上面的3個而是30個或更多時,這個函數會成爲一個龐大的超級函數,便得難以維護。所以,簡單工廠只能作用於創建的對象數量較少,對象的創建邏輯不復雜時使用

1.2 工廠方法模式

工廠方法模式的本意是將實際創建對象的工作推遲到子類中,這樣核心類就變成了抽象類。但是在JavaScript中很難像傳統面向對象那樣去實現創建抽象類。所以在JavaScript中我們只需要參考它的核心思想即可。我們可以將工廠方法看作是一個實例化對象的工廠類。雖然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');
複製代碼

1.3 抽象工廠模式

上面介紹了簡單工廠模式和工廠方法模式都是直接生成實例,但是抽象工廠模式不同,抽象工廠模式並不直接生成實例, 而是用於對產品類簇的創建。

上面例子中的superAdminadminuser三種用戶角色,其中user可能是使用不同的社交媒體賬戶進行註冊的,例如:wechat,qq,weibo。那麼這三類社交媒體賬戶就是對應的類簇。在抽象工廠中,類簇一般用父類定義,並在父類中定義一些抽象方法,再通過抽象工廠讓子類繼承父類。所以,抽象工廠其實是實現子類繼承父類的方法

上面提到的抽象方法是指聲明但不能使用的方法。在其他傳統面向對象的語言中常用abstract進行聲明,但是在JavaScript中,abstract是屬於保留字,但是我們可以通過在類的方法中拋出錯誤來模擬抽象類。

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('微博小李');

複製代碼

2 工廠模式的項目實戰應用

在實際的前端業務中,最常用的簡單工廠模式。如果不是超大型的項目,是很難有機會使用到工廠方法模式和抽象工廠方法模式的。下面我介紹在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方法添加的路由刷新後不能保存,所以會導致路由無法訪問。通常的做法是本地加密保存用戶信息,在刷新後獲取本地權限並解密,根據權限重新添加路由。這裏因爲和工廠模式沒有太大的關係就不再贅述。

3 總結

上面說到的三種工廠模式和單例模式一樣,都是屬於創建型的設計模式。簡單工廠模式又叫靜態工廠方法,用來創建某一種產品對象的實例,用來創建單一對象;工廠方法模式是將創建實例推遲到子類中進行;抽象工廠模式是對類的工廠抽象用來創建產品類簇,不負責創建某一類產品的實例。在實際的業務中,需要根據實際的業務複雜度來選擇合適的模式。對於非大型的前端應用來說,靈活使用簡單工廠其實就能解決大部分問題。

3.1. 什麼時候會用工廠模式?

將new操作簡單封裝,遇到new的時候就應該考慮是否用工廠模式;

3.2. 工廠模式的好處?

舉個例子:

  • 你去購買漢堡,直接點餐、取餐、不會自己親自做;(買者不關注漢堡是怎麼做的)

  • 商店要封裝做漢堡的工作,做好直接給買者;(商家也不會告訴你是怎麼做的,也不會傻到給你一片面包,一些奶油,一些生菜讓你自己做)

外部不許關心內部構造器是怎麼生成的,只需調用一個工廠方法生成一個實例即可;

構造函數和創建者分離,符合開放封閉原則

3.3 實際的一些例子

  • jQuery的$(selector) jQuery中$('div')new $('div')哪個好用,很顯然直接$()最方便 ,這是因爲$()已經是一個工廠方法了;
class jQuery {
    constructor(selector) {
        super(selector)
    }
    //  ....
}

window.$ = function(selector) {
    return new jQuery(selector)
}

複製代碼
  • React的createElement()

React.createElement()方法就是一個工廠方法

 

 

 

 

 

 

 

 

 

  • Vue的異步組件

通過promise的方式resolve出來一個組件


作者:小平果118
鏈接:https://juejin.im/post/5b69c699e51d45348a301ef4
來源:掘金
著作權歸作者所有。商業轉載請聯繫作者獲得授權,非商業轉載請註明出處。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章