淺談 JavaScript 中策略模式的使用:
- 什麼是設計模式
- 什麼是策略模式
- 策略模式在 JavaScript 中的應用(使用策略模式封裝百度AI識別呼叫)
- 策略模式在 Vue 元件封裝中的應用(使用策略模式封裝Select元件)
什麼是設計模式
設想有一個電子愛好者,雖然他沒有經過正規的培訓,但是卻日積月累地設計並製造出了許多有用的電子裝置:業餘無線電、蓋革計數器、報警器等。有一天這個愛好者決定重新回到學校去攻讀電子學學位,來讓自己的才能得到正式的認可。隨著課程的展開,這個愛好者突然發現課程內容都似曾相識。似曾相識的不是術語或表述的方式,而是背後的概念。這個愛好者不斷學到一些名稱和原理,雖然這些名稱和原理原來他並不知道,但事實上他多年以來一直都在使用。整個過程只不過是一個接一個的頓悟。
我們在寫程式碼的時候,一定也遇到過許多類似的場景。隨著經驗的增加,我們對於這些常見場景的處理越來越得心應手,甚至總結出了針對性的“套路”,下次遇到此類問題直接運用“套路”解決,省心又省力。這些在軟體開發過程中逐漸積累下來的“套路”就是設計模式。
設計模式的目標之一就是提高程式碼的可複用性、可擴充套件性和可維護性。正因如此,雖然有時候我們不知道某個設計模式,但是看了相關書籍或文章後會有一種“啊,原來這就是設計模式”的恍然大明白。
如果你看完這篇文章後也有此感覺,那麼恭喜你,你已經在高效程式設計師的道路上一路狂奔了。
什麼是策略模式
策略模式是一種簡單卻常用的設計模式,它的應用場景非常廣泛。我們先了解下策略模式的概念,再通過程式碼示例來更清晰的認識它。
策略模式由兩部分構成:一部分是封裝不同策略的策略組,另一部分是 Context。通過組合和委託來讓 Context 擁有執行策略的能力,從而實現可複用、可擴充套件和可維護,並且避免大量複製貼上的工作。
策略模式的典型應用場景是表單校驗中,對於校驗規則的封裝。接下來我們就通過一個簡單的例子具體瞭解一下:
粗糙的表單校驗
一個常見的登入表單程式碼如下:
<form id='login-form' action="" method="post">
<label for="account">手機號</label>
<input type="number" id="account" name="account">
<label for="password">密碼</label>
<input type="password" id="password" name="password">
<button id='login'>登入</button>
</form>
<script>
var loginForm = document.getElementById('login-form');
loginForm.onsubmit = function (e) {
e.preventDefault();
var account = document.getElementById("account").value;
var pwd = document.getElementById("password").value;
if(account===null||account===''){
alert('手機號不能為空');
return false;
}
if(pwd===null||pwd===''){
alert('密碼不能為空');
return false;
}
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(account)) {
alert('手機號格式錯誤');
return false;
}
if(pwd.length<6){
alert('密碼不能小於六位');
return false;
}
// ajax 傳送請求
}
</script>
以上程式碼,雖然功能沒問題,但是缺點也很明顯:
程式碼裡遍地都是 if 語句,並且它們缺乏彈性:每新增一種、或者修改原有校驗規則,我們都必須去改loginForm.onsubmit內部的程式碼。另外邏輯的複用性也很差:如果有其它表單也是用同樣的規則,這段程式碼並不能複用,只能複製。當校驗規則發生變化時,比如上文的正則校驗並不能匹配虛擬運營商14/17號段,我們就需要手動同步多處程式碼變更(Ctrl+C/Ctrl+V)。
優秀的表單驗證
接下來我們通過策略模式的思路改寫一下上段程式碼,首先抽離並封裝校驗邏輯為策略組:
var strategies = {
isNonEmpty: function (value, errorMsg) {
if (value === '' || value === null) {
return errorMsg;
}
},
isMobile: function (value, errorMsg) { // 手機號碼格式
if (!/(^1[3|4|5|7|8][0-9]{9}$)/.test(value)) {
return errorMsg;
}
},
minLength: function (value, length, errorMsg) {
if (value.length < length) {
return errorMsg;
}
}
};
接下來修改 Context:
var loginForm = document.getElementById('login-form');
loginForm.onsubmit = function (e) {
e.preventDefault();
var accountIsMobile = strategies.isMobile(account,'手機號格式錯誤');
var pwdMinLength = strategies.minLength(pwd,8,'密碼不能小於8位');
var errorMsg = accountIsMobile||pwdMinLength;
if(errorMsg){
alert(errorMsg);
return false;
}
}
對比兩種實現,我們可以看到:分離了校驗邏輯的程式碼如果需要擴充套件校驗型別,在策略組中新增定義即可使用;如果需要修改某個校驗的實現,直接修改相應策略即可全域性生效。對於開發和維護都有明顯的效率提升。
擴充套件:史詩的表單校驗
有興趣的朋友可以瞭解下 async-validator ,element-ui 和 antd 的表單校驗都是基於 async-validator 封裝的,可以說是史詩級別的表單校驗了
通過表單校驗的對比,相信大家都對策略模式有所瞭解,那麼接下來通過兩個例子具體瞭解下 JavaScript 中策略模式的應用:
使用策略模式呼叫百度AI影像識別
因為百度AI影像識別的介面型別不同,所需的引數格式也不盡相同。然而影像的壓縮及上傳、錯誤處理等部分是公用的。所以可以採用策略模式封裝:
定義策略組
通過定義策略組來封裝不同的介面及其引數:比如身份證識別介面的side
欄位,自定義識別的templateSign
欄位,以及行駛證識別的接收引數為poparamstData
。
/**
* 策略組
* IDCARD:身份證識別
* CUSTOMIZED:自定義識別
* VL:行駛證識別
*/
var strategies = {
IDCARD: function (base64) {
return {
path: 'idcard',
param: {
'side': 'front',
'base64': base64
}
};
},
CUSTOMIZED: function (base64) {
return {
path: 'customized',
param: {
'templateSign': '52cc2d402155xxxx',
'base64': base64
}
};
},
VL: function (base64) {
return {
path: 'vehicled',
poparamstData: {
'base64': base64
}
};
},
};
定義 Context
var ImageReader = function () { };
/**
* 讀取影像,呼叫介面,獲取識別結果
*
* @param {*} type 待識別檔案型別
* @param {*} base64 待識別檔案 BASE64碼
* @param {*} callBack 識別結果回撥
*/
ImageReader.prototype.getOcrResult = function (type, base64, callBack) {
let fileSize = (base64.length / (1024 * 1024)).toFixed(2);
let compressedBase64 = '';
let image = new Image();
image.src = base64;
image.onload = function () {
/**
* 圖片壓縮處理及異常處理,程式碼略
*/
let postData = strategies[type](compressedBase64);
ajax(
host + postData.path, {
data: postData.param,
type: 'POST',
headers: {
'Content-Type': 'application/x-www-form-urlencoded'
},
success: function (res) {
var data = JSON.parse(res);
// 暴露給 UI 層的統一的錯誤碼
if (data.error_code !== undefined && data.error_code !== 0) {
var errorData = {
error: 1,
title: '錯誤 ' + data.error_code,
content: 'error message'
};
callBack(errorData);
} else {
callBack(data);
}
}
});
};
};
呼叫方式
var imageReader = new ImageReader();
imageReader.getOcrResult('IDCARD', this.result.toString(), callback);
使用策略模式封裝 Vue Select 元件
某專案中多處用到了 element-ui 的 select 元件,其內在邏輯類似,都是初始化時獲取下拉選單的資料來源,然後在選中某一項時 dispatch 不同的 action。遂考慮使用策略模式封裝。
Context
在本例中,元件向外部暴露一個 prop,呼叫方指定該 prop 從而載入不同的策略。那麼定義 Context 如下:
<template>
<el-select v-model="selectedValue" placeholder="請選擇" @change="optionChanged" size="mini" clearable>
<el-option v-for="item in options" :key="item.id" :label="item.name" :value="item.id">
</el-option>
</el-select>
</template>
data() {
return {
selectedValue: undefined,
options: [],
action: "",
};
},
props: {
// 暴露給外部的 select-type
selectType: {
type: String
},
},
created() {
// 獲取 options
this.valuation();
},
methods: {
optionChanged() {
this.$emit(this.action, this.selectedValue);
},
setOptions(option) {
this.$store.dispatch(this.action, option);
},
valuation() {
// 獲取 options 資料
}
},
外部通過如下方式呼叫元件:
<MySelect selectType="product"/>
strategies
然後定義策略組:
let strategies = {
source: {
action: "sourceOption",
getOptions: function() {
// 拉取 options
}
},
product: {
action: "productOption",
getOptions: function() {
// 拉取 options
}
},
...
}
非同步
至此該元件的基本結構已經清晰,但還存在一個問題:元件載入時是非同步拉取的 options, 而頁面初始化的時候很可能 options 還沒有返回,導致 select 的 options 仍為空。所以此處應該修改程式碼,同步獲取 options:
// 策略組修改
source: {
action: "sourceOption",
getOptions: async function() {
// await 拉取 options
}
},
// 元件修改
methods: {
...
async valuation() {
...
}
}
繼續優化
但我們不是每次載入元件都需要拉取 options,如果這些 options 在其他元件或者頁面也被使用到,那麼可以考慮將其存入 vuex 中。
最開始的思路是高階元件,即定義一個包裝後的select模板,通過高階元件的方式擴充套件其資料來源與action(變化的部分)然而這個思路不是那麼的vue(主要是slots不太好處理) 於是考慮策略模式改寫該元件
總結
通過以上兩個例子,我們可以看到:
- 策略模式符合開放-封閉原則
- 如果程式碼裡需要寫大量的
if-else
語句,那麼考慮使用策略模式 - 如果多個元件(類)之間的區別僅在於它們的行為,考慮採用策略模式
參考
JavaScript設計模式與開發實踐(曾探) 第五章 策略模式