關於程式碼巢狀的思考 | Thoughts about code nesting

2024-09-08
程式可讀性

波動拳

波動拳!!,圖片來源

前言

“ 如果你需要三層以上的縮進,那麼你的代碼已經有問題了,應該修正你的程式。 ”

― Linux 內核代碼風格

實習一年,出社會一年,雖然只有短短兩年,但因為前後維運了兩個年齡只小我一輪的產品,看到許多巢狀的非常嚴重的程式碼,也就是俗稱的波動拳。

因為是寫 C# ,是一套有 if-else、switch、try-catch、using 的組合拳,然後通常波動拳會跟千百字長文合體,對你展開無量空處。

最可悲的是,一開始接觸只會有一定是我看得不夠熟的心態,說是也不是,畢竟還有可讀性這種東西。

然而當我意識到可讀性時,我跟大部分人一樣,將設計模式視為萬能藥一般,頂禮膜拜,策略模式、狀態模式、模板方法模式,當然還有我們最愛的工廠模式系列,畢竟寫物件導向,必讀GOF的設計模式,那是經典中的經典

但設計模式真的有這麼神嗎? 來談談我對於程式碼巢狀的思考之旅。


探索

開始分享心得前先疊個甲,我說的不一定是對的,只是分享一下心得,有任何錯誤與模糊的地方歡迎在下方留言討論。

設計模式

“ 我自稱DP哥 工廠模式COMBO策略模式 很常用的。 ”

― prag2222

設計模式,程式設計的萬靈丹
  • 遇到if-else跟switch直接套個簡單工廠加策略模式。
  • 遇到物件有狀態直接套個狀態模式。
  • 物件初始化直接套個工廠模式。

這邊套一個策略,那邊套一個工廠,再建個介面,一頓操作猛如虎。過度設計又怎樣 ? 我這是未雨綢繆 ! 程式非常聰明與彈性,覺得自己是個天才,然後維護就爆了。

非常聰明的程式只有非常聰明的人能夠維護,Keep it simple, Stupid ,設計模式在解決未知性的同時將物件間的交互變得間接,進而降低了易讀性。很多 IDE 能快捷鍵查看函式定義,但使用設計模式後,你只會看到介面定義。

我本來沒有想要使用這招的

我本來沒有想要使用這招的,圖片來源

設計模式在解決變化點的同時,增加了複雜度,那如果我將設計模式作為我本來不想使用這招的時候使出的招式,那麼我對付巢狀結構的手段還有哪些呢?

有毒的通用

“ 这种复用其实是有害的。如果一个函数可能做两种事情,它们之间共同点少于它们的不同点,那你最好就写两个不同的函数,否则这个函数的逻辑就不会很清晰,容易出现错误。 ”

― 编程的智慧, 王垠

複用是必須的,但有些複用是有害的,有沒有遇過一種情形,一段程式每個不同的情境都會經過某個函數,點進去後發現,它針對每個情境作不同處理,為甚麼呢 ? 因為他們可能都要做某個步驟。

csharp
📝copy
                                
void GenerateReport(ReportType type)
{
    if(type == ReportType.PDF)
    {
        a();
        b();
    }
    else if(type == ReportType.WORD)
    {
        c();
        d();
    }
    // ... 其他選項
    
    z();
}               
                                
                            

更慘的情況是,進去這個通用的函式後,它的裡面還有更多通用的函式,此時該函式為有毒的通用,擱這套娃呢。而更好的方法是,分成多個函式

csharp
📝copy
                                
    void GeneratePDF()
    {
        a();
        b();
        z();
    }
    
    void GenerateWORD()
    {
        c();
        d();
        z();
    }            
                                
                            

Early Return

“ 概念是讓程式碼盡早的完成任務,避免過深的巢狀導致閱讀的不易。 ”

― 在地上滾的工程師

邏輯正向時會有多層巢狀,那反向是否能夠化解 ? Early Return或Guard Clause 就是在描述這個概念。以下是用正向邏輯撰寫領錢的敘述。

csharp
📝copy
                                
void foo(){
    // 如果銀行有開
    if(bank.isOpen)
    {
        // 如果排隊的人很少的話
        if(waitInLine.FewPeople)
        {
            // 我的銀行餘額大於 3000
            if(account.balance > 3000)
            {
                    // 提款 3000
                    account.Withdraw(3000);
            }
        }
    } 
}        
                                
                            

啪的一下,很快的三層縮排馬上不講武德的招呼到你臉上,但如果我們使用 Early Return 的概念,使用反向邏輯呢 ?

csharp
📝copy
                                
void foo(){
    if(!bank.isOpen)
    {
        return;
    }
    
    if(!waitInLine.FewPeople)
    {
        return;
    }
    
    if(account.balance > 3000)
    {
        account.Withdraw(3000);
    }
}         
                                
                            

看來 Early Return 是我們的解方,但Early Return 假設了一種,幾乎沒有 else 的情況,更多時候else也是有它的邏輯的,那怎麼辦呢?

小而精美的模塊

“ 一个模块应该像一个电路芯片,它有定义良好的输入和输出。实际上一种很好的模块化方法早已经存在,它的名字叫做函数 ”

― 编程的智慧, 王垠

問題或許不是那層層堆疊的巢狀,問題或許是跟永樂大典一樣長的函式,那個函式或許達成了跟它名字一樣的功能,但它不該這麼長。

將部分步驟定義輸入與輸出,作為一個函數抽出去,最重要的是,給該函式合適的名字。

在抽函數時,你大概會發現兩件事
  • 某個參數跟他會用到的地方竟然差了一兩百行,而且他還會七十二變。
  • 某個參數在另一條 else 線根本不會用到。

重構不只是像收納一樣,單純把 A 放到 B,而是能讓你重新思考功能的數據流,並讓每一個區塊顯得獨立並且可讀

有些 IDE 提供了預處理命令,可以折疊程式碼,但就像現實一樣,糟糕的事情不是閉上眼睛就會解決的。


那現在呢?

“Writing is easy. All you have to do is cross out the wrong words. ”

― Mark Twain

作為驗證的 Early Return,作為拆解流程的函數,好像這兩把刀能解決巢狀結構的問題,但還是必須注意,Early Return 會以反向邏輯呈現,而函數要求你能精確命名,遇過一個函術名稱是

HandlingBillTradingCartSandCreditCS

看得出來它在幹嘛嗎 ? 我是看不出來。

所以找到了這趟解決巢狀程式碼的答案嗎? 我不知道,但我知道我有回答這個問題的想法了。


參考資料

也可以看看以下文章