Здесь мы рассмотрим различные способы проведения валидации, что такое контекстная валидация и почему она превосходит все остальные методы.
Вам также может понравиться:
Проверка в приложениях Java
Контекстно-независимая валидация, привязанная к модели данных
Большинство современных фреймворков вынуждают нас, пользователей, проверять модель данных. По крайней мере, режим по умолчанию для большинства из нас — просто привязать правила проверки к конкретным полям в модели данных. Что не так с этим подходом?
Рассмотрим пример, когда гость регистрирует новый заказ на доставку еды. Компания, которая стоит за этой услугой, которая на самом деле готовит заказ, называется SuperFood. Вот как выглядит вся пользовательская история:
Вася в качестве гостя заходит на сайт SuperFood и регистрирует там заказ.
Бэкэнд-сервис SuperFood должен обеспечить несколько ограничений, прежде чем помещать вещи в базу данных. Один из них — убедиться, что передан либо адрес электронной почты, либо номер телефона.
Теперь предположим, что другой пользователь регистрирует заказ в SuperFood, но на этот раз через какой-либо сервис-агрегатор назовите его AggreA. Этот порядок не сильно отличается от того, который зарегистрирован на сайте SuperFood, хотя ограничения, которые должны соблюдаться, отличаются. Например, передача номера телефона необходима для этого агрегатора, а электронная почта не обязательна.
Теперь третий пользователь регистрирует заказ в SuperFood, и она делает это через какой-то еще агрегаторный сервис AggreB. И для этого, передача номера телефона не нужна, но электронная почта является обязательным.
Итак, у нас следующая ситуация. У меня есть одна модель данных для заказа, но есть как минимум три контекста с другим набором ограничений. Я могу пойти традиционным путем: представить сущность, соответствующую строке базы данных, и наложить эти ограничения с помощью аннотаций или файлов конфигурации или любым другим способом, к которому я привык. Проверка модели данных поддерживает следующий подход:
Джава
1
2
public class User
3
{
4
private String phoneNumber;
5
private String email;
7
// ...
9
}
Пользовательская аннотация в ValidContactInfo
конечном итоге приводит нас к пользовательскому валидатору , что-то вроде ContactInfoValidator
. Его четкая реализация отражает ментальную модель product-manager, которая выглядит следующим образом (в псевдокоде):
Джава
xxxxxxxxxx
1
If order is being registered through site, then either email or phone number must be present.
2
If order is being registered through AggreA, phone is required.
3
If order is being registered through AggreB, email is required.
Основная задача - как- то выяснить, в чем конкретно заключается конкретный сценарий, в котором он работает.
Способ модели данных подразумевает, что мы должны делать это в валидаторе, беря поля из объекта данных объекта. Таким образом, я считаю, что это наименее приемлемый способ, поскольку мы не можем использовать возможности модели предметной области и вынуждены постоянно размещать логику проверки в классах обслуживания . Упрощенно это выглядит примерно так:
Джава
xxxxxxxxxx
1
public class ContactInfoValidator
2
{
3
public boolean isValid(Order order)
4
{
5
if (order.getSource.equals(new Site())) {
6
return order.getPhone() != null || order.getEmail() != null;
7
} else if (order.getSource.equals(new AggreA())) {
8
return order.getPhone() != null;
9
} else if (order.getSource.equals(new AggreB())) {
10
return order.getEmail() != null;
11
}
12
throw new Exception("Unknown source given");
14
}
15
}
Теперь представьте беспорядок, в который превращается код проверки, если запрос становится более или менее сложным.
Возможно лучше: контекстно-независимая проверка в доменных объектах
Часто логика проверки, показанная в предыдущем примере, выходит из-под контроля. В этом случае, возможно, было бы более выгодно поместить его в объект домена, отвечающий за бизнес-логику. Кроме того, традиционно, это код домена, который веб-разработчики обычно сначала тестируют. Это может выглядеть следующим образом (ум именование: я переименовал Order
в OrderFromRequest
подчеркнуть разницу между ним и порядком домена):
Джава
xxxxxxxxxx
1
public class DomainOrder
2
{
3
public DomainOrder(OrderFromRequest orderFromRequest, HttpTransport httpTransport, Repository repository)
4
{
5
// set private fields
6
}
7
public boolean register()
9
{
10
if (this.isRegisteredThroughSite() && this.isValidForRegistrationThroughSite()) {
11
// business logic 1
12
} else if (this.isRegisteredThroughAggreA() && this.isValidForRegistrationThroughAggreA()) {
13
// business logic 2
14
} else if (this.isRegisteredThroughAggreB() && this.isValidForRegistrationThroughAggreB()) {
15
// business logic 3
16
}
17
}
18
private boolean isRegisteredThroughSite()
20
{
21
return orderFromRequest.getSource.equals(new Site());
22
}
23
private boolean isValidForRegistrationThroughSite()
25
{
26
return orderFromRequest.getPhone() != null || orderFromRequest.getEmail() != null;
27
}
28
}
Но возникает проблема сбора ошибок и их отображения в пользовательском интерфейсе. Насколько мне известно, не существует чистого решения для этого.
Контекстная проверка, специфичная для конкретной пользовательской истории
Для меня валидация служит ясной цели: рассказать клиентам, что именно не так с их запросами. Но что именно должно пойти на проверку? Это зависит от вашего взгляда на модель домена. На мой взгляд, объекты в доменной модели представляют собой независимые от контекста «вещи», которые могут быть организованы конкретным сценарием любым возможным способом. Они не содержат никаких специфических для контекста ограничений. Они проверяют только универсальные правила, те, которые просто должны быть правдой, иначе эта вещь просто не может быть этой вещью. Это отражает всегда действительный подход, когда вы просто не можете создать объект в недопустимом состоянии.
Например, существует такая вещь, как идентификатор курьера. Он может состоять только из значения UUID. И я определенно хочу убедиться, что это так. Обычно это выглядит следующим образом:
Джава
xxxxxxxxxx
1
public class CourierId
2
{
3
private String uuid;
4
public CourierId(String uuid)
6
{
7
if (/*not uuid*/) {
8
throw new Exception("uuid is invalid");
9
}
10
this.uuid = uuid;
12
}
13
}
Представить свой собственный интерфейс UUID с парой реализаций было бы еще лучше:
Джава
xxxxxxxxxx
1
public class FromString implements CourierId
2
{
3
private UUID uuid;
4
public FromString(UUID uuid)
6
{
7
this.uuid = uuid;
8
}
9
public String value()
11
{
12
return this.uuid.value();
13
}
14
}
Как правило, инварианты модели предметной области довольно просты и просты. Все остальные, более сложные контекстно-зависимые проверки принадлежат конкретному контроллеру (или службе приложений, или пользовательской истории). Вот где Валидол пригодится. Сначала вы можете проверить основные проверки, связанные с форматом, и приступить к любым сложным.
Пример контекстной проверки
Рассмотрим следующий запрос JSON:
JSON
xxxxxxxxxx
1
{
2
"delivery":{
3
"where":{
4
"building":1,
5
"street":"Red Square"
6
}
7
}
8
}
Проверка может выглядеть так:
Джава
xxxxxxxxxx
1
new FastFail<>(
2
new WellFormedJson(
3
new Unnamed<>(Either.right(new Present<>(this.jsonRequestString)))
4
),
5
requestJsonObject ->
6
new UnnamedBlocOfNameds<>(
7
List.of(
8
new FastFail<>(
9
new IsJsonObject(
10
new Required(
11
new IndexedValue("delivery", requestJsonObject)
12
)
13
),
14
deliveryJsonObject ->
15
new NamedBlocOfNameds<>(
16
"delivery",
17
List.of(
18
new FastFail<>(
19
new IndexedValue("where", deliveryJsonObject),
20
whereJsonElement ->
21
new AddressWithEligibleCourierDelivery<>(
22
new ExistingAddress<>(
23
new NamedBlocOfNameds<>(
24
"where",
25
List.of(
26
new AsString(
27
new Required(
28
new IndexedValue("street", whereJsonElement)
29
)
30
),
31
new AsInteger(
32
new Required(
33
new IndexedValue("building", whereJsonElement)
34
)
35
)
36
),
37
Where.class
38
),
39
this.httpTransport
40
),
41
this.dbConnection
42
)
43
)
44
),
45
CourierDelivery.class
46
)
47
)
48
),
49
OrderRegistrationRequestData.class
50
)
51
)
52
.result();
Я допускаю, что это может выглядеть страшно для любого, кто видит код впервые и совершенно не знаком с доменом. Не бойся, все не так сложно. Давайте рассмотрим, что происходит, строка за строкой.
Lines 1-4
: проверьте, представляют ли данные запроса ввода правильно сформированный JSON. В противном случае быстро произойдет сбой и вернется соответствующая ошибка
Line 5
: в случае правильно сформированного JSON вызывается замыкание и передаются данные JSON.
Line 6
: Структура JSON проверена. Структура более высокого уровня - это неназванный блок именованных сущностей. Это очень похоже на Map
.
Line 7
: Подразумевается список с одним именованным блоком.
Line 11
Это называется delivery
.
Line 10
Требуется
Line 9
: Он должен представлять объект JSON.
Line 14
: Если все предыдущие условия выполнены, вызывается закрытие. В противном случае, все это происходит быстро и возвращает соответствующую ошибку.
Line 15
: Именованный блок delivery
состоит из других именованных объектов.
Line 19
А именно: where
блокировать. Это не обязательно, хотя.
Line 20
: Если он присутствует, вызывается закрытие.
Line 23
: Именованный блок where
состоит из других именованных объектов.
Line 28
: А именно, street
что ...
Line 27
: … требуется;
Line 26
: и представляется в виде строки.
Line 33
и building
что ...
Line 32
также требуется;
Line 31
: и должен быть представлен как целое число.
Line 37
: если все предыдущие проверки успешны, создается объект класса Where
. Если честно, это не полноценный объект. Это просто структура данных с удобным, подсказанным типом и автозаполненным IDE доступом к ее полям.
Line 22
: если основные проверки пройдены, адрес гарантированно существует. Следите за вторым аргументом httpTransport
. Это для запроса какой-либо сторонней службы, которая проверяет существование адреса.
Line 21
: Aaa, и, наконец, мы хотим, чтобы курьерская доставка была включена в этой области. Для этого нам понадобится доступ к базе данных, отсюда и dbConnection
аргумент.
Line 45
: Если все было в порядке, CourierDelivery
объект создан. У него есть один аргумент, Where
класс.
Line 49
Наконец, OrderRegistrationRequestData
объект создан и возвращен.
Я намеренно поместил весь проверяющий код в один класс. Если структура данных действительно сложная, я бы рекомендовал создать класс для каждого блока. Проверьте пример здесь .
Заключение
Вот и все. Такой подход может (и на самом деле так выглядит) выглядеть излишним с таким простым запросом, хотя он лучше подходит для более сложных запросов.
Если вам интересно, что еще может делать библиотека Validol, проверьте документацию . Хорошее место для начала - это краткое руководство .