JavaScript Strategy Pattern


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

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

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

// /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

// /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)。

// /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沒有這種東西,但是只要都設定好一樣的方法,當然也是可以直接拿來替換使用)

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