后端架构演进介绍

10年前,你只需要知道 GoF模式,现在需要掌握:N层、DDD、六边形Hexagon、洋葱Onion、Clean架构

想想回到过去的美好时光,根本没有所谓架构,那些日子是多么幸福啊,只有了解 GoF 模式,你就能称自己为架构师。

然而,计算机变得更加强大,用户的需求增加,导致应用程序的复杂性增加。

开发人员解决的第一件事是将UI 与业务逻辑分离。根据 UI 框架的不同,诞生了不同的类似 MVC 的模式:

这在一段时间内有所帮助,但效果并不那么明显。

如果你来自 C# 社区,可能会错误地认为那些图中名为Model的黄色框只是 DTO。

这一切都是因为微软。

这张图让我们对他们的 ASP MVC 框架感到困惑。

事实上,这里的Model代表领域模型,也称为业务逻辑,这在任何应用程序中都相当关键。

你能打赌,上面这三个组件中哪一个造成的问题最多?虽然视图只是简单的图像和按钮,但控制器充当中间人,所有复杂性都集中在模型中。

那是一个 GoF 模式根本不够用的时期。因此必须出现新的想法。我们如何处理复杂性?

分而治之。

我们已经使用 MVC 做到了,所以让我们再做一次。

2002 — N 层

理想的架构并不是凭空出现的。与所有事情一样,它在尝试和错误中走自己的路。

Jesu.. khe Martin Fowler是软件开发架构的先驱,并在未来十年影响了一代又一代的开发人员:

《企业应用架构模式》描述了N层架构。

想法很简单,将所有相关代码组合在一起并依次调用这些层中代码。

然而,还有更多的事情要做。

MF知道不一致的危害很大。

因此,为了防止我们朝自己的腿开枪,他试图指导我们一些限制:

  • 您可以按照您想要的方式命名图层

  • 您可以根据需要拥有任意数量的层

  • 你可以在中间添加层

  • 同一层中可以有多个组件

  • 只需确保各层之间存在清晰的 层次结构,并逐一相互引用

它不仅帮助开发人员消除代码重复,而且最终帮助他们构建代码。

尽管这些规则非常灵活,但实际上,3 层对于大多数项目来说已经足够了。

  • 用户界面(UI)——负责与用户交互。

  • 业务逻辑层 (BLL) — 表示业务概念。它规定了您的应用程序正在执行的操作,并使其与其他应用程序相比如此独特。

  • 数据访问层 (DAL) — 将数据保留在内存中并保持应用程序的状态

对业务逻辑和 UI 进行了明确的分离。事实证明,数据库与业务规则一样重要,因此它值得拥有自己的层。实际上,所有外部技术也可以进入最后一层。

如果您想知道这些彩色矩形和箭头对您意味着什么,请不要担心,这很简单。这些层只是解决方案中的项目,箭头表示这些层之间的依赖关系。

这种分离不一定是项目的物理分离,而可以是文件夹的逻辑分离。您还可以结合使用这两种方法。使用最适合您的。

文件夹和项目之间的区别很大。项目实际上允许您控制依赖项。对于文件夹,您甚至可能不会注意到,当一个层开始使用另一层的组件时。另一方面,项目太多,代码变得更加脆弱且难以维护。

请记住,这没有严格的规则。亲自尝试一下,看看什么最适合您。

这是可靠性和复杂性之间的权衡。

我的建议是不要创建太多项目,除非您确实需要它们,每层一个项目就足够了。

每一层通过其 API 调用下面的一层,该 API 通常以interface.每个类上的访问修饰符与这些层一样重要:

现在这对你来说似乎是显而易见的,但这只是因为你并没有经历过真正艰难的时期。

它总是很容易使用,但很难发明。

2003 — 领域驱动设计

领域驱动设计:解决软件核心的复杂性》,这让世界上至少有一个马丁感到非常难过。

Evans 同意 Fowler 的所有想法,即项目依赖关系应该针对一个方向。然而,他也提到,低级模块调用上面的模块是可以的,除非它不违反依赖方向规则。可以通过回调、观察者模式等来实现。

他还发现控制器有太多逻辑,因此他将其移动到另一个称为“应用程序Application”层。我们开始获得用例的萌芽,但尚未完全。

