六边形架构
目标
为变化而生
“生活中唯一不变的就是变化。” — Heraclitus。这在软件(因此得名)中再正确不过了,如果你只有几周的经验,你就知道“用户不知道他们想要什么!!!”。如果第一句话是真的,那么第二句话就是谬误的!
不是用户不知道他们想要什么,事实上,没有人知道。构建软件不是通过某人告诉其他人该做什么来完成的,而是通过富有成效的合作伙伴关系来完成的。这意味着人们会一次又一次地改变主意(这完全没问题!)
因此,作为专业的软件开发人员,我们必须确保我们正在编写的代码能够欢迎这些更改。这通常从降低解决方案复杂性开始!
我们经常谈论 3 种类型的复杂性:
- 本质复杂性:在构建软件时,我们必须解决复杂度X的问题:这是本质的复杂性。这种复杂性与我们试图解决的问题直接相关,我们不能真正降低它。
- 强制复杂性:无论付出什么努力,我们将不得不为系统增加一些复杂性,因为我们有技术工作要做(持久化数据、发送消息......这种复杂性称为强制复杂性。
- 偶然复杂性:除了前两个复杂性之外,还有偶然的复杂性,我们不想要它,因为它不需要。示例:如果你有一个在过去 10 年中一直停滞不前的配置,你可能不需要把它放在数据库中,在代码中处理它会更容易、更高效(不,你可能不需要一个 3 人团队的微服务)。
六边形架构使我们能够通过为软件的每个部分赋予明确的责任来将所有这些复杂性降至最低。
该架构实施的非常明确的关注点分离简化了每个部分的自动测试,因为它只做一件事。能够构建可靠的测试也是构建易于更改的软件的好方法!
即使架构简化了测试编写,能够编写好的测试也需要时间和练习!
缩短反馈循环
在软件开发中,如果你想走得更快,你必须进行简短的反馈循环。如果你有一个按钮可以在几秒钟内告诉你你的解决方案仍然按预期运行,这将比在任何更新后手动检查它快得多(事实上,你不会在任何更新后手动检查......)
实际上:六边形架构对最快的反馈循环没有帮助,即成对或生物编程中的结对反馈。但是,紧随其后的是编译,为此,六边形架构将有所帮助!由于非常好的关注点分离,您将能够构建具有非常高内聚力和非常低耦合的模块(Java包)。这意味着,基础结构模块中的大多数类将永远不会离开那里,因此允许编译时反馈。
还有另一个很棒的编译时反馈不是直接来自架构,而是来自这些架构中经常使用的实践:类型驱动开发。这个想法非常简单:为每个业务概念创建一个专用类型。例:
Firstname
:是的,这是一个String
但这不是电话号码或克林贡语词典,因此请为其创建一个类型(带有一些检查和格式)。Lastname
:是的,另一个String
也是同样的原因。
只有这两个你可以完全修复很多错误 firstname
和 lastname
方法参数中的反转:如果你发送错误的参数,它就不会编译!
然后,比编译慢一点的是自动化测试。如前所述,此架构简化了测试,因此您将能够从测试中获得快速(此处以秒为单位)和可靠的反馈。
我们之前说过,结对反馈是最快的,但业务专家的反馈呢?使用“经典”(控制器,服务,存储库)架构,我们必须构建一个完整的“东西”,希望得到业务专家的反馈。在这里,域模型代码非常接近业务,因此与业务专家坐在屏幕上并从中获得反馈非常容易!当然,您可能需要解释一些“编码内容”,但是您将能够尽早从业务专家那里获得有关任何给定算法的反馈!
延迟基础架构选择
通常通过会议开始一个项目来“构建架构”,在这种情况下,这意味着选择基础设施元素。因此,在第 0 天,我们试图弄清楚我们是否需要 MongoDB、PostgreSQL 或两者兼而有之(那么 Elasticsearch 呢?)。
问题是:我们经常在没有足够信息的情况下做出这些选择,我们只会做出最好的猜测(因为真正的需求将来自代码)。另一个问题是我们花了很多时间这样做。另一种选择是只选择一件事:语言(我们要Java吗?)。选择语言可能已经足够具有挑战性,但它比选择与语言一起选择无数技术要容易得多。
六边形架构使我们能够在了解语言后立即开始。由此,我们将能够开始构建解决方案,并从代码中显示真正的基础结构需求。当然,我们必须尽快选择一个结构化框架(Spring,Quarkus等),但我们可以将持久性选择推迟相当长的一段时间!
延迟选择允许:
- 更好的选择。即使你说“如果需要,我们会改变”,你也必须与沉没成本谬误作斗争。;
- 更快的首次循环(因为您从引导中删除了大部分强制性复杂性)。
在哪里放置代码
最后,您正在寻找的架构部分:P。
因此,首先要做的是:应用程序由多个“六边形”组成,每个六边形对应一个界定上下文。(是的,有时您只能有一个,但这是一个例外)。我们通常将每个界定上下文作为应用程序中的根包。
最初,此体系结构以六边形(因此得名)呈现,其中心是域模型:
在此风格中,调用流如下所示:
我们可以使用此文件夹组织强制实施此体系结构:
-
my_business_context
:上下文的根包(命名取决于您的技术命名约定)
-
application
:包含应用层代码 -
domain
:包含业务代码 -
infrastructure
:
primary
:包含驱动上下文的适配器实现secondary
:包含上下文驱动的适配器实现
-
正如多次说过的,这里的每个“部分”都有一个特定的关注点,所以让我们跟随那个洞里的兔子。
域模型中的代码
这是真正重要的代码。您可以使用域驱动设计构建基块或任何其他可帮助您构建业务清晰表示的工具来构建它。
这个模型不依赖于任何东西,一切都依赖于它,所以它完全与框架无关,你只需要选择一种语言来构建你的领域模型。
除了用于进行业务操作的代码之外,我们还将在域模型中找到端口。端口用于反转依赖项。由于域模型有时需要端口才能执行某些操作,因此它们只能存在,因为域不依赖于任何内容。interfaces
应用程序中的代码
应用层不得包含任何业务规则,其职责是:
- 非常简单的编排:
- 从港口获取一些东西;
- 对该事物进行操作(在对象上调用方法);
- 使用端口保存该内容;
- 使用端口调度创建的事件。
- 交易管理;
- 授权检查(这是接线点,授权的业务必须在域中)。
主代码
主要部分包含用于驱动我们域的代码的适配器。示例:用于公开 REST Web 服务的代码。这部分在很大程度上取决于框架,并负责尽可能最好地公开业务操作。
二级代码
辅助部分由实现域端口的适配器组成。这部分在很大程度上取决于框架,其职责是尽可能充分利用我们的业务所需的基础设施。