Dec 3, 2009

Явные роли – валидация

В тему предыдущего поста о явных ролях в приложениях можно рассмотреть вопрос валидации. Каждый объект может быть валдиным или нет в зависимости от сценария, в котором он участвует в данный момент. Например правило – пользователь может стать избранным, если он сделал 6 и более заказов. Там приложение контролировало это в самом методе MakePreferred и выбрасывало исключение, если пользователь не удовлетворяет условию.

Не смотря на кажущуюся правильность данного подхода, проверка - это явно другая концепция, которая относится к валидации и должна быть отдельной ролью в приложении.

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

Рассмотрим что это будет значить в рамках подхода явного разделения ответственности между классами. Первое, что стоит учитывать, это что одна и та же сущность может быть валидной или нет, в зависимости от контекста (роли) в данный момент. Пользователь вполне может подходить для создания нового заказа, но не может при этом стать избранным.

Ранее в своих проектах подобные проверки я выполнял прямо перед тем как попытаться выполнить действие, т.е. код выглядел примерно следующим образом:

public void btnMakePreferred_Click(Object sender, EventArgs e)
{
    Customer customer = ORM.Get<Customer>(customerId);
    if(customer.Orders.Count > 5)
    {
        customer.MakePreferred();
    }
}

И это хорошо если он выглядит именно так, т.е. MakePreferred вызывается у самого объекта, тем самым давая возможность относительно легко изменить правило определения избранного пользователя. А иногда бывает и такое:

public void btnMakePreferred_Click(Object sender, EventArgs e)
{
    Customer customer = ORM.Get<Customer>(customerId);
    if(customer.Orders.Count > 5)
    {
        customer.IsPreferred = true;
    }
}

По собственному опыту я знаю что такой код встречается в проектах слишком часто.

Что же можно сделать, чтобы явно указать, что для валидации в данном контексте должно использоваться определенное бизнес правило? Ответ прост – сделать роли явными.

Для этого выделим следующий интерфейс (что из себя представляют Entity и IRole описано в предыдущем посте):

public interface IValidator<TEntity, TRole> where TEntity: Entity
                                            where TRole:   IRole
{
    bool IsValid(TRole entity);
}

Он отвечает на единственный порос, походит ли объект для данной роли. И рассмотрим конкретную реализацию:

public class MakeCustomerPreferredValidator: IValidator<Customer, IPreferredMaker>
{
    public bool IsValid(IPreferredMaker entity)
    {
        Customer customer = entity as Customer;
        if(customer == null)
        {
            throw new ArgumentException("Entity must be Customer", "entity");
        }

        return customer.Orders.Count > 5;
    }
}

При реализации мы точно знаем что валидировать нам придется именно сущность Customer, и можем привести тип, если же был передан не подходящий параметр, то выбросить исключение.

Теперь о том как использовать этот класс, первый вариант (плохой):

IValidator<Customer, IPreferredMaker> validator = new MakeCustomerPreferredValidator();
if (validator.IsValid(preferredMaker))
{
    preferredMaker.MakePreferred();
}

Плохой потому что в этом случае, существует явная зависимость от типа MakeCustomerPreferredValidator. Мне больше нравится для этих целей некоторый Factory класс, который может быть реализован следующим образом:

public class ValidatorsFactory
{
    public IValidator<TEntity, TRole> GetValidator<TEntity, TRole>() where TEntity : Entity
                                                                     where TRole : IRole
    {
        var validators = from t in Assembly.GetAssembly(typeof(ValidatorsFactory)).GetTypes()
                         where t.GetInterfaces().Contains(typeof(IValidator<TEntity, TRole>))
                         select t;

        return (IValidator<TEntity, TRole>) validators.SingleOrDefault();
    }
}

Но я бы рекомендовал использовать любой Service Locator для этих целей.

С использованием его, код, который сделает покупателя избранным будет выглядеть следующим образом:

Repository<Customer> customerRepository = new Repository<Customer>(Global.CurrentSession);
ValidatorsFactory factory = new ValidatorsFactory();

IValidator<Customer, IPreferredMaker> validator = factory.GetValidator<Customer, IPreferredMaker>();
IPreferredMaker preferredMaker = customerRepository.Get<IPreferredMaker>(101);

if(validator.IsValid(preferredMaker))
{
    preferredMaker.MakePreferred();
}

Такой подход обеспечивает явное разделение концепций в приложении, и дает возможность явно обязать разработчиков не разбрасывать код в совершенно неожиданных местах.

Исходный код.

2 comments:

  1. "Валидации пользовательского ввода, и бизнес правил".
    А что делать, когда валидация пользовательского ввода совпадает с бизнесс правилами? (Что на моем предыдущем проекте было довольно часто)

    ReplyDelete
  2. Если же они совпадают, то никто не мешает применить этот же подход. Можно выделить такую роль как ICustomerCreator, который будет содержать метод CreateCustomer, и в этом случае можно будет валидировать пользовательский ввод IValidator<Customer, ICustomerCreator>. Но это, конечно же, зависит доменной модели.

    ReplyDelete