先懺悔一下好了。前篇討論 LINQ 邏輯執行時機的文章不小心示範了「好寫好用但不該推廣的技巧,」但未加註「叔叔有練過,小朋友不要學」或「叔叔在寫 Side Project,不會害到人」警語,恐有誤導之嫌。撰文前其實曾閃過「是否不妥」的念頭,但想想覺得還好便沒放心上。貼文後讀者 Shu Huan Huang 提醒,我意識到確實可能做了不良示範誤導新同學,故補上這篇平衡一下。

許多程式人員會抱著「程式能動就好」的心態寫程式,不在意執行效能、是否有資安漏洞、程式碼是否好理解易修改能擴充,並把「不管黑貓白貓,能抓耗子的就是好貓」常掛嘴上強調正當性。

貓能抓耗子可以解決問題沒錯。但如果這隻會抓耗子的貓:

  • 只吃進口米其林級貓罐頭
  • 每天四處拉屎撒尿抓爛家俱又滿身跳蚤
  • 搬家換個新環境就忘記怎麼抓老鼠
  • 三天兩頭去獸醫院報到

這樣,還能算是一隻好貓嗎?

同樣的,如果某支能動的程式:

  • 只有原作者知道怎麼修改,其他人一碰就壞
  • 程式一天到晚被抓到漏洞,SQL Injection、XSS 樣樣都來
  • 需求稍改就沒法用了,要調整擴充又不知從何下手
  • 系統三天兩頭出錯,救火救到龜藍波火

這樣的程式很難說是好程式吧?

寫了三十多年程式,經過 N 個十萬小時的焠鍊,我寫過不少元件、程式庫及框架,通過多年商業運轉及持續擴充的考驗,我對自己程式碼的簡潔性、安全性、可擴充性還挺有信心,但對可讀性的體會相對較淺,在閒聊 - 「好程式」跟你想的不一樣! 初讀「重構」有感一文提過我曾深受震憾,對可讀性有了全新認識!

當今軟體開發的主流思維,程式可讀性的優先性應排在簡潔、效率之前,理由是低可讀性程式的維護成本常超乎想像,一味講求潔簡、效能,固然能少寫幾行程式少跑幾個指令節省幾個 CPU 時序,代價卻是程式不易理解、難以修改擴充,得不償失。 而當程式碼本身能明確表達意圖,不需要註解、文件或追進程式碼就能理解程式碼的目的,自然能提供良好的可讀性。重構技巧便很注重類別、變數、函式的命名,力求清楚表達意圖減少誤解,讓程式碼好懂易讀。

任何一個傻瓜都能寫出電腦可以理解的程式(能抓耗子的貓),唯有優秀的程式設計師能寫出讓人讀懂的程式(好貓)。 by M. Fowler (1999)

在前篇文章我提到兩個取巧寫法:

  1. 用 jQuery.map() 取代 jQury.each() 跑迴圈,callback 函式可由 function(idx, element) 簡寫成 function(element)
  2. 借用 Select(o => ...).Count() 進行每一筆資料有效性檢查,比 ToList().ForEach() 或 foreach () 少打幾個字元

這兩個取巧寫法執行結果都正確,估計不會有效能或安全性問題,我也曾在不少別人的程式碼看過,但它們稱不上良好的做法,容易混淆程式碼意圖導致後人誤解改錯,不值得鼓勵。這些在實務上常見,乍看有益卻存在瑕疵(可能是效能、安全或可讀性)終致得不償失的程式寫法或系統設計,有個專有名詞叫 Anti-Pattern (反面模式)

回頭來看上述兩個取巧做法錯在哪裡?問題出在會混淆意圖降低可讀性!

jQuery 對 each() / map() 賦與了不同意義,map() 預期該傳回轉換後的陣列,即便它容許 callback 什麼都不回傳,完全不用變數接收傳回陣列依能完美執行不出錯,但借用 MDN 文件關於「什麼時候不要用 map()」的說明:

因為 map 會建立新的陣列,如果在不想建立新陣列時使用該方法,就會變成反模式(anti-pattern):這種情況下,要使用 forEach 或 for-of。 以下情況不應該使用 map;

  1. 不使用回傳的新陣列,
  2. 或/且不需要回傳新陣列。

而用 Select() 取代 ForEach() 觸犯的問題也相似。

當 「Select()/.map() 是用來傳回新陣列」是程式開發人員的普遍認知,借作其他用途易超出其他人的預期而一頭霧水。我自己有個拿捏底線 - 不該在 Select()/map() 更動來源陣列。若會修改來源就會回歸 ForEach()/each(),理由是後人追查邏輯時通常不會優先檢查 Select()/map() 內部,等踏破鐵鞋才發現它被藏在不該出現的地方,肯定奉上成串花惹發,這麼做是不折不扣的挖坑給人跳呀! 與借用 Select()/map() 跑唯讀迴圈需多花點腦筋理解,損陰德程度有別。

開發人員遵守戒律的程度不同,從早晚禮佛、堅持不沾葷腥、吃方便素,一直到允許酒肉穿腸過,信仰深度有別,各有自己的尺度。我肯定不是嚴格奉行紀律的那一型,常覺得 Coding 該像藝術創作,搞到像工程營造樂趣全失,但回歸企業應用系統及團隊開發,一致的 Coding Style 與程式碼可讀性是提升品質降低成本的基本要求,依我的觀點,有些取巧可被忍受,有些偷懶天地難容,我的拿捏尺度介於二者之間,所以文章偶爾會出現為求省事簡潔的取巧寫法,某些已接近 Anti-Pattern,理應加上警語。

前文案案用 Select() 取代 ForEach() 跑迴圈檢核我認為還行(雖然不符正統規範),若在其中更動陣列內容就不 OK 了,必須改用 ForEach() 或 foreach 以免害人。不過在文章裡遺漏說明這點原則,擔心有些人舉一反三發揚發光大,把 Select() 完全當 ForEach() 用大搞更新修改,屆時引發災難可就不妙了,故特別再寫一篇補充說明。

最後,我試著用最小幅度修改讓它更好讀一些,改用夠精簡且意圖更吻合的 LINQ 方法 - All(),寫成:


static IPAddress ParseIp(string ip)
{
    Console.WriteLine($"[DEBUG] Check {ip}");
    IPAddress r;
    if (IPAddress.TryParse(ip, out r)) return r;
    throw new ApplicationException(
        $"{ip} 不是有效 IP 地址");
}
static bool CheckIp(string ip) 
{
    ParseIp(ip);
    return true;
}    
static void Main(string[] args)
{
    var rawData = new Dictionary<string, string>()
    {
        ["IP1"] = "127.0.0.1",
        ["IP2"] = "::1",
        ["BAD"] = "喂! 我不是 IP 啦"
    };
    try
    {
        rawData.Values.All(o => CheckIp(o));
        Console.WriteLine("資料有效");
    }
    catch (Exception ex)
    {
        Console.WriteLine($"資料無效 - {ex.Message}");
    }
    Console.ReadLine();
}

Some thoughts about frequently used coding style which are handy, concise but not always good.


Comments

Be the first to post a comment

Post a comment