第四章、实现用例

第四章、实现用例

最后,让我们看看如何在实际代码中体现我们所讨论的架构。
由于在我们的架构中,应用层、网络层和持久化层是松散耦合的,我们可以 我们完全可以按照我们认为合适的方式对我们的领域代码进行建模。我们可以做DDD,我们可以实现一个丰富的或贫乏的领域模型,或者发明我们的领域模型。
贫乏的领域模型,或者发明我们自己的做事方式。

本章介绍了在六边形架构中实现用例的一种有主见的方式。架构风格中实现用例的一种观点,我们在前几章中已经介绍过。
正如以领域为中心的架构所适合的那样,我们将从一个领域实体开始,然后围绕它建立一个用例 围绕它建立用例。

实现领域模型

我们想实现从一个账户汇款到另一个账户的用例。以面向对象的方式建模的一种方法是创建一个允许提取和存款的帐户实体,以便我们可以从源账户取款并存入目标账户:

帐户实体提供实际帐户的当前快照。每次提取和存款都包含在活动实体中。因为总是加载所有内容是不明智的 一个帐户的活动进入内存,所以账户实体只保存最近几天或几周的活动窗口,在ActivityWindow值对象中捕获。

为了仍然能够计算当前的账户余额,账户实体还有一个属性baselineBalance,代表该账户在活动窗口的第一项活动之前的余额。然后,总余额是基线余额加上窗口中所有活动的余额。

有了这个模型,向账户取钱和存钱只是向活动窗口添加一个新的活动,就像在 withdraw() 和 deposit() 方法中所做的那样。在我们退出之前,我们要检查业务规则,即我们不能透支一个账户。
现在我们有了一个允许我们取款和存款的账户,我们可以向外移动 来建立一个围绕它的用例。

简而言之,这里的一个用例

First, let’s discuss what a use case actually does. Usually, it follows these steps:

  1. Take input

  2. Validate business rules

  3. Manipulate model state

  4. Return output

一个用例从一个传入的适配器获取输入。你可能想知道为什么我不把这一步叫做"验证输入"。答案是,我相信用例代码应该关心领域逻辑,我们不应该用输入验证来污染它。我们不应该用输入验证来污染它。因此,我们将在其他地方进行输入验证,我们就会看到。

但是,该用例负责验证业务规则。它与域实体共享此责任。我们将讨论输入验证和业务规则之间的区别 本章后面的整理

如果业务规则得到满足,用例就会以一种方式操纵模型的状态 或另一种方式来操作模型的状态。通常,它将改变一个领域对象的状态,并将这个新的 状态传递给持久化适配器实现的端口,以便被持久化。一个用例也可以调用任何 其他传出的适配器。

最后一步是将来自输出适配器的返回值转化为一个输出对象,该对象将被返回给调用适配器。

为了避免在第一章“层有什么问题?”,我们将为每个用例创建一个单独的服务类,而不是将所有用例放入单个service类

该服务实现了传入端口接口SendMoneyUseCase并调用传出端口 接口LoadAccountPort来加载一个账户,并调用端口UpdateAccountStatePort来保持 更新账户状态端口,以在数据库中保持更新的账户状态。图11给出了一个相关组件的图形概览。

服务实现一个用例,修改域模型并调用传出端口以持久化修改后的状态

验证输入

现在我们正在讨论验证输入,尽管我刚刚声称这不是一个用例类的责任。一个用例类的责任。但我仍然认为,它属于应用层,所以这里是
来讨论它。

为什么不让调用的适配器在将输入发送到用例之前进行验证?那么,我们是否要相信调用者已经验证了用例所需的一切吗?另外,该用例
可能会被一个以上的适配器调用,所以验证必须由每个适配器来实现。而其中一个可能会弄错或完全忘记它。

应用层应该关心输入验证,因为,否则,它可能会从应用核心之外得到无效的输入。而这可能会对我们模型的状态造成损害。
但是,如果不在用例类中,该把输入验证放在哪里呢?
我们将让输入模型来处理这个问题。对于 "寄钱 "用例,输入模型是 SendMoneyCommand类,我们已经在前面的代码例子中看到了。更准确地说,我们将 在构造函数中做到这一点

对于汇款,我们需要源账户和目标账户的ID,以及要汇款的金额。要转移的金额。所有的参数都不能为空,金额必须大于零。

如果这些条件中的任何一个被违反,我们就会在构造过程中抛出一个异常来拒绝创建对象。拒绝创建对象。
通过使SendMoneyCommand的字段为final,我们有效地使它成为不可变的。因此,一旦 构建成功后,我们就可以确定其状态是有效的,不会被改变成一些 无效的。

由于SendMoneyCommand是用例的API的一部分,它位于传入端口包中。
因此,验证仍然在应用程序的核心(在我们的六边形架构的六边形内),但不会污染神圣的用例代码

但是,当有工具可以为我们做肮脏的工作时,我们真的想手工实现每个验证检查吗?在Java世界中,这类工作的实际标准是BeanVal idation API²⁰.它允许我们将我们需要的验证规则作为一个类的字段上的注释来表示。

抽象类SelfValidating提供了方法validateSelf(),我们只需在构造函数中调用这个方法。我们只需在构造函数中调用该方法作为最后一条语句。这将评估字段上的Bean Validation注解 (@NotNull, in this case),如果违反了,就抛出一个异常。如果Bean Validation对于某个验证来说不够明确,我们仍然可以手工实现它,就像我们在检查金额是否大于0时做的那样。

自我验证类的实现可能是这样的:

通过输入模型中的验证,我们有效地创建了一个反腐层 围绕着我们的用例实现。这不是一个分层架构意义上的层。 而是在我们的用例周围有一个薄薄的、保护性的屏幕,它可以将坏的输入返回给调用者。

The Power of Constructors

我们上面的输入模型,SendMoneyCommand,把大量的责任交给它的构造函数。由于该类是不可变的,构造函数的参数列表包含了该类的每个属性的参数。由于构造函数也验证了参数,所以不可能创建一个状态无效的对象。

在我们的例子中,构造函数只有三个参数。如果我们有更多的参数呢?我们能不能使用构建器模式来使它更方便使用?我们可以把带有长参数列表的构造函数是私有的,并在构建器的 build() 方法中隐藏对它的调用。然后。 我们就可以像这样构建一个对象,而不是调用一个有20个参数的构造函数。

我们仍然可以让构造函数进行验证,这样构建器就不能构造具有无效状态的对象

为不同的用例提供不同的输入模式

验证业务规则

虽然验证输入不是用例逻辑的一部分,但验证业务规则肯定是。业务规则是应用程序的核心,应该适当谨慎地处理。但当 我们是否正在处理输入验证,以及何时正在处理业务规则?

两者之间一个非常实用的区别是,验证业务规则需要访问域模型的当前状态,而验证输入则不需要。输入验证可以很简单 声明式地增强,就像我们使用上面的@NotNull注释一样,而业务规则需要更多的上下文

丰富与贫乏的领域模型

针对不同用例的不同输出模型

关于只读用例的情况?

这将如何帮助我构建可维护的软件呢?

加上紧密的输入验证,使用特定案例的输入和输出模型对可维护的代码库有很大的帮助

总结

Domain entity应该怎么写,以及业务逻辑应该写在哪?

Domain entity里面不仅仅只放一些属性字段,还有一些业务规则,如果这个entity还作为了某个输入参数,那还应该在他的构造方法中进行相关的参数校验。以及构造函数是否使用构建器。同时我们针对不同的用例应该有不同的输入模型和参数出模型。

寄钱的服务去实现给用户寄钱的接口,形参传入是一个SendMoneyCommand。我们要对这个参数进行验证,

1、那是交给调用者验证还是我们事先在这个实体类中就验证好呢?

不同的调用者调用这个寄钱服务的时候都要去验证,太麻烦而且容易忘记。所以我们就选择在SendMoneyCommand这个实体类中进行验证。

2、怎么验证?

我们可以在SendMoneyCommand这个作为输入类的的构造方法中去手工写个工具类去校验每个参数,并且将这个参数都定义为final不可变,这样对象一旦创建就不可变,不能被更改为无效的状态。

手工创建工具类又太繁琐了,我们可以使用ValidationAPI直接在需要约束的字段加上对应的注解即可。对于某些验证我们还可以自定义去实现。

这样我们可以防止将不合适的输入传给调用者。

3、构造函数有什么用?

SendMoneyCommand由于该类是不可变的,构造函数的参数列表包含了该类每个属性的一个参数。由于构造函数也验证了参数,所以不可能创建一个 不可能创建一个具有无效状态的对象。

构造函数的参数列表包含每个属性的每个参数,同时还可以在构造函数中验证参数。

4、如果构造函数有很多的参数怎么办?

使用构建器模式,将长参数的构造函数设置为私有的,之后我们可以使用build创建一个对象,而不是创建一个对象要整排填写多个参数,使代码更可观。

但是这样做也是会有坏处的。如果我们在构建器中少添加了某个字段,他编译是不会报错提示的。

但是如果我们直接使用构造函数,不用构建器,idea会给我们相应的提示。

结合综合考虑那还是直接使用构造函数,让编译器提示。

5、我们应该对于不同的用例创建不同的输入模型

因为如果采用统一的输入模型,那输入模型中的字段对某些用例可能是无用的,而且有可能他想要的字段属性,统一模型中还没有。

6、输入验证和业务规则验证的区别?

验证业务规则需要访问域模型的当前状态,而验证输入则不需要。输入验证可以很简单 声明式地增强,就像我们使用上面的@NotNull注释一样,而业务规则需要更多的上下文。让我们以“来源帐户不能透支”为规则。根据上面的定义,这是一个业务规则,因为它需要访问模型的当前状态来检查是否有源和获取帐户确实存在。相反,“传输量必须大于零”的规则可以在不访问模型的情况下进行验证,因此可以作为输入验证的一部分来实现。

然而,上述区别使我们有助于将某些验证放在代码库中,并在以后很容易地再次找到它们。这就像回答验证是否需要访问的问题一样简单 是否为当前模型状态。这不仅有助于我们首先实现该规则,而且还可以帮助未来的维护工程师再次找到它

7、我们如何实现一个业务规则呢?

最好的方法是将业务规则放入一个域实体中,以后很容易地再次找到它们。

如果在domain entity中验证业务规则不可行,我们可以在服务对实体类操作之前,先对其实体类进行相关的校验。

8、富实体 vs 贫穷实体

富:这些实体提供了更改状态的方法,并且只允许根据业务规则进行有效的更改。许多业务规则都位于实体中,而不是位于 implementation中。

贫:They usually only provide fields to hold the state and getter and setter methods to read and change it. They don’t contain any domain logic.

这意味着域逻辑是在the use case classes中实现的

The “richness” is contained within the use cases instead of the entities.“丰富度”包含在用例中,而不是包含在实体中。

9、怎么更可维护?

独立地为用例创建输入和输出模型同时还要加上严格的输入验证

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值