领域驱动分层设计-达人平台

前言

在市场当前推广方式的多样化下,软件的功能也变得愈加复杂起来,业务程序也随之愈加复杂,功能边界容易变得模糊不清。传统自底向上的开发模式在业务规模增大的情况下,已难以驱动复杂多变的业务,甚至到最后不得不对业务系统进行重构。

此外,多人协作的场景下,也使得代码风格容易因人而异,代码互相理解困难,团队合作成本上升。因此需要一个具有高可维护性、高可扩展性、业务驱动型、团队合作型、灵活的设计模式,以应对日益复杂的系统。

领域驱动设计(DDD)

领域驱动设计(DDD)是一种架构设计方法,它利用领域、子域、核心域、通用域、界限上下文、自顶向下的概念作为黏合剂,利用实体、值对象、聚合和聚合根等构造物作为砖头,将复杂的系统构造成多个从整体到局部高内聚、低耦合的子系统。

领域驱动设计的概念本文就不再赘述,可以看下其他大佬写的DDD理念探索,接下来会从两个方面介绍:

1、为什么使用DDD的设计思想

2、实战应用-达人平台分层设计

为什么使用DDD的设计思想

正交性

正交,学过数学的我们都不会陌生,比如平面上两个向量垂直,我们则可以认为它是正交。

那两个垂直的向量能做什么呢?两个垂直的单位向量能通过简单的加减乘运算组成平面上任何一个向量,转化到代码设计,如果代码的每块功能都互相正交,那么我们可以用组合的方式灵活的将已有的功能组合成更大的功能,使每一块代码都得到复用的可能,这也符合Golang多组合、少继承的理念,而DDD正是通过领域划分将系统变得更符合正交性。

高内聚和低耦合

我们可以用领域划分,将一个系统划分成若干子系统,再将子系统划分成若干子模块,再将子模块划分成若干子子模块......这样最后对于同层级的模块来说,它们之间是互相独立的,在它们的上层调用方,通过组合的形式将它们衔接起来形成系统的若干功能。

这样的系统每一部分模块功能都是单一的,模块与模块之间又不形成任何依赖,它们仅仅被它们更上层模块依赖,最后对某一层级某个模块的调整只会影响到它自己本身以及依赖它的上层模块。

团队协作效率

DDD鼓励使用分层架构将系统划分为不同的层次,并将不同层的职责进行分离。这样可以使团队成员专注于各自的领域,并且在不同层之间通过定义清晰的接口进行协作。每个团队成员可以关注自己负责的领域部分,减少对其他部分的依赖,提高团队协作的效率。

业务驱动型

DDD是基于业务的自上而下的设计模式,我们可以根据对业务的拆分划分领域模型,设置边界上下文,做微服务的划分,这使得系统能够更好地应对变化和演化,保持与业务的高度一致性。在实际业务中划分领域的边界是比较困难的,有时也需要随着业务来进行调整,在本文后续会介绍业务划分思想

实战应用分享-达人平台分层设计

达人平台是为了承接和服务市场KOC推广方式下机构以及短视频推广达人的新项目,终端包含PC站koc.wtzw.com 以及猫推达人平台小程序,主要功能包含达人/机构注册、结算、关键词管理、数据统计、公告展示等。机构以及达人借助达人平台能更加有效的参与完成七猫产品的市场推广,从而KOC推广方式在推广总量中的占比也在不断提升。

接下来将从业务角度对达人平台进行服务划分,根据DDD的架构设计方案对项目进行领域划分,进一步完成代码分层设计。

领域服务划分

从达人平台本身来看,按照业务角度划分为如下模块:

  1. 消息模块:负责消息通知、公告展示等功能
  2. 推广管理模块:提供达人关键词查看、添加、导出关键词等管理功能
  3. 数据中心:提供达人查看、导出关键词推广量等功能
  4. 用户中心:提供登陆、注册、个人信息管理、结算等功能

根据以上功能,我们将达人平台划分为四个服务,分别是message(消息和公告管理)、promotion(推广管理)、data(数据中心)、user(用户中心)。

由于我们需要给前端提供接口,有两种方案:

1、由各自模块提供RPC调用的同时提供出HTTP调用,服务之间可以互相调用。

