2.动手实践整洁架构 - 分层架构存在哪些问题

553 阅读11分钟

您很可能开发过很多分层(web)应用程序,甚至你现在可能正在开发这样的分层应用程序。 在计算机科学课程、教程和最佳实践中,我们已经进行了分层架构的诸多思考。

图 1 显示了非常常见的三层架构的高级视图。我们有一个 Web 层,用于接收请求并将它们路由到域层中的服务,该服务执行一些业务魔法,并从持久层调用组件来查询或修改域实体的当前状态。

众所周知,层是一种可靠的架构模式!如果我们做对了,我们就能够构建独立于 Web 层和持久层的域逻辑。如果我们愿意,我们可以切换 Web 层或持久层技术,而不会影响我们的域逻辑。我们可以在不影响现有特性的情况下添加新的特性。

凭借良好的分层架构,我们可以保持选择的开放性,并能够快速适应不断变化的需求和外部因素。

那么,这样的分层架构有什么问题呢?

根据我的经验,分层架构有太多开放的侧翼,这些侧翼会导致坏习惯的蔓延,并使软件随着时间的推移越来越难以改变,最终演变成大泥球。 在接下来的部分中,我将告诉您原因。

它促进了以数据库驱动设计的开发方式

首先,Web 层依赖于领域层,而领域层又依赖于持久层,进而依赖于数据库。 一切都建立在持久层之上,由于多种原因,这是有问题的。

让我们退一步思考一下我们正在构建的几乎所有应用程序想要实现的目标。我们通常会尝试创建一个管理业务的规则或“策略”模型,以便让用户更轻松地与他们交互。

我们主要是试图模拟行为,而不是陈述。是的,状态是任何应用程序的重要组成部分,但行为会改变状态从而驱动业务! 那么为什么我们将数据库作为架构的基础而不是领域逻辑呢?

回想一下任何应用程序中的业务功能,您是最先开始实现领域逻辑层还是持久层?最有可能的是,你已经考虑过数据库结构是什么样子,然后才继续在其之上实现领域逻辑。

这在传统的分层架构中是有意义的,因为我们正在遵循依赖关系的自然流程。但从商业角度来看,这完全没有意义!我们应该在做其他事情之前构建领域逻辑!只有这样我们才能知道我们的理解是否正确。只有当我们知道我们正在构建正确的领域逻辑时,我们才应该继续围绕它构建持久层和 Web 层。

这种以数据库为中心的架构的驱动力是对象关系映射 (ORM) 框架的使用。不要误会我的意思,我喜欢这些框架,并且每天都在使用 JPA 这样的 ORM 框架。

但是,如果我们将 ORM 框架与分层架构结合起来,我们很容易将业务规则与持久层方面混合在一起。

通常,我们将 ORM 管理的实体作为持久层的一部分,如图 2 所示。由于层可以访问它们下面的层,因此允许域层访问这些实体。如果允许使用它们,它们就会被使用。

这在持久层和域层之间创建了强耦合。我们的服务使用持久层模型作为其业务模型,不仅需要处理域逻辑,还需要处理急切加载与延迟加载、数据库事务、刷新缓存和类似的内务任务。

持久层代码实际上已融合到域代码中,因此很难在不更改其中一个的情况下进行更改。这与灵活和保持选择开放相反,而这应该是我们架构的目标。

容易走捷径

在传统的分层架构中,唯一的全局规则是从某一层开始,我们只能访问同层或下一层的组件。

开发团队可能还同意其他规则,其中一些甚至可能通过工具强制执行,但分层架构风格本身并不会将这些规则强加给我们。

因此,如果我们需要访问上一层中的某个组件,我们只需将该组件下推一层就可以访问它。问题解决了。

这样做一次可能就可以了。但做了一次就为第二次打开了大门。如果允许其他人这样做,我也可以这样做,对吗?

我并不是说作为开发人员,我们会轻易采取这种捷径。但如果可以选择做某事,就会有人去做,尤其是在最后期限迫在眉睫的情况下。而如果以前做过某件事,那么有人再做一次的门槛就会大大降低。这是一种被称为“破窗理论”的心理效应——更多内容请参见第 11 章“有意识地走捷径”。

经过多年的软件项目开发和维护,持久层很可能会如图 3 所示。

当我们将组件向下推入各层时,持久层(或者更通用的术语:最底层)将会变得越来越厚。完美的候选者是辅助组件或实用组件,因为它们似乎不属于任何特定层。

因此,如果我们想禁用架构的“快捷模式”,层并不是最好的选择,至少在不强制执行某种附加架构规则的情况下是这样。我所说的“强制”并不是指高级开发人员进行代码审查,而是指当规则被破坏时使构建失败的规则。

测试变得越来越困难

分层架构中的一个常见演变是层被跳过,我们可以直接从 Web 层访问持久层,因为我们只操作实体的单个字段,因此我们不需要打扰域层,对吧?

