Я знаю, что вы думаете (или, возможно, кричите), "не другой вопрос, спрашивающий, где валидация относится к многоуровневой архитектуре?!?" Ну, да, но, надеюсь, это будет немного другой взгляд на эту тему.
Я твердо убежден в том, что валидация принимает разные формы, основана на контексте и варьируется на каждом уровне архитектуры. Это основа для пост - помогает определить, какой тип проверки должен быть выполнен на каждом уровне. Кроме того, часто возникает вопрос: где принадлежат проверки авторизации?
Пример сценария происходит из приложения для кейтерингового бизнеса. Периодически в течение дня водитель может сдавать в офис любые лишние денежные средства, накопленные им при доставке грузовика с места на место. Приложение позволяет пользователю записать «денежное сбрасывание» путем сбора идентификатора водителя и суммы. Вот некоторый скелетный код для иллюстрации задействованных слоев:
public class CashDropApi // This is in the Service Facade Layer
{
[WebInvoke(Method = "POST")]
public void AddCashDrop(NewCashDropContract contract)
{
// 1
Service.AddCashDrop(contract.Amount, contract.DriverId);
}
}
public class CashDropService // This is the Application Service in the Domain Layer
{
public void AddCashDrop(Decimal amount, Int32 driverId)
{
// 2
CommandBus.Send(new AddCashDropCommand(amount, driverId));
}
}
internal class AddCashDropCommand // This is a command object in Domain Layer
{
public AddCashDropCommand(Decimal amount, Int32 driverId)
{
// 3
Amount = amount;
DriverId = driverId;
}
public Decimal Amount { get; private set; }
public Int32 DriverId { get; private set; }
}
internal class AddCashDropCommandHandler : IHandle<AddCashDropCommand>
{
internal ICashDropFactory Factory { get; set; } // Set by IoC container
internal ICashDropRepository CashDrops { get; set; } // Set by IoC container
internal IEmployeeRepository Employees { get; set; } // Set by IoC container
public void Handle(AddCashDropCommand command)
{
// 4
var driver = Employees.GetById(command.DriverId);
// 5
var authorizedBy = CurrentUser as Employee;
// 6
var cashDrop = Factory.CreateCashDrop(command.Amount, driver, authorizedBy);
// 7
CashDrops.Add(cashDrop);
}
}
public class CashDropFactory
{
public CashDrop CreateCashDrop(Decimal amount, Employee driver, Employee authorizedBy)
{
// 8
return new CashDrop(amount, driver, authorizedBy, DateTime.Now);
}
}
public class CashDrop // The domain object (entity)
{
public CashDrop(Decimal amount, Employee driver, Employee authorizedBy, DateTime at)
{
// 9
...
}
}
public class CashDropRepository // The implementation is in the Data Access Layer
{
public void Add(CashDrop item)
{
// 10
...
}
}
Я указал 10 мест, где я видел проверки в коде. Мой вопрос заключается в том, какие проверки вы, если таковые имеются, выполняете для каждого из следующих бизнес-правил (наряду со стандартными проверками длины, диапазона, формата, типа и т. Д.):
- Сумма сброса должна быть больше нуля.
- Денежный перевод должен иметь действующего водителя.
- Текущий пользователь должен быть авторизован для добавления денежных средств (текущий пользователь не является водителем).
Пожалуйста, поделитесь своими мыслями, как у вас есть или будет подходить к этому сценарию и причины вашего выбора.
источник
CashDropAmount
значения, используя объект-значение вместо использованияDecimal
. Проверка наличия или отсутствия драйвера будет выполнена в обработчике команд, и то же самое касается правил авторизации. Вы можете получить авторизацию бесплатно, выполнив что-то наподобие того,Approver approver = approverService.findById(employeeId)
где она выдает, если сотрудник не в роли утверждающего.Approver
будет просто объект значения, а не сущность. Вы также могли бы избавиться от фабрики или использования фабричного метода на AR вместо:cashDrop = driver.dropCash(...)
.Ответы:
Я согласен, что то, что вы проверяете, будет различным на каждом уровне приложения. Обычно я проверяю только то, что требуется для выполнения кода в текущем методе. Я пытаюсь рассматривать базовые компоненты как черные ящики и не проверяю, как эти компоненты реализованы.
Так, например, в вашем классе CashDropApi я бы только проверял, что «контракт» не является нулевым. Это предотвращает исключения NullReferenceExceptions и все, что необходимо для правильной работы этого метода.
Я не знаю, что я проверял бы что-либо в классе обслуживания или команд, и обработчик проверял бы только то, что «команда» не является нулевой по тем же причинам, что и в классе CashDropApi. Я видел (и делал) валидацию в обоих направлениях по отношению к фабрике и классам сущностей. Один или другой - то, где вы хотите проверить значение «суммы», и что другие параметры не равны нулю (ваши бизнес-правила).
Хранилище должно проверять только то, что данные, содержащиеся в объекте, соответствуют схеме, определенной в вашей базе данных, и операция daa будет выполнена успешно. Например, если у вас есть столбец, который не может быть нулевым или имеет максимальную длину, и т. Д.
Что касается проверки безопасности, я думаю, что это действительно вопрос намерений. Поскольку правило предназначено для предотвращения несанкционированного доступа, я бы хотел выполнить эту проверку как можно раньше, чтобы уменьшить количество ненужных шагов, которые я предпринял, если пользователь не авторизован. Я бы, наверное, положил его в CashDropApi.
источник
Ваше первое бизнес-правило
выглядит как инвариант вашей
CashDrop
сущности и вашегоAddCashDropCommand
класса. Есть несколько способов, которыми я применяю такой инвариант:Ваше второе правило имеет более широкий характер (в свете деталей, указанных в вопросе): действительно ли означает, что у объекта «Водитель» есть флаг, указывающий, что он может управлять автомобилем (то есть не имеет приостановленных водительских прав), означает ли это, что водитель был на самом деле работает в тот день или это просто означает, что идентификатор DriverId, переданный в CashDropApi, действителен в хранилище сохраняемости.
В любом из этих случаев вам нужно будет перемещаться по модели вашего домена и получать
Driver
экземпляр от вашегоIEmployeeRepository
, как вы это делаетеlocation 4
в своем примере кода. Итак, здесь вы должны убедиться, что вызов репозитория не возвращает ноль, и в этом случае ваш driverId был недействительным, и вы не можете продолжить обработку дальше.Для других 2 (моих гипотетических) проверок (есть ли у водителя действительные водительские права, был ли водитель сегодня работающим) вы выполняете бизнес-правила.
Здесь я обычно использую коллекцию классов валидаторов, которые работают с сущностями (точно так же, как шаблон спецификации из книги Эрика Эванса «Дизайн, управляемый доменом»). Я использовал FluentValidation для создания этих правил и валидаторов. Затем я могу составить (и, следовательно, повторно) более сложные / более полные правила из более простых правил. И я могу решить, какие слои в моей архитектуре использовать. Но у меня все они закодированы в одном месте, а не разбросаны по всей системе.
Ваше третье правило относится к сквозной проблеме: авторизация. Поскольку вы уже используете контейнер IoC (при условии, что ваш контейнер IoC поддерживает перехват методов), вы можете выполнить некоторую AOP . Напишите приложение, которое выполняет авторизацию, и вы можете использовать свой контейнер IoC для внедрения этого режима авторизации там, где это необходимо. Большим преимуществом здесь является то, что вы однажды написали логику, но вы можете повторно использовать ее в своей системе.
Чтобы использовать перехват через динамический прокси (Castle Windsor, Spring.NET, Ninject 3.0 и т. Д.), Ваш целевой класс должен реализовать интерфейс или наследовать от базового класса. Вы должны были бы перехватить перед вызовом целевого метода, проверить авторизацию пользователя и запретить переход вызова к фактическому методу (сгенерировать исключение, журнал, вернуть значение, указывающее сбой или что-то еще), если у пользователя нет правильные роли для выполнения операции.
В вашем случае вы можете перехватить вызов либо
Проблемы здесь, может быть,
CashDropService
не могут быть перехвачены, потому что нет интерфейса / базового класса. ИлиAddCashDropCommandHandler
не создается вашим IoC, поэтому ваш IoC не может создать динамический прокси для перехвата вызова. Spring.NET имеет полезную функцию, позволяющую нацеливать метод на класс в сборке с помощью регулярного выражения, поэтому это может сработать.Надеюсь, что это дает вам некоторые идеи.
источник
Для правил:
Я бы сделал проверку в местоположении (1) для бизнес-правила (1) и удостоверился, что идентификатор не является нулевым или отрицательным (при условии, что ноль действителен) в качестве предварительной проверки для правила (2). Причиной является мое правило: «Не пересекайте границу слоя с неверными данными, которые вы можете проверить с помощью доступной информации». Исключением из этого является то, что служба выполняет проверку как часть своего долга перед другими абонентами. В этом случае будет достаточно провести валидацию только там.
Для правил (2) и (3) это должно быть сделано на уровне доступа к базе данных (или на самом уровне db) только потому, что оно включает доступ к db. Нет необходимости намеренно перемещаться между слоями.
В частности, можно избежать правила (3), если мы позволим графическому интерфейсу не допускать посторонних пользователей нажатия кнопки, позволяющей использовать этот сценарий. Хотя это сложнее кодировать, это лучше.
Хороший вопрос!
источник