14

使用以数据为中心的方法在 Spring MVC 中维护整洁的架构

我正在尝试为我们正在制作的基于 Java 的新 Web 应用程序(门户类型应用程序)的前端绘制架构图。我想从第一天开始就做到这一点,我想在这里开始讨论,以帮助我在我的架构设计中实施鲍勃叔叔的清洁架构

这是我们的技术堆栈的快速概述,从上到下(技术不重要,结构很重要):

  • 甲骨文数据库
  • Oracle Service Bus 使用 WSDL 公开服务
  • JAX-WS 从 WSDL 生成 Java 类(我们称之为“生成的服务层”)
  • 由映射到生成的数据对象的 POJO 组成的域模块
  • 将“生成的服务层”暴露给前端应用程序的消费者模块
  • 基于 Spring MVC 的前端模块,使用 FreeMarker 呈现视图

一个关键点:

特别是,在外圈中声明的事物的名称不能被内圈中的代码提及。这包括函数、类。变量或任何其他命名的软件实体。

试图坚持 Bob 的清洁架构,我与自己反复讨论了应用程序逻辑的放置位置,即他的架构中的“用例”层。

这是我想出的方法:

第 1 层 - 实体

实体封装了企业范围的业务规则。

这是包含域对象的域模块所在的位置,这些是自包含对象,彼此之间的依赖性最小。只有与对象本身相关的逻辑可能存在于这些域对象上,而没有特定于用例的逻辑。

与 JPA 或 Hibernate 等 ORM 不同,使用转换数据的服务总线通过 WSDL 公开对我们数据库的访问。正因为如此,我们没有传统意义上的“实体”(带有 ID),而是一种以数据为中心的方法,使该层成为数据访问层,由消费者模块呈现给应用程序的其余部分。

第 2 层 - 用例

该层中的软件包含特定于应用程序的业务规则。

这就是特定于我们应用程序用例的逻辑所在。对此层的更改不应影响数据访问层(第 1 层)。对 GUI 或框架实现 (Spring MVC) 的更改不应影响这一层。

这是有点棘手的地方: 由于我们的数据访问层(在第 1 层)必须保持干净的应用程序逻辑,我们需要一个层来促进以适合用例的方式使用该层。我发现这个问题的一个解决方案是使用我选择称为MVC-VM的“ MVVM 模式”的变体。请参阅下面的说明。其中的“VM”部分位于此用例层中,由封装此用例特定逻辑的类表示。*ViewModel

第 3 层 - 接口适配器

该层中的软件是一组适配器,可将数据从对用例和实体最方便的格式转换为对某些外部机构(如数据库或 Web)最方便的格式。

这就是我们 GUI 的 MVC 架构所在的地方(我们的“MVC-VM”中的“MVC”)。本质上,这是当Controller-classes 从 -classes 获取数据*ViewModel并将其放入 Spring MVC 的ModelMapojects 中,这些 ojects 由 View 中的 FreeMarker-templates 直接使用。

在我看来,在我们的例子中,服务总线也属于这一层。

第 4 层 - 框架和驱动程序

通常,您不会在这一层编写太多代码,除了与内部下一个循环通信的胶水代码。

这一层实际上只是我们应用程序中的一个配置层,即 Spring 配置。例如,这将是我们指定 FreeMarker 用于呈现视图的地方。


模型视图 ViewModel 模式

MVVM 有助于将图形用户界面(作为标记语言或 GUI 代码)的开发与称为模型(也称为数据模型)的业务逻辑或后端逻辑的开发明确分离,以将其与视图区分开来模型)。MVVM 的视图模型是一个值转换器,这意味着视图模型负责从模型中公开数据对象,以便这些对象易于管理和使用。

有关 MVVM 模式的更多信息,请访问Wikipedia