同样,前几次感觉还不错,但如果经常发生,就会有两个缺点(一旦有人迈出了第一步,就会出现这种情况)。

首先,我们在 Web 层中实现域逻辑,即使它只是操作单个字段。如果将来用例扩展怎么办?我们很可能会向 Web 层添加更多域逻辑,混合职责并将基本域逻辑分布到整个应用程序中。

其次,在我们的 Web 层的测试中,我们不仅要模拟领域层,还要模拟持久层。这增加了单元测试的复杂性。复杂的测试准备工作是完全不进行测试的第一步,因为我们没有时间进行测试。

随着 Web 组件随着时间的推移而增长,它可能会积累大量对不同持久性组件的依赖关系,从而增加测试的复杂性。在某些时候,我们需要更多的时间来理解和模拟依赖关系,而不是实际编写测试代码。

它隐藏了业务逻辑

作为开发人员,我们喜欢创建新代码来实现闪亮的新用例。但我们通常花在更改现有代码上的时间比创建新代码要多得多。这不仅适用于那些可怕的遗留项目,在这些项目中我们正在开发几十年前的代码库,而且也适用于在实施初始用例后的热门新绿地项目。

由于我们经常寻找添加或更改功能的正确位置,因此我们的架构应该帮助我们快速浏览代码库。

分层架构在这方面表现如何?

正如上面已经讨论的,在分层架构中,领域逻辑很容易分散在各个层中。如果我们为了“简单”用例而跳过域逻辑,它可能存在于 Web 层中。如果我们将某个组件下推,它可能存在于持久层中,所以可以从域和持久层访问它。这已经使得寻找合适的位置来添加新功能变得困难。 但还有更多,分层架构不会对领域服务的“宽度”强加规则。随着时间的推移,这通常会产生非常广泛的服务,服务于多个用例(见图 5)。

广泛的服务对持久层有许多依赖关系,并且 Web 层中的许多组件也依赖于它。这不仅使服务难以测试,而且使我们很难找到负责我们想要处理的用例的服务。

如果我们拥有高度专业化的窄域服务并且每个服务都服务于一个用例,那么事情会容易得多吗 ? 我们不需要在 UserService 中 搜索用户注册用例 , 而是打开 RegisterUserService 并开始工作。

它使并行工作变得困难

管理层通常希望我们在某个日期完成他们的软件的构建。事实上,他们甚至希望我们能够在一定的预算内完成,但我们不要让事情变得复杂。

除了在我作为软件开发人员的职业生涯中从未见过“完成”软件这一事实之外,在某个日期之前完成通常意味着我们必须并行工作。

即使你没有读过《人月神话》,你可能也知道这个著名的结论:

为一个后期的软件项目增加人力资源会使它成为一个后期的软件项目

在某种程度上,这也适用于尚未迟到的软件项目。您不能期望在任何情况下,由 50 名开发人员组成的大型团队的速度都是由 10 名开发人员组成的小型团队的 5 倍。如果他们正在开发一个非常大的应用程序,他们可以分成多个子团队并在软件的不同部分上工作,这可能会起作用,但在大多数情况下,他们会互相支持。

但在健康的规模下,我们当然可以期望随着更多的人参与该项目,速度会更快。管理层对我们的期望是正确的。

为了满足这一期望,我们的架构必须支持并行工作。这并不容易。分层架构在这里并没有真正帮助我们。

想象一下我们正在向我们的应用程序添加一个新的用例。我们有三位可用的开发人员。我们可以将所需的功能添加到 Web 层,第一个添加到领域层,第三个添加到持久层,对吗?

嗯,在分层架构中通常不会这样工作。由于一切都建立在持久层之上,因此必须首先开发持久层,然后是领域层,最后是 Web 层。因此,只有一名开发人员可以同时处理该功能!

啊,但是你说开发人员可以先定义接口,然后每个开发人员都可以针对这些接口进行工作,而不必等待实际的实现。当然,这是可能的,但前提是我们不进行上面讨论的数据库驱动设计,其中我们的持久性逻辑与领域逻辑如此混合,以至于我们无法单独处理每个方面。

如果我们的代码库中有广泛的服务,甚至可能很难并行处理不同的功能。处理不同的用例将导致并行编辑同一服务,从而导致合并冲突和潜在的回归。

这如何帮助我构建可维护的软件?

如果您过去构建过分层架构,您可能会想到本章中讨论的一些缺点,甚至可能会想到更多缺点。

如果做得正确,并且对其施加一些附加规则,则分层架构可以非常易于维护,并且可以轻松更改或添加代码库。

然而,讨论表明分层架构会导致很多事情出错。如果没有非常严格的自律,随着时间的推移,它很容易退化并且变得难以维护。特别是为开发团队制定新的截止日期时,这种自律通常都会变得不那么严格。

牢记分层架构的陷阱将有助于我们下次反对走捷径并构建更易于维护的解决方案 - 无论是分层架构还是不同的架构风格。