上一章讨论完分层架构之后,您可能期望本章讨论一种替代方法。我们将首先讨论两个 SOLID 原则,然后应用它们来创建一个“整洁”或“六边形”架构,以解决分层架构的问题。
单一职责原则
软件开发中的每个人可能都知道单一职责原则(SRP:Single Responsibility Principle),或者至少假设知道它。 该原则的常见解释是:
一个组件应该只做一件事,并且把它做好。
这是个好建议,但不是 SRP 的实际意图。
“只做一件事”实际上是对单一职责最明显的解释,难怪 SRP 经常被这样解释。SRP 的名称是真具有误导性。
以下是 SRP 的实际定义:
一个组件应该只有一个改变的理由。
正如我们所见,“职责”实际上应该翻译为“改变的理由”,而不是“只做一件事”。也许我们应该将 SRP 重新命名为“单一理由改变原则”。
如果一个组件只有一个改变的理由,那么它最终可能只会做一件事,但更重要的是它只有这一个改变的理由。 这对我们的架构意味着什么?
如果一个组件只有一个原因需要更改,那么如果我们因任何其他原因更改软件,我们根本不必担心这个组件,因为我们知道它仍然会按预期工作。
遗憾的是,更改原因很容易通过组件与其他组件的依赖关系在代码中传播(见图 6)。
在上图中,组件 A 依赖于许多其他组件(直接或传递),而组件 E 根本没有依赖项。 更改组件 E 的唯一原因是当 E 的功能由于某些新需求而必须更改时。
然而,当任何其他组件更改时,组件 A 可能必须更改,因为它依赖于它们。
随着时间的推移,由于违反了 SRP,许多代码变得越来越难以更改,因而成本也越来越高。随着时间的推移,组件会收集越来越多的更改原因。
在收集了许多更改原因后,更改一个组件可能会导致另一组件发生故障。
依赖倒置原则
在分层架构中,跨层依赖关系总是向下指向下一层。当我们在高层应用单一职责原则时,我们注意到上层比下层有更多的理由进行更改。
因此,由于域层对持久层的依赖,持久层中的每次更改都可能需要域层中的更改。但域代码是我们应用程序中最重要的代码!我们不想在持久性代码发生变化时也必须更改它!
那么,我们怎样才能摆脱这种依赖呢?
依赖倒置原则提供了答案。
与 SRP 不同,依赖倒置原则 (DIP:Dependence Inversion Principle) 顾名思义:
我们可以扭转(反转)代码库中任何依赖关系的方向,这是如何运作的?让我们尝试反转域和持久性代码之间的依赖关系,使持久性代码依赖于域代码,从而减少域代码更改的原因。
我们从第 1 章“层有什么问题?”中的图 2 所示的结构开始。我们在域层中有一个服务,可以与持久层中的实体和存储库一起使用。
首先,我们希望将实体拉入领域层,因为它们代表我们的领域对象,而我们的领域代码几乎围绕着这些实体中状态的变化而展开。
但现在,我们在两个层之间存在循环依赖关系,因为持久层的存储库依赖于实体,而实体现在位于域层中。这就是我们应用 DIP 的地方。我们为领域层中的存储库创建一个接口,并让持久层中的实际存储库实现它。结果如图 7 所示。
通过这个技巧,我们将域逻辑从对持久性代码的压迫性依赖中解放出来。这是我们将在接下来的部分中讨论的两种架构风格的核心功能。
整洁架构
罗伯特·C·马丁 (Robert C. Martin) 在他的同名书中巩固了“整洁架构”一词。在他看来,在整洁的架构中,业务规则可以通过设计进行测试,并且独立于框架、数据库、UI 技术和其他外部应用程序或接口。
这意味着域代码不能有任何向外的依赖关系。相反,在依赖倒置原则的帮助下,所有依赖项都指向域代码。
图 8 显示了这种架构在抽象层面上的外观。
该架构中的各层以同心圆形式相互缠绕。这种架构中的主要规则是依赖关系规则,它规定这些层之间的所有依赖关系都必须指向内部。
该架构的核心包含由周围用例访问的领域实体。用例就是我们之前所说的服务,但是更细粒度,具有单一职责(即单一的更改原因),从而避免了我们之前讨论的广泛服务的问题。
围绕这个核心,我们可以找到应用程序中支持业务规则的所有其他组件。例如,这种支持可能意味着提供持久性或提供用户界面。此外,外层可以为任何其他第三方组件提供适配器。
由于域代码不知道使用哪个持久性或 UI 框架,因此它不能包含特定于这些框架的任何代码,并将专注于业务规则。我们拥有对域代码进行建模所需的所有自由。例如,我们可以应用最纯粹的领域驱动设计(DDD)。不必考虑持久性或 UI 特定问题,这就变得更加容易。
正如我们所料,整洁架构是有代价的。由于领域层与持久层和 UI 等外层完全解耦,因此我们必须维护一个模型每个层中应用程序的实体。
例如,假设我们在持久层中使用对象关系映射(ORM)框架。 ORM 框架通常需要特定的实体类,其中包含描述数据库结构以及对象字段到数据库列的映射的元数据。由于领域层不知道持久层,因此我们不能在领域层中使用相同的实体类,而必须在两个层中创建它们。这意味着当域层向持久层发送数据和从持久层接收数据时,我们必须在两种表示之间进行转换。相同的转换适用于域层和其他外层之间。
但这是一件好事!这种解耦正是我们想要实现的,将领域代码从特定于框架的问题中解放出来。例如,Java Persistence API(Java 世界中的标准 ORM-API)要求 ORM 管理的实体有一个默认构造函数,不带我们可能希望在域模型中避免的参数。在第 8 章“边界之间的映射”中,我们将讨论不同的映射策略,包括仅接受域和持久层之间的耦合的“无映射” 策略。
由于 Robert C. Martin 的 整洁架构有些抽象,让我们更深入地了解一下“六边形架构”,它为整洁架构原则提供了更具体的形状。
六边形架构
“六边形架构”一词源于 Alistair Cockburn,并且已经存在相当长一段时间了。它应用了 Robert C. Martin 后来在他的“整洁架构”中以更一般的术语描述的相同原则。
图 9 显示了六边形架构的外观。应用程序核心以六边形表示,这种架构风格因此得名。
然而,六边形没有任何意义,所以我们不妨画一个八边形,称之为“八边形架构”。据说,简单地使用六边形代替常见的矩形,以表明应用程序可以有超过 4 个边将其连接到其他系统或适配器。
在六边形内,我们找到了我们的领域实体以及与它们一起使用的用例。请注意,六边形没有传出依赖关系,因此 Martin 的“整洁架构”中的依赖关系规则成立。相反,所有依赖项都指向中心。
在六边形外,我们发现了与应用程序交互的各种适配器。可能有一个与 Web 浏览器交互的 Web 适配器、一些与外部系统交互的适配器以及与数据库交互的适配器。
左侧的适配器是驱动我们的应用程序的适配器(因为它们调用我们的应用程序核心),而右侧的适配器由我们的应用程序驱动(因为它们被我们的应用程序核心调用)。
为了允许应用程序核心和适配器之间的通信,应用程序核心提供特定端口。对于驱动适配器,这样的端口可能是由核心中的用例类之一实现并由适配器调用的接口。对于从动适配器,它可能是由适配器实现并由核心调用的接口。
由于其核心概念,这种架构风格也称为“端口和适配器”架构。
就像整洁架构一样,我们可以将这种六边形架构组织成层。最外层由在应用程序和其他系统之间进行转换的适配器组成。
接下来,我们可以将端口和用例实现组合起来形成应用程序层,因为它们定义了我们应用程序的接口。最后一层包含领域实体。
在下一章中,我们将讨论在这种架构中如何来组织代码的方法。
这如何帮助我构建可维护的软件?
将其命名为“整洁架构”、“六角形架构”或“端口和适配器架构”——通过反转我们的依赖关系,使域代码不依赖于外部,我们可以将域逻辑与所有这些持久性和 UI 特定问题解耦,并减少更改原因的数量整个代码库。更少的改变理由意味着更好的可维护性。
域代码可以自由建模,以最适合业务问题,而持久性和 UI 代码可以自由建模,以最适合持久性和 UI 问题。
在本书的其余部分中,我们将把六边形架构风格应用到 Web 应用程序中。我们将首先创建应用程序的包结构并讨论依赖项注入的作用。