本文的对象封装建立在良好的代码分层设计之上,在开始了解 GoFrame 的对象封装设计之前,建议先充分了解一下《 代码分层设计 》。

一、Golang包设计

Golang开发语言并没有完整实现OOP特性,因此我们只能采用包封装的方式来践行”高内聚,低耦合”的功能设计。在进行代码分层管理之后,我们会发现包命名变得很困难。大部分时候,我们都习惯按照业务领域来进行命名,例如,在api/dao/service分层下,我们可能都会同时存在一个以user命名的包名(表示用户相关的功能逻辑),虽然我们可以通过不同的路径在import的时候进行区分(这也是《Effective Go》推荐的管理方式,允许同名包名称存在并通过import不同路径来区分不同职责的相同包名),但是由于包名相同,在使用的时候却有极大的困扰。一个主要痛点是管理过多重复的包名工程效率太低,为了解决重复包名往往会创建很多引用别名(import alias)也不方便维护,另一个是在工程管理上容易引起包的循环引用(cycle import)问题。

需要注意的是,Golang语言层面的包循环依赖检测其实是很棒的一个特性,它以package作为代码封装基本单位,使得程序逻辑在package之间的执行路径都是单向调用链,可以帮助你梳理出清晰的package依赖关系、编写出更加健壮性的代码。

对于业务项目而言,业务的复杂度会不断/快速增长,我们期望设计的模块复杂度尽可能的小、职责尽可能的单一。而直接使用包封装设计会使得每个包管理的资源比较多、单个包复杂度会比较高、并且存在过多同名包问题。因此我们需要将代码做分层设计(划分职责)、将包内容做进一步拆分(细化粒度),并将代码模块的粒度细化为了”对象“方式进行封装(这里的”模块“从package细化为了object)。目的是使得整体模块设计更加的解耦,能够快速响应业务发展的变化。对于业务项目而言,我们采用对象封装设计后,将会失去包循环依赖检测特性带来的好处,转而由开发者自行维护对象之间的依赖关系。

GoFrame开发框架经过大量的项目工程实践,本着从简约、简洁、高效、易维护的设计理念出发,总结出了一些关于包设计和命名约束的最佳实践,可供参考。

二、对象封装设计

在代码分层设计之后,我们尽量地减少封装包的数量、降低包的复杂度,尽可能采用结构化对象的方式来封装代码处理逻辑。

1、业务包命名约束

在三层架构设计模式下,我们的业务包命名仅会有apidaomodelservice四个包。每个业务包仅对外暴露实例化的对象用于该业务领域的具体功能逻辑封装,同一层级下不同的业务领域逻辑通过不同文件来分别管理。包对外的公开对象采用业务领域名称来命名,包内部的数据结构定义采用业务领域名称+分层名称来命名,其中分层名称一般为apiservice,例如:

可以将业务领域名称看作特定业务的模块名字,例如:用户(user)、商品(product)、订单(order)、支付(payment)等。业务模块根据业务复杂度以及业务拆分粒度可大可小。

对象封装设计 - 图1

api层公开对象及内部数据结构命名

对象封装设计 - 图2

service层公开对象及内部数据结构命名

特别需要强调的是,在api/dao/service层级中的代码,有且仅有需要导出的实例化对象才能公开。并且由于同一包下包含多个业务领域的数据结构定义,因此在命名的时候务必遵从命名约束,否则容易出现命名冲突。采用单包管理以及实例化对象引用的设计,整个包对外引用简洁清晰、内部维护紧凑简便、规避循环引用问题。例如:

1、api层的对象访问

对象封装设计 - 图3

路由注册时访问api对象

对象封装设计 - 图4

注册具体业务领域对象的方法

2、service层的对象访问

对象封装设计 - 图5

api访问service层对象

对象封装设计 - 图6

访问service层对象具体操作

2、model数据结构

model层级中的代码仅包含数据结构定义,不包含任何的方法封装。同时,model中也会包含公开的常量的一些定义。数据模型的命名直接使用业务领域名称,定义api/service输入输出的数据结构命名采用业务领域名称+分层名称,其他自定义的数据结构均需要带业务领域名称前缀,以方便区分同一包下的不同领域相关资源。例如,内容业务contentmodel中的所有数据结构定义均带有Content前缀:

对象封装设计 - 图7

model数据结构命名