2、新建一个gateway层提供http接口对外暴露,通过组合各个RPC调用的方式整合功能,另外gateway不包含具体的业务逻辑,它只需要做一些组合、校验的功能,具体逻辑由各个RPC服务实现。

我们选择方案2,原因是由gateway通过组合的方式实现平台功能,可以保证各个RPC服务功能的纯粹,避免一个功能同时涉及多个模块而出现业务逻辑划分不清、模块界限模糊不清的尴尬场景

从整个市场业务来看,达人平台的业务逻辑具有一定历史背景,需要依赖历史业务,因此我们需要从历史业务中剥离出一个新的服务,在承担历史逻辑的同时为达人提供接口

在上文我们基于业务的划分,确定了各个服务的边界,接下来将具体探讨服务的架构与代码的分层设计。

清晰架构[1]

清晰架构(Explicit Architecture)是DDD的其中一种实现架构,它提出了用户接口层(User Interface)、应用核心层(Application Core)、基础设施层(Infrastructure)的概念。

1、User Interface->Application Core->Infrasturcture

清晰架构对外提供接口(如Web接口、终端命令等),对内设有基础设施层(如Mysql、Kafka、SMS Client等),核心业务逻辑内聚到应用核。一条命令或一个请求通过接口层,流转到应用核,应用核内做了一些逻辑后流转到基础设施层,最后再流转到接口层。

为了减小应用核心层与用户接口层和基础设施层的耦合,清晰架构提出了和端口(Port)和适配器(Adapter)的概念,简单来说,端口用来制定数据结构、传输规则,适配器用来适配端口的规则。

对于用户层来说,通过总线(Bus)到指定的端口调用系统功能,无需关心系统内部如何实现,数据格式通过适配后返回到用户层。

对于基础层来说,根据应用核心层的需要制定端口规则,适配器需要实现端口规则,注入到应用核心层,这样应用层可以在任何时候接收外界的数据或做一些持久化的操作。

总的来说,一个Port,可以是一个Serivce的接口,也可以是一个Repository的接口,调用方依赖的是被调用方提供的Port(接口),而不依赖于具体的实现。

到此,我们的系统和外界的交互结构就确认完毕,我们可以列一下目前需要的层级:【用户接口层】-【应用核心层】-【基础层】,层与层之间交互依赖【端口-适配器】

2、应用核心层内部分层

应用核心层又被划分为如下层次:

  1. 应用层(Application):应用服务(Service)、Query&Command
  2. 领域层(Domain):领域服务(Domain-Service)、领域模型(Domain-Model)

Application作为应用核心层的一等公民,它可以直接访问基础层的仓储接口做一些查询或者持久化的动作,也可以由领域层做一些业务逻辑,它包含了应用服务(业务逻辑聚合,主要实现上层逻辑)和Query&Command(责任分离机制,在下文中会详细介绍)。

如果说Application是应用核心层的一等公民,那Domain则是妥妥的二等公民了,一条业务链路中,它可以为Application提供领域服务,也可以对接基础层承接基础层提供的服务,也可以没有。它由领域服务和领域模型组成,领域模型里包含着实体、值对象等,而领域服务会操作若干个实体、值对象做一些领域逻辑。

举个例子:我们可以把收音机当作一个实体,收音机本身提供了{打开、关闭、音量调节}的入口,把光盘当成一个值对象,把{打开收音机、调整音量、换频道、放光盘}这些围绕使用收音机的行为作为领域服务。更上一个层次,我们一个人在家的一天可以听收音机、可以看电视、打游戏,每个活动都可以当成一个领域,共同组成《我们的一天》这个应用服务。

3、清晰架构最终形态

确认好应用核心层的结构后,清晰架构的最终形态就确认了,可以看到整个清晰架构最外围主要是和第三方对接,通过一定的媒介与应用核心层交互,层级间依赖接口而非具体实现,应用核心层内部自外而内划分了应用层->领域层,结构划分确实清晰。

4、Query&Command

Query&Command是我们基于CQRS应用的变种,有相似的地方,比如职责分离,但又并不是为了读写分离,它的目的主要是为了保证在应用层实现的功能单一。

设想在应用层仅有一个service包,且有一个UserService对象,在迭代的过程中用户的所有行为逻辑可能都会被放到这个对象中,演变到最后UserService就变成了超级对象,这显然不符合单一职责原则。

5、代码结构落地

代码架构

层级划分

├── api // 对外定义的api接口,与下面internal/presentation共同组成用户接口层
│   ├── http
│   └── rpc
├── bin // 生成的二进制文件
├── build // DockerFile
├── cmd // 程序入口
├── deployments // 部署文件
├── internal
│   ├── application // 对应到应用核-application
│   │   ├── command
│   │   │   └── wire.go
│   │   ├── query
│   │   │   └── wire.go
│   │   ├── service
│   │   │   └── wire.go
│   │   └── wire.go
│   ├── domain // 对应到应用核-domain
│   │   ├── model // domain-model
│   │   │   └── doc.go
│   │   ├── service // domain-service
│   │   │   └── wire.go
│   │   └── wire.go
│   ├── infrastructure // 基础层,用来读写数据库、第三方服务、消息队列,基础层对外提供的接口都定义在port中,由adapter去实现
│   │   ├── client
│   │   │   ├── adapter
│   │   │   │   └── wire.go
│   │   │   ├── port
│   │   │   │   └── doc.go
│   │   │   └── wire.go
│   │   ├── converter  // 基础层和应用层之间的数据结构转换在这处理,应用层不会直接操作到仓储层的数据结构,进一步对层级之间进行解耦
│   │   │   ├── converters.go
│   │   │   └── wire.go
│   │   ├── publisher
│   │   │   ├── adapter
│   │   │   │   └── wire.go
│   │   │   ├── port
│   │   │   │   └── doc.go
│   │   │   └── wire.go
│   │   ├── po // 数据库的字段结构,通过converter转化到应用层的数据结构,不会直接被应用层引用
│   │   ├── repository
│   │   │   ├── adapter
│   │   │   │   └── wire.go
│   │   │   ├── port
│   │   │   │   └── doc.go
│   │   │   └── wire.go
│   │   └── wire.go
│   ├── presentation // 展现层,与上面api共同组成用户接口层
│   │   ├── assembler // 用户接口层和应用核心层之间的数据结构转换在这,进一步对层级之间进行解耦
│   │   │   ├── assemblers.go
│   │   │   └── wire.go
│   │   ├── bus // 所有Call应用核心层的操作都需要通过bus
│   │   │   ├── commands.go
│   │   │   ├── queries.go
│   │   │   └── wire.go
│   │   ├── console // 命令行入口
│   │   │   └── wire.go
│   │   ├── controller // http接口实现层
│   │   │   └── wire.go
│   │   ├── provider // RPC接口实现层
│   │   │   ├── provider.go
│   │   │   └── wire.go
│   │   └── wire.go
│   └── wire.go
├── pkg // 通用工具或常用命令,如数据库client的初始化、grpc客户端的初始化等
└── scripts // 脚本

依赖管理

我们使用Google的开源工具wire作为依赖管理工具,由于DDD的理念,未来代码中实体会非常多,同时一个上层应用可能会依赖更多下层的实体,wire能帮我们轻松的管理实体间的依赖问题,我们只需要在NewObject中声明需要什么类型的依赖即可。

一些小工具

合适的工具可以帮助我们提高开发效率,省略一部分流程化的工作,达人平台搭配一些提效工具:

gors:提供快速生成gin http路由的工具,只需要在api层定义接口注释即可

cqrs:提供快速生成Query&Command代码结构的工具,只需要在api层定义接口注释即可

gorsx: 提供快速生成presentation层的代码工具,只需要在api层定义接口注释即可

总结与展望

虽然DDD与清晰架构能极大的提高业务的可扩展性和系统的可维护性,但同时这种复杂的设计对于开发人员的要求会提高,而且为了解除层级之间的耦合性,需要更间接的调用方式和数据传输方式。不过好在可以根据业务的需要设计更多的代码生成工具,减少一些流程化的coding,提高开发效率。

未来我们将进一步规范代码结构、提高自动化生成率,开发人员只需要专注业务逻辑的编写,从而提高生产效率。

致谢

感谢延铖大佬引进的设计思想。

参考文档

[1] https://herbertograca.com/2017/11/16/explicit-architecture-01-ddd-hexagonal-onion-clean-cqrs-how-i-put-it-all-together/

展示评论