首頁 > 軟體

如何刪掉程式設計中的 Switch 語句

2022-09-06 18:06:50

多重方法是一種有趣的方式,可以幫你擺脫令人討厭的 switch。而且,這也有助於提升程式碼的可讀性。所以,在決定繼續堅持使用 switch 之前,一定要先試一試。

本文最初發佈於 Bits and Pieces。

很多開發者都討厭switch語句,包括我。並不是因為這個語句沒用,也不是因為它太難了。

理解switch語句的工作原理非常簡單,問題是當你真的遇到它時,就必須停下手頭的一切工作,集中精力閱讀它,以確保不會遺漏任何東西,比如,缺少break語句可能會導致一些意想不到的行為,或者一個case中大約有 20 行程式碼。

關鍵是,原諒我使用一個花哨的術語:理解switch語句(在現實世界中)所需要的認知負荷相當重。我相信,作為開發人員,我們的目標是編寫方便人類閱讀的程式碼。在這方面,這個語句提供不了什麼幫助。

但是,我寫這篇文章不是為了對它進行抨擊,我是要向你(之前也包括我)展示三個關於如何避免使用switch語句的範例,讓我們來看一種函數語言程式設計技術:多重方法。

什麼是多重方法?

我第一次聽到這個詞,還是在播客“20 MinJS”中採訪 Yehonathan Sharvit 時。當時的採訪是關於他即將由 Manning 出版的著作《面向資料的程式設計》。

他提出這一概念是為了從功能上取代繼承,這無疑是可行的。在這個過程中,他展示了switch語句是如何被取代的。因此,讓我們暫時把 OOP 放在一邊,只關注第二部分:消除程式碼中醜陋的switch

什麼是多重方法?它只是一個能夠根據接收到的引數選擇最佳實現的函數。換句話說,想象一下,如果你把醜陋的switch語句放在函數中,然後對所有人隱藏實現。

唯一的區別是,你的解決方案只適用於一個函數。今天我們將討論如何在執行中生成多個多重方法。

多重方法是什麼樣子?

當然,每種語言都有自己的變體,但我今天主要講 JavaScript。

在這種語言中,多重方法的使用方法如下:

//我們將使用的資料
const myDog = {
    type: "dog",
    name:"Robert"
}
const myCat = {
    type: "cat",
    name: "Steffan"
}
//自定義函數實現
function greetDogs (dog) {
    console.log("Hello dear Dog, how are you today", dog.name, "?")
}
function greetCats(cat) {
    console.log("What's up", cat.name, "?")
}
//定義我們的多重方法
let greeter = null
greeter = multi(
    animal => animal.type,
    method("dog", greetDogs),
    method("cat", greetCats)
)(greeter)
// 呼叫多重方法
greeter(myDog)
greeter(myCat)

這個例子做了很多事,讓我來說明下:

  • 我定義了 2 個物件myCatmyDog,我將把它們作為引數,多重方法將根據它們確定自己的行為。

  • 我定義了 2 個自定義函數greetDogsgreetCats,它們的實現稍有不同。它們將代表switch中每個case語句裡的程式碼。

  • 然後我呼叫一些函數,尤其是multimethod,來定義多重方法greetermulti函數接收 3 個屬性:一個分配器(dispatcher),我們將用它返回的值來確定要執行的邏輯片段;還有兩個方法,分別代表switch的一個case語句。請注意,每次呼叫method時,要首先指定觸發第二個引數的值(這是實際的邏輯所在)。

  • 最後,我使用同一個函數(我的多重方法)來執行兩個不同的邏輯片段,而不需要在任何地方使用switchif語句。

多重方法有什麼好處?

當然,我們在這裡沒有施展任何型別的魔法,我們只是重寫了決策邏輯的表達方式,類似下面這樣的switch語句:

switch(animal.type) {
  
  case "dog":
    greetDogs(animal);
  break;
  case "cat":
    greetCats(animal);
 break;
}

那麼,如果我們可以直接這樣做,為什麼還要大費周章地使用多重方法呢?問題的關鍵是可讀性。

switch語句非常開放,顯示了我們的決策邏輯的實現。換句話說,這個語句是命令式的。它向你展示了決策樹的內部運作情況,這意味著閱讀程式碼的人將不得不在頭腦中解析程式碼。因此,我們又回到了認知負荷的概念。這使得開發者要閱讀並在頭腦中解析程式碼。