3、dao数据访问

我们通过将业务逻辑解耦后,dao层的代码仅包含一些通用的数据操作方法,代码往往通过工具生成,很少会有自定义数据封装方法的情况。并且dao往往只能被service调用,不应当被api调用,否则会出现循环依赖的问题。

对象封装设计 - 图8

service调用dao对象示例

4、对象访问安全

由于各分层中的封装对象都是以”可变变量“的形式对外暴露使用,因此存在被修改的安全风险。因此大家注意这些公开的对象不要包含可修改方法、不要设置公开属性(通过公开方法暴露内部属性),并且建议以非指针(也尽量不要以接口)方式公开这些对象,例如:

对象封装设计 - 图9

安全风险:指针对象、公开属性

对象封装设计 - 图10

安全风险:指针对象、公开属性,修改建议

三、常见问题解答

1、如何自定义修改model的属性字段,如何扩展model属性字段

goframe提供了自动化的model生成工具,生成的数据表对象结构存放于仅可被model包内部访问的internal目录下。我们可以对model目录下程序中的数据结构进行自定义的修改和扩展。例如,我们有一个user表,生成的数据表实体对象数据结构如下:

对象封装设计 - 图11user数据表实体对象数据结构

对象封装设计 - 图12model中直接引用内部的数据表实体对象数据结构

假如该实体对象数据结构会被用于接口直接返回JSON被客户端,而我们不想返回其中的Password, CreatedAt, UpdatedAt 字段,我们可以直接修改model.User数据结构,将internal.User作为embedded内嵌数据结构,并在model.User数据结构中定义覆盖指定的字段属性(同样的,我们也可以如此扩展字段),如:

这里使用到了Golang的结构体embedded内嵌特性,Golang的内嵌特性可以简单实现类似于OOP中的继承和多态,并不是一个完整的OOP特性。除了struct可以内嵌,interface也支持内嵌特性,感兴趣的小伙伴可以执行研究一下,这里不作详细介绍。

对象封装设计 - 图13 数据结构embedded并自定义定义修改覆盖

关于扩展原有的数据结构示例,也可以参考后续【如何覆盖定义dao中已存在的方法,如何为dao扩展自定义方法】章节部分。

2、如果接口只需要表中的几个字段,如何对model结构体进行字段裁剪

举个例子。一个content内容数据表,字段会比较多,我们在对内容进行分页展示的时候,其实并不需要这么多字段。并且其中的Content字段内容较大,不适合列表展示,查询时也比较浪费数据库及带宽资源。因此我们可以根据不同的业务场景需求,重新定义该业务场景接口所需要的数据结构,单独维护该数据结构,而不是直接使用数据表的实体对象结构。

这里的数据表struct是数据模型,而用于列表展示中的struct数据结构其实是业务模型,具体的介绍请参考:数据模型与业务模型

对象封装设计 - 图14

content数据表实体数据结构

重新定义的列表接口数据结构如下:

对象封装设计 - 图15 列表接口需要的数据结构,对数据表实体结构进行了字段裁剪

3、如何覆盖定义dao中已存在的方法,如何为dao扩展自定义方法

我们还是拿用户模块来举个例子吧。

覆盖方法:

比如我们要覆盖daoAll方法,增加状态名称和性别名称信息:

对象封装设计 - 图16

扩展原有数据结构

对象封装设计 - 图17

覆盖dao对象方法,重新自定义

扩展方法:

一般来说我们不需要为dao扩展方法(基本方法已经很通用)的需求,这个问题还困扰了我好久,因此我随便写个方法做个示范吧。为dao扩展方法很简单,直接为对应的dao对象定义新的方法即可,例如在本示例中,我们为userDao对象增加一个ExtendMethod方法:

对象封装设计 - 图18

dao对象扩展定义方法

4、全局变量封装在哪里维护

我们推荐尽量使用更加安全的常量形式作为全局访问的资源,全局的常量可以按照命名规范定义到model中进行维护。如果业务场景确实需要全局变量,可以将其封装到service层中对应的业务领域对象上进行维护,往往通过私有成员变量的形式定义(公开成员变量的形式存在被外部直接修改的风险),并且通过定义公开方法对外暴露对应的内部成员变量,这样可以保证变量访问的安全性。例如:

对象封装设计 - 图19

通过service维护对应业务领域的全局变量

Content Menu