这是我参与「第三届青训营 -后端场」笔记创作活动的第3篇笔记
无论是 Java 还是 Ruby,它们最著名的框架都深受 MVC 架构模式 的影响,我们从 Spring MVC 的名字中就能体会到 MVC 对它的影响,而 Ruby 社区的 Rails 框架也与 MVC 的关系非常紧密,这是一种 Web 框架的最常见架构方式,将服务中的不同组件分成了 Model、View 和 Controller 三层。于是我开始对这样一种饱受开发者喜爱的架构产生了浓厚的兴趣,最终遍访名师(百度和Google)了解到了一点皮毛,记录下来!
按层级拆分
这种模块拆分的方式其实就是按照层级进行拆分,Rails 脚手架默认生成的代码其实就是将这三层不同的源文件放在对应的目录下:models、views 和 controllers,我们通过 rails new example 生成一个新的 Rails 项目后可以看到其中的目录结构:
$ tree -L 2 app
app
├── controllers
│ ├── application_controller.rb
│ └── concerns
├── models
│ ├── application_record.rb
│ └── concerns
└── views
└── layouts
而很多 Spring MVC 的项目中也会出现类似 model、dao、view 的目录,这种按层拆分模块的设计其实有以下的几方面原因:
MVC 架构模式 — MVC 本身就强调了按层划分职责的设计,所以遵循该模式设计的框架自然有着一脉相承的思路; 扁平的命名空间 — 无论是 Spring MVC 还是 Rails,同一个项目中命名空间非常扁平,跨文件夹使用其他文件夹中定义的类或者方法不需要引入新的包,使用其他文件定义的类时也不需要增加额外的前缀,多个文件定义的类被『合并』到了同一个命名空间中; 单体服务的场景 — Spring MVC 和 Rails 刚出现时,SOA 和微服务架构还不像今天这么普遍,绝大多数的场景也不需要通过拆分服务; 上面的几个原因共同决定了 Spring MVC 和 Rails 会出现 models、views 和 controllers 的目录并按照层级的方式对模块进行拆分。
按职责拆分
Go 语言在拆分模块时就使用了完全不同的思路,虽然 MVC 架构模式是在我们写 Web 服务时无法避开的,但是相比于横向地切分不同的层级,Go 语言的项目往往都按照职责对模块进行拆分:
对于一个比较常见的博客系统,使用 Go 语言的项目会按照不同的职责将其纵向拆分成 post、user、comment 三个模块,每一个模块都对外提供相应的功能,post 模块中就包含相关的模型和视图定义以及用于处理 API 请求的控制器(或者服务):
$ tree pkg
pkg
├── comment
├── post
│ ├── handler.go
│ └── post.go
└── user
Go 语言项目中的每一个文件目录都代表着一个独立的命名空间,也就是一个单独的包,当我们想要引用其他文件夹的目录时,首先需要使用 import 关键字引入相应的文件目录,再通过 pkg.xxx 的形式引用其他目录定义的结构体、函数或者常量,如果我们在 Go 语言中使用 model、view 和 controller 来划分层级,你会在其他的模块中看到非常多的 model.Post、model.Comment 和 view.PostView。
这种划分层级的方法在 Go 语言中会显得非常冗余,并且如果对项目依赖包的管理不够谨慎时,很容易发生引用循环,出现这些问题的最根本原因其实也非常简单:
Go 语言对同一个项目中不同目录的命名空间做了隔离,整个项目中定义的类和方法并不是在同一个命名空间下的,这也就需要工程师自己维护不同包之间的依赖关系; 按照职责垂直拆分的方式在单体服务遇到瓶颈时非常容易对微服务进行拆分,我们可以直接将一个负责独立功能的 package 拆出去,对这部分性能热点单独进行扩容; 小结 项目是按照层级还是按照职责对模块进行拆分其实并没有绝对的好与不好,语言和框架层面的设计最终决定了我们应该采用哪种方式对项目和代码进行组织。
Java 和 Ruby 这些语言在框架中往往采用水平拆分的方式划分不同层级的职责,而 Go 语言项目的最佳实践就是按照职责对模块进行垂直拆分,将代码按照功能的方式分到多个 package 中,这并不是说 Go 语言中不存在模块的水平拆分,只是因为 package 作为一个 Go 语言访问控制的最小粒度,所以我们应该遵循顶层的设计使用这种方式构建高内聚的模块。