本文详细介绍了 Go 项目结构的分层方式,包括社区版、MVC 分层和 DDD 分层。同时,还介绍了 DDD(领域驱动设计)的基础理论知识,并与 MVC 进行了优劣比较。文章还探讨了依赖倒置原则的重要性,并提供了关于在 Go 中实践 DDD 的代码示例。通过阅读本文,读者将能够更好地理解和应用 Go 项目结构的分层方式以及 DDD 的实践方法。
Go 工程化项目结构
Go 项目结构分层
关于 Go
项目的目录结构如何设计这一问题?Go
官方其实并没有定义标准的项目结构分层,但社区维护了一个 project-layout 仓库,其中包含了一个通用的项目结构示例,大家在实践中基本会遵循这个规范。
下面是一个通用的项目结构示例,并且可能因项目的规模、需求和偏好而有所不同。
textmate
project/
├── api/
│ ├── handler/ # API请求处理程序
│ ├── middleware/ # API中间件
│ └── router.go # 路由定义
├── cmd/
│ ├── app/ # 应用程序入口点
│ └── cli/ # 命令行接口入口点
├── config/ # 配置文件和配置相关代码
├── internal/ # 私有应用程序和库代码
│ ├── model/ # 数据库模型
│ ├── repository/ # 数据库访问层
│ ├── service/ # 业务逻辑层
│ └── util/ # 工具函数
├── migrations/ # 数据库迁移文件
├── pkg/ # 可公共使用的库代码
├── scripts/ # 构建、安装等脚本
├── test/ # 测试相关代码
├── web/ # Web前端相关代码
├── .gitignore # Git忽略文件列表
├── LICENSE # 项目许可证
├── README.md # 项目说明文件
└── go.mod # Go模块定义文件
MVC 传统架构
MVC 经典模型
textmate
┌───────────────┐ ┌────────────┐ ┌──────────────┐
│ Controller │◄─────►│ Service │◄─────►│ Repository │
└───────────────┘ └────────────┘ └──────────────┘
(view layer) (controller layer) (model layer)
MVC 目录结构
Controller + VO
/Service + BO
/Repository + entity
bash
server
.
├── cmd # 执行目录
├── common # 通用包
├── configs # 配置参数变量
├── database # 数据库连接
├── go.mod
├── internal
│ ├── controller # 接口层
│ │ └── vo # VO(View Object)模型
│ ├── repository # 访问持久化层
│ │ └── entity # Entity 实体
│ ├── pkg # 内部库
│ └── service # 业务逻辑层
│ └── bo # BO(Business Object)模型
├── main.go # 主入口
├── pkg # 外部库
├── router # 路由系统
└── test # 测试目录
提示
对于传统面向过程的贫血模型而言,MVC 架构更加注重业务逻辑的封装和处理。在这种模型中,重点是强调 Service
层的职责,而对于 BO
(Business Object)层则相对较轻。
DDD 架构设计
DDD 目录结构
bash
server
├── README.md # 项目文档
├── application # 业务调度层
│ ├── event # 微服务事件推送或订阅
│ │ ├── publish # 事件发布
│ │ └── subscribe # 事件订阅
│ └── service # 用于连接 Controller 和 Domain,进行三方接口调用等其他操作
├── domain # 领域服务层(领域逻辑和领域对象,主要的业务逻辑,采用充血模型)
│ ├── aggregate01 # Aggregate 聚合根目录
│ │ ├── entity # entity 实体、VO 值对象以及工厂模式(Factory)相关
│ │ ├── event # 事件实体以及与事件活动相关的业务逻辑代码
│ │ ├── repository # 持久化领域对象,通常仅包括仓储接口,仓储实现应放到基础架构层实现
│ │ └── service # 领域服务代码,一个领域服务是多个实体组合出来的一段业务逻辑
│ ├── aggregate02
│ └── ...
├── go.mod # 依赖文件
├── infrastructure # 基础设施层
│ ├── api # 第三方 API/SDK
│ ├── configs # 配置参数变量
│ ├── database # 初始化数据库
│ ├── mq # 消息队列连接和配置
│ ├── persistence # 数据持久化(Domain 层 repository 的具体实现,数据库 CRUD 操作)
│ └── pkg # 工具函数
│ ├── common # 与业务相关包
│ └── utils # 公共基础包
├── interfaces # 接口层
│ ├── assembler # 实现 DTO 数据传输对象与 Domain Entity 之间的相互转换和数据交换,从表示层(Presentation Layer)向领域层(Domain Layer)进行数据传递
│ ├── controller # 控制器路由函数
│ └── dto/vo # 可包含多个领域对象的属性:DTO(Data Transfer Object)主要关注数据的传输,通常用于面向服务或接口设计,用于在系统的不同部分之间传递数据。VO(View Object)则更常用于用户界面 UI 层,用于呈现数据给用户
└── main.go # 主入口
提示
在 DDD
(领域驱动设计) 中,需要将重点放在领域层(Domain Layer
)进行业务逻辑和规则的实现。服务层(Service Layer
)主要负责协调和调度各个领域对象之间的交互。
简单来说,对于面向对象充血模型的 DDD
而言,是重 Domain
轻 Service
!
DDD 分层
textmate
┌────────────────────────┐
│ Infrastructure │
/ └────────────────────────┘ \
/ | Repository 将仓储实现放到 Infra 目录内
/ | \
▼ ▼ ▼
┌───────────────┐ ┌──────────────┐ ┌─────────────┐
│ Controller │ ────►│ Service │ ────►│ Domain │ 这里仅定义仓储接口
└───────────────┘ └──────────────┘ └─────────────┘
▲ ▲
│ │
▼ ▼
┌───────────────┐ ┌────────────────────────┐
│ 前端页面 │ │ 第三方 API 或其他微服务 │
└───────────────┘ └────────────────────────┘
层级 | 说明 |
---|---|
Controller (控制器) |
负责处理用户请求和响应,将请求转发给 Service 层,并返回响应给客户端。 |
Service (服务层) |
负责协调领域层的操作,提供一些额外的处理逻辑。Service 层依赖于 Domain 层,并通过调用 Domain 中的方法来实现业务逻辑。Service 层还可以与其他服务进行交互,如调用第三方 API 或其他微服务。 |
Domain (领域层) |
包含了核心的领域逻辑和领域对象。它定义了领域的实体、值对象、聚合根以及领域服务等。Domain 层是解决问题的核心,其中包含了业务规则和行为的具体实现。Domain 层不应直接依赖于外部的存储库或服务,而是通过接口与 Repository 层进行交互。 |
Repository (仓储层) |
负责与数据持久化层进行交互,将领域对象进行存储和检索。Repository 层实现了与数据库或其他持久化机制的交互,提供了数据的持久化和查询操作。它为 Domain 层提供了一个抽象接口,让 Domain 层能够通过接口与 Repository 层进行通信。 |
DDD 领域驱动设计(基础理论篇)
为何要有代码架构
时间久远的项目往往会有许多开发人员参与其中,如果没有良好的指导规则进行约束,很容易变得混乱不堪。在这种情况下,很少有开发者愿意花时间去整理和简化代码。对于那些被迫接手的开发者来说,即便只是迭代一个小功能,可能都需要面临着堆积如山的代码,这无疑是低效且令人痛苦的。
然而,这是一个普遍存在的问题,也有许多开发者尝试解决它。经过多年的发展积累,产生了许多软件架构。其中一些广为人知的包括六边形架构 (Hexagonal Architecture)
、洋葱架构 (Onion Architecture)
、整洁架构 (Clean Architecture)
等。尽管这些架构在细节上存在差异,但它们的核心目标都是相同的:实现软件系统的 关注点分离 (separation of concerns)
。
关注点分离之后的软件系统应具备如下特征:
特点 | 说明 |
---|---|
不依赖特定前端 UI | UI 可以任意替换,不会影响系统中的其他组件。无论是 Web UI、桌面 UI 还是命令行 UI,业务逻辑都不会受到影响。 |
不依赖特定 WF 框架 | 无论是使用 Go 语言的 web 框架 gin、echo,还是使用桌面应用框架 fyne,甚至是命令行框架 cobra,业务逻辑都不会受到影响。只有与框架接入的那一层会受到影响。 |
不依赖特定应用组件 | 系统可以随意选择 MySQL、PostgreSQL 或 MongoDB 作为数据库,也可以选择 Redis 或 etcd 作为键值存储等。业务逻辑不会因为这些外部组件的替换而变化。 |
代码易于维护与测试 | 核心业务逻辑可以在不依赖于 UI、数据库或 Web 服务器等外部组件的情况下进行测试。这种纯粹的代码逻辑使得测试变得清晰且容易实施。 |
DDD 基本概念
在 DDD
的世界中,涉及到了很多概念,包括但不限于以下内容:
- 统一语言
- 限界上下文
- 领域、子域、支撑域
- 聚合、实体、值对象
- 接口层、应用层、领域层、基础设施层
- 贫血模型、充血模型
我们将重点介绍其中的一些基础概念,并简单解释它们的含义。
名词 | 英文 | 说明 |
---|---|---|
实体 | Entity | 在业务领域中具有唯一标识的对象。它们有自己的生命周期和状态,并通过标识属性进行区分。实体通常具有行为和属性,并可以包含其他值对象或实体。 |
值对象 | Value Object | 没有标识的对象,仅通过属性值来定义。通常用于描述特定概念或概念组合,在整个系统中广泛重用。与实体不同,值对象通常是不可变的。 |
领域服务 | Domain Service | 处理复杂业务逻辑、跨实体操作或无法由单个实体或值对象表达的功能的机制。领域服务的方法可能涉及多个实体或值对象,并可在整个领域模型中使用。 |
聚合及聚合根 | Aggregate, Aggregate Root | 相关实体和值对象的集合,以聚合根为核心。聚合根是聚合内部的一个实体,负责保护聚合的一致性和完整性。通过限制对聚合根之外实体的直接访问,维护聚合的内部一致性。 |
工厂 | Factory | 创建复杂对象或聚合的机制,封装了对象的创建过程。工厂可以处理对象的初始化逻辑、依赖注入和复杂的构建步骤,并返回创建的对象。降低对象创建的复杂性并提供可测试性。 |
仓储 | Repository | 持久化和检索领域对象的机制。封装了数据访问层的实现细节,为领域层提供简单的接口操作持久化状态的对象。负责管理对象的加载、保存和查询,并确保与持久化存储之间的交互。 |
领域事件 | Domain Event | 描述业务领域中重要事件的对象。用于在不同领域对象之间传递信息并触发相关行为。领域事件被广播给感兴趣的订阅者,进行后续处理或响应。 |
DDD 架构图
如果我们能用图来清晰地表达,那就不必使用冗长的文字叙述了。
DDD
分层架构图DDD
微服务消息订阅DDD
四层结构模型- 从
MVC
到DDD
的架构演进
www.processon.com/view/654730...
为什么是 DDD
贫血模型与充血模型
从何说起呢,相信大多数软件开发工程师对 MVC
三层架构都非常熟悉。
目前,许多 Web
应用都采用前后端分离的方式,后端负责提供接口供前端调用。在这种情况下,我们通常将后端项目分为三层:M(Model)
、V(View)
和 C(Controller)
,至于什么目录命名其实无所谓的,在 Python Django
可能叫做 MVT
,其实分层的思路大体都是相似的。
MVC
是基于 "贫血" 模型(Anemic Domain Model)
进行开发的,将数据和业务逻辑分开到两个类中,这种基于贫血模型的设计思路本质上破坏了面向对象编程的封装特性,属于典型的面向过程编程风格。
而 DDD
是基于 "充血" 模型(Rich Domain Model)
进行开发的。数据和对应的业务逻辑被封装到同一个类中,属于典型的面向对象编程风格。
总结,基于 "贫血" 模型的传统的开发模式,重 Service
层,轻 BO
;基于 "充血" 模型的 DDD
开发模式,轻 Service
层,重 Demain
层。
何时使用 DDD
- 在大部分情况下,我们开发的系统的业务都比较简单,只涉及基于
SQL
的CRUD
操作,因此我们不需要过于复杂的 "充血" 模型设计。对于这种简单的业务开发, "贫血" 模型已经足够应付。考虑到业务的简单性,我们选择了 "贫血" 模型进行设计,一个原因是领域模型相对较简单,另一个原因是考虑到ROI
(投入产出比)的因素。 - "充血" 模型的设计难度比 "贫血" 模型更大,因为 "充血" 模型更加贴近面向对象编程的风格。在开始设计时,我们需要考虑哪些操作需要对数据进行暴露,以及定义哪些业务逻辑。而不像 "贫血" 模型那样,最初只需定义数据,之后根据功能需求在
Service
层进行相关操作,无需事先进行过多的设计。 - 这种基于 "贫血" 模型的传统开发模式已经存在多年,深入人心,是大多数开发人员习以为常的。如果在现有开发模式下没有遇到太大的问题,转向使用 "充血" 模型或领域驱动设计势必会增加学习成本和转型成本。在没有遇到紧迫的开发需求的情况下,许多开发人员可能不愿意进行这样的转变。
DDD 与 MVC 的优劣
DDD 的优势
- 高度关注业务领域:
DDD
通过将重点放在核心业务领域上,提供了一种更好地理解和建模复杂领域的方法。它强调领域驱动的设计,使开发人员能够更好地与业务专家进行沟通,并更好地反映真实世界的业务逻辑。 - 模块化和可重用性:
DDD
鼓励将代码组织成聚合和领域服务,以及其他相关概念。这种模块化和组件化的设计可以提高代码的可重用性,减少耦合,并使系统更加灵活和可扩展。 - 解决复杂性和变化:
DDD
通过将复杂业务问题分解为可管理的领域对象和领域服务来应对系统的复杂性。它强调通过限界上下文和聚合根来处理业务规则和一致性,并尽可能地减少对外部依赖的影响。这使得系统更容易理解、演化和适应变化。
DDD 的劣势
DDD
只是一套先进的方法论,虽然已经诞生多年并随着微服务的兴起而变得热门,但它其实也有一些缺点需要注意:
性能方面:DDD
基于聚合来组织代码,对于高性能场景下,加载聚合中大量无用的字段可能会严重影响性能。 事务方面:DDD
中的事务被限定在限界上下文内,处理跨多个限界上下文的情况时,开发人员需要额外考虑分布式事务问题。 实现难度和推广成本:DDD
项目需要领域专家的参与,同时也要求开发者对业务、建模和面向对象编程有深入了解。
MVC 的优势
- 简单易上手:传统的
MVC
架构具有简单清晰的组织结构,易于理解和学习。它将应用程序分为模型、视图和控制器,提供了一种直观的方式来组织代码并处理用户交互。 - 快速开发和迭代:
MVC
架构适用于快速开发和迭代的场景。它将前端展示和后端逻辑分离,使开发团队能够并行工作,并更容易实现界面的变化和改进。 - 广泛的支持和成熟的生态系统:
MVC
架构已经存在多年,拥有广泛的支持和成熟的生态系统。有许多成熟的框架和工具可供选择,可以加快开发过程,并提供丰富的功能和插件。
简单来说,对于大多数项目而言,MVC
架构已经足够应对需求。然而,对于长期、持续投入的复杂业务系统,DDD
架构会更适合。在选择架构时,应根据具体项目需求、团队能力(学习成本/开发时间)和预期系统复杂性进行评估。
DDD 与依赖倒置
领域驱动设计(Domain-Driven Design
,简称 DDD
)是一种软件开发方法论,它强调在复杂业务场景下,以领域为核心进行建模和设计。而依赖倒置原则(Dependency Inversion Principle
)可以说是 DDD
中的一个重要概念和实践之一。
在 DDD
中,依赖倒置原则被广泛应用于解耦领域模型与基础设施、应用服务等其他层次的依赖关系。具体来说,DDD
鼓励将领域模型定义为独立于具体技术和外部实现细节的抽象,通过依赖注入或依赖查找等方式,将具体实现的依赖关系反转到抽象接口上。
这种依赖倒置的表现在 DDD中
具有以下特点:
-
领域层依赖于抽象:领域模型应该依赖于抽象的接口或抽象类,而不是具体的实现。这样可以确保领域模型的独立性和可替换性。
-
基础设施层实现抽象接口:基础设施层负责具体的技术实现,例如数据库访问、消息队列等,它们会实现领域层定义的抽象接口。
-
依赖注入:在
DDD
中,常常使用依赖注入来实现依赖倒置。通过将具体实现注入到领域模型中,领域模型不需要关心具体的实现细节,而是专注于业务逻辑的处理。
通过依赖倒置原则的应用,DDD
能够实现解耦、灵活性和可测试性等优势。领域模型的独立性和复用性得到了提升,不同团队成员可以独立地开发和测试各个领域模型,并且可以方便地替换底层技术实现,以满足不同的需求变化。
依赖倒置原则
依赖倒置(Dependency Inversion)是面向对象设计中的一种原则,它提倡将高层模块不依赖于低层模块的具体实现细节,而是依赖于抽象的接口或抽象类。
传统的软件设计通常是由高层模块直接依赖于底层模块,这样会导致高层模块与底层模块紧密耦合,难以维护和扩展。而依赖倒置原则通过引入抽象来解决这个问题,使得高层模块和底层模块都依赖于抽象,从而降低了它们之间的耦合性。
具体来说,依赖倒置原则包括以下几点:
- 高层模块定义抽象接口或抽象类,而不依赖于具体的低层模块。
- 低层模块实现这些抽象接口或抽象类,并被高层模块所依赖。
- 高层模块通过依赖注入(Dependency Injection)或者依赖查找(Dependency Lookup)来获取具体的低层模块实例。
依赖倒置能够带来许多好处,包括:
- 提高代码的可维护性和可测试性:通过依赖倒置,高层模块不再直接依赖于底层模块的具体实现,可以更容易地进行单元测试和模块替换。
- 支持松耦合的架构设计:通过依赖注入,模块之间的依赖关系变得灵活,可以方便地更改或扩展底层模块,而不会影响到高层模块。
- 促进团队协作与开发效率:使用依赖倒置原则,不同模块之间的职责清晰划分,不同开发人员可以同时独立开发和测试各自负责的模块。
总而言之,依赖倒置原则是一种重要的设计原则,能够提升软件的灵活性、可维护性和可测试性,帮助构建高质量的面向对象系统。
代码示例
依赖注入(Dependency Injection)是一种设计模式,用于解耦组件之间的依赖关系。在下面的示例中,通过将 UserRepository 注入到 UserDomain 中,再将 UserDomain 注入到 UserService 中,实现了依赖关系的传递和解耦。
这种依赖注入的方式使得代码更具灵活性和可测试性。通过将依赖项作为参数传递,而不是在类内部创建依赖项的实例,我们可以更容易地替换和测试依赖项。
go
// 在领域驱动设计(DDD)的世界中,通常会使用类似下面的代码 "套路" 来进行层层包装和依赖注入
srvs := service.SomeService(domain.SomeDomain(persistence.SomeRepository(database.DB)))
对 DDD 的理解
精心设计的
DDD
(领域驱动设计) 体现了 依赖倒置原则 ,它是一种 "自底向上" 的设计思想。我们通过 "自上而下" 的 依赖注入 方式来实现底层的接口依赖。
传统 MVC
中看似面向对象,实则面向过程的编程范式,随着项目业务逻辑的复杂,会变得难以维护。
设计模式:接口与实现分离,并通过层级嵌套的方式实现 DDD
四层架构中的接口方法调用,完全遵循了面向对象编程(或面向接口编程)的编程范式!
对应传统 MVC 的 V (View)
接口层:作为最顶层,主要由控制器 (Controller
)、数据传输对象 (DTO
)/视图对象 (VO
) 和装配器 (Assembler
) 组成,负责与外部系统进行交互,并处理数据的传递和转换。
对应传统 MVC 的 M(Model)
基础设施层:其中一个关键部分是数据库持久化 (Repository
实现),包括持久化对象 (PO
) 和装配器 (Assembler
),用于处理数据的存储和检索等基础设施操作。当然基础设施层还有很多其它的功能代码。
对应传统 MVC 的 C (Controller)
- 服务层:负责协调多个领域服务、第三方
API
、基础设施和其他功能组件。这一层通常不涉及复杂的业务逻辑,如果业务逻辑较简单,则该层可能非常薄。 - 领域层:作为
DDD
的核心部分,包含了主要的业务逻辑实现。在领域层中,聚合根 (Aggregate
)、实体对象 (Entity
)、值对象 (VO
)、仓储接口定义 (Repository
) 和领域服务等概念被具体实现和应用。
DDD 的 Go 代码实战
代码仓库
项目介绍
这是一个围绕 User
领域聚合的简单 DDD
实例,接口包括如下:用户登录、注销、用户菜单、用户信息、以及用户的 CRUD
。
通过学习该项目,你将会收获到以下内容:
-
Go
工程化项目实践:DDD
项目分层设计GORM RBAC
权限控制表设计AuthMiddleware
与JWT
的实践方案OPTION
函数式编程在业务代码中的场景KMS
密码管理的功能开发指导建议Swagger
接口文档Makefile
项目构建
-
容器
CI/CD
服务部署:- 包含
mysql-db
、go-app
、nginx-web
- 使用
Docker-compose
进行容器部署 Container
容器之间网络端口相互访问Nginx
反向代理配置SQL
数据库脚本导入Dockerfile Go
应用程序镜像构建
- 包含
代码详解
以下示例代码为了不占过多的无用篇幅,均进行简化,比如错误处理、硬编码等,实际内容以代码仓源码为准。
接口路由 /interfaces/adapter
- 首先,无论是什么代码,当然先浏览的是
main.go
文件了:
go
func main() {
initialize.App()
}
- 点击进入
App()
方法,一探究竟:
go
func App() {
// 注册服务
serviceRegister() // DDD 的核心就是这个,这里先欠着大家,后面再讲...
// 启动WF,注册路由
routeEngine().Run(":8080")
}
- 继续点击进入
routeEngine()
方法:
go
func routeEngine() *gin.Engine {
r := gin.New()
r.Use(...各种middlewares)
router.ApiRouter(r) // 这里就是注册的所有子路由了
return r
}
- 点击进入
ApiRouter()
方法,下面我们会挑选其中两个具有代表性的endpoint
进行详细说明:
go
func ApiRouter(r *gin.Engine) {
sysGroup := r.Group("/api/v1/sys")
{
sysGroup.POST("/login", controller.APIs.Sys.Login) // [POST] /api/v1/sys/login - 用户登录接口,使用username&password的请求表单用来换取access_token,该接口无需认证
}
userGroup := r.Group("/api/v1/user")
{
userGroup.GET("/info", controller.APIs.User.Userinfo) // [GET] /api/v1/user/info - 获取登录用户信息接口,该接口需要Bearer JWT token进行身份认证
}
}
控制器 /interfaces/controller
- 用户登录接口
/interfaces/controller/sys/login.go
go
import (
"server/application/service/user"
dto "server/interfaces/dto/sys" // 我们可以看到 dto 其实就是前端 form、json 接收与响应 struct 结构体
)
type EndpointCtl struct {
Srv user.Service // 这里拿到了应用服务的接口
}
func (ctl *EndpointCtl) Login(c *gin.Context) {
// 1. 通过dto进行前端表单验证与解析处理
var loginData dto.LoginData
_ = c.ShouldBind(&loginData)
// 2. 将前端获取的Username和Password的值,传递给应用服务的LocalLogin接口进行处理
userToken, _ := ctl.Srv.LocalLogin(loginData.Username, loginData.Password)
// 3. 将处理完成的结果,返回给前端,这里依然通过dto进行处理
c.JSON(200, dto.TokenResp{
AccessToken: userToken,
})
}
应用服务 /application/service
- 用户本地登录服务
/application/service/user/application_service.go
go
import (
"server/domain/user/entity"
"server/domain/user/service"
"server/infrastructure/common/jwt"
)
// 应用服务接口定义
type Service interface {
LocalLogin(username, password string) (string, error)
}
// 应用服务实现
type ServiceImpl struct {
ud service.UserDomain
}
func NewServiceImpl(srv service.UserDomain) Service {
return &ServiceImpl{ud: srv}
}
// 应用服务LocalLogin接口实现
func (srv *ServiceImpl) LocalLogin(username, password string) (string, error) {
// 1. 通过函数选项来简化 "不确定条件" 的代码,这里可以获取到用户对象
user, _ := srv.ud.GetUserWithOpts(
entity.WithUsername(username),
entity.WithPassword(password),
)
// 2. 调用infra层的jwt相关方法
token, _ := jwt.CreateJwtToken(jwt.TokenData{
LoginUser: user.Name,
UserID: user.ID,
AccessToken: user.UUID.String(),
})
// 3. 返回获取的jwtToken值
return token, nil
}
领域服务 /domain/service
- 用户领域服务
/domain/user/service/domain_service.go
go
import (
"server/domain/user/entity"
"server/domain/user/repository"
)
// 领域服务接口定义
type UserDomain interface {
GetUserWithOpts(opts ...entity.UserOpt) (*entity.User, error)
}
// 领域服务实现
type UserDomainImpl struct {
ur repository.UserRepository
}
func NewUserDomainImpl(repo repository.UserRepository) UserDomain {
return &UserDomainImpl{ur: repo}
}
func (ud *UserDomainImpl) GetUserWithOpts(opts ...entity.UserOpt) (*entity.User, error) {
// 应用option模式
user := new(entity.User)
entity.UserOpts(opts).Apply(user)
// 判断相关字段是否不为零值
hasFieldName := user.Name != ""
hasFieldUsername := user.Username != ""
hasFieldPassword := user.Password != ""
hasFieldEmployeeID := user.EmployeeID != 0
hasFieldEmail := user.Email != ""
// 通过字段是否有值,来选择执行的具体的仓储接口
switch {
case hasFieldName:
return ud.ur.FindByName(user.Name)
case hasFieldUsername:
if hasFieldPassword {
// 由上可知,本次应该走这个接口
return ud.ur.FindByUsernameAndPassword(user.Username, user.Password)
}
return ud.ur.FindByUsername(user.Username)
case hasFieldEmployeeID:
return ud.ur.FindByEmpID(user.EmployeeID)
case hasFieldEmail:
return ud.ur.FindByEmail(user.Email)
default:
return ud.ur.GetUser(int64(user.ID))
}
}
领域仓储 /domain/repository
- 领域仓储接口定义
/domain/user/repository/user.go
go
import (
"server/domain/user/entity"
)
// 用户仓储接口定义(domain层)
type UserRepository interface {
FindByUsernameAndPassword(username, password string) (*entity.User, error)
}
仓储实现 /infrastructure/persistence
- 基础设施持久化仓储
/infrastructure/persistence/user/user_repo.go
go
import (
"server/domain/user/entity"
"server/domain/user/repository"
"server/infrastructure/persistence/user/converter"
"server/infrastructure/persistence/user/po
"gorm.io/gorm"
)
// 用户仓储接口实现(infra层)
type UserRepositoryImpl struct {
db *gorm.DB
}
func NewUserRepository(db *gorm.DB) repository.UserRepository {
return &UserRepositoryImpl{db: db}
}
// 实现通过用户名和密码查找用户条目对象的接口
func (ur *UserRepositoryImpl) FindByUsernameAndPassword(username, password string) (*entity.User, error) {
// 1. 初始化PO用户对象
user := &po.User{}
// 2. ORM查询数据库操作
result := ur.db.Preload("Roles").Where("username = ? AND password = ?", username, password).First(user)
// 3. 将PO转换成领域Entity实体对象
entityUser := converter.UserPOToEntity(user)
// 4. 返回Entity用户对象
return entityUser, nil
}
初始化服务
到这里,细心的同学可能会有疑问,谁去调用上面这个 NewUserRepository(db *gorm.DB)
工厂函数呢? 大家肯定还没有忘记,在最开始的时候,还欠着大家一个注册服务的 serviceRegister()
函数,那么我们现在来一起看下:
go
func serviceRegister() {
// 从环境变量读取db配置信息
dbConfig := os.Getenv("DB_CONFIG")
// 初始化数据库实例
db := database.InitDB(dbConfig)
// 注册用户仓储服务
ur := persistence.NewUserRepository(db)
// 注册用户领域服务
ud := userDomainSrv.NewUserDomainImpl(ur)
// 注册用户应用服务
us := userAppSrv.NewServiceImpl()
// 注入控制器中
controller.InitSrvInject(us)
}
这其实就是所谓的依赖倒置的设计思想和依赖注入实现方式,而 DDD
的实现精髓也就在于此。
由于笔者比较懒 🥱,本想讲解两个接口,但码字实在是太累了,就先到这里吧,剩下的放到代码仓了。
更详细的目录树、环境部署(Dokcer
手动/Docker-compose
自动)、功能扩展相关建议,请查看 README.md 相关内容。
写在后面
通过上面的介绍,现在,再回头看 DDD
是不是觉得明朗许多了呢?其实,DDD
并不是一种神秘或者高大上的东西,而是一种实用的方法论。
它能够提供可维护性,解决复杂业务逻辑的同时也带来了极大的挑战。在实际开发中,我们需要具体情况而定,不要一味指望它能成为代码的银弹!
参考
programmingpercy.tech/blog/how-to...
betterprogramming.pub/the-clean-a...
www.andrewgordon.me/posts/Clean...