但埃文斯所做的最重要的事情是说“去它的数据库,业务逻辑更重要”。 他这么说,然后什么也没做。

不过,从架构的角度来看,他并没有太大的改变。

在他的架构中,定义了下一层:

  • 表示层——负责与用户交互。

  • 应用层——协调任务并将工作委托给域对象。

  • 领域层——代表业务概念。它规定了您的应用程序正在执行的操作,并使其与其他应用程序相比如此独特。

  • 基础设施层——将数据保存在内存中并保持应用程序的状态

你可以看到,他做了一些重命名。

用户界面意味着您有用户,但情况并非总是如此。有时是用户的GUI(图形用户界面),有时是开发人员的CLI(命令行界面),更多时候是程序的API(应用程序编程接口)。表示层只是一个更通用和合适的名称。

业务逻辑对于一些开发人员来说是令人困惑的,特别是对于那些根本不做业务的人来说,因此引入了一个新名称——Domain领域层。

数据库不是我们使用的唯一外部工具,因此所有电子邮件发送器、事件总线、SQL 和其他都移至基础设施。

基本上就是这样。这里有一些重命名。在那里加上一个新层。我们为该领域付出了很多努力。但它是具有相同依赖关系的相同架构。如果他知道依赖倒置原则就好了。

2005 年 — Hexagon(端口和适配器)

以前,模块必须引用行中的下一个模块。随着依赖倒置DI/IOC的发现,一切都改变了。

这对于软件开发人员来说是一个难以置信的机会。我们终于学会了如何控制依赖项的方向,以我们喜欢的方式指向它们!这意味着业务逻辑不再引用数据访问。

潜力的人是阿利斯泰尔·科伯恩(Alistair Cockburn)。那家伙很嗨,画了一个六边形,试图召唤撒旦,等等。我不需要告诉你,你自己更清楚摇滚派对是如何进行的。

这里没什么特别的,有一天你抽了一些麻醉,第二天,早上醒来,宿醉很厉害,发现自己意外地发现了一个新的架构。

阿利斯泰尔厌倦了矩形,所以他画了一个六边形,为所有东西想出了两个名字,试图让它变得神圣。但别吓到我的开发者小伙子。事实上,这种架构并不比 N 层架构复杂多少:

阿利斯泰尔让埃文斯梦想成真。现在,领域已成为系统的核心组成部分,不仅在言语上,而且在行动上。它不引用任何其他项目。

为了强调它确实是心脏,Business Logic 更名为Core。

基础设施模块分为两半——抽象(接口)和实现。抽象成为业务逻辑的一部分,并被重命名为端口Port。实施停留在基础设施层。现在它们被称为适配器Adapter。

实践证明,UI 和 DB 是同一个框架层,所以也遭遇了同样的命运。

在您的业务逻辑中拥有基础设施的接口,可以使域具有自主性和无依赖性。

因此,业务逻辑可以在任何环境中使用任何工具工作。您想更改数据库吗?只需更改实现,实现所需的适配器,并将其“插入”到可用端口即可。

任何适配器(数据库、电子邮件发送器、用户界面)的更改都不会影响业务逻辑。接口保持不变。

每个组件都可以单独部署。如果更改数据访问,则只需重建数据访问。如果更改 UI,则仅更改 UI。

由于模块可以单独部署,也就意味着可以单独开发。

调用我们系统的适配器称为主(驱动)。那些被我们系统调用的称为次要(驱动)。

就解决方案结构而言,这些最适合我:

同样,文件夹与项目是您应该自己决定的。

只需遵循参考文献并确保它们不会交叉到不应该交叉的地方:

2008 — 洋葱架构

杰弗里·巴勒莫。这是一个充满悲伤和黑暗的悲伤故事,讲述了一个男孩天真的童年被洋葱的残酷沉思所破坏的故事。随着他的成长,一种炽热的仇恨在他内心熊熊燃烧,燃烧着他有一天会实现的复仇承诺:

相信我,他永远信守了自己的诺言。他的小洋葱让全世界数以百万计的开发者一边哭一边跑到妈妈的怀里。

