跳至主要内容

[GoF] 策略模式 Strategy

strategy example source code @ pjchender github

策略模式指的是:「定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換(JavaScript 設計模式與開發實踐)」。

  • 策略類別(Strategy):可以改變的部分,方便添加新的規則,這些規則的值通常會是函式
  • 環境類別(Context):不會變的部分。執行策略的地方,不需要隨著規則的增減而改變。

使用策略模式的好處在於可以很容易的添加不同的策略,而不用動到執行的函式本身。

備註

這裡的「策略」和「規則」會混著用,基本上代表同樣的意思。

適合的時機

在函式有很多判斷式時

在函式中用了很多的 if...elseswitch 判斷式:

// 這通常可以改成使用策略模式
enum INCOME_LEVEL {
A = 'A',
B = 'B',
C = 'C',
}
function getTaxAmount({
rawTax,
incomeLevel,
}: {
rawTax: number;
incomeLevel: `${INCOME_LEVEL}`;
}): number {
if (incomeLevel === INCOME_LEVEL.A) {
return rawTax;
} else if (incomeLevel === INCOME_LEVEL.B) {
return rawTax * 1.1;
} else if (incomeLevel === INCOME_LEVEL.C) {
return rawTax * 1.2;
}

throw new Error(`unknown tax strategy: ${incomeLevel}`);
}

改成策略模式後:

/**
* 策略類別定義在這,方便後續擴充(可以改變的部分)
**/
enum INCOME_LEVEL {
A = 'A',
B = 'B',
C = 'C',
}

// 策略通常是 value 為 function 的物件,當取到對應的 key 後,就可以執行對應的函式
const TAX_STRATEGY = {
[INCOME_LEVEL.A]: (tax: number) => tax,
[INCOME_LEVEL.B]: (tax: number) => tax * 1.1,
[INCOME_LEVEL.C]: (tax: number) => tax * 1.2,
};

// 環境類別(不需要改變的部分)
function getTaxAmount({
rawTax,
incomeLevel,
}: {
rawTax: number;
incomeLevel: `${INCOME_LEVEL}`;
}): number {
return TAX_STRATEGY[incomeLevel](rawTax);
}
提示

策略通常是 value 為 function 的物件,當取到對應的 key 後,就可以執行對應的函式。

表單驗證

(function formValidationInit() {
// STEP 1:定義 strategy rules,方便添加要套用規則(會改變的部分)
enum STRATEGY {
isNonEmpty = 'isNonEmpty',
minLength = 'minLength',
isMobileFormat = 'isMobileFormat',
}
const strategies = {
[STRATEGY.isNonEmpty]: ({
value,
errorMsg,
}: {
value: string;
errorMsg: string;
}): string | undefined => {
if (value === '') {
return errorMsg;
}
return;
},
[STRATEGY.minLength]: ({
value,
minLength,
errorMsg,
}: {
value: string;
minLength: number;
errorMsg: string;
}): string | undefined => {
if (value.length < minLength) {
return errorMsg;
}
return;
},
[STRATEGY.isMobileFormat]: ({
value,
errorMsg,
}: {
value: string;
errorMsg: string;
}): string | undefined => {
const pattern = /^09[0-9]{8}$/;
if (!pattern.test(value)) {
return errorMsg;
}
return;
},
};

// STEP 3:實作 Validator(Context 類別,不需要改變的部分)
class Validator {
caches: (() => string | undefined)[] = [];

// 添加要驗證的規則
add(formElement: HTMLFormElement, rule: string, errorMsg: string) {
const arr = rule.split(':');
this.caches.push(() => {
const [strategy, minLength] = arr;

if (!isStrategyType(strategy)) {
throw new Error(`unknown strategy ${strategy}`);
}
return strategies[strategy]({
value: formElement.value,
minLength: Number(minLength),
errorMsg,
});
});
}

// 進行驗證
start() {
const errorMsgs = this.caches.map((cache) => {
const errorMsg = cache();
if (errorMsg) {
return errorMsg;
}
return;
});

return errorMsgs.filter((errorMsg) => !!errorMsg);
}
}

// STEP 2:添加表單要套用的驗證規則
const validate = (formElement: HTMLFormElement) => {
const validator = new Validator();

validator.add(formElement.userName, 'isNonEmpty', '使用者名稱不能為空');
validator.add(formElement.password, 'minLength:6', '密碼不能少於 6 位');
validator.add(
formElement.phoneNumber,
'isMobileFormat',
'手機號碼格式錯誤'
);

const errorMsg = validator.start();
return errorMsg;
};

const registerForm = document.getElementById(
'registerForm'
) as HTMLFormElement;

const handleSubmit = (e: any) => {
e.preventDefault();
e.stopPropagation();

// STEP 4:執行驗證
const errorMsgs = validate(registerForm);
console.log(errorMsgs);

if (errorMsgs.length > 0) {
return false;
}

console.log('submit');
return;
};

registerForm?.addEventListener('submit', handleSubmit);

function isStrategyType(strategy: any): strategy is STRATEGY {
return Object.values(STRATEGY).includes(strategy);
}
})();

參考

Giscus