再探 Clean Code

2023-03-12

筆記關於 Scott Allen 在 C# Programming Paradigms | pluralsight 所說明的 Crafting C# Code 的十個 Rules,讓 C# / ASP.NET 以及 .NET Framework 開發人員寫出更高品質的程式碼。

logo

說明

之前曾筆記 Cory House 在 Pluralsight 的 Clean Coding Principles in C# 所介紹的 Clean Coding Principles;這次則是學習了 Scott Allen 在 C# Programming Paradigms | pluralsight 所說明的 Crafting C# Code 的 10 個 Rules,同樣是對於 Clean Code 的探討 😀

Don't Use Region

大G:嗨,小N,我們今天來談談C#中的 Region 功能。你知道 Region 是什麼嗎?

小N:我知道它可以將一段程式碼區塊包裝在一個區域內,可以展開或折疊。

大G:對的,不過我們公司的程式碼風格指南建議不要使用 Region,你知道為什麼嗎?

小N:不知道,可以請大G解釋一下嗎?

大G:當你使用 Region 時,通常是想隱藏一些程式碼。這會讓其他人在閱讀程式碼時很難理解整個方法或類別的邏輯。這也會讓你的程式碼更加難以維護和測試。另外, Region 不會影響編譯器生成的 IL 碼,所以它不會真正影響代碼的性能。

小N:那如果我只是想將一些相關的代碼放在一起,而不是隱藏它們呢?

大G:這種情況下,我們建議使用區域變數(local variables)或區域函數(local functions)來將相關的代碼放在一起。這樣會使你的代碼更加清晰易讀,並且還能夠提高代碼的可維護性和可測試性。

public class MyClass
{
    public void MyMethod()
    {
        // Local Variables
        int x = 5;
        int y = 10;

        // Local Function
        int Add(int a, int b)
        {
            return a + b;
        }

        int result = Add(x, y);

        Console.WriteLine("x + y = " + result);
    }
}

使用 Regions 的目的是避免過大的單一檔案,避免影響程式碼的閱讀性,因為閱讀性越差,維護就越不便。

Keep Small Class

大G:嗨,小N,你知道為什麼類別要盡量設計的越小越好嗎?

小N:其實我不太清楚,大G,能跟我解釋一下嗎?

大G:當一個類別的大小增加時,它變得更難以維護和測試。如果一個類別做太多事情,就會變得很複雜,很難去理解它的功能,也很難去找到錯誤的原因。

小N:那麼,你認為一個類別的大小應該是多少?

大G:這個問題有很多不同的答案,但是通常建議是保持類別小於200行程式碼。如果你的類別越小,它就越容易閱讀和理解,也更容易測試和維護。此外,一個小的類別也更容易進行重構和修改。

延續著不使用 Regions 的討論,保持小的類別並且拆分在多個檔案,謹記 SOLID 所提的單一職責以及開放與封閉原則。

Keep Method Short

大G: 你知道「Keep Method Short」嗎?

小N: 大概是說我們在寫程式時,每個方法不要太長太複雜,是吧?

大G: 沒錯,方法的長度和複雜度是影響程式碼可讀性和可維護性的重要因素之一。如果一個方法太長,閱讀和理解起來就會變得困難,而且在修改和調試時也會更加麻煩。

小N: 那麼,方法的長度要控制在多少行程式碼以內比較好呢?

大G: 這個沒有一個絕對的標準,但一般來說,我們建議每個方法的長度控制在 10 行到 20 行之間比較適合。如果一個方法超過了這個範圍,就應該考慮把它拆分成更小的方法來實現同樣的功能。

小N: 那麼,如果有一個方法有很多的邏輯判斷,我們應該怎麼處理呢?

大G: 這個問題很好,其實可以通過把邏輯判斷拆分成多個方法來解決。比如,你可以把每個邏輯判斷封裝成一個小方法,然後在主方法中調用這些小方法。這樣可以讓代碼更加清晰易讀,也更容易維護和擴展。

類別小,方法同樣要短,對於開發人員致力於寫出容易閱讀的程式碼是最重要的事情 😀

資深工程師這樣寫.cs

public class MyList<T>
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        items.Add(item);
    }

    public bool Contains(T item)
    {
        return items.Contains(item);
    }

    public void Remove(T item)
    {
        items.Remove(item);
    }

    public int Count()
    {
        return items.Count;
    }
}

菜鳥工程師這樣寫.cs

public class MyList<T>
{
    private List<T> items = new List<T>();

