第91课 - 整洁架构实践
学习目标
- 理解整洁架构(Clean Architecture)的核心理念:掌握其分层思想和依赖规则。
- 掌握Go语言中实现整洁架构的典型分层结构:能够将业务逻辑与框架、数据库等外部依赖解耦。
- 实践依赖反转原则:通过接口定义端口,在用例层和适配器层之间建立清晰的边界。
- 编写易于测试的业务逻辑代码:理解整洁架构如何为单元测试和集成测试提供天然便利。
核心概念
想象一下,你正在建造一座城堡。城墙(核心业务逻辑)是城堡最关键、最稳固的部分,不应因为护城河(数据库)干涸或瞭望塔(Web框架)更换而轻易改动。整洁架构的核心就是构建这样一座“城堡”,让业务逻辑位于最核心、最稳定的位置,而外部工具(数据库、Web框架、消息队列等)则处于外层,可以灵活替换,且不影响内核。
其依赖规则(也称为“依赖反转”)可以用一句话概括:源码依赖方向必须从外向内,指向核心。 核心的业务逻辑不应该知道外层任何具体细节(如用的是MySQL还是PostgreSQL,是用Gin还是Echo框架)。
在Go中,一个典型的整洁架构(或类似变体,如端口与适配器/六边形架构)项目结构如下:
myapp/
├── domain/ # 最核心:实体和业务规则
│ ├── entity/ # 领域实体(如 User, Order)
│ └── repository/ # 端口(Port):定义核心层需要的接口
├── usecase/ # 应用业务规则(用例)
│ └── user/ # 具体的用户相关用例
├── adapter/ # 适配器(Adapter)层
│ ├── persistence/# 持久化适配器(如数据库实现)
│ ├── delivery/ # 交付机制适配器(如HTTP处理器,gRPC服务)
│ └── external/ # 外部服务适配器(如第三方API客户端)
├── infrastructure/ # 框架和驱动细节(如数据库连接,配置)
└── main.go # 组装点,进行依赖注入
关键角色:
- Entity:最内层的对象,包含业务数据和基本规则(如
User实体)。 - Use Case:编排实体以完成特定业务操作(如
CreateUser用例)。它通过接口(端口)调用外部。 - Port:用例层定义的接口,描述它需要外部世界提供什么能力(如
UserRepository接口)。 - Adapter:实现端口接口的具体技术细节(如用GORM实现
UserRepository,用Gin实现HTTP处理器)。
代码示例
让我们用一个简单的“用户管理系统”来演示。核心功能:创建用户。
1. 领域层 (domain/)
// domain/entity/user.go
package entity
// User 是最核心的领域实体,不包含任何技术细节。
type User struct {
ID string
Name string
Email string
}
// domain/repository/user_repository.go
package repository
import "go-clean-arch/domain/entity"
// UserRepository 是一个端口(Port)。它定义了核心业务逻辑需要的持久化能力。
// 具体的数据库实现(如MySQL、PostgreSQL)是适配器(Adapter),需要实现此接口。
type UserRepository interface {
Create(user *entity.User) error
FindByID(id string) (*entity.User, error)
}
2. 用例层 (usecase/)
// usecase/user/create.go
package user
import (
"go-clean-arch/domain/entity"
"go-clean-arch/domain/repository"
)
// CreateUserUseCase 封装了“创建用户”这个业务操作。
// 它只依赖于领域实体和端口接口,与数据库、Web框架完全无关。
type CreateUserUseCase struct {
userRepo repository.UserRepository // 依赖于接口,而非具体实现
}
// NewCreateUserUseCase 是构造函数。
func NewCreateUserUseCase(repo repository.UserRepository) *CreateUserUseCase {
return &CreateUserUseCase{userRepo: repo}
}
// Execute 执行用例。这里可以包含业务验证、逻辑编排等。
func (uc *CreateUserUseCase) Execute(name, email string) (*entity.User, error) {
// 可以在这里添加业务规则验证,例如邮箱格式检查
if email == "" {
return nil, ErrInvalidEmail
}
newUser := &entity.User{
ID: generateID(), // 一个简单的ID生成函数
Name: name,
Email: email,
}
// 调用端口(仓库)来持久化用户。用例不关心这是存到了MySQL还是内存里。
if err := uc.userRepo.Create(newUser); err != nil {
return nil, err
}
return newUser, nil
}
// 自定义业务错误
var ErrInvalidEmail = fmt.Errorf("email cannot be empty")
func generateID() string {
// 简化示例,实际可用UUID等
return "user_123"
}
3. 适配器层 (adapter/)
// adapter/persistence/in_memory_user_repository.go
package persistence
import (
"fmt"
"sync"
"go-clean-arch/domain/entity"
)
// InMemoryUserRepository 是一个适配器,它实现了 domain/repository.UserRepository 接口。
// 它使用内存存储,非常适合单元测试和原型开发。
type InMemoryUserRepository struct {
mu sync.RWMutex
users map[string]*entity.User
}
func NewInMemoryUserRepository() *InMemoryUserRepository {
return &InMemoryUserRepository{
users: make(map[string]*entity.User),
}
}
// Create 实现了UserRepository接口的Create方法。
func (r *InMemoryUserRepository) Create(user *entity.User) error {
r.mu.Lock()
defer r.mu.Unlock()
if _, exists := r.users[user.ID]; exists {
return fmt.Errorf("user with ID %s already exists", user.ID)
}
r.users[user.ID] = user
return nil
}
// FindByID 实现了UserRepository接口的FindByID方法。
func (r *InMemoryUserRepository) FindByID(id string) (*entity.User, error) {
r.mu.RLock()
defer r.mu.RUnlock()
user, ok := r.users[id]
if !ok {
return nil, fmt.Errorf("user not found")
}
return user, nil
}
// adapter/delivery/http/user_handler.go
package http
import (
"encoding/json"
"net/http"
"go-clean-arch/usecase/user"
)
// UserHandler 是HTTP适配器。它依赖于用例层。
type UserHandler struct {
createUserUC *user.CreateUserUseCase
}
func NewUserHandler(uc *user.CreateUserUseCase) *UserHandler {
return &UserHandler{createUserUC: uc}
}
// HandleCreateUser 处理HTTP POST /users 请求。
func (h *UserHandler) HandleCreateUser(w http.ResponseWriter, r *http.Request) {
var req struct {
Name string `json:"name"`
Email string `json:"email"`
}
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "invalid request", http.StatusBadRequest)
return
}
// 调用用例层。HTTP处理器不包含任何业务逻辑。
newUser, err := h.createUserUC.Execute(req.Name, req.Email)
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(newUser)
}
4. 组装点 (main.go)
// main.go
package main
import (
"log"
"net/http"
httpAdapter "go-clean-arch/adapter/delivery/http"
"go-clean-arch/adapter/persistence"
"go-clean-arch/usecase/user"
)
func main() {
// 1. 创建适配器(这里用内存实现)
userRepo := persistence.NewInMemoryUserRepository()
// 2. 创建用例,并注入仓库适配器(依赖注入)
createUserUC := user.NewCreateUserUseCase(userRepo)
// 3. 创建HTTP处理器,并注入用例
userHandler := httpAdapter.NewUserHandler(createUserUC)
// 4. 设置路由
http.HandleFunc("/users", userHandler.HandleCreateUser)
log.Println("Server starting on :8080...")
log.Fatal(http.ListenAndServe(":8080", nil))
}
实践练习
练习1:基础扩展
基于示例代码,为 UserRepository 端口添加一个新的方法 FindAll() ([]*entity.User, error)。然后在 InMemoryUserRepository 中实现它。最后,创建一个新的 GetAllUsersUseCase 用例,并在HTTP处理器中添加一个 GET /users 端点。
预期输出:调用 GET /users 应能返回所有已创建的用户列表。
练习2:适配器替换
将示例中的 InMemoryUserRepository 替换为一个基于文件的实现 FileUserRepository。该实现应将用户数据存储为JSON文件(例如 users.json)。保持用例层和HTTP处理器代码不变。
要求:
- 创建
adapter/persistence/file_user_repository.go。 - 实现
Create和FindByID方法(FindAll可选)。 - 在
main.go中简单替换userRepo的实例化代码即可完成切换。
练习3:引入依赖注入工具(进阶)
使用上一节课学习的 wire 工具,将 main.go 中的手动依赖注入自动化。你需要为 InMemoryUserRepository、CreateUserUseCase 和 UserHandler 编写 wire 的 Provider 和 Injector。
常见错误
- 违反依赖规则:在
domain或usecase包中导入了adapter或infrastructure的包(如导入gorm或gin)。这是最严重的错误,破坏了架构的核心价值。 - 过度设计:对于非常简单的应用(如一个CLI工具或微型服务),强制套用完整的整洁架构层次可能导致不必要的复杂性。理解原则比机械套用结构更重要。
- 忽略测试优势:整洁架构的最大好处之一是易于测试。初学者常常不编写针对用例层的单元测试。通过为用例编写测试(使用
InMemoryUserRepository作为模拟实现),可以验证核心业务逻辑的正确性。 - 错误处理不当:适配器层(如HTTP处理器)应该将技术性错误(如“数据库连接失败”)转换为对用户友好的错误信息,并避免将核心层的错误细节(如SQL查询)暴露给最终用户。
小结
本节课我们学习了如何将整洁架构的理念应用于Go项目:
- 核心是依赖规则:让业务逻辑位于最内层,不依赖任何外部框架或工具。
- 通过接口(端口)进行解耦:用例层定义它需要什么,适配器层提供具体实现。
- 分层结构清晰:
domain->usecase->adapter,每一层都有明确的职责。 - 为可维护性和可测试性奠基:更换数据库或Web框架变得容易,核心业务逻辑可以通过模拟对象进行充分测试。
整洁架构不是银弹,但它为我们构建复杂、长期维护的Go应用程序提供了一个强大而清晰的蓝图。掌握它,将帮助你写出更健壮、更灵活的代码。在下一课中,我们将学习如何使用Docker Compose来编排这种分层架构下的多服务应用。