正文
进行这样的分包之后,controller 层只能访问到 model 层提供的公共接口,初步达成我们隐藏实现的目的。当然,它们各自的包内部还是无法隐藏实现的,但至少层次间的隔离已经达成。
不过,这个做法有一个非常别扭的地方。考虑当我们需要在 controllers 包外引用 TaskController 的情形,代码会出现 controllers.TaskContoller 这样的形式。Golang 官方的 Effective Go 对于这样会造成引用方出现重复短语的包名称,认为这并不是好的命名方式。这个问题的本质其实又涉及到 Golang 的另一个独特之处,就是从其他包中引入的常量、变量、函数、结构体以及接口,都需要加上包的前缀来进行引用。
有的同学可能会说,Golang 也可以 dot import 来去掉这个前缀。不幸的是,这个做法并不常规,并且不被建议。或许 Golang 有类似 Python 的 from controllers import TaskController 或者 Java 的 import controllers.TaskController 这样的可选择性 import 机制的话,这个情况会改善很多。
MVC 是按照功能层次进行横向划分,相对的,另外一种常见的划分方式是按照模块进行垂直划分。如果继续以上面的 Todo 为例,那么 task 相关的 controller 和 model 都会被放到 tasks package 下:
在 tasks 包外引用其中的一些定义时,原先的 controllers.TaskController 变成了 tasks.Controller,看起来好多了。但是原先的 models.Task 也变成了 tasks.Task,可真是按下葫芦浮起瓢。
此外,另一个不采用这种方式的重要原因,就是我们的项目都是采用微服务的理念,所以通常一个项目只包含了一个模块。在这个前提下,如果继续采用这种方式,那么几乎就回到了单一 package 的样子了。
在抛弃了上面三种不理想的方式后,我们只得在搜索引擎中寻找更好的答案。并不困难的,我们找到了这篇 技术文章,其中介绍了一个非常不错的组织方式。巧的是文章作者也经历了和我们一样的困惑和尝试,才最终形成了他文章中的结论。为了方便无法访问原链接的同学们进行理解,截取原文中的一些代码来简要介绍下。
首先,根 package 需要定义整个项目的 domain,并且不依赖项目中的任何其他 package:
接下来,按照外部依赖对实现代码进行包的划分。例如如果 UserService 是依赖 PostgreSQL 作为存储实现的,那么可以用一个 postgres 的子 package 来包含实现代码:
从 MVC 的角度看,这里的 UserService 属于 model 层次。在其上还有对接 HTTP API 的 view-controller 层。可以想象 view-controller 层需要依赖并利用 UserService,这里不再展开代码。这里的关键是,包的命名不再是 models 或这 controllers,而是按照外部依赖而命名。那么当包外引用 UserService 的时候,它的形式会是 postgres.UserService,非常简洁易理解。
此外,需要看到的是,这种组织方式并不只是简单的给包换了个合适的名字,它还抽象出来了 domain。这一层抽象带来了一定的灵活性。比如想象一下,如果此时需要迁移到 MongoDB 作为数据存储,那么新的基于 MongoDB 的 UserService 实现,对于上层的 view-controller 来说是透明的。因为不管是基于什么实现的 UserService,只要它符合 UserService 的接口,那么对它的使用者都是可以无缝替换的。更进一步的,这一层抽象也给我们的测试带来了很多的便利,这方面我们会在后面关于测试的部分进行更多展开。