六边形架构原文翻译:Hexagonal Architecture: three principles and an implementation example

先上原文链接为敬:https://blog.octo.com/hexagonal-architecture-three-principles-and-an-implementation-example/

在看《微服务架构设计模式》第二章时,作者提到了六边形架构,书中对六边形架构介绍不多,本人查阅资料后,找到这篇文章,个人感觉讲的很棒,遂翻译之。
六边形架构也是一种架构风格,但与分层架构不同,六边形架构不是上下分层,而是内外分层,内层是业务逻辑,外层是入站适配器和出站适配器,或者叫用户端和服务端。入站适配器通过调用入站端口处理来自外部的请求。出站适配器实现出站端口,并通过调用外部应用程序或服务处理来自业务逻辑的请求这里的入站端口就是业务逻辑暴露给外部API,供入站适配器调用。出站端口是就暴露给外部的SPI,供出站适配器实现,业务逻辑自己调用。所以六边形架构中,外部的一切都是依赖内部的。因为端口的的定义都是在业务逻辑。在这里插入图片描述

六边形架构:三个原则和一个实现示例

六边形架构是一种具有许多优点的软件架构,2005年由Alistair Cockburn提出,自2015年以来重新引起了人们的兴趣。
六边形架构的初衷是允许应用程序同样由用户、程序、自动化测试或批处理脚本驱动,并在与最终运行时设备和数据库隔离的情况下进行开发和测试。
为了探索通过自动化测试来引导应用程序的好处,或者在与数据库隔离的情况下进行开发和测试,我们建议您阅读我们最近发布的测试金字塔系列博客文章: the test pyramid by practice

这个承诺相当有吸引力,而且它还有另一个好处:它允许隔离应用程序的核心业务,独立于其他一切自动测试其行为。这可能就是为什么这个体系结构吸引了领域驱动设计(DDD)实践者的注意。但要注意,DDD和六边形架构是两个截然不同的概念,它们可以相互加强,但不一定要一起使用。但这是另一个话题了!

最后,这个体系结构设置起来并不复杂。它基于一些简单的规则和原则。让我们来探讨一下这些原则,看看它们在实践中的意义。

六边形架构原理

六边形架构基于三个原则和技术:

  • 明确地分离用户端、业务逻辑和服务器端
  • 依赖关系从用户端和服务器端转移到业务逻辑
  • 通过使用端口和适配器隔离边界

原则:显式地分离用户端、业务逻辑和服务器端

第一个原则是明确地将代码分成三个大的形式化区域。
在这里插入图片描述

左边是用户端

这是用户或外部程序与应用程序交互的一面。它包含允许这些交互的代码。通常,您的用户接口代码、API的HTTP路由、对使用你的应用程序的程序的JSON序列化都在这里。这一面是我们驱动业务逻辑的参与者。注:Alistair Cockburn也称其为左侧。

中间是业务逻辑

