工程目录设计是代码分层设计的进一步落地,建议您先仔细阅读:代码分层设计

这是GoFrame框架针对业务项目的目录设计,主体的思想来源于三层架构,但在具体实现中,对其进行了一定的改进和细化使其更符合工程实践和时代进步。

一、工程目录结构

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

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

🔥重要提示🔥:框架的工程目录采用了通用化的设计,以满足不同复杂程度业务项目的需求,但实际项目中可以根据项目需要适当增减默认的目录。例如,没有i18n/template/protobuf需求的场景,直接删除对应目录即可。又例如,非常简单的业务项目(如验证/演示项目),不考虑使用严谨的dao/logic/model目录及特性,那么直接删除对应目录即可,可以在controller中直接实现业务逻辑。一切都可以由开发者灵活选择组装

目录/文件名称说明描述
api对外接口对外提供服务的输入/输出数据结构定义。考虑到版本管理需要,往往以api/xxx/v1...存在。
hack工具脚本存放项目开发工具、脚本等内容。例如,CLI工具的配置,各种shell/bat脚本等文件。
internal内部逻辑业务逻辑存放目录。通过Golang internal特性对外部隐藏可见性。
  - cmd入口指令命令行管理目录。可以管理维护多个命令行。
  - consts

常量定义

项目所有常量定义。

  - controller接口处理接收/解析用户输入参数的入口/接口层。

  - dao

数据访问数据访问对象,这是一层抽象对象,用于和底层数据库交互,仅包含最基础的 CURD 方法
  - logic业务封装业务逻辑封装管理,特定的业务逻辑实现和封装。往往是项目中最复杂的部分。
  - model结构模型数据结构管理模块,管理数据实体对象,以及输入与输出数据结构定义。
    - do领域对象用于dao数据操作中业务模型与实例模型转换,由工具维护,用户不能修改。
    - entity数据模型数据模型是模型与数据集合的一对一关系,由工具维护,用户不能修改。
  - service业务接口用于业务模块解耦的接口定义层。具体的接口实现在logic中进行注入。
manifest交付清单包含程序编译、部署、运行、配置的文件。常见内容如下:
  - config配置管理配置文件存放目录。
  - docker镜像文件Docker镜像相关依赖文件,脚本文件等等。
  - deploy部署文件部署相关的文件。默认提供了Kubernetes集群化部署的Yaml模板,通过kustomize管理。
  - protobuf协议文件GRPC协议时使用的protobuf协议定义文件,协议文件编译后生成go文件到api目录。
resource静态资源静态资源文件。这些文件往往可以通过 资源打包/镜像编译 的形式注入到发布文件中。
go.mod依赖管理使用Go Module包管理的依赖描述文件。
main.go入口文件程序入口文件。

对外接口

对外接口包含两部分:接口定义(api)+接口实现(controller)。

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

接口定义 - api

api包用于与客户端约定的数据结构输入输出定义,往往与具体的业务场景强绑定。

接口实现 - controller

controller用于接收api的输入,可以直接在controller中实现业务逻辑,或者调用一个或多个service包实现业务逻辑,将执行结果封装为约定的api输出数据结构。

业务实现

业务实现包含两部分:业务接口(service)+业务封装(logic)。

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

在后续的章节介绍中,我们会将业务实现统一称作service,大家注意它其实包含两部分即可。

业务接口 - service

service包用于解耦业务模块之间的调用。业务模块之间往往不会直接调用对应的业务模块资源来实现业务逻辑,而是通过调用service接口。service层只有接口定义,具体的接口实现注入在各个业务模块中。

业务封装 - logic

logic包负责具体业务逻辑的实现以及封装。项目中各个层级代码不会直接调用logic层的业务模块,而是通过service接口层来调用。

结构模型

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

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

数据模型 - entity

与数据集合绑定的程序数据结构定义,通常和数据表一一对应。

业务模型 - model

与业务相关的通用数据结构定义,其中包含大部分的方法输入输出定义。

数据访问 - dao

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

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