MVC-VM 角色将在我们的应用程序中实现,如下所示:

  • 模型-ModelMap由视图模板使用的 Spring MVC 中的数据结构简单地表示。
  • 查看 - FreeMarker 模板
  • 控制器Spring 的Controller类,将HTTP URL 请求定向到特定的处理程序(以及诸如 FrontController 之类的函数)。这些类中的处理程序负责从用例层获取数据并在显示数据时将其推送到视图模板(HTTP GET),以及向下发送数据以进行存储(HTTP POST)。通过这种方式,它本质上用作ViewModel 和 View 之间的绑定器,使用 Model

  • ViewModel - 这些类负责 1) 以 View 可用的方式构造来自数据访问层的数据,以及 2) 处理来自 View 的数据输入。“处理”意味着验证和分解数据,以便可以将其发送到堆栈以进行存储。该层将在我们的 Spring MVC 前端模块<UseCase>VM中以包中的类的形式出现。viewmodel

ModelMap这里的一个关键组件是在 Spring MVC和 FreeMarker 模板之间发生的隐式绑定。模板仅使用模型 ( ModelMaps),其中控制器已将数据置于其可以使用的格式中。这样我们就可以制作这样的模板:

<body>
  <h1>Welcome ${user}!</h1>
  <p>Our latest product:
  <a href="${latestProduct.url}">${latestProduct.name}</a>!
</body>

我为冗长的解释道歉,但我无法用更少的词来解释这个(相对简单的)架构。

我将非常感谢我在这里的方法的一些输入 - 我在正确的轨道上吗?MVC-VM 的东西有意义吗?我是否违反了任何清洁架构原则?

当然有很多解决方案,但我试图找到一个解决方案,它 1) 没有过度设计和 2) 遵守 Bob 的清洁架构的原则。


更新:

我认为让我失望的关键问题是“用例”层在这个应用程序中采用什么形式。请记住,我们有一个从数据访问层获取数据的 MVC 前端。如果 MVC 部分适合 Bob 的“接口适配器”,而数据层的域模型适合 Bob 的“实体”层,那么我将实现应用程序逻辑的用例类称为什么?我很想将它们称为<UseCase>Models 并将它们放入 MVC 项目中,但根据 Bob

模型可能只是从控制器传递到用例,然后从用例返回到演示者和视图的数据结构。

所以这意味着我的模型对象应该是“哑巴的”(就像 Spring 中的简单Map.一样ModelMap),然后控制器负责将 Use Case 类中的数据放入这个 Map 结构中。

再说一遍,我的用例类采用什么形式?怎么样<UseCase>Interactor

但总而言之,我意识到 MVC-MV 的东西是过度设计的(或者根本不正确)——正如“mikalai”在下面指出的那样,它本质上只是当前形式的两层应用程序;一个数据访问层和一个前端 MVC 层。就那么简单。

4

3 回答 3

14

哇,那太多了。而且我认为您已将 Bob 叔叔的行话翻译到您的 Spring Java 应用程序中。

由于架构主要是意见,而且您的问题有点要求...

有许多不同风格的建筑,而且……大多数都被高估了。因为大多数都是一样的:通过间接和抽象实现更高的内聚更松散的耦合。

最重要的(恕我直言)是依赖项。制作大量小项目而不是一个巨大的单体项目是获得“干净”架构的最佳方式。

清洁架构最重要的技术不是“Spring MVC”技术或“Freemarker”模板语言,也不是 Dobb 博士的另一篇带有方框图、六边形图和各种其他抽象多边形的文章。

专注于您的构建和依赖管理技术。这是因为这项技术将强制执行您的架构规则。

此外,如果您的代码难以测试..您的架构可能很糟糕。

专注于使您的代码易于测试并编写大量测试。

如果你这样做了,你可以很容易地改变你的代码而不用担心......你甚至可以改变你的架构:)

谨防过于关注一个公牛#%$@# 架构规则。说真的:如果您的代码易于测试、易于更改、易于理解且性能良好,那么您就有了一个好的架构。没有 6 周到 6 块腹肌的文章可以做到这一点(对不起鲍勃叔叔)。这需要经验和时间……没有灵丹妙药的计划。

所以这里是我自己的“干净”架构......我的意思是指导方针:

  • 做很多小项目
  • 使用依赖管理(即Maven、Gradle)
  • 不断重构
  • 理解和使用某种依赖注入(Spring)
  • 编写单元测试
  • 了解横切关注点(即当您需要 AspectJ、元编程等时)
于 2012-10-15T04:17:22.380 回答
9

我的解决方案

所以事实证明,在 Java/Spring MVC 中实现 Bob 的“干净架构”是非常重要的,并且需要比我最初包含的更多架构方面。
而且我实际上在网上找不到任何实现示例。

显然,我的架构缺少“用例”层的单独模块,因为这个逻辑不应该存在于 Spring MVC Web 模块中(也不应该被称为“ *ViewModel”)。Web/MVC 模块只是应用程序的一个细节,应用程序逻辑应该与它完全分离,并且可以单独测试。

这个新的“用例”模块现在包含*Interactor从域模块(实体)获取数据的类。此外,需要“请求/响应对象”来促进 MVC/Web 模块和用例模块之间的通信。

我的依赖链现在看起来像这样:

Spring MVC 模块 -> 用例模块 -> 域模块

其中每个箭头(依赖项)都采用Boundary形式,这意味着在箭头右侧的模块中定义了一个接口,该接口在需要的地方实现并在需要的地方注入(控制反转)。

以下是我最终得到的接口(每个用例):

I<UseCase>Request- 在 MVC 模块中实现,在 Controller 中实例化 I<UseCase>Response- 在 Use Case 模块中实现,在 Interactor 中实例化 I<UseCase>Interactor- 在 UseCase 模块中实现,在 Controller 中注入 I<UseCase>Consumer- 在 Domain 模块中实现,在 Interactor 中注入

这个怎么运作?从 HTTP 请求中获取参数并将其打包到
传递给. 从域模块中获取它需要的数据并将其应用程序特定的逻辑强加于它,然后将其放入 a并将其发送回. 然后最后简单地将这个(现在对 GUI 友好的)简单数据放在一个对象中,并将其转发到 FreeMarker 模板,然后该模板直接使用这些数据并呈现 HTML。ControllerRequestModelInteractorInteractor*ConsumerResponseModelControllerControllerMap

APresenter可以参与其中的最后一部分,使其成为 Model-View-Presenter 模式的实现,但我现在要离开它。

我的结论

严格来说,我最终得到了比开发早期所需的文件更多的文件。然而,随着应用程序的复杂性和规模的增长,我相信这种结构使我们能够轻松地保持低耦合和高内聚。此外,Web 模块现在很容易替换——它只是将请求传递给用例模块并接收响应对象。此外,应用程序的每一层(域逻辑、应用程序逻辑和 GUI 逻辑)都是可单独测试的,只有视图部分需要 Web 服务器才能进行测试。

感谢我在这里收到的所有建议和指示。请评论我的解决方案 - 我并不声称它是完美的。

于 2012-10-17T18:51:10.267 回答
6

正因为如此,我们没有传统意义上的“实体”(带有 ID),而是一种以数据为中心的方法,使该层成为数据访问层,由消费者模块呈现给应用程序的其余部分。

在那部分我觉得有些奇怪。为什么即使您从 Web 服务获取实体也没有 ID?

在 Clean Architecture 方法中,实体层并不是数据访问层。数据访问应该是架构中的一个细节,而不是核心问题。正如您自己所说,实体包含特定于域的业务规则。业务规则或行为与您获取数据的方式非常不同。

实体是所有域逻辑发生的地方,而不是您从中获取数据的地方。根据 Clean Architecture,您可以从网关获取持久数据或外部数据。

我发现这个问题的一个解决方案是使用我选择称为 MVC-VM 的“MVVM 模式”的变体。请参阅下面的说明。其中的“VM”部分位于此用例层中,由封装此用例特定逻辑的 *ViewModel 类表示。

ViewModel显然是指一个视图,它是一个演示工件 - 另一个细节。用例/交互器应该没有这些细节。相反,交互者应该通过边界发送和接收与交付机制无关的数据结构(RequestModels 和 ResponseModels)。

我知道这是您的自定义模式,不涉及对演示框架的引用,但“视图”一词只是具有误导性。

于 2012-10-15T16:05:02.013 回答