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

小平果118發表於2019-03-02

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()方法就是一個工廠方法

JavaScript設計模式與實踐–工廠模式
JavaScript設計模式與實踐–工廠模式
JavaScript設計模式與實踐–工廠模式
  • Vue的非同步元件

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

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

參考:https://segmentfault.com/a/1190000014196851

相關文章