这是我们要从左右两边分离出来的部分。它包含所有涉及和实现业务逻辑的代码。业务词汇表和纯业务逻辑与解决您的应用程序的具体问题相关,使其丰富和具体的一切都处于中心位置。理想情况下,一个不知道如何编码的领域专家可以阅读这部分中的一段代码,并指出不一致的地方(真实的故事,这些事情可能会发生在您身上!

右边是外部模式

在这里,我们可以找到应用程序需要的东西,这些东西驱动着应用程序工作、
它包含基本的基础设施细节,例如与数据库交互的代码、对文件系统的调用,或者处理对您所依赖的其他应用程序的HTTP调用的代码。
在这里,我们可以找到由业务逻辑管理的参与者。
注:Alistair Cockburn也称其为Right Side。
以下原则将允许在用户端、业务逻辑和服务端之间实现这种逻辑分离。

这为什么重要

这种分离的第一个重要特征是它分离了问题。在任何时候,您都可以选择专注于单个逻辑,几乎独立于其他两个逻辑:用户端逻辑、业务逻辑或服务器端逻辑。它们在不混合的情况下更容易理解,并且每个逻辑的约束对其他逻辑的影响更小。(ps:个人理解意思就是把他们三个分开了,我们任何时候可以只关注一个 ,不容易混淆它们,它们之间的约束也比较小)

另一个特点是我们把业务逻辑放在代码的最前面。它可以被隔离在一个目录或模块中,以便对所有开发人员都是显式的。它可以被定义、改进和测试,而不需要承担程序其余部分的认知负荷。这一点很重要,因为最终是开发人员对业务的理解进入了生产环境。

最后,就自动化测试而言(正如我们将在下面看到的),我们将通过合理的努力成功进行测试:

  • 整个业务逻辑是独立的
  • 用户端和谈业务逻辑之间的集成,独立于服务端
  • 业务逻辑和服务端之间的集成独于哟用户端
说明:一个应用程序的小例子

为了更具体地说明这些原则,我们将使用2017年由Thomas Pierrain (@tpierrain)和Alistair Cockburn (@TotherAlistair)本人提出的“Alistair in the Hexagon”活动中使用的小示例。注意:您将在文章末尾找到视频和事件代码。
这个小应用程序的目的是提供一个命令行程序,将诗歌写入控制台的标准输出中。
该应用程序预期输出的示例:

$ ./printPoem
Here is some poem:
I want to sleep
Swat the files
Softly, please.
-- Masaoka Shiki (1867 - 1902)
Type enter to exit...

为了正确地说明三个区域(用户端、业务逻辑、服务器端),这个应用程序将在一个外部系统中搜索诗歌:一个文件。我们也可以将这个应用程序连接到数据库,原理是相同的。
在这种情况下,我们如何应用第一个原则,即分为三个区域? 左边(什么驱动)、中间(核心业务)和右边(驱动什么)该如何分配?
在这里插入图片描述

用户端

用户的角度来看,程序是作为一个控制台应用程序呈现的,所以控制台在用户端。用户将通过控制台驱动域。

服务端

从技术上讲,我们的诗是存储在一个文件里的。这个文件的概念可以在右边的服务器端找到。业务将通过试点这一侧来提出其poetry的请求,具体由PoetryLibraryFileAdapter实现。在这里,正如上面提到的,我们可以轻松地交换我们的诗歌来源(一个文件、一个数据库、一个web服务……)。因此,源代码文件的实际实现是一个技术细节(也称为技术实现细节)。

业务逻辑

在这个例子中,我们的核心业务,也就是对用户有价值的东西,是可以"阅读诗歌"。我们可以在代码中使用PoetryReader类来实现这个业务。

用户端→业务逻辑 交互

从业务的角度来看,请求是否来自控制台应用程序并不重要,重要的是我们希望能够抽象出技术细节。这正是最初的动机之一:“既由用户驱动,也由测试驱动”。因此,在业务逻辑中没有控制台的概念。然而,从用户的角度来看,我们的应用程序允许(=它提供的服务)是可以请求诗歌。我们将在业务逻辑中发现这个概念(由irequestverse具体化),它将允许用户端与业务逻辑交互。

业务逻辑→服务器端 交互

类似地,从业务逻辑的角度来看,诗歌来自文件还是数据库并不重要。我们都希望能够独立于外部系统测试我们的应用程序。在业务逻辑中没有文件的概念。具体操作而言,域名仍然需要获取诗歌。我们发现在业务逻辑中可以以IObtainPoems 接口的形式获取诗歌的概念。它可以使得允许许领域与服务器端交互。

注意:从这里开始,当您阅读图表时,您可以开始观察表示类之间关系的箭头。实箭头表示调用或组合交互。没有填充的箭头表示继承关系(如UML)。但不需要马上分析所有内容,稍后我们会详细探讨。

注意:IRequestVerses和IObtainPoems代表了许多接口,我们将遵循一个原则来讨论它们。有趣的是,界面名称以«i»开头的惯例已经不再流行,但Thomas Pierrain将界面名称读作第一人称单数的句子。例如:IRequestVerses 读作:I request verses,我喜欢这个想法。

原则: 依赖朝内

这是实现目标的基本原则。我们已经在前面的原则中看到了这一点。
在这里插入图片描述
原则:依赖关系进入业务逻辑
程序可以通过控制台和测试来控制,在业务逻辑中没有控制台的概念。业务逻辑不依赖于用户端,而是用户端依赖于业务逻辑。用户端(ConsoleAdapter)依赖于poem request的概念IRequestVerses (它定义了用户的通用的«poem request»机制)。
类似地,程序可以独立于其外部系统进行测试,业务逻辑不依赖于服务器端,反之亦然。服务器端依赖于业务逻辑,通过iobtainpoetry的概念。从技术上讲,服务器端的类将继承并实现业务逻辑中定义的接口,我们将在下面详细讨论依赖倒置。

如果我们将依赖关系(<<依赖于…>>)视为箭头,则此原则将中心业务逻辑定义为内部,其他所有内容定义为外部(参见图)。当我们讨论六边形建筑时,我们经常会发现这些内外概念。它甚至可以成为一个用来记忆和传授的基本要点:依赖于内部

换句话说,一切都依赖于业务逻辑,业务逻辑不依赖于任何东西。Alistair Cockburn坚持用内部和外部区分,比用户端和服务器端之间的区分更结构化,以解决最初的问题。
](https://img-blog.csdnimg.cn/d4995ee2312e40da91366321840c37d0.png)

原则:边界与接口隔离

在这里插入图片描述
总而言之,用户端代码通过定义在业务代码中定义的接口(这里是IRequestVerses)驱动业务代码。业务代码通过定义在业务逻辑中的接口(IObtainPoems)驱动服务器端。这些接口充当内部和外部之间的显式"绝缘体"。

一个比喻:端口和适配器

六边形架构使用端口和适配器的比喻来表示内部和外部之间的交互。如图所示,业务逻辑定义端口,如果各种适配器遵循端口定义的规范,它们就可以在端口上互换连接。
在这里插入图片描述
例如,我们可以想象业务逻辑的一个端口,我们将在单元测试期间连接硬编码的数据源,或者在集成测试中连接真实的数据库。只需在服务器端编写相应的实现和适配器,业务逻辑不受此更改的影响。

这些由业务代码定义的接口是端口和适配器比喻的端口,它们隔离并允许与外部世界进行交互。注意:如前所述,端口是由业务定义的,因此它们位于内部。

另一方面,适配器代表外部代码,它将端口与其他用户端代码或服务器端代码粘合在一起。这里,适配器分别是ConsoleAdapter和PoetryLibraryFileAdapter。这些适配器在外面。

另一个比喻:六边形
在这里插入图片描述
另一个给这种架构命名的比喻是六边形,正如我们在前面的图中看到的那样。为什么是六边形?主要原因是它是一个易于绘制的形状,在图上留下了表示多个端口和适配器的空间。事实证明,即使六边形最终是相当轶事(可能就是有点搞笑点的意思吧,和六边形没啥关系看着,哈哈哈),表达成六边形架构比表达成端口和适配器模式更受欢迎。可能是因为听起来更好?

理论部分结束了,没有其他原则:对于其他一切,我们都是完全自由的。

细节:代码内部和外部是如何组织的?

除了上述原则之外,我们可以完全自由地按照自己的意愿组织每个区域内的代码。

对于业务代码,在内部,一个好主意是选择根据业务逻辑来组织其模块(或目录)。

要避免的一种组织是按类型对类进行分组。例如,«ports»目录,或者«repositories»目录(如果您使用此模式),或者«services»目录。在你的业务代码中100%考虑业务,包括模块或目录的组织!理想的情况是能够打开一个目录或一个业务逻辑模块,并立即了解您的程序解决的业务问题;而不是只看到«存储库»、«服务»或其他«管理器»目录。

参见这个话题:
https://medium.com/@msandin/strategies-for-organizing-code-2c9d690b6f33
https://martinfowler.com/bliki/PresentationDomainDataLayering.html

细节:在运行时

如何实例化所有这些来满足运行时依赖关系?如果您正在使用依赖注入框架,则可能不需要问自己这个问题。但我认为要理解六边形架构,看看应用程序启动时发生了什么是很有趣的。要做到这一点,至少在本文期间不要使用依赖注入框架。

例如,如果我们手动实例化所有内容,我们将如何编写应用程序的入口点:

class Program
{
    
    
    static void Main(string[] args)
    {
    
    
        // 1. Instantiate right-side adapter ("go outside the hexagon")
        IObtainPoems fileAdapter = new PoetryLibraryFileAdapter(@".\Peoms.txt");

        // 2. Instantiate the hexagon
        IRequestVerses poetryReader = new PoetryReader(fileAdapter);

        // 3. Instantiate the left-side adapter ("I want ask/to go inside")
        var consoleAdapter = new ConsoleAdapter(poetryReader);

        System.Console.WriteLine("Here is some...");
        consoleAdapter.Ask();

        System.Console.WriteLine("Type enter to exit...");
        System.Console.ReadLine();
    }
}

实例化顺序通常是从右到左:

  1. 首先,我们实例化服务器端,这里是读取文件的fileAdapter。
  2. 我们实例化将由应用程序驱动的Business Logic类,即通过注入构造函数将fileAdapter注入其中的poeyreader。
  3. 安装用户端,即将驱动poeyreader并写入控制台的consoleAdapter。这里,poetryReader通过注入构造函数注入到consoleAdapter中。

我们说过内部不应该依赖外部。那么,为什么要将fileAdapter(来自服务器端的代码)注入到poeyreader(来自业务逻辑的代码)中呢?

我们可以这样做,因为通过查看模式和代码,除了作为PoetryLibraryFileAdapter(服务器端)之外,fileAdapter也是IObtainPoems的继承实例。

在实践中,PoetryReader不依赖于PoetryLibraryFileAdapter,而是依赖于IObtainPoems,后者在业务逻辑代码中有很好的定义。您可以通过查看其构造函数的签名来检查它。

public PoetryReader(IObtainPoems poetryLibrary)
{
    
    
    this.poetryLibrary = poetryLibrary;
}

PoetryLibraryFileAdapter和PoetryReader是弱耦合的。

细节:右侧的依赖项反转

fileAdapter的定义依赖于业务(此处为继承依赖关系),但在运行时poetryReader可以在实践中控制fileAdapter的实例,这是依赖关系反转的典型情况。

事实上,如果没有IObtainPoems接口,业务代码的定义将依赖于服务器端代码,我们希望避免这种情况:
在这里插入图片描述
该接口允许反转此依赖关系的方向:

在这里插入图片描述
除了使业务独立于外部系统之外,右边的这个接口还允许满足著名的SOLID的D,即依赖反转原则。这个原则说:
高级模块不应依赖于低级模块。两者都必须依赖于抽象。
摘要不应依赖于细节。细节必须取决于抽象。
如果我们没有接口,我们将有一个高级模块(业务逻辑),它将依赖于一个低级模块(服务器端)。
注意:对于左侧和业务代码之间的交互,依赖关系自然是正确的。

在这里插入图片描述
除了使业务独立于外部系统之外,右边的这个接口还允许满足著名的SOLID的D,即依赖反转原则。这个原则说:
高级模块不应依赖于低级模块。两者都必须依赖于抽象。
摘要不应依赖于细节。细节必须取决于抽象。
如果我们没有接口,我们将有一个高级模块(业务逻辑),它将依赖于一个低级模块(服务器端)。
注意:对于左侧和业务代码之间的交互,依赖关系自然是正确的。

细节:为什么界面在左边?
由于用户端和业务逻辑之间的依赖关系已经朝着正确的方向发展,IRequestVerses接口的作用不是反向依赖关系。
然而,它仍然有一个兴趣:明确限制用户端代码和业务逻辑代码之间的耦合面。
在实践中,PoetryReader类可以有IRequestVerses接口以外的其他方法。重要的是,ConsoleAdapter没有意识到这一点。
它与另一个SOLID原则,界面分离原则相一致。
不应强迫客户依赖他们不使用的方法。
在这里插入图片描述
但是,一旦您理解了意图,如果左侧的端口只有一个方法,并且它的实现只有一个(如我们的示例中所示)方法,那么接口真的有必要吗?在一种最终会通过鸭子打字工作的动态语言中?
我们可以回答一个问题:您的团队对此有何看法?隔离目标是否对每个人都清楚,甚至不需要一个界面来触发对话?完全由你来决定。

六边形架构测试

这种软件体系结构的一个重要好处是它促进了测试自动化,这是其初衷的一部分。

如何替换用户端的一些代码?

在一般情况下,左侧代码的角色可以由测试框架直接扮演。实际上,测试代码可以直接驱动业务逻辑代码。
在这里插入图片描述
注意:该图显示了一个集成测试,因为右边的部分没有被替换。它也是可以被替换的,见下文。

如何替换服务端的一些代码?

右边的代码必须由业务驱动。一般来说,如果你想编写一个单元测试,你可以用mock或任何其他形式的测试替身来代替它,这取决于你想要测试的内容。
在这里插入图片描述
达到目标!
允许应用程序由用户、程序、自动测试或批处理脚本驱动,并在与其可能的执行系统和数据库隔离的情况下进行开发和测试。
注意!这并不妨碍您测试用户端和服务端代码,任何代码都值得测试。关于这个问题,我再次向你介绍“实践测试的金字塔”系列。
事实上,通过结合我们所取代或不取代的东西,我们看到,通过这种架构,我们可以测试我们想要的东西:

  • 整个业务逻辑独立
  • 用户端和业务逻辑之间的集成,独立于服务端
  • 业务逻辑和服务端之间的集成,独立于用户端

更进一步

作为一个团队来谈论它,谁已经知道如何在家里做到这一点了?
继续,在现实生活中对你的代码进行实践。例如,一个小的个人项目,或与您的团队一起的小项目。对你来说什么是容易的,什么是困难的?
以下是您在实施过程中可能遇到的一些其他问题:

  • 一个端口只能有一个方法,或者可以将多个方法分组。在你的场景下,怎么样是合理的?
  • 即使它很好地遵循了依赖性原则,代码也不一定要分为三个显式模块、目录、包或名称空间。与Thomas Pierrain的代码一样,我在«域»目录中多次看到业务逻辑代码,在«基础设施»目录中也多次看到用户端和服务器端代码。在他的示例中,内部代码位于HexagonalThis.Domain命名空间中,外部代码位于HeHexagonal This.Infra命名空间中。

快速提醒:没有灵丹妙药。六边形架构是复杂性和能力之间的一个很好的折衷,也是发现我们所讨论的主题的一个非常好的方式。但这只是其他解决方案中的一个。对于简单的情况,可能过于复杂,对于复杂的情况,则可能过于简单。还有其他值得探索的软件体系结构。例如,Clean Architecture在形式化和隔离方面走得更远。或者在一个不同但兼容的轴上,CQRS可以更好地将阅读和写作分开。

参考文献

活动“Alistair in The Hexagone”的视频在这里。该事件的代码在Thomas Pierrain的github上。
你也可以阅读这些关于这个主题的好文章:
http://alistair.cockburn.us/hexagonal-architecture
http://wiki.c2.com/?HexagonalArchitecture/
https://martinfowler.com/bliki/PresentationDomainDataLayering.html
https://fr.slideshare.net/ThomasPierrain/coder-sans-peur-du-changement-avec-la-meme-pas-mal-hexagonal-architecture
http://tpierrain.blogspot.fr/2016/04/hexagonal-layers.html
http://alistair.cockburn.us/Configurable+Dependency
http://blog.cleancoder.com/uncle-bob/2016/01/04/ALittleArchitecture.html

最后,感谢Thomas Pierrain允许我复用他的样本代码,并感谢以下人士的建议和校对:Etienne Girot、Jérôme Van Der Linden、Jennifer Pelisson、Abel André、Nelson Da Costa、Simon Renault、Florian Cherel Enoh、Mathieu Laurent、Mickael Wegerich、Bertrand Le Foulgoc、Marc Bojoly、Jasmine Lebert、Benoît Beraud、Jonathan Duberville和Eric Favre。
更新说明:在本文的第一个版本中,我们使用了“应用程序”、“域”和“基础结构”来代替“用户端”、“业务逻辑”和“服务器端”。我们回到原来的单词,因为这种替换有时模棱两可,没有帮助。

猜你喜欢

转载自blog.csdn.net/Theflowerofac/article/details/132815671