你要知道,大多數開發人員在遇到像上面這樣的switch時,不會有什麼反應。但是,這也不是一個實際的例子。通常情況下,case語句包含的程式碼更多,也更難閱讀。

而多重方法隱藏了決策邏輯的內部結構,你所知道的只是你對它做了設定,它將以某種方式工作。你更關心的是功能而不是實際的實現。這被稱為“宣告式程式設計”,有助於提高程式碼的可讀性,同時降低開發人員的認知負擔。這是因為它在邏輯上增加了一層抽象,為我們提供了更接近人類語言的表達工具。

如果這還不能說服你,還有一個優點:可延伸性。

如果你需要在switch中新增另一個選項,就必須回到程式碼中修改同一個switch,如果你,比如說,碰巧忘記新增break語句,就有可能造成問題,就像下面這樣:

switch(animal.type) {
  case "rabbit":
    greetRabbits(animal);
  case "dog":
    greetDogs(animal);
  break;
  case "cat":
    greetCats(animal);
 break;
}

還是個非常簡單的例子,但如果是真實世界中一段更長的程式碼,那麼這種情況出現的機率就更大了。

以防你對這種行為不熟悉,請讓我做個說明。第一個case中缺失break,會導致在動物型別為“rabbit”時也執行第二個case下的邏輯。

然而,有了多重方法,我們就可以不斷地根據需要對它進行擴充套件:

let extendedGreeter = multi(
    animal => animal.type,
    method("parrot", sayHiParrot)
)(greeter)

現在,這個新方法extendedGreeter對“dog”、“cat“、”parrot“就都有效了,而我們不必再回去修改已有的程式碼。

這是一個很大的好處,因為我們都知道,每次我們觸碰可以正常工作的程式碼時,都有一點可能引入 Bug。在這裡,我們把可能性降低到 0。

實現一個多重方法庫

首先,你要知道,已經有一些庫在處理這個問題了,其中一個例子是@arrows/multimethod

儘管如此,對這些實現進行逆向工程總是很有趣,所以讓我們看一看如何實現一個基本的多重方法庫,以適應到目前為止所展示的例子。

理解這個問題的關鍵是,我們需要一個分配器函數來給提供一個實際的值,我們將用它作為判斷執行哪個方法的鍵。而且,我們不能對switch語句進行寫死,因為選項的數量是不固定的。

不能光說不練,下面是實現:

function method(value, fn) {
    return {value, fn}
}
function multi(dispatcher, ...methods) {
    return (originalFn) => {
        return (elem) => {
            let key = dispatcher(elem)
            let method = methods.find( m => m.value === key)
            if(!method) {
                if(originalFn) {
                    return originalFn(elem)
                } else {
                    throw new Error("No sure what to do with this option!")
                }
            }
            return method.fn(elem)
        }
    }
}

method函數只是把鍵和實際的邏輯耦合在一起,沒有別的。multi函數中的程式碼才有趣,它返回一個匿名函數,以原始函數為引數並返回一個新函數,後者根據分配器程式碼(我們的第一個引數)返回的值執行不同的東西。

讓我們逐行看下:

  • 首先,呼叫第 8 行的函數時提供一個屬性(比方說myDog)。

  • 第 9 行的分配器邏輯會獲取myDog並返回其型別,即“dog”。

  • 然後在第 10 行,我們找到第一個與該型別匹配的方法。

  • 如果沒有方法匹配,但我們有一個有效的“originalFn”(也就是說,我們正在擴充套件一個原始的多重方法),我們會讓它來處理這種情況。否則,我們將丟擲一個異常,因為我們對此無能為力。

  • 然而,如果找到了匹配的方法,就在第 18 行執行它,並將原始屬性“myDog”傳遞給它。

就是這樣。沒那麼複雜,對嗎?當然,如果你想提供“預設”情況處理而不是丟擲一個異常,或者你想處理多屬性決策(比如根據屬性typename決定邏輯,而不是隻根據第一個屬性),就得編寫更多的程式碼了。

不過,還是那句話,如果你打算使用多重方法,建議你使用一個現有的庫,而不是自己去實現。

多重方法是一種有趣的方式,可以幫你擺脫令人討厭的switch。而且,這也有助於提升程式碼的可讀性。所以,既然你已經瞭解了多重方法,那麼在決定繼續堅持使用switch之前,一定要先試一試。

到此這篇關於如何刪掉程式設計中的 Switch 語句的文章就介紹到這了,更多相關Switch 語句刪掉內容請搜尋it145.com以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援it145.com!


IT145.com E-mail:sddin#qq.com