重构复杂条件的规则设计模式 - levelup


通过编写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;
    }
}

创建规则的责任可以放在一个单独的工厂类中。
规则设计模式,像其他解决方案一样,有其优点和缺点。
优点。

  • 验证规则可以简单地根据各种条件,如配置设置、用户偏好、运行时间等,以任何顺序动态地组成。
  • 验证规则对象可以简单地在应用程序的不同部分重复使用。
  • 添加新的验证规则就像向集合中添加一个新的对象一样简单。
  • 单元测试很容易,因为与单体条件语句相比,验证规则对象更小。
  • 与单片式条件运算符相比,循环复杂度指标会更好。

劣势。
  • 对于新手开发者来说不是一个明显的选择。