Javascript tips

怎麼避免一堆 if-else 或是 switch-case?

Azole (小賴)
Aiworks
Published in
9 min readApr 26, 2022

--

圖片來源: https://twitter.com/r_tificialintel/status/1206319228954894336

今天跟學生們討論到一些程式的寫法,其中一個情境是,程式碼中需要判斷一些情境,針對不同的情境做不同的事,例如,不同角色要做的事情不一樣,或是不同類別要做的事情不一樣,但這些不一樣中,又有一些相同的部分,這時候很容易寫出這樣的程式碼來:

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 of if 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 ,而且沒有去修改到原本的 AdderSubstrator

怎麼處理共用/重複的程式碼?

如果 AdderSubstrator有共同的程式碼,但不想要用重複程式碼(複製-貼上)來完成,那要怎做呢?來看看兩種不同的寫法:

繼承的寫法

用繼承的方式,可以把共用的屬性與函式放在父類別中,然後不同的操作以不同的子類別來實現,這樣重複的程式碼就可以大幅地降低。

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

--

--

Azole (小賴)
Aiworks

As a passionate software engineer and dedicated technical instructor, I have a particular fondness for container technologies and AWS.