流行的Web框架大多数是MVC框架,MVC这个概念最早由Trygve Reenskaug在1978年提出,为了能够对GUI类型的应用进行方便扩展,将程序划分为:
- 控制器(Controller)- 负责转发请求,对请求进行处理。
- 视图(View) - 界面设计人员进行图形界面设计。
- 模型(Model) - 程序员编写程序应有的功能(实现算法等等)、数据库专家进行数据管理和数据库设计(可以实现具体的功能)。
随着时代的发展,前端也变成了越来越复杂的工程,为了更好地工程化,现在更为流行的一般是前后分离的架构。可以认为前后分离是把V层从MVC中抽离单独成为项目。这样一个后端项目一般就只剩下 M和C层了。前后端之间通过ajax来交互,有时候要解决跨域的问题,但也已经有了较为成熟的方案。图 5-13 是一个前后分离的系统的简易交互图。
图里的Vue和React是现在前端界比较流行的两个框架,因为我们的重点不在这里,所以前端项目内的组织我们就不强调了。事实上,即使是简单的项目,业界也并没有完全遵守MVC框架提出者对于M和C所定义的分工。有很多公司的项目会在Controller层塞入大量的逻辑,在Model层就只管理数据的存储。
这往往来源于对于model层字面含义的某种擅自引申理解。认为字面意思,这一层就是处理某种建模,而模型是什么?就是数据呗!
这种理解显然是有问题的,业务流程也算是一种“模型”,是对真实世界用户行为或者既有流程的一种建模,并非只有按格式组织的数据才能叫模型。不过按照MVC的创始人的想法,我们如果把和数据打交道的代码还有业务流程全部塞进MVC里的M层的话,这个M层又会显得有些过于臃肿。
对于复杂的项目,一个C和一个M层显然是不够用的,现在比较流行的纯后端API模块一般采用下述划分方法:
- Controller,服务入口,负责处理路由,参数校验,请求转发。
- Logic/Service,逻辑(服务)层,一般是业务逻辑的入口,可以认为从这里开始,所有的请求参数一定是合法的。业务逻辑和业务流程也都在这一层中。常见的设计中会将该层称为 Business Rules。
- DAO/Repository,这一层主要负责和数据、存储打交道。将下层存储以更简单的函数、接口形式暴露给 Logic 层来使用。负责数据的持久化工作。
每一层都会做好自己的工作,然后用请求当前的上下文构造下一层工作所需要的结构体或其它类型参数,然后调用下一层的函数。在工作完成之后,再把处理结果一层层地传出到入口
上层总是依赖其下层,依赖关系不跨层。
除表现层外,同一层之间方法不允许相互调用。如果同层发生调用,一定是调用的对外不可见的工具方法。
从服务层出发进行设计,根据系统需要提供的功能进行分析,确定 Service 接口中的方法。而不是从数据库的表出发,创建DAO,再创Domain,然后Service,这实际上是对系统分层的误解。
Controller/Handler 层
- 调用 Service 逻辑设计层的接口来控制业务流程,接收前端参数,再将处理结果返回到前端。
- Controller 调用 Service 层是:一对一接口调用,且 Controller 层不做任何业务处理,目的是为了后续拓展直接替换 Controller 为 RPC 框架而准备。
我现在的项目 Controller还创建流程,有一个taskcenter来调用流程,流程去调用Handler
Service 层
- 业务逻辑。
- 只能在 Service 中注入 DAO。
- 一个 Service 只调用一个 Dao, 控制 Service 调用 Dao 层的复杂度。
- Service 不能互相调用。
- 一般情况下事务放在 service 层,为了避免事务嵌套或单个事务过大等问题的。
DAO/Repository 层
- 封装 Entity 对象,对数据库进行数据持久化操作,他的方法语句是直接针对数据库操作的,主要实现一些增删改查操作。
- DAO 只能操作单表数据。
Entity/Model 层
- 实体层,用于存放我们的实体类,与数据库中的属性值基本保持一致。
划分为CLD三层之后,在C层之前我们可能还需要同时支持多种协议。本章前面讲到的thrift、gRPC和http并不是一定只选择其中一种,有时我们需要支持其中的两种,比如同一个接口,我们既需要效率较高的thrift,也需要方便debug的http入口。即除了CLD之外,还需要一个单独的protocol层,负责处理各种交互协议的细节。这样请求的流程会变成图所示。
这样我们Controller中的入口函数就变成了下面这样:
http.Requesthttp.ResponseWriter
协议(Protocol)层
处理http协议的大概代码如下:
理论上我们可以用同一个请求结构体组合上不同的tag,来达到一个结构体来给不同的协议复用的目的。不过遗憾的是在thrift中,请求结构体也是通过IDL生成的,其内容在自动生成的ttypes.go文件中,我们还是需要在thrift的入口将这个自动生成的结构体映射到我们logic入口所需要的结构体上。gRPC也是类似。这部分代码还是需要的。
http.Request
先来看看HTTP对应的结构体、thrift对应的结构体和我们协议无关的结构体分别长什么样子:
我们需要通过一个源结构体来生成我们需要的HTTP和thrift入口代码。再观察一下上面定义的三种结构体,我们只要能用一个结构体生成thrift的IDL,以及HTTP服务的“IDL(只要能包含json或form相关tag的结构体定义信息)” 就可以了。这个初始的结构体我们可以把结构体上的HTTP的tag和thrift的tag揉在一起:
然后通过代码生成把thrift的IDL和HTTP的请求结构体都生成出来,如图 5-16所示
图 5-16 通过Go代码定义结构体生成项目入口
至于用什么手段来生成,你可以通过Go语言内置的Parser读取文本文件中的Go源代码,然后根据AST来生成目标代码,也可以简单地把这个源结构体和Generator的代码放在一起编译,让结构体作为Generator的输入参数(这样会更简单一些),都是可以的。
当然这种思路并不是唯一选择,我们还可以通过解析thrift的IDL,生成一套HTTP接口的结构体。如果你选择这么做,那整个流程就变成了图 5-17所示。
图 5-17 也可以从thrift生成其它部分
看起来比之前的图顺畅一点,不过如果你选择了这么做,你需要自行对thrift的IDL进行解析,也就是相当于可能要手写一个thrift的IDL的Parser,虽然现在有Antlr或者peg能帮你简化这些Parser的书写工作,但在“解析”的这一步我们不希望引入太多的工作量,所以量力而行即可。
既然工作流已经成型,我们可以琢磨一下怎么让整个流程对用户更加友好。
比如在前面的生成环境引入Web页面,只要让用户点点鼠标就能生成SDK,这些就靠读者自己去探索了。
虽然我们成功地使自己的项目在入口支持了多种交互协议,但是还有一些问题没有解决。本节中所叙述的分层没有将中间件作为项目的分层考虑进去。如果我们考虑中间件的话,请求的流程是什么样的?见图 5-18所示。
图 5-18 加入中间件后的控制流
thrift stuffstuff
这也是很多企业项目所面临的真实问题,遗憾的是开源界并没有这样方便的多协议中间件解决方案。当然了,前面我们也说过,很多时候我们给自己保留的HTTP接口只是用来做调试,并不会暴露给外人用。这种情况下,这些非功能性的代码只要在thrift的代码中完成即可。
补充总结:
- 路由层,你可以把他命名为route,handler,http(也勉强说得过去),这个层只提供对外访问的访问入口,其它一切参数校验的逻辑放在这都是不合理的。
- controller层,几乎在百分之90以上的mvc框架中都是必须有的,也是最重要的一层,但是很多golang的开发人员经常把这层省了,尤其是用gin框架的同学,经常把controller做的事情放到了路由层,个人觉得是极其不合理的设计。
- service层,如前文所说,很多做PHP的同学经常直接在controller层干了所有的事情,导致controller十分臃肿,代码的层次不清晰,其实service层还可再拆分成model层和dao层,如果你嫌麻烦,觉得项目不大,直接在service层做掉数据访问,逻辑处理也是可以的。
- 但是我建议你不要在service层里面把业务逻辑,缓存,数据访问,orm管理全做了,你至少至少至少都要把model层拆分出来,因为基于golang的项目,你写着写着就会发现struct满天飞了,所以将DB或ES的映射关系的struct单独拿出来维护很有必要,不要高估你的眼力,当代码越来越臃肿的时候,每当更改了一个映射关系的时候,你修改起来就会很费劲。
models和dao层
正如前文所说,models单独维护是非常有必要的,拆的再细分一点,基于业务的struct和基于db映关系的struct区别对待,拆分管理在日趋庞大的系统中很有必要。我们为什么把数据访问——DAO层单独拿出来,当然是为了结构清晰,当数据访问出了问题,你立马就会定位到dao层,而不需要在臃肿的service里面去翻代码,对于后期维护来说,会更加的轻松。
common
通常我们在做一个项目的时候,不仅仅要考虑到当前的项目,大多数时候,一个web项目分为前后台,有时候很多的方法和数据访问是相同的,有很多的同学认为,对于这种场景,应该把前后端通用的逻辑放到common中,其实这是一种非常危险的行为。因为,通常情况下,后台的需求都是公司内部人员使用,或者说使用面积不会像前台那么广,有时候可能因为一些后台的业务需求变动,改变了这部分通用的需求,导致前台出了问题。这其实是一个代码复用性和解藕的矛盾,但是在这种情况下,为了项目的稳定,我们只能选择牺牲代码的复用性,因此common里面不适合放多个模块通用的业务逻辑处理。那common的作用也就一目了然了,他只适合提供通用的工具,如类型转换,上传,算法等等。