作者 | Kislay Verma
翻译 | 刘雅梦
在企业代码库中,目前最流程的代码组织方式是按照技术栈的层次对所有类进行分组,即“堆栈”(stack)风格。但这种风格存在抽象不恰当、低内聚、难变更及设计选择受限等问题,从而作者提出了一种替代方案 “实体”风格的代码组织方式。
在企业代码库中,你遇到的最流行的代码组织方式是什么样的?我最常见到的一种方式是按照技术栈的层次对所有类(假设是 Java 领域)进行分组。所以在一个 MVC 风格的系统中,所有的控制器都在一起,所有的服务都在一起,所有的存储层都在一起,所有的 POJO 也都在一起,等等。我们把这种代码组织方式称为“堆栈”(stack)风格。
这是一种糟糕的代码组织方式,我将会在下面解释原因。但首先,我会先提供一种替代方案。
一种更好的代码组织方式是,根据代码所表示的逻辑实体对其进行分组。我们称这种代码组织方式为“实体”(entity)风格。其目的是确保与单一概念相关的所有类都聚集在一起。通过将逻辑实体放在首位,我们可以优化人的理解能力(编译器才不在乎你把对应的类放在哪里呢)。通过代码本身的呈现方式,开发人员可以对实际系统的边界做出更明智的选择。不是将概念界定在 SomethingRepository 和 SomethingElseRepository 之间,而是界定在 Something 和 SomethingElse 之间。
现在,我将解释下为什么我认为实体模型比堆栈模型更好。
1抽象不恰当
人们不会按堆栈的层次来阅读代码。没有人会说“给我展示下这个系统所有的 API”或“给我展示下这个系统触发的所有查询”。人们沿着领域边界阅读代码。在酒店管理系统中,人们考虑的是房间、客人和价格等等。
由于“堆栈”风格的代码是按照技术层组织的,因此通过系统在存储库中的存在方式来理解系统的逻辑模型。“堆栈”风格暴露的边界是技术层。我们无法从这段代码中理解“名词”以及它们之间的关系。必须还得再深挖一层。对于一个刚开始阅读代码的新人来说,这种“逻辑”结构的混淆是一个巨大的分歧点。
在我们的酒店管理示例中,“实体”风格将所有与客人相关的代码(无论技术层如何)放在一个包中,所有与房间相关的代码放在另一个包中,依此类推。这些包中的每一个都可以有自己的“堆栈”风格的内部组织结构,或者是几个处于同一级别的类。这样可以很容易地在一个地方找到与客人有关的所有内容。
2低内聚
“堆栈”风格组织方式的另一个常见论点是,它将独立的模块放在了技术栈的不同层中。例如,控制器与服务层、服务层与存储层等是明显分离的。在技术栈的不同层次上查找类,你需要转到对应的层次包中。这鼓励了不同层之间的解耦。
这个论点的问题在于,它只关注了耦合,而忽略了另一个关键属性——内聚。我们希望在哪些类之间增加内聚,哪些类之间减少耦合呢?既然所有的服务层都在一起,那么我们是否可以说,它们具有高内聚,并且与模型类或存储库之间是解耦的呢?我们是否可以让所有存储库高度依赖彼此,但与服务层的业务逻辑解耦呢?显然答案是否定的!这种代码将是教科书级的“大泥潭”。将这种系统重构为更小的系统绝对会是一场噩梦,因为你必须要在技术栈的每一层中解耦类。它违背了使用 MVC 风格的全部目的。
另一方面,“实体”风格,促进了内聚,同时仍为技术栈风格的解耦留出了空间。如果所有与酒店相关的类都相互依赖(技术上或概念上的),这是可以的,因为它们无论如何都是一个单一的工作单元。它还能使得以后的重构更容易,因为它的逻辑边界比“堆栈”风格更清晰。
3难变更
在以“堆栈”风格组织的代码库中,开发人员进行任何有意义的变更,都必须跨越多个包进行编码。例如,要在一个实体及其 CRUD API 中添加新字段,需要修改所有的包。这会产生认知负载,因为开发人员必须修改许多“事物”,而不是一个单一的逻辑事物。
在“实体”风格中,如果你要变更某个对象,只需在一个逻辑边界内进行更改即可。这使得对它们的变更更容易,因为如果变更的是单个实体,我们只需处理代码库的一小部分即可。如果更改跨越了顶级包,那么你就是在变更跨越其定义的逻辑结构,这将提醒你,代码存在潜在的耦合风险。
4设计选择受限
由于代码是按技术栈或功能组织的,它限制了人们对系统设计的思考方式。例如,由于业务逻辑应该放在“服务”中,开发人员拒绝使用适当的设计结构,而宁愿将所有内容都塞进服务中,从而创建了长达数千行的噩梦类。即使他们使用了好的设计原则,代码的组织也会抵制他们,因为每个新的“类型”都必须在一个单一的包中。如果想在不同的服务中使用工厂模式,那么必须开发一个名为 factory 的全新包层次结构,此后所有的工厂都应该聚集在这里,无论它们彼此之间是否有任何关联。
如前所述,“实体”并不限定每个逻辑包在其内部的组织方式。它可以是“堆栈”风格,也可以是按需拥有尽可能多的包类型,只要不影响另一个实体包的选择即可。
这里需要关注的一个问题是如何组织跨实体的事物。例如,在多个实体上运行的工作流。这两种风格都没有给出一个简明的答案,但在我看来,“实体”风格在这方面做得更好,因为它会强制要求在所有实体包之外再创建一个新包。这突出了工作流是一个新概念,并且可能是一个应该独立开发的系统边界。其思想是将相似的概念组合在一起,但不受单一概念约束的事物仍然可以在此基础上拥有自己的逻辑家园。
我觉得我们对代码组织所提倡的思维模式考虑得还不够。这类似于代码库级别的康威定律。我很想听到更多关于如何组织代码,以及它是如何塑造开发人员行为、心理模型或效率的信息。在评论区留言!
— 本文结束 —