洋葱片架构 - odrotbohm


15年的洋葱架构是时候整容了。

自 Jeffrey Palermo 发布他的洋葱架构系列第一篇博客以来,已经过去了几乎整整 15 年。在那篇文章中,他总结了本质上构成Alistair Cockburn六边形架构方法延续的想法。
尽管我一直认为这两种代码组织方法不一定形成“架构”,但我总是发现它们有助于塑造有关如何构建代码库的心理模型。
多年来,我看到很多团队试图遵循这些模型,但遇到了误解和问题。在这篇博文中,我想总结其中的一些发现,并提出一种看待洋葱架构的精致方法。

洋葱架构
首先,让我们简要回顾一下该方法的最初理念。代码是以同心圆的形式组织起来的,而领域位于所有代码的中心。这部分代码库应该包含所有基本的领域抽象。然后,该核心被应用环包围,顾名思义,应用环包含特定于应用的代码。

杰弗里最初的提议甚至将该环划分为 "领域服务 "和 "应用服务",这通常会在团队内部引发大量关于代码归属的讨论。
其目的是将用例或工作单元中的应用逻辑暴露给其他代码,从而形成一致性范围。

最外面一个环又被基础架构环包围,基础架构环包含将特定应用接口转换为特定技术的代码。在公开方面,这可能是渲染网页的控制器,或生成适合特定 API 风格(如 JSON)的表示法。此外,数据库或消息代理的连接器也在该环中。这样,杰弗里就抽象出了阿利斯泰尔在六边形架构中引入的不同类型的适配器(驱动与被驱动)。

最后一块是定义环之间的依赖方向:外环依赖于内环,为其依赖者提供接口,供其调用(与应用服务一起工作的控制器)或实现(应用环提供的存储库接口的特定数据库实现)。虽然对该方法的严格解释表明,一个环只能依赖于下一个内环,但在实践中,跳过一个环通常是允许的,因为执行严格的分层会导致模板代码。

问题
既然我们已经对情况有了共识,那么让我们来看看这种方法所面临的挑战。

洋葱架构(以及六边形架构)的一个核心问题是,它将领域视为单一、不透明的区块。这些架构都广泛关注领域代码与基础架构代码的分离,这很可能是这些架构起源时间的证明。将两者混杂在一起是造成严重代码质量问题的原因,尤其是在当时的企业中,这主要是由于缺乏测试未将这两方面分开的代码的能力。

不过,埃里克-埃文斯(Eric Evans)在 2000 年代初发布的 "领域驱动设计"(Domain-Driven Design)一书中已经指出--这一点在最近几年变得更加明显--:代码库质量问题的主要根源在于缺乏与业务的一致性,以及存在反映这一点的功能分离。这就提出了一个问题,即采用一种架构方法来构建代码库是否是一个好主意,因为这种方法基本上忽略了你所面临的主要挑战。

在基础架构环中,与六边形架构相比,抽象程度更高,这就产生了一个技术性稍强的问题。在该环中驻留的应用程序接口(API)/Web 和数据库/消息代理适配器都表明它们的实现方式具有某种统一性。这通常反映在通过将不同模型相互映射来与目标基础架构解耦的方法中。

在数据库方面,领域模型应该映射到持久性模型上,而持久性模型又映射到特定于数据库的结构上。当然,在目标数据存储被不同应用程序共享,且开发一个应用程序的团队会受到该存储变更影响的情况下,这是一种有效的方法。这种情况在 2000 年代中期非常普遍。如今,应用程序通常拥有自己的数据存储,我们可以选择更为简单的方法来实现持久性。在不同模型之间进行映射的需求往往被保持模型简单并在派生的其他模型中立即反映模型变化的愿望所优先考虑。例如,我们希望重命名领域模型中聚合体的一个属性,最好能立即反映到数据库迁移中,同时重命名列名,以避免今后的认知过载。

在基础架构的暴露方面,这是一个完全不同的讨论,因为我们不能随意更改目标模型。我们甚至可能不了解我们所有的 API 消费者,因此我们更希望尽可能保护这些消费者,使其免受内部结构变更的影响。区分这两种情况有助于避免大量模板代码,尤其是在设计持久化实现时。当然,洋葱架构并不阻止我们进行这种区分,但将所有基础架构放在同一个环中意味着统一性,这可能会产生误导。

最后,为了解决技术上的难题,将所有基础架构适配器分配到同一个逻辑桶中,可以使它们不受环之间定义的依赖方向的限制。存储库的实现可能依赖于控制器,而架构方法却无法捕捉到这一点。

构建领域
如上所述,构建可维护性软件系统的一个基本挑战是建立一个功能架构,以支持我们所构建应用程序的业务需求。

为了简单起见,我们暂且用 "领域 "来表示该功能架构的各个元素。

如果您遵循领域驱动设计(Domain-Driven Design),那么这将映射为一个有界上下文(Bounded Context)或其中的一个模块(Module)。

如果我们要处理多个领域,那么与这些领域协同工作的洋葱架构会是什么样子,这对领域之间的交互又意味着什么?

一种方法是将函数架构仅应用于我们安排的领域核心,
我们可以将领域驱动设计(Domain-Driven Design)应用于我们的整体领域,确定其中的不同部分,并定义这些部分之间允许的依赖方向。

