「表现-领域-数据」分层

欧雷 发表于

0 条评论

对信息丰富的程序进行模块化的最常见方法之一是将其分为三个宽泛的层:表现(UI)、领域逻辑(又叫「业务逻辑」)和数据访问。因此,你经常会看到 web 应用被划分为知道如何处理 HTTP 请求和渲染 HTML 的 web 层、包含验证和计算的业务逻辑层,和整理如何管理数据库或远程服务中持久数据的数据访问层。

基本模式
基本模式

总的来说,我发现这是一种对很多应用有效的模块化形式,同时也是我经常使用和鼓励的。(对我而言)它最大的优点是能够让我相对独立地思考这三个主题,从而减少我的关注范围。在写领域逻辑代码时,我几乎可以忽略 UI 并将任何与数据源的交互看作是一个函数的抽象集合,为我提供所需数据并根据需要进行更新。当处理数据访问层时,我关注的是将数据转换成接口所需形式的细节。而在搞表现时,我能够仅关注 UI 行为,把要显示或更新的数据都视为通过函数调用后神奇地出现的。通过分离这些元素,缩小了我在每一部分的思考范围,这让我更容易遵循需要做的事。

缩小范围并不意味着要按照某种顺序去编写各层代码——我经常发现自己需要在不同层之间反复切换。我可能会根据对 UX 的最初理解去构建数据层和领域层;但当在改善 UX 时,我需要对领域层做些更改,这会导致数据层也发生变更。但即便会伴随着这种跨层反复切换,我仍觉得在进行修改时同一时间只关注一个层会很容易。这与你在重构时的两顶帽子间进行思维模式切换十分相似。

模块化的另一个原因是让我可以给模块替换不同的实现。这种分离使我能够在同一套领域逻辑上构建多个表现,而不必去重复它。多个表现可以是 web 应用中单独的页面、同时拥有 web 应用和移动原生应用、用于脚本的 API、或者甚至是老套的命令行界面。将数据源模块化让我能够优雅地应对数据库技术的变化,或者去支持会悄无声息发生变化的持久服务。然而,我不得不提一嘴,尽管经常听到数据访问替换是分离数据源层的驱动,但很少听说有人真那么去做。

模块化还支持可测试性,这自然对我这个自测代码的忠实粉丝很有吸引力。模块边界暴露出很适用于测试的接缝。UI 代码通常很难测试,因此最好将尽可能多的逻辑写到易于测试的领域层中,而不必非得做体操通过 UI 访问程序。数据访问一般都缓慢且笨拙,所以围绕数据层使用测试替身通常会使领域逻辑测试起来更加容易且响应迅速。

尽管可替换性和可测试性的确是这样分层的好处,但我必须强调,纵然没有上述任何原因,我依然会像这样分层。光减少关注范围这一理由本身就足够了。

当谈论这一点时,我们可以把它视为一种模式(表现-领域-数据),也可将其分为两种模式(「表现-领域」和「领域-数据」)。这两种看法都很有用——我认为「表现-领域-数据」是「表现-领域」和「领域-数据」的组合。

我将这些层看作是模块的一种形式,这是我用来描述如何将软件划分为几个相对独立部分的通用词。究竟这与代码如何对应起来取决于我们所处的编程环境。通常来说,最低级的是某种形式的子程序或函数。面向对象语言会有收集函数和数据结构的类的概念。大多数语言都有叫做包或命名空间的更高级的形式,这些通常可以形成层次结构。模块可以与可单独部署的单元相对应,如库或服务,但它们不必非得如此。

这些级别的形式中的任何一个都可分层。小的程序也许只是将各层的独立函数放到不同的文件中;较大的系统则可以有与包含很多类的命名空间相对应的层。

在本文中我提到了三层,但看到有着三层以上的架构是很常见的。一种常见的变体是在领域和表现之间放置一个服务层,或者是用类似表现模型的东西把表现层拆分为几个单独的层。我没发现更多的层会打破这基本模式,因为那核心分离仍然保留。

扩展模式
扩展模式

依赖关系通常是从上到下贯穿整个层栈:表现依赖于领域,领域又依赖于数据源。一种常见的变体是在领域层和数据源层之间引入映射器来进行编排,以使领域不依赖于其数据源。这种方法通常被称为「六边形架构」。

这些层是逻辑层而非物理层。我可以在笔记本电脑上运行这三层全部;可以在带有服务端数据库的台式电脑上运行表现和领域模型;可以通过浏览器中的富客户端和服务器上的服务于前端的后端分离表现。在这种情况下,我将 BFF 视为表现层,因为它专注于支持特定的表现选项。

尽管「表现-领域-数据」分离是种常见的方法,但它应该仅用在相对小的粒度上。随着应用发展,每一层都能变得足够复杂,这时需要对其进行进一步的模块化。发生这种情况时,一般来说最好不要将「表现-领域-数据」作为更高级别的模块。通常框架会鼓励你去用「视图-模型-数据」这类作为顶级命名空间;这对较小的系统来说还可以,但是一旦任意一层变得太大,你就该把顶级命名空间拆分成内部分层的面向领域的模块。

顶级模块
顶级模块

开发人员不一定要是全栈的,但开发团队应该是。

Martin Fowler

我曾见过因这种分层而导致组织误入歧途的,一种常见的反模式就是按这些层去分离开发团队。这看起来挺吸引人,因为前端开发和后端开发需要不同的框架(甚至语言),这使开发人员可以轻松地专攻其中一个。将具有共同技能的人放在一起可以实现技能共享,让组织把团队当作是单一且明确工作类型的提供者。同样地,将所有的数据库专家聚集起来以适应数据库和模式的通用集中化。但层与层之间不断地相互作用导致它们之间需要进行频繁的交换。当在同一团队中有可以合作自如的专家时这并非难事,但团队边界会增加相当大的摩擦,同时会降低个人对于系统里重要的跨层理解的学习动力。更糟糕的是,按层去分离团队会拉大开发人员与用户之间的距离。开发人员不一定要是全栈的(尽管这值得称赞),但开发团队应该是。