Шаблоны проектирования: Rules

Сложная логика приложения часто приводит к нагромождению условных операторов в разных частях проекта. С точки зрения метрик ухудшаются такие показатели как Cyclomatic complexity и Maintainability index, с точки зрения принципов проектирования могут нарушаться SRP, OCP, DRY, а простыми словами — страдают читабельность кода и простота внесения в него изменений.

Шаблон проектирования Rules решает проблему, которая может выглядеть так:

if (/*условие*/)
{
    //код
}
else if (/*условие*/)
{
    //код
}
//еще 10+ else if'ов
else
{
    //код
}

или так:

if (/*условие*/)
{
    if (/*условие*/)
    {
        if (/*условие*/)
        {
             //код
        }
    }
    else if (/*условие*/)
    {
         //код
    }
    else
    {
        //код
    }
}
else
{
    //код
}


/*
рефакторинг вложенных условий при помощи 
шаблона Rules разберем в другом посте
*/

Для рефакторинга таких комплексных условий существует большое количество техник, выбор которых зависит от контекста. Если условные операторы проверяют состояние объекта, можно рефакторить к шаблону проектирования State. Условную логику, которая выбирает некоторый алгоритм, можно заменить шаблоном Стратегия. Но если условия используются для проверки некоторого входящего параметра только на соответствие большому количеству правил, то одним из хороших решений рефакторинга будет шаблон Rules.

Решение задачи без шаблона Rules

Есть элементарная задача — валидировать загружаемый в систему файл и выдавать пользователю все ошибки, если такие имеются.

Первым делом создадим класс ValidationResult, в котором будет храниться информация о результатах валидации файла.

public class ValidationResult
{
    public List<string> Errors { getset; } = new List<string>();
    public bool IsValid => Errors.Count == 0;
}
//для того, чтобы пример был максимально простым, здесь намеренно
 пропущена техника инкапсуляции коллекции

Теперь опишем сам метод валидации файла с тремя правилами: файл должен иметь расширение только .txt или .docx, файл не должен быть пустым и не быть тяжелее 1 мегабайта.

public class FileValidator
{
    public ValidationResult IsFileValid(FileInfo file)
    {
        var result = new ValidationResult();

        if (file.Extension != ".txt" && file.Extension != ".docx")
            result.Errors.Add("Extension is not supported");

        if (file.Length == 0)
            result.Errors.Add("File is empty");

        if (file.Length > 1000000)
            result.Errors.Add("File is too big");

        return result;
    }
}

Подобное решение наверное является самым простым из всех возможных, что есть его самым большим и единственным плюсом.

Недостаток решения заключается риске превратиться в спагетти-код или быть скопированным много раз целиком либо по частям. Представим, что определенной группе пользователей можно загружать пустые файлы. В метод IsFileValid придется передавать и проверять дополнительный параметр, который бы определял для кого именно валидируется файл. Подобных дополнительных параметров в реальных системах обычно очень много, в результате чего читабельность такого метода значительно усложнится. Другим решением будет скопировать метод и удалить из него проверку на пустой контент. В таком случае, при необходимости изменить правила валидации общие для всех пользователей, необходимо будет отыскать все дубликаты правил и изменить их.

Применение шаблона Rules

Суть шаблона Rules заключается в отделении правил валидации от логики по их обработке, путем инкапсуляции правил в отдельные классы.

Первым шагом будет объявления интерфейса, на основе которого будут реализованы конкретные правила валидации:

public interface IFileValidationRule
{
    ValidationResult IsValid(FileInfo file);
}

Затем вынесем условие проверки расширения файла в класс FileExtensionValidationRule.

public class FileExtensionValidationRule : IFileValidationRule
{
    public ValidationResult IsValid(FileInfo file)
    {
        var result = new ValidationResult();

        if (file.Extension != ".txt" && file.Extension != ".docx")
            result.Errors.Add("Extension is not supported");

        return result;
    }
}

Правила проверки файла на отсутствие контента и на превышение максимально допустимого размера вынесем в классы FileEmptyValidationRule и FileMaxSizeValidationRule соответственно.

public class FileEmptyValidationRule : IFileValidationRule
{
    public ValidationResult IsValid(FileInfo file)
    {
        var result = new ValidationResult();

        if (file.Length == 0)
            result.Errors.Add("File is empty");

        return result;
    }
}


public class FileMaxSizeValidationRule : IFileValidationRule
{
    public FileMaxSizeValidationRule(long maxSize) => _maxSize = maxSize;

    private readonly long _maxSize;

    public ValidationResult IsValid(FileInfo file)
    {
        var result = new ValidationResult();

        if (file.Length > _maxSize)
            result.Errors.Add("File is too big");

        return result;
    }
}

Метод IsFileValid теперь будет выглядеть так:

public class FileValidator
{
    private readonly List<IFileValidationRule> _rules;

    public FileValidator()
    {
        _rules = new List<IFileValidationRule>()
        {
            new FileExtensionValidationRule(),
            new FileEmptyValidationRule(),
            new FileMaxSizeValidationRule(1000000)
        };
    }

    public ValidationResult IsFileValid(FileInfo file)
    {
        var result = new ValidationResult();

        foreach (var rule in _rules)
        {
            var fileValidationResult = rule.IsValid(file);
            result.Errors.AddRange(fileValidationResult.Errors);
        }

        return result;
    }
}

Первым шагом здесь создается коллекция rules и заполняется нужными валидационными правилами. В foreach выполняется проверка файла на соответствие каждому из правил и результаты объединяются в единственный объект ValidationResult. Теперь при необходимости изменить валидацию для отдельной группы пользователей может быть создать другой класс, например SimpleFileValidator, в конструкторе которого компоновались бы только два правила. Другим решением изменения валидации было бы оставить единственный класс FileValidator для всех случаев, но вынести создание коллекции правил в фабрику, которая бы знала какие правила для кого создавать.

Плюсы и минусы использования шаблона Rules

После рефакторинга метода IsFileValid с hard-code условиями к шаблону Rules, кода стало только больше и условия разошлись из одного места по отдельным классам, но открылся ряд других преимуществ, а именно:

  • Правила валидации файла отделены от логики по их обработке (соблюдение принципа Open/Closed), в результате чего упрощается внесение в них изменений или добавление новых правил.
  • Каждое правило или группа тесно связанных правил инкапсулированы в отдельные классы (соблюдение принципа Single Responsibility). Это позволяет переиспользовать правила валидации сколько угодно раз в разных частях системы без необходимости прибегать к copy and paste programming.
  • Возможность компоновать правила динамически. Для каждого отдельного сценария можно компоновать определенный набор правил, менять их порядок, отключать/включать некоторое правило в зависимости от конфигурации и тд.
  • Отсутствие огромных лестниц if…else (уменьшение цикломатической сложности кода).

Почитать дальше

Шаблон проектирования Rules на pluralsight

Добавить комментарий

Заполните поля или щелкните по значку, чтобы оставить свой комментарий:

Логотип WordPress.com

Для комментария используется ваша учётная запись WordPress.com. Выход /  Изменить )

Google photo

Для комментария используется ваша учётная запись Google. Выход /  Изменить )

Фотография Twitter

Для комментария используется ваша учётная запись Twitter. Выход /  Изменить )

Фотография Facebook

Для комментария используется ваша учётная запись Facebook. Выход /  Изменить )

Connecting to %s