二、请求分层流转

cmd

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

api

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

controller

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

model

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

service

service是接口层,用于解耦业务模块,service没有具体的业务逻辑实现,具体的业务实现是依靠logic层注入的。

logic

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

dao

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

三、常见问题解答

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

当然!

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

apimodel层存在重复数据结构时如何维护

api中定义的数据结构是对外使用的,与具体的业务场景绑定(如具体的页面交互逻辑、单一的接口功能),数据结构是由上层展示层前置决定的;model中定义的数据结构是服务内部使用的,数据结构是在接口实现和抽象的过程中才能定义的,并且model中的数据结构可以随意在内部修改而并不会影响对外api接口的兼容性。

注意model中的数据结构不应该直接暴露给外部使用,并且在框架的工程设计中刻意将model目录放到了internal目录下。也不应该在api层中对model中的数据结构进行别名类型定义供外部访问。一旦将model中的数据结构应用到了api层中,内部model数据结构的修改会直接影响到api接口的兼容性。

如果两者出现重复的数据结构(甚至常量、枚举类型),建议将数据结构定义到api层中。服务内部逻辑可以直接访问api层的数据结构。model层的数据结构也可以直接引用api层的数据结构,但是反之则不行。

我们来看一个示例,便于更好地理解:

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

controller层处理Req/Res外部接口请求。负责接收、校验请求参数,可以直接在controller中实现业务处理逻辑,或者调用一个或多个 service来实现业务逻辑处理,将执行结果封装为约定的api输出数据结构返回service层处理Input/Output内部方法调用。负责内部可复用的业务逻辑封装,封装的方法粒度往往比较细。

通常来讲,开发接口时只需要编写controller层中的接口实现业务逻辑即可,当存在重复代码逻辑时,再从各个controller接口实现逻辑中抽象沉淀到service。如果从controller层直接透传Req对象给service,同时service直接返回Res数据结构对象,该方法也就与外部接口耦合,仅面向外部接口服务,难以复用,这样会增加技术债务成本。

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

这是一个很经典的问题。

痛点:

常见的,开发者把数据相关的业务逻辑实现封装到了dao代码层中,而service代码层只是简单的dao调用,这么做的话会使得原本负责维护数据的dao层代码越来越繁重,反而业务逻辑service层代码显得比较轻。开发者存在困惑,我写的业务逻辑代码到底应该放到dao还是service中?

业务逻辑其实绝大部分时候都是对数据的CURD处理,这样做会使得几乎所有的业务逻辑会逐步沉淀在dao层中,业务逻辑的改变其实会频繁对dao层的代码产生修改。例如:数据查询需求,在初期的时候可能只是简单的逻辑,目前代码放到dao好像也没问题,但是查询需求增加或变化变得复杂之后,那么必定会继续维护修改原有的dao代码,同时service代码也可能同时做更新。原本仅限于service层的业务逻辑代码职责与dao层代码职责模糊不清、耦合较重,原本只需要修改service代码的需求变成了同时修改service+dao,使得项目中后期的开发维护成本大大增加。

建议:

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

为什么要使用internal目录包含业务代码

internal目录是Golang语言专有的特性,防止同级目录外的其他目录引用其下面的内容。业务项目中存在该目录的目的,是避免若项目中存在多个子项目(特别是大仓管理模式时),多个项目之间无限制随意访问,造成难以避免的多项目不同包之间耦合。




Content Menu

  • No labels

