codecamp

GoFrame 工程开发设计-工程目录设计

框架的工程设计没有采用复杂的设计思路,而是采用了务实、稳健和成熟的工程设计,以快速解决业务痛点、降低开发维护成本为第一考量。

一、工程目录结构

GoFrame​业务项目基本目录结构如下(以​Single Repo​为例):

/
├── api
├── internal
│   ├── cmd
│   ├── consts
│   ├── controller
│   ├── model
│   │   └── entity
│   └── service
│       └── internal
│           ├── dao
│           └── do
├── manifest
├── resource
├── utility
├── go.mod
└── main.go 

工程目录采用了通用化的设计,实际项目中可以根据项目需要适当增减模板给定的目录。例如,没有i18n及template需求的场景,直接删除对应目录即可。

 目录/文件名称  说明  描述
 api  接口定义  对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以api/v1...存在
 internal  内部逻辑  业务逻辑存放目录。通过Golang internal特性对外部隐藏可见性
 -cmd  入口指令  命令行管理目录。可以管理维护多个命令行
 -consts  常量定义  项目所有常量定义
 -controller  接口处理  接收/解析用户输入参数的入口/接口层
 -model  结构模型  数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义
 -entity  数据模型  数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改
 -service  逻辑封装  业务逻辑封装管理,特定的业务逻辑实现和封装
 -dao  数据访问  数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的CURD方法
 -do  领域对象  用于dao数据操作中业务模型与实例模型转换,由工具维护,用户不能修改
 mainfest  交付清单  包含程序编译、部署、运行、配置的文件
 -config  配置管理  配置文件存放目录
 -docker  镜像文件  Docker镜像相关依赖文件,脚本文件等等
 -deploy  部署文件  部署相关的文件。默认提供了Kubernetes集群化部署的Yaml模板,通过kustomize管理
 resource  静态资源  静态资源文件。这些文件往往可以通过资源打包/镜像编译的形式注入到发布文件中
 go.mod  依赖管理  使用Go Module包管理的依赖描述文件
 main.go  入口文件  程序入口文件

业务接口 - api

业务接口包含两部分:接口定义(​api​)+接口实现(​controller​)。

api​包的职责类似于三层架构设计中的UI表示层,负责接收并响应客户端的输入与输出,包括对输入参数的过滤、转换、校验,对输出数据结构的维护,并调用 ​service实现业务逻辑处理。

逻辑封装 - service

service​包的职责类似于三层架构设计中的​BLL​业务逻辑层,负责具体业务逻辑的实现以及封装。

数据访问 - dao

dao​包的职责类似于三层架构中的​DAL​数据访问层,数据访问层负责所有的数据访问收口。

结构模型 - model

model​包的职责类似于三层架构中的​Model​模型定义层。模型定义代码层中仅包含全局公开的数据结构定义,往往不包含方法定义。

这里需要注意的是,这里的​model​不仅负责维护数据实体对象(​entity​)结构定义,也包括所有的输入/输出数据结构定义,被​api/dao/service​共同引用。这样做的好处除了可以统一管理公开的数据结构定义,也可以充分对同一业务领域的数据结构进行复用,减少代码冗余。

image2022-1-18_0-47-31

三层架构设计与框架代码分层映射关系

二、请求分层流转

cmd

cmd​层负责引导程序启动,显著的工作是初始化逻辑、注册路由对象、启动​server​监听、阻塞运行程序直至​server​退出。

api

上层​server​服务接收客户端请求,转换为​api​中定义的​Req​接收对象、执行请求参数到​Req​对象属性的类型转换、执行​Req​对象中绑定的基础校验并转交​Req​请求对象给​controller​层。

controller

controller​层负责接收​Req​请求对象后做一些业务逻辑校验,随后调用一个或多个​service​实现业务逻辑,将执行结构封装为约定的​Res​数据结构对象返回。

model

model​层中管理了所有的业务模型,​service​资源的​Input/Output​输入输出数据结构都由​model​层来维护。

service

service​层的业务逻辑需要通过调用​dao​来实现数据的操作,调用​dao​时需要传递​do​数据结构对象,用于传递查询条件、输入数据。​dao​执行完毕后通过​Entity​数据模型将数据结果返回给​service​层。

dao

dao​层通过框架的​ORM​抽象层组件与底层真实的数据库交互。

image2022-1-18_10-38-49

三、常见问题解答

框架是否支持常见的MVC开发模式

当然!

作为一款模块化设计的基础开发框架,​GoFrame​不会局限代码设计模式,并且框架提供了非常强大的模板引擎核心组件,可快速用于​MVC​模式中常见的模板渲染开发。相比较​MVC​开发模式,在复杂业务场景中,我们更推荐使大家用三层架构设计模式。

如何清晰界定和管理service和controller的分层职责

controller​层处理​Req/Res​外部接口请求。负责接收、校验请求参数,并调用一个或多个 ​service​来实现业务逻辑处理,根据返回数据结构组装数据再返回。

service​层处理​Input/Output​内部方法调用。负责内部可复用的业务逻辑封装,封装的方法粒度往往比较细。

因此, 禁止从​controller​层直接透传​Req​对象给​service​,也禁止​service​直接返回​Res​数据结构对象,因为​service​服务的主体与​controller​完全不同。当您错误地使用​service​方法处理特定的​Req​对象的时候,该方法也就与对于的外部接口耦合,仅为外部接口服务,难以复用。这种场景下​service​替代了​controller​的作用,造成了本末倒置。

如何清晰界定和管理service和dao的分层职责

这是一个很经典的问题。

痛点:

  • 常见的,开发者把数据相关的业务逻辑实现封装到了​dao​代码层中,而​service​代码层只是简单的​dao​调用,这么做的话会使得原本负责维护数据的​dao​层代码越来越繁重,反而业务逻辑​service​层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到​dao​还是​service​中?
  • 业务逻辑其实绝大部分时候都是对数据的CURD处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在​dao​层中,业务逻辑的改变其实会频繁对​dao​层的代码产生修改。例如:数据查询在初期的时候可能只是简单的逻辑,目前代码放到​dao​好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的​dao​代码,同时​service​代码也可能同时做更新。原本仅限于​service​层的业务逻辑代码职责与​dao​层代码职责模糊不清、耦合较重,原本只需要修改​service​代码的需求变成了同时修改​service+dao​,使得项目中后期的开发维护成本大大增加。

建议:

  • 我们的建议。​dao​层的代码应该尽量保证通用性,并且大部分场景下不需要增加额外方法,只需要使用一些通用的链式操作方法拼凑即可满足。业务逻辑、包括看似只是简单的数据操作的逻辑都应当封装到​service​中,​service​中包含多个业务模块,每个模块独自管理自己的​dao​对象,​service​与​service​之间通过相互调用方法来实现数据通信而不是随意去调用其他​service​模块的​dao​对象。


GoFrame 工程开发设计-代码分层设计
GoFrame 工程开发设计-对象封装设计
温馨提示
下载编程狮App,免费阅读超1000+编程语言教程
取消
确定
目录

GoFrame 核心组件

GoFrame 核心组件-数据库ORM

GoFrame 模块列表

GoFrame 模块列表-单元测试

GoFrame 模块列表-功能调试

GoFrame WEB服务开发

关闭

MIP.setData({ 'pageTheme' : getCookie('pageTheme') || {'day':true, 'night':false}, 'pageFontSize' : getCookie('pageFontSize') || 20 }); MIP.watch('pageTheme', function(newValue){ setCookie('pageTheme', JSON.stringify(newValue)) }); MIP.watch('pageFontSize', function(newValue){ setCookie('pageFontSize', newValue) }); function setCookie(name, value){ var days = 1; var exp = new Date(); exp.setTime(exp.getTime() + days*24*60*60*1000); document.cookie = name + '=' + value + ';expires=' + exp.toUTCString(); } function getCookie(name){ var reg = new RegExp('(^| )' + name + '=([^;]*)(;|$)'); return document.cookie.match(reg) ? JSON.parse(document.cookie.match(reg)[2]) : null; }