    public void Add(T item)
    {
        // 這裡有一些很長的邏輯,難以理解
        if (items.Count > 0)
        {
            for (int i = 0; i < items.Count; i++)
            {
                if (items[i].Equals(item))
                {
                    throw new InvalidOperationException("Item already exists");
                }
            }
        }

        items.Add(item);
    }

    public bool Contains(T item)
    {
        // 這裡有一些很長的邏輯,難以理解
        for (int i = 0; i < items.Count; i++)
        {
            if (items[i].Equals(item))
            {
                return true;
            }
        }

        return false;
    }

    public void Remove(T item)
    {
        // 這裡有一些很長的邏輯,難以理解
        for (int i = 0; i < items.Count; i++)
        {
            if (items[i].Equals(item))
            {
                items.RemoveAt(i);
                break;
            }
        }
    }

    public int Count()
    {
        // 這裡有一些很長的邏輯,難以理解
        int count = 0;
        for (int i = 0; i < items.Count; i++)
        {
            count++;
        }
        return count;
    }
}

Avoid Boolean Parameter

大G: 嗨,小N,聽說你現在負責開發一個新功能,對吧?

小N: 是的,大G。我在開發一個功能,需要使用一個 Boolean Parameter,你有什麼建議嗎?

大G: 呃,關於 Boolean Parameter,我會建議盡量避免使用它們。

小N: 為什麼呢?我看其他代碼中經常使用這種類型的參數。

大G: 是的,它們是常用的參數類型之一,但是使用 Boolean Parameter可能會導致代碼的可讀性和可維護性下降。

小N: 為什麼呢?

大G: 首先, Boolean Parameter可能會使代碼變得很難理解。當你在閱讀代碼時,如果看到一個 Boolean Parameter,你很難知道這個參數的含義是什麼,它代表的是什麼狀態或行為。另外, Boolean Parameter只有兩個值,真或假,這可能會讓代碼的意圖不夠明確。

小N: 我明白了。那麼,有什麼替代方案嗎?

大G: 當然。你可以使用枚舉類型或對象來替代 Boolean Parameter。比如,如果你需要傳遞一個表示狀態的參數,你可以定義一個枚舉類型,列舉出所有可能的狀態,這樣代碼的可讀性就會得到提高。如果你需要傳遞一個表示行為的參數,你可以定義一個對象,封裝行為相關的屬性和方法。

小N: 謝謝您的建議,大G。我會記住這些,避免過度使用 Boolean Parameter。

大G: 不客氣,小N。如果你有任何問題,隨時可以問我。

使用 Enum 取代 Boolean.cs

csharp
Copy code
public enum State
{
    Idle,
    Running,
    Jumping
}

public class Player
{
    public void Move(State state)
    {
        switch (state)
        {
            case State.Idle:
                // 玩家進入閒置狀態
                break;
            case State.Running:
                // 玩家進入奔跑狀態
                break;
            case State.Jumping:
                // 玩家進入跳躍狀態
                break;
            default:
                break;
        }
    }
}

Use Exception for Error

大G:嗨,小N,你知道為什麼我們在代碼中使用例外 Exception 來處理錯誤嗎?

小N:嗯,我知道 Exception 是一種代碼異常,它會在程序運行時引發,但是我不太確定為什麼要使用 Exception 來處理錯誤。

大G:嗯,當我們編寫代碼時,我們通常會遇到各種不同的錯誤情況,例如無效的輸入、數組越界、文件不存在等等。在這些情況下,我們需要在代碼中添加錯誤處理代碼,以保證程序的正確性和可靠性。

小N:那麼為什麼不能使用 if 或其他方法來處理這些錯誤呢?

大G:這是因為使用 if 來處理錯誤可能會讓代碼變得冗長。此外,if也不利於代碼的 reuse,因為我們需要在每個需要錯誤處理的地方添加 if 。而使用 Exception 可以讓我們更清晰地區分代碼的正常執行和錯誤處理,也能夠讓我們更好地進行代碼的重用。

小N:但是,如果我們在代碼中使用了太多的 Exception ,是否會對性能產生影響呢?

大G:當然,如果 Exception 使用不當,就可能對性能產生影響。因此,我們應該在代碼中適當地使用 Exception ,並盡可能地捕獲和處理常見的錯誤情況。此外,我們還應該避免在性能敏感的代碼區塊中使用 Exception ,例如循環或者經常使用的函數中。

Set Warning As Error

大G:嗨,小N,聽說你最近開始使用 Visual Studio 了,我來給你講一下 Set Visual Studio Warning As Error 的重要性。

