谈谈后端架构的演进过程:N-Layered和DDD架构介绍

嵌入式技术

1372人已加入

描述

你想成为一名架构师,对吗?别对我撒谎,我知道你想成为架构师。即使你不想,你还是想成为一名更好的开发者。否则,你就不会花时间阅读这篇文章

这种态度值得赞赏。毕竟,我们都希望在自己所从事的领域变得更好,即使不能称为最好。我在这里就是为了帮助你实现这一目标。

那么,你如何成为一名架构师呢?当然是通过学习所有的架构!显然这不现实。你不需要知道所有的架构。你也不需要对所有的架构都有经验。但是,至少了解最流行的几种架构 ,比如 N-Layered、DDD、Hexagon、Onion 和 Clean 架构;了解它们的历史、用途以及它们之间的区别,无疑会让你在与其他开发者的比较中脱颖而出。

希望你感兴趣,让我们开始吧。

一切始于何处?

回到那些美好的过去,根本没有架构的概念。那是多么幸福的日子啊,你只需要了解 GoF 设计模式,就能自称为架构师。

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

开发人员首先解决的问题是将用户界面与业务逻辑分离。 根据不同的用户界面框架,出现了各种类似 MVC 的模式:

代码

这虽然有效,但效果不是很好。如果你和我一样来自 C# 社区,你可能错误地认为那些图表上称为 “Model” 的黄色方框只是 DTO(数据传输对象)。这完全是因为微软的错。他们用 ASP MVC 框架把我们搞糊涂了。可恶的微软!

实际上,在这里,“Model” 代表的是领域模型,也就是业务逻辑,在任何应用程序中都非常关键。

你能猜到上述三个组件中哪个引起的问题最多吗?视图只是简单的图像和按钮,控制器充当中间人,而所有的复杂性都集中在模型中。

代码

那个时期,GoF 设计模式已经不够用。因此,新的想法必须出现。我们如何处理复杂性呢?没错!分而治之。 我们已经在 MVC 中这样做过了,所以让我们再次这样做。

2002 年:N-Layered(N 层架构)

理想的架构并非一蹴而就。就像所有事物一样,它是通过尝试和错误发展而来的。

那位开创软件开发架构并对接下来的几代开发者产生影响的人叫 Martin Fowler。他的观点是:

于是他们开始行动。

他发表了《企业应用架构模式》一书,其中描述了 N 层架构。

这个想法很简单,就是将所有相关的代码分组并将其称为不同的层

代码

但是,还有更多的事情要做。Fowler 知道不一致的危害有多大。因此,为了避免我们自己给自己惹麻烦,他试图给出一些限制和指导:

你可以按照自己的方式为各个层命名。

你可以根据需要设置任意多的层。

你可以在层之间添加新的层。

你可以在同一层中拥有多个组件。

只需确保层之间存在明确的层级关系,以便它们按顺序相互引用。

代码

这些规则不仅帮助开发人员摆脱了代码重复,而且最终能帮助他们构建代码。

尽管这些规则相当灵活,但在实践中,对于大多数项目来说,3 个层已经足够了。

代码

用户界面(UI):负责与用户进行交互。

业务逻辑层(BLL):表示业务概念。它定义了应用程序的行为,使其与其他应用程序独特区别开来。

数据访问层(DAL):在内存中持久化数据并维护应用程序的状态。

在这里,我们明确将业务逻辑与用户界面分离开来。数据库的重要性与业务规则相当,因此它有自己的层次。实际上,所有外部技术也可以放在这最后一层。一切都按照书中所说的进行。

如果你对那些彩色矩形和箭头的意义感到困惑,不用担心,很简单。这些层只是解决方案中的项目,箭头表示它们之间的依赖关系。

代码

分离并不一定要通过项目进行物理上的分离,而可以通过文件夹进行逻辑上的分离。你也可以将两种方法结合起来,使用最适合你的方式。

代码

文件夹和项目之间的区别很大。 实际上,项目能够帮助你更好地控制依赖关系。而使用文件夹时,你可能甚至不会意识到某个层开始使用另一个层的组件。另一方面,如果项目过多,代码会变得更加脆弱和难以维护。

请记住,这并没有严格的规定。你可以根据实际情况选择适合你的方式。这总是一个可靠性和复杂性之间的权衡。我在这里的建议是,除非确实需要,不要创建过多的项目 ,一个项目对应一个层已经足够了。

每个层通过其 API 调用下一层,通常以接口的形式表示。每个类的访问修饰符和这些层同样重要:

代码

现在这对你来说可能显而易见,但那只是因为你没有经历过真正困难的时刻。使用总是容易的,发明却很难。

2003 年:DDD(领域驱动设计)

在 2003 年,来自波士顿的一位年轻开发者 Eric Evans 发表了他自己的书《领域驱动设计:软件核心复杂性应对之道》,这本书至少让 Martin 感到非常伤心。

实际上,DDD 是一个独立的主题,需要在自己的系列文章中详细描述,所以我们现在不会展开介绍,只关注它所引入的所有架构变化。