这种架构在端口和适配器方面得到了很大的增强。它仍然涉及依赖倒置。它通过抽象和实现来分割代码。端口仍然是业务逻辑的一部分。只是这次巴勒莫从埃文斯模式添加了应用程序层,它也可以包含一些端口。

这种架构的最大挑战是模块之间的依赖关系,它导致了如此多的混乱。

然而,规则很简单:任何外层只能且仅依赖于内层。

域位于最中间。它内部没有内层,因此它不应该依赖于任何其他层。

应用程序仅包装域,因此这正是它应该具有的唯一依赖项。

基础设施层和表示层位于同一级别,它们不能相互依赖,但可以依赖于应用程序和域,其中定义了所有需要的接口。

您还可以看到它具有 DDD 架构中的所有模块,但处理方式不同。

这实际上是一件大事!这里的关键是,中间的组件很少修改,而边缘的组件经常更改。应用程序或任何其他层的更改不会影响域,只会影响依赖层。域发生变化的唯一原因是业务逻辑发生变化,而这种情况无论如何都会影响整个系统。

理论看起来就是这样。实际上,您的组合根(Main()注册所有依赖项并将模块组合在一起的函数)将是表示层(ASP、WPF、CLI)的一部分,因此该图将具有以下外观:

你看起来很熟悉吗?它是N层架构,但组件的顺序不同。

无论它看起来如何,六边形,端口或洋葱,您的最终目标应该是让您的依赖关系以无环图或树的形式指出。

2012 — 清洁Clean架构

有一个叫鲍勃叔叔的人,

他是工作中最干净的程序员,

凭借他敏捷的动作和架构,

他会让你的代码焕然一新,

他看到了围绕架构的所有炒作,并决定破坏这个聚会。马丁知道任何开发人员的主要秘密,所以他甚至不想隐藏它。只是厚颜无耻地窃取别人的想法并称其为自己的。

开个玩笑,现在没什么原创的想法了,大家互相抄袭

我们可怜的域名再次被重命名。现在是实体。然而,不仅如此。这意味着您没有领域服务和贫乏的模型,而是具有数据和行为的丰富类。

存储库和其他端口的接口从域移至应用层。这又得到了一个更合适的名称——用例。

表示层和基础设施层保持不变。然而,Martin 还在上面添加了一层额外的层,其中包括框架、DLL 和其他外部依赖项。这并不一定意味着您的数据库将引用实体,它只是阻止您从内层引用那些外部工具。

再次强调,没有严格的规则。您可以根据需要在任何级别添加任意数量的层。因此,如果您想为域服务定义一个层,则可以。

马丁还在架构大图附近画了一个小图。

它显示用户通过触发控制器的端点与系统进行通信,该端点调用用例,然后通过演示器返回数据(黑线)。用例可以通过接口(绿线)调用任何类似的端口。而实际实现是外层的一部分(橙色线)。

它试图强调执行流程(虚线)并不总是对应于依赖方向(直线),这就是依赖倒置原则。

基本上,它再次强调了控制反转的用法。当我们讨论端口和适配器时,您已经看到了这一点。

通常在 ASP 中我们没有单独的 Presenter 组件。这也是由控制器完成的。因此整个图可以用如下代码表示:

class OrderController : ControllerBase、IInputPort、IOutputPort

{

[ HttpGet ]

public IActionResult Get ( int id )

{

_getOrderUserCase.Execute(id); }

return DisplayOutputPortResult();

}

}

其他形式的隔离

所有这些架构的目标都是通过划分职责来将一个代码与另一个代码隔离。然而,还有其他形式的隔离:垂直切片、有界上下文、模块、微服务等等。这里的目标是按功能拆分代码。

有些人不认为它们是“真正的”架构方法,而有些人则认为。它是由你决定。最终,他们将发展到仍然会使用上面的任何架构风格,甚至是这些风格的组合:

结论

在本文中,我们讨论了 N 层、DDD、Hexagon、Onion 和 Clean 架构。这些并不是唯一存在的架构。然而,所描述的是最著名的。您可能还听说过 BCE、DCI 等。

尽管细节上存在细微差别,但所有架构几乎都是相同的。它们都服务于同一个目的——分担责任。他们都是通过将代码拆分到不同的层来实现的。全部区别在于定义了哪些组件以及这些层之间存在哪些依赖关系。

更多话题: