重構複雜條件的規則設計模式 - levelup

banq發表於2022-02-10

通過編寫if else條件語句來驗證物件是軟體開發中的一項常見任務。

想象一下,開發人員收到了以下檔案驗證要求:

  • 只允許txt和html副檔名。
  • txt 檔案的大小不能超過 5 MB。
  • html 檔案的大小不能超過 10 MB。
  • 檔名不能超過 50 個字元。

以下是如何在程式碼中實現這些需求:

public class FileValidator
{
    public bool IsValid(FileInfo file)
    {
        if (file.Extension != "txt" && file.Extension != "html")
            return false;
        else if (file.Extension == "txt" && file.Length > 5 * 1024 * 1024)
            return false;
        else if (file.Extension == "html" && file.Length > 10 * 1024 * 1024)
            return false;
        else if (file.FullName.Length > 50)
            return false;

        return true;
    }
}

需求第二天可能會有所變化,這是正常情況。客戶可以要求開發者為高階使用者關閉檔名長度檢查,也可以根據管理員頁面上的設定實現啟用或禁用檔案大小檢查的功能。

public class FileValidator
{
    public bool IsValid(FileInfo file, User user, Config config)
    {
        if (file.Extension != "txt" && file.Extension != "html")
            return false;
        else if (config.CheckFileSize && file.Extension == "txt" && file.Length > 5 * 1024 * 1024)
            return false;
        else if (config.CheckFileSize && file.Extension == "html" && file.Length > 10 * 1024 * 1024)
            return false;
        else if (file.FullName.Length > 50 && user.Status != UserStatus.Premium)
            return false;

        return true;
    }

此程式碼有幾個優點和缺點。讓我們分析它們,看看它是否需要重構,或者我們是否可以讓實現保持原樣。

好處:

  • 實現很簡單,所以每個熟悉 if-else 語句的開發人員都可以快速讀寫這段程式碼。

缺點:

  • 該實現不可擴充套件或不可維護。目前,我們只有 3 個檔案驗證規則(大小、副檔名和檔名)和 2 個影響驗證邏輯的附加條件(使用者狀態和配置)。但在實際系統中,這些數字可能會增長到數十甚至數百。
  • 程式碼不可重用。方法發展得越多,在新的上下文中重用它就越困難。條件語句中的單個謂詞根本不能重用。想象一下,有人需要在應用程式的另一部分重用這個確切的謂詞:file.FullName.Length > 50。最有可能的是,這行程式碼將被複制,因為開發人員只是害怕重構此程式碼以避免迴歸錯誤。
  • 該實現很難進行單元測試,因為有很多分支,所以有些很容易錯過。
  • 圈複雜度會很高,因此程式碼可能無法通過客戶或專案架構師定義的質量閾值。

複雜的條件實現有許多缺點,可以通過重構設計規則來避免。

 

重構規則模式

規則設計模式幫助開發人員將每個業務規則封裝在一個單獨的物件中,並將業務規則的定義與其處理分離。無需修改其餘應用程式邏輯即可新增新規則。

第一步是定義IFileValidationRule介面,該介面有一個IsValid方法,它接受一個FileInfo引數並返回一個布林值。然後我們簡單地為每個檔案驗證規則實現一個單獨的類:FileExtensionValidationRule, FileSizeForExtensionValidationRule, 和MaxFileLengthValidationRule。

public interface IFileValidationRule
{
    bool IsValid(FileInfo info);
}

public class FileExtensionValidationRule : IFileValidationRule
{
    private string[] AllowedExtensions { get; set; }

    public FileExtensionRule(string[] extensions) => AllowedExtensions = extensions;

    public bool IsValid(FileInfo info)
    {
        return AllowedExtensions.Contains(info.Extension);
    }
}

public class FileSizeForExtensionValidationRule : IFileValidationRule
{
    private string Extension { get; set; }
    private long Bytes { get; set; }

    public FileSizeForExtensionRule(string extension, long bytes)
    {
        Extension = extension;
        Bytes = bytes;
    }

    public bool IsValid(FileInfo info)
    {
        if (info.Extension != Extension) return true;
        return info.Length <= Bytes;
    }
}

public class MaxFileLengthValidationRule : IFileValidationRule
{
    private long Length { get; set; }
    
    public MaxFileLengthRule(long length) => Length = length;

    public bool IsValid(FileInfo info) => info.Length <= Length;
}

然後,開發人員只需要建立一個驗證規則的列表,並通過每個規則驗證 FileInfo 物件。

public class FileValidator
{
    public User User { get; set; }
    public AdminConfig AdminConfig { get; set; }

    public FileValidator(User user, AdminConfig config)
    {
        User = user;
        AdminConfig = config;
    }

    public bool IsValid(FileInfo fileInfo)
    {
        var rules = new List<IFileValidationRule> { new FileExtensionRule(new string[] { "txt", "html" }) };

        if (AdminConfig.CheckFileSize)
        {
            rules.Add(new FileSizeRule("txt", 5 * 1024 * 1024));
            rules.Add(new FileSizeRule("html", 10 * 1024 * 1024));
        }

        if (User.Status != UserStatus.Premium)
        {
            rules.Add(new MaxFileLengthRule(50));
        }

        bool isValid = rules.All(rule => rule.IsValid(fileInfo));
        return isValid;
    }
}

建立規則的責任可以放在一個單獨的工廠類中。

規則設計模式,像其他解決方案一樣,有其優點和缺點。

優點。

  • 驗證規則可以簡單地根據各種條件,如配置設定、使用者偏好、執行時間等,以任何順序動態地組成。
  • 驗證規則物件可以簡單地在應用程式的不同部分重複使用。
  • 新增新的驗證規則就像向集合中新增一個新的物件一樣簡單。
  • 單元測試很容易,因為與單體條件語句相比,驗證規則物件更小。
  • 與單片式條件運算子相比,迴圈複雜度指標會更好。

劣勢。

  • 對於新手開發者來說不是一個明顯的選擇。

相關文章