
清洁建筑是Robert C. Martin创造的一个术语。其主要思想是。
实体和用例是独立于框架、用户界面、数据库和外部服务的。
清洁的架构风格对可维护性有积极的影响。
- 我们可以在没有框架、UI和基础设施的情况下测试领域实体和用例。
- 技术决策可以改变而不影响领域代码。反之亦然。甚至可以通过有限的努力切换到一个新的框架。
我的目标是使学习曲线扁平化,并减少实施清洁架构的努力。这就是为什么我创建了现代清洁架构 库。
在这篇文章中,我将向你展示如何用现代清洁架构创建一个应用程序。从HTML/JavaScript前端到Spring Boot后端。重点将放在后端。
让我们从示例应用程序的概述开始。一个永恒的经典。
待办事项列表示例程序
一个待办事项_列表_是一个_任务_的集合。一个任务有一个_名字_,并且要么_完成,要么不完成_。作为一个用户,你可以。
- 创建一个单一的待办事项列表,并将其持久化
- 添加一项任务
- 完成一项任务,或 "未完成 "一项任务
- 删除一项任务
- 列出所有任务
- 过滤已完成/未完成的任务
下面是一个有1个未完成任务和2个已完成任务的列表的情况。
我们从应用程序的核心开始,即领域实体。然后我们再向外扩展到前端。
领域实体
TodoList 实体包含。
- 一个唯一的ID。
- 一个任务的列表。
- 用于添加、完成、删除任务的领域方法...
TodoList 实体不包含公共设置器。设置器会破坏正确的封装。
这里是TodoList实体的一部分。Lombok注解缩短了代码。
AggregateRoot 接口有什么用?聚合根是Eric Evans的 领域驱动设计(DDD)中的一个术语。
AGGREGATE是一个关联对象的集群,我们将其作为一个单元来处理数据变化。每个AGGREGATE都有一个根和一个边界。边界定义了AGGREGATE中的内容。根是包含在AGGREGATE中的一个单一的、特定的实体。
我们只能通过聚合根来改变聚合的状态。在我们的例子中,这意味着:我们总是要使用_TodoList_来添加、删除或改变一个任务。
这使得_TodoList_ 可以强制执行约束。例如,我们不能在列表中添加一个名字为空的任务。
AggregateRoot 接口是jMolecules库的一部分。该库使DDD概念在领域代码中显性化。在构建过程中,一个ByteBuddy插件将注释映射到Spring Data的注释中。
因此,我们只有一个单一的模型。既可以代表领域概念,也可以代表持久性。不过,我们在领域代码中没有任何针对持久性的注释。我们不把自己与任何框架联系起来。
这个 任务 类是类似的,但它实现了jMolecules_Entity_ 接口。
任务 的构造函数是封装私有的。所以我们不能从领域包之外创建一个_Task_ 的实例。而且_任务_ 类是不可变的。它的状态不可能从聚合的边界之外被改变。
我们需要一个用于存储_TodoList的存储库。_ 为了在领域代码中坚持使用领域术语,它被称为 TodoLists:
同样,代码使用了jMolecues注解。Repository。在构建过程中,ByteBuddy插件将其转换为Spring Data仓库。
我们将跳过域的异常,因为它们没有什么特别之处。这就是完整的领域包。
行为(也就是用例
接下来,我们定义终端用户可见的应用程序的行为。用户与应用程序的任何交互发生如下。
-
用户界面发送一个_请求_。
-
后台通过执行一个_请求处理程序_ 做出反应_。_ 请求处理程序做一切必要的事情来完成请求
:- 访问数据库
- 调用外部服务
- 调用域实体方法 -
请求处理程序可以 返回一个_响应_。
我们用Java 8的功能接口实现一个_请求处理程序_。
一个返回_响应的_ 处理程序,实现了 _java.util.Function_接口。下面是代码中的 AddTask 处理程序。这个处理程序
- 提取了一个 "待办事项列表 "的ID和任务名称。 AddTaskRequest,
- 找到资源库中的待办事项列表(或抛出一个异常)。
- 添加一个与请求名称相同的任务到列表中。
- 返回一个 AddTaskResponse ,并附上新增任务的ID。
Lombok创建了一个构造函数,将_TodoLists_ 资源库接口作为构造函数参数。我们把任何外部依赖性作为接口 传递给处理程序的构造函数。
请求和响应是不可变的值对象。
现代清洁架构库将它们从/到JSON进行序列化。
接下来是一个不返回响应的处理程序的例子。这个处理程序 删除任务处理程序收到一个 DeleteTaskRequest.由于该处理程序不返回响应,它实现了_消费者_ 接口。
还有一个问题:谁来创建这些处理程序?
答案是:一个实现 行为模型(BehaviorModel 接口。行为模型将每个_请求_ 类映射到这种请求的_处理程序_。
这里有一部分的 TodoListBehaviorModel:
user(...)语句定义了请求类。对于返回_响应_的处理程序,我们使用systemPublish(...)。而system(...)则用于没有回复的处理程序。
_行为模型_有一个构造函数,其外部依赖关系以接口形式传递。它创建所有的处理程序,并将适当的依赖关系注入其中。
通过配置_行为模型_的依赖关系,我们配置了所有的处理程序。这正是我们想要的:一个我们可以改变或切换技术的依赖关系的中心位置。这就是技术决定如何改变而不影响领域代码的方式。
网络层(即适配器
现代清洁架构中的网络层可以是非常薄的。在其最简单的形式中,它只由2个类组成。
- 一个用于配置依赖关系的类
- 一个用于异常处理的类
这里是 TodoListConfiguration 类。
Spring将_TodoLists_ 资源库接口的实现注入到_behaviorModel(...)_方法中。该方法创建了一个作为Bean的_行为模型_实现。
如果应用程序使用外部服务,配置类是将具体实例创建为Bean的地方。并将它们注入到_行为模型_中。
那么,所有的控制器都在哪里?
这个嘛。没有任何你必须要创建的控制器。至少如果你只处理POST请求的话。(关于GET请求的处理,请看后面的问答)。
该 spring-behavior-web 库是_现代清洁架构_ 库的一部分。我们为POST请求定义了一个单一的端点。我们将该端点的URL指定在 application.properties:
behavior.endpoint = /todolist
如果该属性存在,spring-behavior-web会在后台为该端点设置一个控制器。该控制器接收POST请求。
我们不需要编写Spring特定的代码来添加新的行为。
我们不需要添加或改变控制器。
以下是端点收到POST请求时的情况。
- spring-behavior-web对请求进行反序列化。
- spring-behavior-web将请求传递给由行为模型配置的行为。
- 该行为将请求传递给适当的请求处理程序(如果有的话)。
- spring-behavior-web将响应序列化,并将其传回给端点(如果有的话)。
默认情况下,spring-behavior-web将对请求处理程序的每一次调用都包裹在一个事务中。
发送POST请求
一旦我们启动了Spring Boot应用程序,我们就可以向端点发送POST请求。
我们在JSON内容中包含一个@type属性。这样spring-behavior-web就可以在反序列化时确定正确的请求类别。
例如,这是To Do List应用程序的一个有效的curl命令。它发送了一个 FindOrCreateListRequest 到端点。
curl -H "Content-Type: application/json" -X POST -d '{"@type": "FindOrCreateListRequest"}' http://localhost:8080/todolist
这就是在Windows PowerShell中使用的相应的语法。
iwr http://localhost:8080/todolist -Method 'POST' -Headers @{'Content-Type' = 'application/json'} -Body '{"@type": "FindOrCreateListRequest"}'
异常处理
Spring-behavior-web的异常处理与 "普通 "Spring应用程序没有区别。我们创建一个带有@ControllerAdvice注释的类。我们在其中放置带有@ExceptionHandler的方法注解。
参见 TodoListExceptionHandling为例。
请注意,在实际应用中,不同的异常类型需要不同的处理方式。
前台
To Do List应用程序的前端由以下部分组成。
- a HTML页面,
- a CSS文件 ,用于格式化。
- 和一个 main.js JavaScript文件
我们在这里重点讨论_main.js_。它发送请求并更新网页。
下面是它的部分内容。
因此,举例来说,这是一个JSON对象,用于 ListTasksRequest:
const request = {"@type": "ListTasksRequest", "todoListUuid":todoListUuid};
post(...)方法将_请求 发送到后端,并将_响应 传递给_响应处理器_。(你作为第二个参数传入的回调函数)。
这就是关于To Do List应用程序的全部内容。
问题与解答
如果...
...我想发送GET请求而不是POST请求?
......我想让网络层与行为层分开发展?
...我想使用一个不同于Spring的框架?
...我有一个比To Do List样本大得多的应用程序。我应该如何构建它呢?
以下是答案。
总结
在这篇文章中,我向你介绍了实现清洁架构的一种特殊方法。还有很多其他的方法。
我的目标是减少构建清洁架构的努力。并使学习曲线更加平坦。
为了实现这个目标,现代清洁架构库提供了以下功能。
- 对不可变的请求和响应进行序列化,不需要序列化的特定注释。
- **没有必要使用DTOs。**你可以在Web层和用例中为请求/响应使用相同的不可变的值对象。
- 接收和转发POST请求的通用端点。新的行为和领域逻辑可以被添加和使用,而不需要编写框架特定的代码。
在我的下一篇文章中,我将描述如何测试一个现代清洁架构。
我邀请你访问现代清洁架构的GitHub页面。
并请在评论中给我反馈。你觉得怎么样?
如果你想了解我在做什么或给我留言,请在LinkedIn或Twitter上关注我。
鸣谢
感谢Surya Shakti发布了原始的仅有前台的todo列表代码。
感谢Oliver Drotbohm给我指点了很棒的jMolecules库。
现代清洁架构》最初发表于Medium上的Javarevisited,人们在这里通过强调和回应这个故事来继续对话。
