JavaScript Strategy Pattern

策略模式作爲一種軟體設計模式,指對象有某個行爲,但是在不同的場景中,該行爲有不同的實現算法。比如每個人都要「交個人所得稅」,但是「在美國交個人所得稅」和「在中國交個人所得稅」就有不同的算稅方法。  – By WIKI

其實WIKI上面說得很清楚了,用我自己理解的話來說的話,就比如每天上班的路線,也許周一到周五都有不同的路線,但是一樣都能到達目的地。這些不同的路線就是【可被替換的演算法】,而決定採用哪一種演算法的條件,就是【今天星期幾】。

照慣例還是先從書本上的範例開始學習,一樣是從書中取得的原始範例後再加以調整重構。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
// /src/index.js
var Validator = require('./Validator.js')
var strategies = require('./ValidatorStrategy.js')

var data = {
firstName: "Super",
lastName: "Man",
age: "unknown",
userName: "o_O"
}

let validator = new Validator(strategies)
validator.config = {
firstName: "isNonEmpty",
age: "isNumber",
userName: "isAlphaNum"
}

validator.validate(data)
if (validator.hasErrors()) {
console.log(validator.messages.join("\n"))
}

範例是模擬表單驗證的前端Code,假設表單的資料收集起來之後是data物件,則預先設定我們的表單驗證規則物件strategies,並且將資料傳遞給validator,透過validator來幫我們做表單驗證的動作。當然此處我們會先設定我們的表單驗證規則,firstName的部分我們採用的規則叫做【isNonEmpty】;age的規則叫做【isNumber】;userName的規則則是使用【isAlphaNum】。

以isNumber這個規則名稱為範例來說明,當然也可以替換為更適合的演算法名稱,只是因為範例中的驗證部分演算法的確內容就是判斷是否為數字,所以才命名為isNumber

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
// /src/Validator.js
class Validator {
constructor(types) {
this.types = types
this.messages = []
this.config = {}
}

validate(data) {
this.messages = []
for (let i in data) {
if (data.hasOwnProperty(i)) {
let type = this.config[i]
let checker = this.types[type]

if (!type) {
continue
}

if (!checker) {
throw {
name: "ValidationError", message: "No handler to validate type:" + type
}
}
let result = checker.validate(data[i])
if (!result) {
let msg = "Invalid value for *" + i + "*, " + checker.instructions
this.messages.push(msg)
}
}
}
return this.hasErrors()
}
hasErrors() {
return this.messages.length !== 0
}
}
module.exports = Validator

作為Validator最主要的功能,就是依據傳入的設定與資料,判斷該使用何種演算法進行驗證。透過for…in的語法與hasOwnProperty()的技巧,取得物件的屬性名稱(也就是程式中的i),再透過屬性名稱去找傳入的設定,如果沒有該項設定,則略過該屬性的驗證;如果有找到,那在去找演算法是否存在,不存在就拋例外,存在就呼叫演算法內所定義的validate方法,並且將表單的該項資料拿去做驗證。若驗證有誤,再將錯誤訊息紀錄於陣列messages中。而最終判斷是否有通過表單驗證,就判斷陣列長度是否等於0就可以了。

說起來一長串,其實看程式碼會比較容易理解,這邊需要注意的部分就是,一樣是在validate()這個方法內,實作的細節都是由策略物件提供的(也就是this.types)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// /src/ValidatorStrategy.js
module.exports = strategy = {
isNonEmpty: {
validate: function (value) {
return value !== ""
},
instructions: "the value cannot be empty"
},
isNumber: {
validate: function (value) {
return !isNaN(value)
},
instructions: "the value can only be a valid number, e.g. 1, 3.14 or 201"
},
isAlphaNum: {
validate: function (value) {
return !/[^a-z0-9]/i.test(value)
},
instructions: "the value can only contain characters and numbers, no spe"
}
}

這一支程式就很單純的就是把各種演算法都放在這個物件之內。為了要讓它們可以被替換,每一種演算法都提供了相同的呼叫方法及屬性(就像是C#的Interface有先定義好介面,讓子類別繼承;而javascript沒有這種東西,但是只要都設定好一樣的方法,當然也是可以直接拿來替換使用)

照慣例一樣附上練習的程式碼