小N:好的,謝謝大G。但是,我不太清楚這個設置的作用是什麼?

大G:這個設置的作用是把 Visual Studio 警告當成錯誤來處理,這樣可以避免在開發過程中出現潛在的問題。因為警告一般都代表了一些不符合最佳實踐的行為,可能會導致潛在的問題,因此我們需要對其進行嚴格的處理。

小N:那如果我們不進行這個設置,會出現什麼問題呢?

大G:如果你忽略這些警告,可能會導致一些意想不到的問題。例如,當你在編譯代碼時出現一個警告,你可能會認為這只是一個小問題,但是如果這個警告代表了一個潛在的錯誤,那麼這個問題可能會在運行時發生,從而導致嚴重的後果。

小N:明白了,那如果我們碰到一些警告不是真正的問題呢?

大G:如果你確定某個警告不會導致問題,你可以將其排除在警告列表之外。但是,你需要先仔細地檢查警告,確定它們不會導致問題。在設置警告時,我們需要謹慎處理,以確保代碼的品質和穩定性。

在每個專案屬性獨立設定

一開始可能會因為要處理警告訊息造成無法編譯而崩潰,但克服之後就是為軟體的品質前進一大步 😁

Avoid Too Many Parameters

大G:嗨,小N,最近你在寫程式的時候,是否曾經遇到過需要 Method 使用太多參數的情況?

小N:是的,有時候我需要傳遞很多不同的參數給一個函數或方法,但是這樣做會讓程式碼看起來很雜亂。

大G:沒錯,這是一個常見的問題。寫程式的時候,我們應該盡量避免使用太多參數。

小N:為什麼呢?使用太多參數會對程式碼有什麼不好的影響?

大G:使用太多參數會讓函數或方法變得很複雜,不易讀取和維護。此外,使用太多參數還會導致以下問題:

  1. 當我們在調用函數或方法時傳遞太多參數時,我們可能會犯錯誤,因為我們很容易搞混這些參數的順序和意義。
  2. 當函數或方法的參數太多時,它們的測試和調試也會變得更加困難。我們需要測試和確認每個參數的不同組合,這樣會耗費更多時間和精力。
  3. 使用太多參數也會讓代碼的可重用性降低。如果我們在不同的函數或方法中多次使用同樣的參數,那麼當我們需要更改這些參數時,我們需要更改所有使用這些參數的函數或方法。

小N:那麼,如果我們需要傳遞很多不同的參數,應該怎麼做?

大G:我們可以考慮使用一些替代方案,例如將參數封裝到一個對象中,或者使用默認參數值和可選參數等技巧。這些方法可以讓代碼更加簡潔和易於維護。

資深工程師這樣寫.cs

public class User {
    public string Name { get; set; }
    public int Age { get; set; }
    public string Email { get; set; }
    public string Phone { get; set; }
    public string Address { get; set; }
}

public class UserService {
    public void AddUser(User user) {
        // ...
    }
}

菜鳥工程師這樣寫.cs

public class UserService {
    public void AddUser(string name, int age, string email, string phone, string address) {
        // ...
    }
}

這樣寫起來好讀多了 😉

Encapsulate Complex Expressions

大G: 小N,你有聽過「Encapsulate Complex Expressions」這個概念嗎?

小N: 有聽過,但不是很清楚這是什麼意思。

大G: Encapsulate Complex Expressions 是一種軟體設計原則,也是提高程式碼可讀性和可維護性的重要手段之一。簡單來說,它的目的是將複雜的運算和邏輯表達式封裝在一個函數或方法中,以減少重複代碼和提高程式碼的可讀性。

小N: 為什麼要這樣做呢?

大G: 好問題。當我們寫一些複雜的運算和邏輯表達式時,如果直接寫在程式碼中,會使得程式碼變得冗長且難以理解。而且,如果這些運算和邏輯表達式在程式碼中多次使用,也會造成代碼的重複,增加了修改和維護的難度。

小N: 了解了,那麼如何實現 Encapsulate Complex Expressions 呢?

大G: 簡單來說,你可以將這些複雜的運算和邏輯表達式封裝在一個函數或方法中,然後在需要使用的地方調用該函數或方法。這樣可以避免代碼的重複,同時也提高了程式碼的可讀性和可維護性。

資深工程師這樣寫.cs

if (account.IsPastDue)
{
  ...
}

菜鳥工程師這樣寫.cs

if (account.Balance > 0 && !account.IsVip && account.DueDate > CurrentDate)
{
  ...
}

在追求程式碼可讀性的道路,沒有極限 😎

Avoid Comments

大G:嗨,小N,我們今天來聊聊為什麼要避免在程式碼中使用註解。

小N:為什麼呢?我覺得註解可以讓我們更好地理解代碼。

大G:是的,註解確實可以提供一些說明和背景信息,但是過多的註解可能會導致代碼難以維護。

小N:為什麼呢?

大G:因為註解可能會與程式碼不同步,尤其是當修改程式碼時。當註解不再反映實際代碼時,它們可能會誤導其他開發人員。此外,如果代碼本身不夠清晰易讀,那麼添加註解可能只會掩蓋問題而不是解決問題。

小N:那麼該怎麼辦呢?

大G:寫出乾淨易讀的代碼是最好的做法。如果代碼真的需要註解,那麼應該優先考慮修改代碼本身,以使其更易於理解。此外,只添加必要的註解,並且確保它們保持最新。最後,盡可能使用自文檔化的程式設計,例如良好的命名和設計模式,以減少對註解的需求。

個人的體悟是不是完全不寫註解,而是只寫必要有助於閱讀程式碼的註解,而且在 C# 應優先使用 Summary 註解以及 IDE 有特殊支援的 TODO 或 HACK 註解

/// <summary>
/// 這個方法用來計算兩個數字的和。
/// </summary>
/// <param name="a">第一個數字。</param>
/// <param name="b">第二個數字。</param>
/// <returns>兩個數字的和。</returns>
public int Add(int a, int b)
{
    return a + b;
}

// TODO: 處理錯誤訊息的顯示。
public void DisplayErrorMessage()
{
    // 顯示錯誤訊息。
}

// HACK: 這個方法需要重構。
public void DoSomething()
{
    // 複雜的程式碼...
}

Avoid Multiple Exits

大G:嗨,小N,你聽說過「避免多個出口」這個概念嗎?

小N:聽過,好像是寫程式時要盡量減少函數或迴圈中的 return、break 等語句,是吧?

大G:沒錯!這個概念的核心是讓你的程式碼更易於閱讀和維護。如果你在函數或迴圈中使用多個出口,會增加程式的複雜度,使得閱讀和維護變得困難。

小N:那麼,如果我有一個很長的函數,裡面有很多判斷語句,怎麼辦呢?難道就不能使用多個出口了嗎?

大G:當然可以,但要控制好數量。你可以使用一個出口作為默認情況,並在必要時添加其他出口。此外,你還可以將函數分解為多個小函數,以使代碼更易於管理。

Avoid Multiple Exits VS Return Early

兩者是不同的觀念,Avoid Multiple Exits 建議在函數或迴圈中盡量避免多個出口,以提高代碼的可讀性和可維護性。這意味著讓函數只有一個出口,即使有多個判斷語句,也應該只有一個 return 語句,避免函數的控制流程變得過於複雜。

Avoid Mutiple Exits.cs

public int Calculate(int input)
{
    int result = 0;

    if (input < 0)
    {
        result = -1;
    }
    else if (input == 0)
    {
        result = 0;
    }
    else if (input > 0 && input <= 10)
    {
        result = 1;
    }
    else
    {
        result = 2;
    }

    return result;
}

Return Early 是一種常見的代碼技巧,指在函數開始處檢查一些必要的條件,如果不滿足條件,就提前返回函數,而不是繼續執行函數的主體部分。這種方式可以提高程式碼的可讀性和性能,因為它避免了不必要的代碼執行。

Return Early.cs

public int CalculateTotalCost(int baseCost, int shippingCost, bool hasDiscount)
{
    // 如果 baseCost 或 shippingCost 為負數,就提前返回函數
    if (baseCost < 0 || shippingCost < 0)
    {
        return -1;
    }

    // 計算總成本
    int totalCost = baseCost + shippingCost;

    // 如果有折扣,應用折扣
    if (hasDiscount)
    {
        totalCost = (int)(totalCost * 0.8);
    }

    // 返回總成本
    return totalCost;
}
double GetDiscountPrice(double price, string level)
{
    // 如果價格小於等於 0 ,直接回傳 0
    if (price <= 0) return 0;

    // 如果會員等級為 VIP ,回傳 8 折
    if (level == "VIP") return price * 0.8;

    // 如果會員等級為 Gold ,回傳 9 折
    if (level == "Gold") return price * 0.9;

    // 其他情況回傳原價
    return price;
}

Avoid Multiple Exits & Return Early 兩者的搭配使用並不衝突 😆

相關連結

初探 Clean Code

Clean Code concepts adapted for .NET/.NET Core | GitHub