Evans 赞同 Fowler 的所有观点,即项目的依赖关系应该是单向的。然而,他也提到低层模块可以调用上层模块,前提是不违反依赖关系的方向规则。这可以通过回调、观察者模式等方式实现。

他还注意到控制器具有过多的逻辑,于是将其移至另一个称为应用层的层级中。我们开始看到用例的雏形,但尚未完全发展起来。

然而,Evans 所做的最重要的事情是说 “忽略数据库,业务逻辑更重要 ”。他说了这句话,然后却没有采取实质性的行动。是的,是的,我知道……DDD 等等。然而,从架构的角度来看,他并没有做出太多改变。

代码

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

表示层(Presentation Layer):负责与用户进行交互。

应用层(Application Layer):协调任务并将工作委派给领域对象。

领域层(Domain Layer):代表业务概念。它决定了应用程序要做什么,并使其与其他应用程序独特区别开来。

基础设施层(Infrastructure Layer):在内存中持久化数据并维护应用程序的状态。

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

用户界面(User Interface)意味着你有用户,但并不总是这样。有时它是针对用户的图形用户界面(GUI),有时是针对开发人员的命令行界面(CLI),而通常它是针对程序的应用程序编程接口(API)。表示层(Presentation Layer)只是一个更通用和合适的名称。

业务逻辑(Business logic)对一些开发人员来说很令人困惑,尤其是那些根本没有做业务的开发人员,因此引入了一个新名称 —— 领域(Domain)。

数据库并不是我们使用的唯一外部工具,所以所有的电子邮件发送器、事件总线、SQL 和其他琐碎的东西都被移动到了基础设施层。

基本上就是这样。在这里进行了一些重命名,再加上新增了一个层级。我们在该领域付出了很多努力。但这仍然是相同的架构,具有相同的依赖关系。要是他当时知道依赖反转原则就好了。

2005 年:六边形架构(Ports and Adapters)

以前,模块必须引用行中的下一个模块。随着依赖反转原则的发现,一切都改变了。

这对于软件开发人员来说是一个难得的机会。我们终于学会了如何控制依赖关系的方向,将其指向我们希望的方式!这意味着业务逻辑不再引用数据访问层。如果你想知道为什么这是可能的,以及接口与此有何关系,你可以在这里找到答案:

https://medium.com/@iamprovidence/from-3-layered-to-ddd-architecture-in-one-step-f3de204bec2e

代码

第一个看到这个潜力的人是 Alistair Cockburn。这家伙吸很嗨,画了一个六边形,试图召唤撒旦,等等。我不需要告诉你,你自己更了解在摇滚派对上发生的情况。没什么特别的,有一天你喝了很多酒,第二天早上醒来时带着严重的宿醉,你意外地发现了一种新的架构。

Alistair 对矩形感到厌倦,于是他画了一个六边形,为每个东西想出了两个名字,试图让它们变得神圣起来。但不要被吓到,我的开发伙伴。实际上,这种架构并不比 N 层架构复杂:

这其中有很多旋转和移动,让我们看看实际发生了什么。

Cockburn 实现了 Evans 的梦想。现在,Domain 是系统的核心组件,不仅在言辞上,而且在行动上也是。它不再引用其他项目。

为了强调它真正是核心,业务逻辑被重命名为核心(Core)。

基础设施模块被分成两部分 —— 抽象(接口)和实现 。抽象成为业务逻辑的一部分,并被重命名为端口(ports)。实现部分保留在基础设施层中,现在它们被称为适配器(adapters)。实际上,UI 和数据库(DB)位于相同的框架层,因此它们经历了相同的命运。

将基础设施的接口放在业务逻辑中,使 Domain 变得自治且无依赖。结果,业务逻辑可以在任何环境中使用任何工具。想要更改数据库?只需更改实现部分,实现所需的适配器,并将其 “插入” 到可用的端口中。

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

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

由于可以单独部署模块,这意味着它们可以单独开发。

只有优点。

代码

我忘了提到,调用我们系统的适配器称为主要适配器(驱动)。被我们系统调用的适配器称为次要适配器(被驱动)。虽然这不重要,但了解这一点会让你听起来很博学。

就解决方案结构而言,以下对我来说效果最好:

代码

再次强调,文件夹与项目是你自己决定的事情。

只需按照引用关系,并确保它们不会跨越不应跨越的地方:

代码

2008 年:洋葱架构

这个故事有点令人毛骨悚然,所以做好准备吧。

Jeffrey Palermo。这是一个充满悲伤和黑暗的故事,讲述了一个男孩童年时被洋葱的残忍思考所困扰的悲伤故事。随着他的成长,他心中燃烧着一种愤恨,怀着复仇的承诺。

而相信我,他对这个承诺始终如一。这个小洋葱让全世界数百万开发人员哭泣,向他们的母亲寻求安慰。

这种架构从端口和适配器中得到了很多提升。它仍然涉及依赖反转。它按照抽象和实现分割代码。端口仍然是业务逻辑的一部分。只不过这次,Palermo 从 Evans 的模式中添加了应用层,该层也可以包含一些端口。

这种架构面临的最大挑战,也是导致混淆的原因,是模块之间的依赖关系。

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

