层压力与孤立主义






4.95/5 (32投票s)
N层系统中的逻辑层应被设计为仅与相邻层交互并受其影响。这种限制经常被违反,这对系统有害。在本文中,我将讨论为什么这种情况很常见,其后果以及为什么我们应该关注层级孤立主义。
引言
N层系统中的逻辑层应被设计为仅与相邻层交互并受其影响。这种限制经常被违反,这对系统有害。在本文中,我将讨论为什么这种情况很常见,其后果以及为什么我们应该关注层级孤立主义。
本文侧重于概念并进行了详细讨论。本文建立在“伙计,我的业务逻辑在哪儿?”中讨论的概念之上。
物理层
为了便于比较,我们首先考虑物理层是如何关联的
此示例表示一个三层系统。每层只能与相邻层通信。
不允许客户端层直接访问存储层,因为它们不是相邻的。大多数开发人员甚至不会考虑尝试这种跳跃。然而在本文中,我将向您展示开发人员如何对逻辑层犯下同样的错误,而通常他们甚至没有意识到。
层级
“层级(tier)”和“层(layer)”这两个术语经常互换使用。当我使用“层级(tier)”时,我指的是物理层。当我使用“层(layer)”时,我指的是一个独立的软件层。层必须存在于一个层级上,因此层级包含层。
层并不局限于层级。事实上,在接下来的两个图中,您可以看到业务层可以在不同的部署场景中在层级之间移动。并非所有层都是可移动的,但许多都是。部署场景取决于网络拓扑和其他因素。
存储层上的业务层
客户端层上的业务层
逻辑困境
为了讨论,我将使用三个层,尽管许多N层系统拥有更多层。通常,每个层都有子层。每个层都应该与其相邻层通信并依赖于它,并且只依赖于其相邻层。
当放置在层级上时,通常在高级别上它会变成一对一的关系。
我们就是这样思考逻辑层的,如果只考虑接口连接,这是一个准确的描述。对于逻辑层,我们不仅需要考虑接口连接,还需要考虑接口设计、影响和其他约束。这些是被忽视或通常甚至没有被识别的逻辑层的幽灵。
这是最常见的实现方式。表示层与存储层没有物理连接,但两者是围绕彼此设计的,并且由于彼此施加的约束而做出了切实的妥协。
当重新定位这些层以更好地显示它们之间的关系时,情况变得更加明显。它不再是一个清晰的从前到后的系统,而是清晰地表明它实际上是一个循环系统。
我以高层次概述的方式表示了这些层。然而,每个层都有额外的子层。
当添加典型的逻辑链接时,您可以清楚地看到违反了“仅邻居访问”规则。
在某些情况下,不仅逻辑链接违反了规则,还建立了跳过相邻层的物理链接。
这样的系统极其脆弱,难以升级、扩展或调试。要对任何层进行操作,开发人员必须成为一个“圣诞老人开发人员”。也就是说,他应该看到整个系统中的一切,无论好坏。
通过移除层级并重新排列图中层的位置,可以更容易地看到所创建的模式。很明显,不仅层被越界了,而且还形成了一个蜘蛛网。
模式
存储层驱动
使用存储层驱动模式,首先设计存储层,然后围绕存储层设计表示层。一旦完成,业务层也围绕存储层设计,因为表示层已经围绕存储层设计。这导致了表示层中的人为约束和业务层中有限的转换。从业务层返回的结果集通常受限于可由SQL查询或存储过程执行的简单转换。
这种模式非常普遍,因为它类似于传统的客户端-服务器开发和围绕现有数据库设计的系统。由于表示层是围绕存储层设计的,因此表示层通常通过不直观的屏幕模仿存储层中数据的实际结构。
通常,从表示层到存储层还会有一个额外的反馈循环。当某些内容无法以方便表示层的格式检索时,就会发生反馈循环。然后,开发人员请求对存储层进行更改,以利于表示层,但这不利于存储层。这些更改是人为的,如果没有这些限制,则没有必要。这些更改经常违反或至少使数据库设计的正确原则受到压力,导致不必要的数据重复和反范式化。
表示层驱动
使用表示层驱动模式,存储层围绕表示层设计。业务层的实现通常通过简单的SQL查询完成,很少进行转换或隔离。数据库设计不佳,并遭受性能问题,因为它们主要是为了方便表示层访问而设计的,而不是使用规范化和其他存储层概念。
隔离驱动
采用隔离驱动模式,表示层和存储层独立开发,通常并行进行。这两个层在设计时互不影响,以免引入人为约束或有害的设计元素。这两个层设计完成后,业务层被设计出来,业务层的职责是执行所有转换,而无需更改存储层或表示层。
由于表示层和存储层现在完全独立,因此可以根据需要通过更新业务层进行更改。对两个物理断开的层的更改不会影响或直接冲击另一个层。这使得存储层或表示层的结构更改能够快速响应用户需求,而无需进行系统范围的更改或更新。
比较
存储层 驱动 |
表示层 驱动 |
隔离性 驱动 |
|||||||
数据库 |
|
|
|
||||||
商用版 要求 |
|
|
|
||||||
用户界面 |
|
|
|
||||||
可扩展性 |
|
|
|
分区角色
由于旧习惯难以改变,团队很难适应隔离驱动模式。为了促进团队中的隔离驱动模式,使用分区角色非常有利。使用分区角色还有其他好处,包括更快更好的开发。
为了防止功能被不正确地分配或放置在错误的层级,开发人员被分配到特定的层。在特定层上工作的职能被称为一个角色。
存储层角色
- 根据业务层开发人员的要求,创建业务层所需的视图。
- 创建用于插入、删除和更新表的存储过程。
- 通过计划优化、索引维护等方式,审查和优化视图及提交的SQL。
业务层角色
- 使用 Web 服务或其他远程处理能力创建并维护暴露给表示层的外部接口。
- 定义供表示层使用的外部接口。
- 请求视图以从存储层角色检索数据,并请求存储过程以修改数据。
- 实现虚拟层和物理层之间的所有数据转换。
- 创建用于构建和测试业务层的初始回归测试。
表示层角色
- 实现最终用户界面。
- 使用业务层角色提供的接口将最终用户界面连接到业务层。
- 设计并将虚拟数据集或其他数据传输契约提交给业务层角色。
角色漫游
虽然开发人员应被分配到特定角色,但开发人员也应在不同角色之间漫游。
在小型团队中,角色间的漫游可以更好地分配资源。在大型团队中,漫游确保开发人员了解整个系统,理解其在相邻层中实现的影响,并确保如果开发人员生病或离职,系统没有任何部分受到影响。
然而,角色之间的漫游不应该随心所欲,必须受到适当的控制。开发人员应在特定时间担任特定角色。如果可能,开发人员不应在同一个模块中执行多个角色。也就是说,如果一个系统有一个客户模块,一个开发人员不应被分配到客户模块中的多个角色。但是,如果一个开发人员在客户模块中担任业务层角色,然后切换到供应商模块的表示层角色,这是可以接受的,并且可以更有效地分配资源。
此图演示了角色分配的正确示例。
此图演示了不当的角色分配。Joe和Adam都在同一个模块中担任了两个角色。允许这种违规行为会鼓励层与层之间的相互影响和牺牲。
上面所示的示例仅用于演示目的,并且每个模块和层级只显示一名开发人员。然而,这种情况很少见,实际上多个开发人员分担工作是很常见的。只要开发人员在给定模块内不移动到另一个层级,这些准则就得到维护。
在这些示例中,我展示了 Pete 专门负责存储层。这不是一个规则,事实上 Pete 可以在其他层中工作。其他开发人员也可以在存储层中工作。但通常存储层由 DBA 处理,而且通常只有 DBA,这就是我这样表示的原因。
小团队
在小型团队中,让开发人员专注于给定角色会造成资源缺口。在小型团队中,开发人员必须更频繁地在角色之间移动,因为当依赖项完成后,某个角色可能会暂停。虽然移动更频繁,但它们仍然必须受到控制。从一个角色到另一个角色的移动应该是明确的,并且只有当当前分配的角色中没有更多待处理的工作时才能进行移动。
微型团队和单人开发者
在非常小的团队和个人开发者的情况下,别无选择,只能在单个模块中处理多个层。开发人员应在给定时间专注于一个层,并且只能在层之间进行明确的移动。
一种常见的模式是构建每个模块,只有在完成后才进入下一个模块。这种方法是可以接受的。
然而,开发人员在2/3的移动中是横向移动的。在每次移动中,如果出现问题,倾向于后退一步做出妥协,然后返回。
逐层完成而不是逐模块完成通常很有帮助。这使得各层相互隔离。然而,这种方法并非总是可行,需要事先进行完整的详细设计。一些开发人员也可能觉得这种方法不舒服,并且在层之间顺序移动但在模块之间反复跳跃会感到困惑。
如果使用这种方法,请注意即使在这种情况下也执行了两次水平移动
如果您使用这种方法,在层之间移动时,请务必也包含模块移动。下图演示了最佳实践。