在一个设计模式课程中,我对建模域逻辑进行了一次有趣的讨论。具体来说,它是关于隔离域逻辑。应用程序通常分为三个部分:
全班同学发现依赖箭头指向领域逻辑部分很有趣。他们问:“图表是故意弄错的吗?域逻辑部分不应该依赖于持久性存储吗?”这是一个很好的问题。我想在这里分享和发布讨论和解释。
大多数开发人员通常会有这种误解。
而这种误解很大程度上是由于操作顺序造成的。它通常以表示层中的触发器(例如用户单击按钮或链接)开始,然后调用域逻辑层中的某些内容,然后调用基础结构层中的内容(例如更新数据库表记录)。
虽然这 是正确的操作顺序,但领域逻辑层的实现方式有些微妙。这与依赖倒置有关。
域逻辑层可能需要基础设施层的一些东西,比如某种形式的访问以从持久性存储中检索。通常的模式是:DAO 和存储库。我不会在这里解释这两种模式。相反,我会指出接口定义放在域逻辑层中,而它们的实现放在另一个单独的层中。
将(DAO 和存储库)接口定义放在领域逻辑层中意味着定义它的是领域逻辑层。它是一个规定需要哪些方法以及期望返回类型的方法。这也标志着领域逻辑的边界。
接口和实现之间的这种分离可能很微妙,但却很关键。仅放置接口定义允许域逻辑部分不受基础架构细节的影响,并允许在没有实际实现的情况下对其进行单元测试。在单元测试期间,接口可以具有模拟实现。这种细微的差异在快速验证(开发团队对)业务规则方面产生了很大的不同。
这种分离是经典的依赖倒置原则在起作用。域逻辑(高层模块)不应依赖于 DAO 和存储库实现(低层模块)。两者都应该依赖于抽象。领域逻辑定义了抽象,而基础设施的实现依赖于这些抽象。
我见过的大多数新手团队将 DAO 和存储库接口与其特定于基础架构的实现放在一起。例如,假设我们有一个 StudentRepository
及其特定于 JPA 的实现 StudentJpaRepository
。我通常会发现新手团队将它们放在同一个包中。虽然这很好,因为应用程序仍会成功编译。但是分离消失了,领域逻辑不再孤立。
现在我已经解释了域逻辑部分为什么以及如何不依赖于基础设施部分,我想谈谈表示部分是如何意外地与域逻辑纠缠在一起的。
我经常在新手团队中看到的另一件事是他们最终如何将领域逻辑与他们的表现纠缠在一起。这导致了这种讨厌的循环依赖。这种循环依赖在逻辑上比物理上更重要。这使得检测和预防变得更加困难。
我不会在这里使用丰富的 GUI 演示示例,因为 Martin Fowler 已经写了一篇很棒的文章。相反,我将使用基于网络浏览器的演示文稿作为示例。
大多数基于 web 的系统将使用 web 框架来表示。这些框架通常实现某种形式的 MVC(模型-视图-控制器)。使用的模型通常是直接来自领域逻辑部分的模型。不幸的是,大多数 MVC 框架都需要一些关于模型的东西。在 Java 世界中,大多数 MVC 框架都要求模型遵循 JavaBean 约定。具体来说,它要求模型具有公共零参数构造函数以及 getter 和 setter。零参数构造函数和设置器用于自动将参数(来自 HTTP POST)绑定到模型。吸气剂用于在视图中渲染模型。
由于演示中使用的 MVC 框架的这一隐含要求,开发人员将向其所有域实体添加公共零参数构造函数、getter 和 setter。他们会证明这是必需的。不幸的是,这妨碍了域逻辑的实现。它与演示文稿纠缠在一起。更糟糕的是,我看到域实体被代码污染,这些代码发出 HTML 编码的字符串(例如,带有小于和大于符号编码的 HTML 代码)和 XML,仅仅是因为表示。
如果可以将域实体实现为 JavaBean,那么直接在演示中使用它就可以了。但是如果域逻辑变得有点复杂,并且需要域实体失去它的 JavaBean 特性(例如,不再有公共零参数构造函数,不再有 setter),那么建议域逻辑部分实现域逻辑,并通过创建另一个 JavaBean 对象来满足其 MVC 需求,从而使表示部分适应。
我经常使用的一个示例是用于对用户进行身份验证的 UserAccount
。在大多数情况下,当用户希望更改密码时,还需要旧密码。这有助于防止未经授权更改密码。下面的代码清楚地显示了这一点。
public class UserAccount { ... public void changePassword( String oldPassword, String newPassword) {…} }
但这不遵循 JavaBean 约定。如果 MVC 表示框架不能很好地与 changePassword
方法配合使用,一种天真的方法是删除错误的方法并添加 setPassword
方法(如下所示)。这削弱了域逻辑的隔离性,并导致团队的其他成员到处实现它。
public class UserAccount { ... public void setPassword(String password) {…} }
开发人员了解表示取决于领域逻辑很重要。而不是相反。如果表示有需求(例如 JavaBean 约定),那么它不应该让域逻辑符合该需求。相反,表示应该创建具有相应域实体知识的附加类(例如 JavaBeans)。但不幸的是,我仍然看到很多团队仅仅因为展示而强迫他们的领域实体看起来像 JavaBeans,或者更糟的是,让领域实体创建 JavaBeans(例如 DTO)用于展示目的。
这是安排您的应用程序的提示。将您的域实体和存储库放在一个包中。将您的存储库和其他基础设施实现放在一个单独的包中。将与演示相关的类保存在自己的包中。请注意哪个包取决于哪个包。包含领域逻辑的包最好位于这一切的中心。其他一切都取决于它。
使用 Java 时,包看起来像这样:
com.acme.myapp.context1.domain.model
com.acme.myapp.context1.infrastructure.persistence.jpa
com.acme.myapp.context1.infrastructure.persistence.jdbc
com.acme.myapp.context1.presentation.web
请注意,我使用了 context1
,因为在给定的应用程序(或系统)中可能有多个上下文(或子系统)。我将在以后的帖子中讨论具有多个上下文和多个模型。
目前为止就这样了。我希望这个简短的解释可以让那些想知道为什么他们的代码以某种方式排列和拆分的人有所了解。
感谢 Juno Aliento 在这次有趣的讨论中帮助我上课。
节日快乐!
标签2: Java教程地址:https://www.cundage.com/article/jcg-isolating-domain-logic.html