拨开荷叶行,寻梦已然成。仙女莲花里,翩翩白鹭情。
IMG-LOGO
主页 文章列表 使用Spring Boot整洁架构

使用Spring Boot整洁架构

白鹭 - 2021-11-24 711 0 0

1.概述

在开发长期系统时,我们一个可变的环境。

通常,由于各种原因,我们的功能要求,框架,I / O设备,甚至我们的代码设计都可能会更改。考虑到这一点,考虑到我们周围的所有不确定因素,“整洁架构”是高可维护代码的指南

在本文中,我们将根据Robert C. Martin的Clean Architecture创建一个用户注册API的示例。我们将使用他的原始层-实体,用例,接口适配器和框架/驱动程序。

2.整洁架构概述

整洁的体系结构可编译许多代码设计和原理,例如SOLID ,稳定的抽像等。但是,核心思想是**根据业务价值将系统划分为多个级别**。因此,最高级别具有业务规则,每一个较低级别的业务规则都离I / O设备越来越近。

同样,我们可以将级别转换为层。在这种情况下,情况恰恰相反。内层等于最高级别,依此类推:

使用Spring

考虑到这一点,我们可以根据业务需要设置多个级别。但是,始终要考虑依赖性规则–较高的级别绝不能依赖较低的级别**。

3.规则

让我们开始为我们的用户注册API定义系统规则。一,业务规则:

  • 用户密码必须超过五个字符

其次,我们有应用规则。它们可以是不同的格式,例如用例。我们将使用用例的短语:

  • 系统接收用户名和密码,验证用户是否不存在,并保存新用户以及创建时间

请注意,这里没有提到任何数据库,UI或类似内容。因为我们的公司不在乎这些细节,所以我们的代码也不在乎**。

4.Entity实体层

正如整洁架构所建议的那样,让我们从业务规则开始:

interface User {

 boolean passwordIsValid();



 String getName();



 String getPassword();

 }

并且,一个UserFactory

interface UserFactory {

 User create(String name, String password);

 }

我们创建用户工厂方法的原因有两个。保留稳定的抽象原理并隔离用户创建。

接下来,让我们同时实现:

class CommonUser implements User {



 String name;

 String password;



 @Override

 public boolean passwordIsValid() {

 return password != null && password.length() > 5;

 }



 // Constructor and getters

 }
class CommonUserFactory implements UserFactory {

 @Override

 public User create(String name, String password) {

 return new CommonUser(name, password);

 }

 }

如果我们的业务很复杂,那么我们应该尽可能清晰地构建域代码。因此,此层是应用设计模式的好地方。特别是,应该考虑领域驱动的设计。

4.1单元测试

现在,让我们测试我们的CommonUser

@Test

 void given123Password_whenPasswordIsNotValid_thenIsFalse() {

 User user = new CommonUser("Baeldung", "123");



 assertThat(user.passwordIsValid()).isFalse();

 }

如我们所见,单元测试非常清楚。毕竟,缺少模拟是这一层的一个好信号

通常,如果我们在这里开始考虑模拟,也许我们正在将实体与用例混合在一起。

5.用例层

用例是**与系统自动化相关**的**规则**。在“干净的体系结构”中,我们将其称为“交互器”。

5.1。 UserRegisterInteractor

首先,我们将构建我们的UserRegisterInteractor以便我们可以看到前进的方向。然后,我们将创建并讨论所有使用的部分:

class UserRegisterInteractor implements UserInputBoundary {



 final UserRegisterDsGateway userDsGateway;

 final UserPresenter userPresenter;

 final UserFactory userFactory;



 // Constructor



 @Override

 public UserResponseModel create(UserRequestModel requestModel) {

 if (userDsGateway.existsByName(requestModel.getName())) {

 return userPresenter.prepareFailView("User already exists.");

 }

 User user = userFactory.create(requestModel.getName(), requestModel.getPassword());

 if (!user.passwordIsValid()) {

 return userPresenter.prepareFailView("User password must have more than 5 characters.");

 }

 LocalDateTime now = LocalDateTime.now();

 UserDsRequestModel userDsModel = new UserDsRequestModel(user.getName(), user.getPassword(), now);



 userDsGateway.save(userDsModel);



 UserResponseModel accountResponseModel = new UserResponseModel(user.getName(), now.toString());

 return userPresenter.prepareSuccessView(accountResponseModel);

 }

 }

如我们所见,我们正在执行所有用例步骤。同样,该层负责控制实体的舞蹈。尽管如此,我们**并未对UI或数据库的工作方式做任何假设。**但是,我们正在使用UserDsGatewayUserPresenter 。那么,我们怎么不认识他们呢?因为,连同UserInputBoundary ,这些都是我们的输入和输出边界。