虽然这样做肯定比以前更好,但我们所有的应用程序和基础架构代码仍然是一个不透明的整体,缺乏我们现在在架构核心建立的结构。如果我们将这一想法延伸开来,对其他每个环重复结构化练习,我们最终就会在每个域中形成一个洋葱。

这是朝着正确方向迈出的一步。每个 "洋葱 "都自成一体,专注于一个领域,并拥有自己的应用界面和技术适配器。不过,各个领域之间的互动现在必须通过基础设施来建立。如果我们决定将 "洋葱 "映射到单个应用程序上,例如,采用微服务安排,这可能是一个很好的选择。

视域的粒度而定,这可能不是将我们的逻辑架构投射到 "物理 "世界的最佳方式,而且还会在交互中引入大量复杂性和成本。要在系统间交换一个简单的域事件,我们必须有相应的基础设施(例如消息代理),必须在发送方将事件序列化,并在接收方将其反序列化。

综上所述,最初的洋葱架构显然缺乏对领域的关注,而在最初构想的范围内解决问题的方法要么不能令人满意,要么会带来复杂性并刺激特定的部署安排。是时候将其提升到一个新的高度了。

洋葱切片架构
我对洋葱架构提出的基本改进建议是切掉洋葱的两边。虽然这乍听起来微不足道,但这一想法对概念定义的精确性和实际应用的适用性有着重大影响。

首先,通过切边,重新建立了将应用概念映射到外部客户端(通过应用程序接口)的代码与映射到内部基础设施(如数据库和消息代理)的代码之间的概念分离。我们还隐含地确定了这样一个事实,即在默认情况下,这两者之间根本不会相互影响。应用环仍然保护单个域。

按照最初的设想,它可以暴露在基础设施中,但也可以从侧面开放,以便与形状相似的各方(见下文)进行低摩擦交互和测试。在最初的洋葱架构方法中,它们位于基础架构层,我一直觉得这有点奇怪。当然,它们充当了应用环的客户端。然而,基础架构环中的适配器也需要 "从外部 "进行集成测试,这将导致围绕基础架构环的另一个环,与整体构想不太相符。采用我们改进的方法,测试可以从侧面连接到代码,并根据其主要交互的部分调整不同类型的测试。他们可以专注于直接与应用环中的应用程序接口交互的细粒度测试,也可以通过基础架构适配器采用更全面的方法。

从根本上说,"洋葱切片 "方法仍然适合作为独立系统进行部署。尽管如此,洋葱切片的主要优势在于,我们可以在其他部署安排中安排多个洋葱切片,使其相互影响。


例如,我们可以将一组洋葱放入一个单一的部署单元,即上图中浅绿色方框所示的单元。这与我们的 "每个域的洋葱"(Onion-per-domain)方法非常相似。暴露应用环边的核心优势在于,我们可以利用运行时环境提供的功能,让各个切片洋葱相互交互。我们可以通过发布进程内部事件(下图中六边形的灯泡)或调用应用程序接口(圆圈中的豆)来实现。

例如,在 Spring 应用程序的上下文中,可以映射为使用其应用程序事件机制或直接引用另一个洋葱切片(Sliced Onion)暴露的 Spring Bean。我通常建议在洋葱切片之间采用异步、基于事件的交互方式,因为这样可以自然而然地在一个洋葱切片中保持强大的一致性,从而强调它们的自足性。此外,不引用外来洋葱暴露的 Spring Bean 可以让测试仅在被测洋葱的范围内运行。

模块
一旦我们讨论了单个切片洋葱之间的交互,我们就可以将架构抽象上升到模块,因为模块的内部洋葱结构实际上已经不再重要,而成为了一个实现细节。模块暴露了用于内部交互的事件或应用程序接口,以及连接每个模块与外部世界的基础设施适配器。因此,它们在整个应用程序安排中形成了自成一体的元素。事实上,这就是模块化应用架构的核心。

模块是发展应用程序架构的一个很好的起点,因为它们具有自然的、低成本的接缝,这在我们需要重组整体安排时很有帮助。首先,由于内部交互不会暴露给第三方,我们可以在各个模块之间轻松移动代码。集成开发环境的重构工具成为了我们强大的助手。

此外,如果有实际的组织或技术需要,这些接缝还可以在后期用于拆分系统。特别是,仅通过(异步)事件进行交互的模块,可以通过将代码移入新项目的方式,转换成另一个可部署的模块。以前已经在内部发布的事件可以外部化为一些消息基础架构,而原来的应用程序可以通过在监听模块上部署相应的基础架构适配器来与新的安排集成。


尽管如此,我们一开始采用的完全模块化的安排可能足以确保我们系统的可进化性,而且我们可能永远也不会真的分裂开来。

概述
洋葱切片架构通过加强对领域及其功能结构的关注,增强了原始方法的理念。纵向洋葱切片允许将这一想法应用到更广泛的部署选项中,并在模块化应用安排的背景下构成可演进架构的基石。

banq注:洋葱切片架构=洋葱架构+切片架构=洋葱架构+单体模块=洋葱架构+微服务