Java中的上下文验证

在这里,我们将考虑执行验证的不同方法,什么是上下文验证以及为什么它胜过所有其他方法。
绑定到数据模型的上下文无关的验证
当前大多数框架都迫使我们及其用户将验证放入数据模型中。至少对于我们大多数人来说,默认模式是将验证规则简单地绑定到数据模型中的特定字段。这种方法有什么问题?
考虑一个示例,其中客人注册了新的食物交付订单。该服务背后的公司实际上是在做饭,称为SuperFood。这就是整个用户故事的样子:
SuperFood的后端服务在将内容放入数据库之前必须确保多个约束。其中一项是确保传递电子邮件或电话号码。
现在,假设另一个用户在SuperFood中注册了一个订单,但是这次通过某种聚合服务,将其称为AggreA。尽管要执行的约束条件有所不同,但该命令与在SuperFood网站上注册的命令没有太大不同。例如,传递电话号码对于该聚合器是必不可少的,而电子邮件是可选的。
现在,第三个用户在SuperFood中注册了一个订单,然后她通过其他聚合服务AggreB进行了订购。对于那个,不需要传递电话号码,但是电子邮件是必须的。
因此,我们有以下情况。我有一个订单的单个数据模型,但是至少有三个带有不同约束集的上下文。我可以采用传统方式:引入一个与数据库行相对应的实体,并通过注释或配置文件或我习惯的任何方式施加这些约束。数据模型验证支持以下方法:
Java

1
@ValidContactInfo
2
public class User
3
{
4
private String phoneNumber;
5
6
private String email;
7
8
// …
9
}
自定义注解ValidContactInfo最终将我们带到自定义验证器,例如ContactInfoValidator。它最清晰的实现反映了产品经理的心智模型,它类似于以下内容(使用伪代码):
Java

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.
主要目的是找出在某种程度上它究竟是什么范围内操作的具体方案。
数据模型方式意味着我们应该在验证器中执行此操作,并从实体数据对象中获取字段。这样一来,我相信它是最不受欢迎的一种,因为我们无法使用域模型的强大功能,而必须将验证逻辑驻留在服务类中。简化后,大致如下所示:
Java

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
13
throw new Exception(“Unknown source given”);
14
}
15
}
现在,想象一下,如果请求变得越来越复杂,您的验证代码就会变成一团糟。
可以说是更好的:域对象中与上下文无关的验证
通常,上一个示例中显示的验证逻辑会失控。在这种情况下,将其放在负责业务逻辑的域对象中可能会更有利。此外,传统上,它是Web开发人员倾向于首先进行测试的域代码。它可能看起来像以下(记的命名:我改名Order,以OrderFromRequest强调它和域秩序之间的差异):
Java

1
public class DomainOrder
2
{
3
public DomainOrder(OrderFromRequest orderFromRequest, HttpTransport httpTransport, Repository repository)
4
{
5
// set private fields
6
}
7
8
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
19
private boolean isRegisteredThroughSite()
20
{
21
return orderFromRequest.getSource.equals(new Site());
22
}
23
24
private boolean isValidForRegistrationThroughSite()
25
{
26
return orderFromRequest.getPhone() != null || orderFromRequest.getEmail() != null;
27
}
28
}
但是出现了收集错误并将其映射到UI的问题。据我所知,没有干净的解决方案。
特定于具体用户故事的上下文验证
对我来说,验证有一个明确的目的:告诉客户他们的要求到底出了什么问题。但是究竟应该去验证什么呢?这取决于您对域模型的看法。我认为,域模型中的对象表示上下文无关的“事物”,可以由特定方案以任何可能的方式对其进行编排。它们没有任何特定于上下文的约束。他们只检查通用规则,那些规则必须是真实的,否则,那东西根本不可能是那样。当您根本无法创建处于无效状态的对象时,这反映了一种始终有效的方法。
例如,有快递ID之类的东西。它只能包含UUID值。而且,我绝对希望确保确实如此。通常如下所示:
Java

1
public class CourierId
2
{
3
private String uuid;
4
5
public CourierId(String uuid)
6
{
7
if (/not uuid/) {
8
throw new Exception(“uuid is invalid”);
9
}
10
11
this.uuid = uuid;
12
}
13
}
引入自己的UUID接口以及一些实现会更好:
Java

1
public class FromString implements CourierId
2
{
3
private UUID uuid;
4
5
public FromString(UUID uuid)
6
{
7
this.uuid = uuid;
8
}
9
10
public String value()
11
{
12
return this.uuid.value();
13
}
14
}
通常,领域模型不变式非常基本和简单。所有其他更复杂的上下文特定检查都属于特定控制器(或应用程序服务或用户故事)。这就是Validol派上用场的地方。您可以先检查与格式相关的基本验证,然后进行复杂的验证。
上下文验证示例
考虑以下JSON请求:
JSON

1
{
2
“delivery”:{
3
“where”:{
4
“building”:1,
5
“street”:“Red Square”
6
}
7
}
8
}
验证可能如下所示:
Java

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:Aaaand,最后,我们要确保在该区域启用快递。为此,我们需要数据库访问权限,因此需要dbConnection论证。
Line 45:如果一切正常,CourierDelivery则创建一个对象。它只有一个参数,一个Where类。
Line 49:最后,OrderRegistrationRequestData创建并返回对象。
我有意将所有验证代码放在一个类中。如果数据结构确实很复杂,建议您为每个块创建一个类。在此处查看示例。
结论
就是这样。通过这样一个简单的请求,这种方法可能(实际上确实)看起来像是过分杀伤,尽管在更复杂的请求中却显得尤为突出。
最后,开发这么多年我也总结了一套学习Java的资料与面试题,如果你在技术上面想提升自己的话,可以关注我,私信发送领取资料或者在评论区留下自己的联系方式,有时间记得帮我点下转发让跟多的人看到哦。在这里插入图片描述

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值