5.2。输入和输出边界

边界是定义组件如何交互的契约。**输入边界暴露出我们的用例到外层是:**

interface UserInputBoundary {

 UserResponseModel create(UserRequestModel requestModel);

 }

接下来,我们有了利用外层的输出边界。首先,让我们定义数据源网关:

interface UserRegisterDsGateway {

 boolean existsByName(String name);



 void save(UserDsRequestModel requestModel);

 }

二,视图展示者:

interface UserPresenter {

 UserResponseModel prepareSuccessView(UserResponseModel user);



 UserResponseModel prepareFailView(String error);

 }

请注意,我们使用的是**依赖倒置原则,使我们的业务摆脱了数据库和UI等细节的困扰**。

5.3。解耦模式

在继续之前,请注意**边界是**如何**定义系统的自然划分的契约**。但是我们还必须决定如何交付我们的应用程序:

  • 整体式-可能使用某些封装结构来组织
  • 通过使用模块
  • 通过使用服务/微服务

考虑到这一点,我们可以**使用任何去耦模式达到干净的架构目标。因此,我们应该准备根据当前和将来的业务需求在这些策略之间进行更改**。选择了我们的解耦模式后,应根据我们的边界进行代码划分。

5.4。请求和响应模型

到目前为止,我们已经使用接口跨层创建了操作。接下来,让我们看看如何跨这些边界传输数据。

注意我们所有的边界如何仅处理StringModel对象:

class UserRequestModel {



 String login;

 String password;



 // Getters, setters, and constructors

 }

基本上,只有**简单的数据结构才能跨越边界**。而且,所有Models都只有字段和访问器。另外,数据对象属于内部。因此,我们可以保留依赖性规则。

但是为什么我们有这么多类似的物体?当我们得到重复的代码时,它可以有两种类型:

  • 错误或偶然的重复–代码相似是偶然的,因为每个对像都有不同的更改原因。如果我们尝试删除它,我们将冒违反单一责任原则的风险。
  • 真正的重复–出于相同的原因,代码会更改。因此,我们应该将其删除

由于每个模型都有不同的责任,所以我们得到了所有这些对象。

5.5。测试UserRegisterInteractor

现在,让我们创建单元测试:

@Test

 void givenBaeldungUserAnd12345Password_whenCreate_thenSaveItAndPrepareSuccessView() {

 given(userDsGateway.existsByIdentifier("identifier"))

 .willReturn(true);



 interactor.create(new UserRequestModel("baeldung", "123"));



 then(userDsGateway).should()

 .save(new UserDsRequestModel("baeldung", "12345", now()));

 then(userPresenter).should()

 .prepareSuccessView(new UserResponseModel("baeldung", now()));

 }

我们可以看到,大多数用例测试都是关于控制实体和边界请求的。而且,我们的界面使我们可以轻松地模拟细节。

6.接口适配器

至此,我们完成了所有业务。现在,让我们开始插入我们的细节。

我们的业务应该只处理最方便的数据格式,我们的外部代理(如数据库或UI)也应该处理但是,这种格式通常是不同的。因此,接口适配器层负责转换数据

6.1。使用JPA的UserRegisterDsGateway

首先,让我们使用JPA映射user表:

@Entity

 @Table(name = "user")

 class UserDataMapper {



 @Id

 String name;



 String password;



 LocalDateTime creationTime;



 //Getters, setters, and constructors

 }

如我们所见, Mapper目标是将我们的对象映射到数据库格式。

接下来,使用我们的实体进行JpaRepository

@Repository

 interface JpaUserRepository extends JpaRepository<UserDataMapper, String> {

 }

假设我们将使用spring-boot,那么这就是保存用户的全部。

现在,是时候实现我们的UserRegisterDsGateway:

class JpaUser implements UserRegisterDsGateway {



 final JpaUserRepository repository;



 // Constructor



 @Override

 public boolean existsByName(String name) {

 return repository.existsById(name);

 }



 @Override

 public void save(UserDsRequestModel requestModel) {

 UserDataMapper accountDataMapper = new UserDataMapper(requestModel.getName(), requestModel.getPassword(), requestModel.getCreationTime());

 repository.save(accountDataMapper);

 }

 }

在大多数情况下,代码可以说明一切。除了我们的方法外,请注意UserRegisterDsGateway's名称。如果我们改为选择UserDsGateway ,那么其他User用例将很容易违反接口隔离原则

6.2。 User注册API

现在,让我们创建我们的HTTP适配器:

@RestController

 class UserRegisterController {



 final UserInputBoundary userInput;



 // Constructor



 @PostMapping("/user")

 UserResponseModel create(@RequestBody UserRequestModel requestModel) {

 return userInput.create(requestModel);

 }

 }

如我们所见,这里唯一目标是接收请求并将响应发送给客户端。

6.3 格式化响应

在响应之前,我们应该格式化响应:

class UserResponseFormatter implements UserPresenter {



 @Override

 public UserResponseModel prepareSuccessView(UserResponseModel response) {

 LocalDateTime responseTime = LocalDateTime.parse(response.getCreationTime());

 response.setCreationTime(responseTime.format(DateTimeFormatter.ofPattern("hh:mm:ss")));

 return response;

 }



 @Override

 public UserResponseModel prepareFailView(String error) {

 throw new ResponseStatusException(HttpStatus.CONFLICT, error);

 }

 }

我们的UserRegisterInteractor迫使我们创建一个演示者。但是,表示规则仅与适配器有关。此外,W henever东西是很难测试,我们应该把它分成一个可测试和谦虚的对象因此, UserResponseFormatter可以轻松地使我们验证演示规则:

@Test

 void givenDateAnd3HourTime_whenPrepareSuccessView_thenReturnOnly3HourTime() {

 UserResponseModel modelResponse = new UserResponseModel("baeldung", "2020-12-20T03:00:00.000");

 UserResponseModel formattedResponse = userResponseFormatter.prepareSuccessView(modelResponse);



 assertThat(formattedResponse.getCreationTime()).isEqualTo("03:00:00");

 }

如我们所见,我们在将所有逻辑发送到视图之前已经对其进行了测试。因此,只有较不起眼的物体处于较难测试的部分

7.驱动程序和框架

实际上,我们通常不在此处编写代码。这是因为该层表示与外部代理的最低连接级别。例如,H2驱动程序连接到数据库或Web框架。在这种情况下,我们将使用spring-boot作为Web和依赖注入框架。因此,我们需要它的启动点:

@SpringBootApplication

 public class CleanArchitectureApplication {

 public static void main(String[] args) {

 SpringApplication.run(CleanArchitectureApplication.class);

 }

 }

到目前为止,我们**在业务中**没有使用任何**spring注释**。除了特定于弹簧的适配器,如UserRegisterController 。这是因为**我们应该**将spring-boot视为其他任何细节

8.可怕的主要阶级

最后,最后一块!

到目前为止,我们遵循稳定的抽象原理。同样,我们通过反转控制来保护我们的内层免受外部代理的攻击。最后,我们将所有对象创建与使用分开。在这一点上,我们需要创建剩余的依赖项并将它们注入到我们的项目中

@Bean

 BeanFactoryPostProcessor beanFactoryPostProcessor(ApplicationContext beanRegistry) {

 return beanFactory -> {

 genericApplicationContext(

 (BeanDefinitionRegistry) ((AnnotationConfigServletWebServerApplicationContext) beanRegistry)

 .getBeanFactory());

 };

 }



 void genericApplicationContext(BeanDefinitionRegistry beanRegistry) {

 ClassPathBeanDefinitionScanner beanDefinitionScanner = new ClassPathBeanDefinitionScanner(beanRegistry);

 beanDefinitionScanner.addIncludeFilter(removeModelAndEntitiesFilter());

 beanDefinitionScanner.scan("com.baeldung.pattern.cleanarchitecture");

 }



 static TypeFilter removeModelAndEntitiesFilter() {

 return (MetadataReader mr, MetadataReaderFactory mrf) -> !mr.getClassMetadata()

 .getClassName()

 .endsWith("Model");

 }

在我们的例子中,我们使用spring-boot依赖注入来创建所有实例。因为我们没有使用@Component ,所以我们正在扫描根包,而只忽略Model对象。

尽管此策略可能看起来更复杂,但它使我们的业务与DI框架脱钩。另一方面,主要阶级掌握了我们整个系统的力量。这就是为什么干净的体系结构在一个包含所有其他层的特殊层中考虑它的原因:

使用Spring

9.结论

在本文中,我们了解了Bob叔叔的整洁架构是如何在许多设计模式和原则之上构建的。另外,我们使用Spring Boot创建了一个用例。

尽管如此,我们仍保留了一些原则。但是,他们全都朝着同一方向前进。我们可以通过引用它的创建者来概括它:“一个好的架构师必须最大化未做出的决定的数量。”并且我们通过使用边界保护我们的业务代码不受细节影响来做到这一点。

标签:

0 评论

发表评论

您的电子邮件地址不会被公开。 必填的字段已做标记 *