Javascript tips
怎麼避免一堆 if-else 或是 switch-case?
今天跟學生們討論到一些程式的寫法,其中一個情境是,程式碼中需要判斷一些情境,針對不同的情境做不同的事,例如,不同角色要做的事情不一樣,或是不同類別要做的事情不一樣,但這些不一樣中,又有一些相同的部分,這時候很容易寫出這樣的程式碼來:
if(user.role === 'admin') {
// do something A1 for admin
} else {
// do something A2 for user
}// do something B for both// do something C for bothif(user.role === 'admin') {
// do something D1 for admin
} else {
// do something D2 for user
}// do something E for both
// ...
除了可能在程式碼中散落一堆 if-else 之外,如果情況或種類多一點,可能還會想要動用到 switch。改善的方式有很多種,最近蠻喜歡的一種是利用 class,例如:
class Admin {
doSomethingA() {
// do something A1 for admin
} doSomethingD() {
// do something D1 for admin
}
}class User { doSomethingA() {
// do something A2 for user
} doSomethingD() {
// do something D2 for user
}
}// main function
// 用一次判斷,省去後續數次的判斷
let role = user.role === 'admin' ? new Admin() : new User();role.doSomethingA();
// do something B for both
// do something C for both
role.doSomethingD();
// do something E for both// B, C, E 也都可以有更好的做法,例如做一個 class 出來,具有 B, C, E 函式,然後讓 Admin, User 去繼承這個 class,使其都有這些共同的功能。
來個能執行的例子:
class Adder {
constructor(x, y) {
this.x = x;
this.y = y;
} calculate() {
return this.x + this.y;
}
}class Subtractor {
constructor(x, y) {
this.x = x;
this.y = y;
}
calculate() {
return this.x - this.y;
}
}// main function
let type = 'sub';
// 決定 calculator 是哪一個,之後就可以直接呼叫同名的函式
let calculator = type === 'add' ? new Adder(1, 2) : new Subtractor(1,2);
let result = calculator.calculate();
console.log(result);
後來想想,既然是 Javascript,應該也可以…
class Adder {
...
}class Subtractor {
...
}// main function
let type = 'sub';// 這裡做一個 hash table 出來,然後就可以透過 type 直接從這個 hash 中取出相對應的 class,完全不需要 if-else 或 switch 了
let map = { add: Adder, sub: Subtractor };
let calculator = new map[type](1, 2);let result = calculator.calculate();
console.log(result);
利用 Javascript 語言的特性,直接避免掉一堆 if-else 或是 switch,是不是蠻好玩的?
相關的參考資料
有興趣的可以趁這個機會找一下相關資料來讀讀看,這邊提供兩個參考:
- 重構中提到的 switch 壞味道
在重構這本書中,switch 是被列在壞味道中的一種,可以參考 switch-statements:
You have a complex
switch
operator or sequence ofif
statements.
其中一種解法就是 Replace Conditional with Polymorphism. 上述的寫法並不是 Polymorphism,但在這題上,效果蠻接近的。
- 設計模式中的工廠模式與策略模式
在設計模式中有兩個蠻常被使用到的模式:工廠模式與策略模式,我們把不同的策略(行為)給封裝了起來,然後利用可以工廠模式來建立相對應的物件。
為什麼要這樣寫呢?
那這樣做,除了在程式中可以不要有一堆 if-else / switch 之外,還有什麼好處呢?我自己覺得有一個很大的優點是,當我們想要加上新的功能(例如新的情境或類別)時,只需要去新增一個 class,而不需要去修改到原本的程式碼。
以上述程式碼為例,當我們想叫新增一個乘法器時,不需要去修改到原本的 Adder
或是 Substrator
,只需要新增一個 Multipler
的類別,並且調整一下 map 即可:
// class Adder {...}
// class Subtractor {...}class Multiplier {
constructor(x, y) {
this.x = x;
this.y = y;
} calculate() {
return this.x *this.y;
}
}let map = { add: Adder, sub: Subtractor, mul: Multiplier};
// ...
這也符合了開放封閉原則(Open/Closed Principle):
software entities (classes, modules, functions, etc.) should be open for extension, but closed for modification.
當我們要為系統增加新的功能時,應該藉由增加新的程式碼來達成,而不是去修改原本已經存在的程式碼,例如我們新增了 Multiplier
,而且沒有去修改到原本的 Adder
及 Substrator
。
怎麼處理共用/重複的程式碼?
如果 Adder
跟 Substrator
有共同的程式碼,但不想要用重複程式碼(複製-貼上)來完成,那要怎做呢?來看看兩種不同的寫法:
繼承的寫法
用繼承的方式,可以把共用的屬性與函式放在父類別中,然後不同的操作以不同的子類別來實現,這樣重複的程式碼就可以大幅地降低。
class Calculator {
constructor(initialNum) {
this.value = initialNum;
} // Adder 與 Subtractor 共用的程式碼放在父類別這層
getValue() {
return this.value;
}
}class Adder extends Calculator {
calculate(num) {
return (this.value += num);
}
}class Subtractor extends Calculator {
calculate(num) {
return (this.value -= num);
}
}// main function
let type = 'sub';
let map = { add: Adder, sub: Subtractor };
let calculator = new map[type](1);
calculator.calculate(2);
console.log(calculator.getValue());
以聚合/組合取代繼承的寫法
但由於繼承的耦合性很高,因為會建議「優先使用物件聚合(aggregation, 或是組合 composition),而不是類別繼承」,所以也可以改寫成:
class Calculator {
operationMap = {
add: Adder,
sub: Subtractor,
}; constructor(op) {
// Calculator 「有一個」運算
this.operation = new this.operationMap[op]();
}
calculate(a, b) {
return this.operation.calculate(a, b);
}
}class Adder {
calculate(op1, op2) {
return op1 + op2;
}
}class Subtractor {
calculate(op1, op2) {
return op1 - op2;
}
}// main function
let type = 'sub';
let calculator = new Calculator(type);
let result = calculator.calculate(1, 2);
console.log(result);
這邊也補充一下,在討論 OO 的文章中,有時會出現 is-a
跟 has-a
的討論:
is-a
代表的是繼承,以繼承範例那個寫法來說,Adder 繼承了 Calculator,因為我們會說 Adder is a Calculator,Adder 是一種計算器。has-a
顧名思義就是有一個,以聚合的寫法來說,Calculator has a Adder,Calculator 有 Adder 這個加法器。
會建議盡可能採用 has-a
而非 is-a
,原因是為 has-a
耦合性比較低,比較有彈性。舉個例子來說,當 class B, C, D 都繼承了 class A,但 class B, C 都需要一個方法 X,但 class D 不需要呢?如果把這個方法 X 分別實作在 class B 跟 C 裡,那程式碼就重複了,但如果實作在父類別 A 裡,那不需要這個方法的 D 就被強迫擁有這個方法。但如果是將方法 X 放在另外一個類別,讓 B 跟 C 去「擁有」這個類別的實體,這樣 B 跟 C 就可以使用這個方法 X,但 D 不需要,就不用被強迫中獎了。
結論
我的 OO 學得不算好,工作中遇到時就多少看一點,Javascript 也不是基於類別(class)的物件導向語言,但 OOP 中大多數的觀念跟原則,對開發 Javascript 來說也是很有幫助的。
如果有其他的做法,也歡迎一起討論~
補充資料
這是 91 哥錄製的不同程式語言的重構過程,對以上寫法有興趣,但又不知道怎麼重構的,可以看看喔: https://tdd.best/code-4-fun/polymorphism-replace-conditions/