38 Comments

  1. 有没有前后端项目的目录架构

    1. 工具安装-install, 项目创建-init

      用这个工具, 自己生成一下, 自己去看下. 很完善, 每个目录各司其职., 命令是init

  2. 刚刚用gf工具生成的项目中 没有internal\controller目录 只有handler

    1. 耐心等等,应该是新的版本还没发布,我今天下载的也是这个情况

      1. 我刚刚在git上拉下来gf-cli的源码然后自己编译用上了哈哈

  3. 现在的v2给我的感觉就是从php直接变成java了,适合多人开发,对我们小公司来说一个人负责一个项目的这咱来说,过于繁琐了


    1. v2更强调规范和自动化,例如命名、分层从代码以及工具层面进行了规范;通过工具对重复代码、繁琐的接口文档维护进行了自动化生成。从企业研效以及整体工程设计来讲,是很大的一次进步。

    2. 我感觉这种目录结构, gf只是给个建议, 你要不要按这样, 还是干脆连目录都不要, 完全看你个人意愿.

  4. 建议出个视频教程吧.我是v1版本过来的,现在看到v2的目录描述文档,真是摸不到头脑啊.各个目录的功能,能否用实例的方式进行下讲解呢.

    1. 可以看看这篇文章,应该对你有帮助:https://juejin.cn/post/7156119733312438279

      后期我会花时间帮社区出入门视频。

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

    建议举一个例子给我们学习一下,以便更加深刻的理解这句话的含义!

    1. 个人拙见:

      对于 dao 层的 CRUD,如果是单表查询,自然是不会出现问题。但是如果对于数据库设计的范式等级很高的情况呀,很少会出现冗余的情况,往往我们进行查询的时候会连表查询,这时就会出现问题。

      为了满足业务,我们会有两种方式:

      1.每条Sql语句都完成一个 service 层的业务,service层直接调用dao层结果即可,这样的数据具有特征性,当然复用性也会降低。

      2.每条Sql语句尽量只完成自己的最基本的功能,service层会进一步对数据进行处理(这样就保证了dao层的代码尽量保证通用性)。


      当然这里最大的问题就是业务和开发逻辑的冲突(我认为),很难去控制一整个项目都是按照上述两种规则中一种去规范开发。更多的时候我们会按照从易的开发逻辑进行。

  6. gf init 产生的代码里没有包含service的例子,建议增加.

  7. 郭强 大佬请教一下,我在实际开发中,常常需要对一些第三方包进行二次封装,例如jwt、casbin、一些SDK等。

    这些包有着如下特征:

    • 通常初始化时读取config配置信息
    • 没有任何数据库的操作
    • 相对独立的,不依赖外部服务
    • 通常在service层被使用,例如:service/Oauth2,调用了jwt、casbin

    问题是:这些包放到哪里比较合适??我感觉放在utility比较别扭,毕竟这些包都是专门为某个service提供的

    1. 可以把jwt,casbin放到 components 组件层,里面封装所有的组件,service层可以调用这些组件。

    2. 感觉放utility里面挺好的,就算只为一个service服务,这也算是个第三方包,况且没准后面其他的service又会用上呢

  8. 感觉挺好的,V1在接口抽象方面设计的不好,这里就细分了Servcie和logic,把抽象和实现分离了,这样上层或者本层只需依赖service里的接口,不需要依赖实现。

    这样单元测试好做一点,V1的单元测试不太好做。

    dao层不分离感觉也比较对,因为dao层比较薄,方法比较固定,也不能互相依赖,所以没必要分离。

  9. 为啥工程开发设计下面少了一个对象封装设计???

    1. 那章文档需要更新,为了防止产生误导性,先隐藏了,等更新完成再公开。

  10. internal/packed 这个包是干啥的
  11. 郭强 大佬,您好!对于您的工程目录设计,我感觉很清晰、合理;尤其是结构化设计的思想,将server传递给controller的api,以及controller调用service的input以及output都封装为结构体进行传递,这是我也比较认可的设计。

    但是在实际的项目中,我感觉这会带来一个问题,就是需要有大量的结构体和转换和拷贝,很多人担心会对接口处理的性能有影响。

    我经过一些思考和尝试,目前我是通过如下的方式来实现的:

    就是controller调用service时,不是直接传递结构体,也就是说,对于service提供的接口,其入参和出参,不是定义为结构体,而是定义为interface,由controller中的api实现这些interface 。

    controller调用service的接口时,参数传入controller中定义的api。

    这样避免了一次api到service的input和output的结构体转换和拷贝操作。

    不知 你以前有没有考虑过这种方案,如有更好的意见和建议,还望指导,谢谢!

    1. 是否controller,和api和service可以使用相同的model,入参出参了

    2. 你controller实现的这些interface,输出是不是就是service用到的input结构体?这样的话,和直接传指针的效果一样吧

  12. V2最新版的视频教程在这里:https://www.bilibili.com/video/BV1Ng41167fW/

    大家帮忙顶一下,觉好留赞。

  13. 工程目录真不敢苟同,给我的感觉就是下层依赖上层设计,而且使项目目录太过混乱,service层本来就是做业务细分,每个service方法功能是单一的,所有业务请求处理都是依赖service层的。

    dao和model是两个完全不一样的东西,他们的边界前者是更底层的,而后者是对业务的,有时候这两个东西,开发人员搞不清楚或是在设计工程结构的时候偷懒,经常会把两者混为一谈,这是错误的,更不要说dao和model是做相同的事。

    1. 我感觉这种目录结构, gf只是给个建议, 你要不要按这样, 还是干脆连目录都不要, 还是要mvc结构,  完全看你个人意愿. 关键看你项目吧, 

  14. 项目的sql文件建议放哪?

  15. 工程目录这般设计是为了解耦,非常同意这样的做法。后面可以做个工具完善以下事项:

    1. 在gconv里面加个方法实现从req的结构copy到input,从output copy到res
    2. 不需要重复去手写业务模型,在接口定义完成后,直接生成model的业务对象(req对应input, res对应output)。 比如 gf gen model
  16. 有多年php和java工程经验,呆过多个业务团队,目前为止遇到过最好的目录结构:

    1、php laravel框架工程结构

    2、饿了么内部 java工程结构

    ├── APP-META
    ├── README.md
    ├── anubis-marketing-api                  # api接口定义层(java特有,打包jar给外部调用)
    ├── anubis-marketing-common        # 工具类相关
    ├── anubis-marketing-core               # 业务核心层(service层承载过重或者可以复用的部分放到这)
    ├── anubis-marketing-infra               # 基础工具层(redis/es等配置引用基础类)
    ├── anubis-marketing-integration    # 外部依赖接口包装
    ├── anubis-marketing-main              # springboot启动程序
    ├── anubis-marketing-orm               # model entity map repository等 数据库相关
    ├── anubis-marketing-service         # java里api是接口定义,这里实现接口
    ├── anubis-marketing-test               # 测试代码目录
    └── pom.xml

    调用关系:

    api → service → core → common、infra、integration、orm

                     ↓ → → → → →

    启动:

    main


    1. 请问怎么适配微服务

  17. model层和api层数据结构差不多的情况下,model层引用api层,貌似在controller层会出现"循环引用"问题

    请大佬帮忙解答。

    1. 考虑换一种工程设计,使用兼容ddd的整洁架构的目录设计,请参考B站开源的kratos的目录设计。goframe这一套是mvc的产物,同时奔着全家桶去了,把golang变得臃肿了,不过对于快速开发一些小型项目,比较方便,效率较高

    2. 在规范使用的前提下,不会存在循环引用问题的,因为apimodel都是结构定义,是被引用模块。

  18. apimodel层存在重复数据结构时如何维护

    这里没太看懂,api内不可以使用model的数据,但又可以使用model下的entity吗?有哪位大佬能帮忙解释吗?谢谢~


    1. 鄙人的个人看法是,api和model是严格分离的。api只负责请求的校验和转发,分配给特定的controller执行具体任务。api自己会根据用户前端的需要而特别定义一套适合于传递给前端的数据结构。model下的entity则是专门与数据库中的表对应的数据结构,所以api严格意义上来说是不能直接使用entity的结构来返回数据给前端的。就好比前端需要用户的name和addr,而底层数据库中,name和ID在一张表中,ID和addr又在另外一张表中,这个时候就需要api将表内容进行拆分组合,形成自己的name-addr结构体返回给前端。