按层级组织
学习如何按层次组织代码。
简介
如果仅通过查看代码就能识别出系统的架构,那岂不是很理想?
在本章中,我们将探讨不同的代码组织方式,并介绍一种能够直观体现六边形架构思想的、富有表达力的包结构。
绿地软件项目
在绿地软件项目中,我们最先希望做好的就是包结构。我们会设计一个看起来非常整洁的目录结构,并打算在整个项目周期中沿用它。然而随着开发推进,情况逐渐变得混乱,最终发现这个包结构仅仅是一个漂亮的外表,其下掩盖的是缺乏组织的代码乱象。某些包中的类会随意导入本不该依赖的其他包中的类。
代码组织方式的选择
- 按层组织
- 按功能特性组织
我们将以之前课程中介绍的 BuckPal 示例应用为例,讨论这两种代码组织方式。具体来说,我们会聚焦于“转账”这一用例,即用户从自己的账户向另一个账户汇款的功能。
项目结构
组织代码的第一种方式是按层划分。我们可以这样安排结构:
1buckpal 2├── domain 3| ├── Account 4| ├── Activity 5| ├── AccountRepository 6| └── AccountService 7├── persistence 8| └── AccountRepositoryImpl 9└── web 10 └── AccountController
解释说明
我们为每一层——web、domain 和 persistence——分别设立了独立的包。正如在章节《分层架构有什么问题?》中所讨论的那样,简单的分层结构由于多种原因可能并不适合我们的代码组织。因此,我们在此已经应用了依赖倒置原则(DIP),只允许依赖关系指向 domain 包中的领域代码。这一点通过在 domain 包中定义 AccountRepository 接口,并在 persistence 包中实现该接口来达成。
然而,这种包结构至少存在三个方面的不足之处:
-
缺乏功能模块或特性的包边界。如果我们新增一个用户管理功能,就需要在
web包中添加UserController,在domain包中添加UserService、UserRepository和User,并在persistence包中添加UserRepositoryImpl。如果没有进一步的结构约束,这些类将迅速堆积在一起,导致不同功能之间产生意外的耦合和副作用。 -
无法直观看出系统支持哪些用例。你能一眼看出
AccountService或AccountController实现了哪些用例吗?当我们需要查找某个特定功能时,只能猜测由哪个服务负责,然后进入该服务内部搜索对应的方法。 -
无法从包结构中清晰识别目标架构。我们或许可以推测这里采用了六边形架构风格,然后去浏览
web和persistence包中的类,找出对应的 Web 适配器和持久化适配器。但我们无法一目了然地知道:哪些功能是由 Web 适配器调用的?哪些功能是持久化适配器为领域层提供的?进出六边形的端口(inbound/outbound ports)被隐藏在代码之中,难以快速识别。