不够简单,对吧?我也是这么想的。那么,让我们来剖析一下这个洋葱。

代码

Domain 位于中心。它内部没有内层,因此不应依赖于任何其他层。

应用层仅包裹领域,所以它只应该依赖于 Domain。

基础设施层和展示层位于同一级别,它们不能相互依赖,但可以依赖于应用层和 Domain,因为所有所需接口都在其中定义。

你还可以看到它拥有 DDD 架构中的所有模块,但以不同的方式处理它们。这实际上非常重要!关键在于将很少发生修改的组件放在中间,并将频繁发生修改的组件放在边缘。

应用层或任何其他层的更改不会影响领域,只会影响相关的层。 只有当业务逻辑发生变化时,Domain 才会发生变化,而这种情况无论如何都会影响整个系统。

这是理论上的情况。在实践中,你的组合根(Main() 函数,在其中注册所有依赖项并将模块组合在一起)将成为展示层(ASP、WPF、CLI)的一部分,因此图表将如下所示:

代码

对你来说这个看起来熟悉吗?这就是 N 层架构,只是组件的顺序不同。

不管它的外观如何,无论是六边形、端口还是洋葱,你的最终目标是将依赖关系以无环图或树形结构的形式呈现出来。

2012 年:清洁架构

有个名叫 Bob 的人, 他是最优雅的程序员, 他的敏捷之舞和完美架构, 让你的代码崭新光亮。

我是说,要讲述关于架构的文章,就不能不提到 Robert C.Martin。

他看到了关于架构的热潮,并决定加入其中。Martin 了解开发者的主要秘密,因此他毫不掩饰地借用别人的想法,并将其称为自己的。

开个玩笑,如今很少有原创的想法,大家都在相互借鉴。让我们看看 Martin 在这里带来了什么:

我们可怜的 Domain 再次改名,现在称为实体(Entities)。但那不仅仅是改名而已。它意味着你不会再有领域服务和贫血模型,而是拥有数据和行为的丰富类。

仓储接口和其他端口从领域层移到应用层。而应用层也得到了一个更合适的名称 —— 用例(Use Cases)。

展示层和基础设施层保持不变。然而,Martin 还在顶部添加了一个额外的层,其中包括框架、DLL 和其他外部依赖。这并不意味着你的数据库将引用实体,它只是防止内部层引用外部工具。

再次强调,没有严格的规定。你可以在任何级别添加任意多的层。所以如果你想为领域服务定义一个层,你可以这样做。

代码

Martin 还在架构旁边画了一个小图。

图中显示用户通过触发控制器的端点与系统进行交互,控制器调用一个用例,然后通过展示器返回数据(黑线)。用例可以通过接口调用任何它所需的端口(绿线)。而实际的实现则位于外层(橙线)。

图表试图强调执行流程(虚线)并不总是与依赖关系方向(实线)相对应,这就是依赖倒置原则。

基本上,它再次强调了控制反转的使用。在我们讨论端口和适配器时,你已经见过这一点。

通常在 ASP 中,我们没有单独的展示器组件。这也由控制器来完成。因此,整个图表可以在代码中表示为:

class OrderController : ControllerBase, IInputPort, IOutputPort
{
    [HttpGet]
    public IActionResult Get(int id)
    {
        _getOrderUserCase.Execute(id);

        return DisplayOutputPortResult();
    }
}

其他形式的隔离

所有这些架构都旨在通过分离责任来将一个代码从另一个代码中隔离出来。然而,还有其他形式的隔离:垂直切片、有界上下文、模块、微服务等。这些方法的目标是根据功能来划分代码。

有些人不认为它们是 “真正的” 架构方法,而有些人则认为它们是。这取决于你的观点。最终,它们可能会发展到一种程度,在那个程度上它们可能会使用上述任何一种架构风格,甚至是它们的组合:

结论

在本文中,我们讨论了 N-layered、DDD、六边形、洋葱和清洁架构。这些只是众多存在的架构中的一部分,是一些比较出名的架构。你可能还听说过 BCE、DCI 等。

尽管在细节上可能存在一些差异,但所有这些架构实际上是非常相似的。它们都有着相同的目标 —— 分离责任 。它们通过将代码分割成不同的层来实现这一目标。唯一的区别在于定义了哪些组件以及这些层之间存在什么样的依赖关系。

现在你对整个情况有了全面的了解,我强烈鼓励你再次阅读本文。自己明白不同架构之间的差异。你还可以尝试自己动手进行项目实践。编写一些带有接口的类,关注项目之间的引用关系,接口和实现的放置位置,以及所使用的访问修饰符。

希望从现在开始,每当你创建一个类、审查一个 Pull Request,或者与你的同事进行讨论时,你都能有意识地思考并质疑这些事情。

编辑:黄飞

 

打开APP阅读更多精彩内容
声明:本文内容及配图由入驻作者撰写或者入驻合作网站授权转载。文章观点仅代表作者本人,不代表电子发烧友网立场。文章及其配图仅供工程师学习之用,如有内容侵权或者其他违规问题,请联系本站处理。 举报投诉

全部0条评论

快来发表一下你的评论吧 !

×
20
完善资料,
赚取积分