【问题标题】:Maintaining Clean Architecture in Spring MVC with a data-centric approach使用以数据为中心的方法在 Spring MVC 中维护整洁的架构
【发布时间】:2012-10-04 17:26:19
【问题描述】:

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

我正在尝试为我们正在开发的基于 Java 的新 Web 应用程序(门户类型应用程序)的前端规划架构。我想从第一天开始就做到这一点,我想在这里开始讨论,以帮助我在我的架构设计中实现 Bob 大叔的 Clean Architecture

下面是我们的技术栈的简要介绍,从上到下(技术不重要,结构才是):

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

一个关键点:

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

为了遵循 Bob 的简洁架构,我在应用程序逻辑(即他的架构中的“用例”层)的放置位置上与自己反复讨论。

这是我想出的方法:

第 1 层 - 实体

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

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

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

第 2 层 - 用例

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

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

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

第 3 层 - 接口适配器

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

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

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

第 4 层 - 框架和驱动程序

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

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


模型视图 ViewModel 模式

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

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

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

  • Model - 由视图模板使用的 Spring MVC 中的 ModelMap 数据结构简单地表示。
  • 查看 - FreeMarker 模板
  • Controller Spring 的 Controller-classes 将 HTTP URL 请求 定向到特定 handlers(以及 FrontController 等功能) )。这些类中的处理程序负责从用例层获取数据并在显示数据时将其推送到视图模板(HTTP GET),以及向下发送数据以进行存储(HTTP POST)。这样,它本质上是作为 ViewModel 和 View 之间的 binder 的,使用 Model。

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

这里的一个关键组件是在 Spring MVC 中 ModelMap 和 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 的“实体”层,那么我将实现应用程序逻辑的用例类称为什么?我很想打电话给他们&lt;UseCase&gt;Models 并将它们放在 MVC 项目中,但根据 Bob

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

这意味着我的模型对象应该是“哑”的(就像 Spring 中的简单 Map.ModelMap),然后控制器负责将用例类中的数据放入此映射结构.

同样,我的用例类采用什么形式? &lt;UseCase&gt;Interactor呢?

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

【问题讨论】:

  • Adam 是对的——好的架构是多年来一直在建造的,而不是从头开始的。因此,您的数据驱动应用程序实际上是 2(!)层系统。您可以轻松地从 1) UI 层开始成为任何合适的 MVC 框架(因为它在 UI 增长的同时处理复杂性),以及 2) 通过一些通用接口 (Repository) 的简单数据访问层。然后,在添加模式和更多功能后,您应该重构。您应该添加抽象来解决您的确切问题,而不是一开始就计划它们。

标签: java spring-mvc model-view-controller mvvm architecture


【解决方案1】:

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

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

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

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

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

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

另外,如果你的代码很难测试......你可能有糟糕的架构。

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

如果你这样做了,你可以轻松地更改你的代码而无需担心......你甚至可以改变你的架构:)

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

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

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

【讨论】:

  • 感谢大家的洞察力,这里有很多明智的话。我意识到我们的架构在我们前进的过程中将非常依赖于它本身,但我想让我们尽可能地摆脱跳跃。我可能有点过度设计,或者至少将我们与“清洁架构”联系得太严格了。随着代码行数的增加,如果难以测试或依赖项变得难以管理 - 那么是时候重新考虑架构了。
  • 不要泛泛而谈,不要刻板印象,不要使用陈词滥调,但有GSD的“黑客”和写很多文章的“思想家”......有些人可以同时做到我更愿意成为/听 Kohsuke、John Resig、Jeff Atwood 的声音,而不是 Martin Fowler、Joel Spolesky 或 Bob 叔叔的声音。但这只是我。
【解决方案2】:

我的解决方案

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

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

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

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

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

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

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

I&lt;UseCase&gt;Request - 在 MVC 模块中实现,在 Controller 中实例化 I&lt;UseCase&gt;Response - 在用例模块中实现,在交互器中实例化 I&lt;UseCase&gt;Interactor - 在 UseCase 模块中实现,在 Controller 中注入 I&lt;UseCase&gt;Consumer - 在 Domain 模块中实现,在 Interactor 中注入

它是如何工作的?
Controller 从 HTTP 请求中获取参数并将其打包到 RequestModel 中,然后将其传递给 InteractorInteractor 从域模块*Consumer 获取它需要的数据,并将其应用程序特定的逻辑强加于其上,然后将其放入ResponseModel 并将其发送回ControllerController 最后简单地将这个(现在对 GUI 友好的)简单数据放在 Map 对象中,并将其转发到 FreeMarker 模板,然后直接使用这些数据并呈现 HTML。

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

我的结论

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

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

【讨论】:

  • 嗨阿克塞尔,很好的答案!我想知道你是否可以提供一个你的方法的例子,比如一个小项目或类似的东西。
【解决方案3】:

正因为如此,我们没有传统意义上的“实体” (带有 ID),但是以数据为中心的方法使该层成为数据 访问层,由 消费者模块。

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

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

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

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

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

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

【讨论】:

  • 感谢您提请我注意这些问题。我们可以在我们的领域对象中有 ID,我们只是没有,因为它们在应用程序的前端部分没有任何用途 - 所有主要的数据转换(数据库到对象)都在服务总线中完成,因此 ID有点“迷失在翻译中”。此外,消费者模块不仅仅是数据库的“网关”,它以适合 MVC 前端的方式提供对服务返回结果的访问。在这方面,将这个Consumer模块放在“用例”层可能更合适?
  • 至于ViewModel 的关注,我同意你的观点,我认为这让我感到厌烦——我不知道如何称呼这个类。请参阅我原来的问题中的更新。
  • 1) 好的,但是这些域对象首先有行为吗?如果您的消费者模块处理 MVC 定制的数据结构,它应该位于架构的接口适配器部分,而不是用例,因为用例应该像 Bob 叔叔所说的那样“与外部性隔离”。
  • 2) Bob 叔叔称这些类为 RequestModels 和 ResponseModels。除了博客文章之外,我强烈建议您观看他的一篇演讲(vimeo.com/43612849youtube.com/watch?v=WpkDN78P884),以更深入地了解清洁架构背后的哲学。
  • 感谢您提供的链接,观看这肯定让 Bob 想要什么更清楚了——而且对我来说,这也变得更加明显,这需要比我预期的更多的架构方面。在实践中,我需要另一个模块用于“用例”层,这个模块将是 Bob 的“交互器”和“请求/响应接口”所在的地方。那么 Spring MVC 模块将仅代表 Web GUI,不包含应用程序逻辑,因此很容易更改。
猜你喜欢
  • 1970-01-01
  • 2016-08-15
  • 2021-12-26
  • 1970-01-01
  • 2022-01-16
  • 1970-01-01
  • 2019-07-29
  • 2017-04-17
  • 2020-08-13
相关资源
最近更新 更多