91·工程实践高级

整洁架构实践

clean-archhexagonaldomainrepository

第91课 - 整洁架构实践

学习目标

  1. 理解整洁架构(Clean Architecture)的核心理念:掌握其分层思想和依赖规则。
  2. 掌握Go语言中实现整洁架构的典型分层结构:能够将业务逻辑与框架、数据库等外部依赖解耦。
  3. 实践依赖反转原则:通过接口定义端口,在用例层和适配器层之间建立清晰的边界。
  4. 编写易于测试的业务逻辑代码:理解整洁架构如何为单元测试和集成测试提供天然便利。

核心概念

想象一下,你正在建造一座城堡。城墙(核心业务逻辑)是城堡最关键、最稳固的部分,不应因为护城河(数据库)干涸或瞭望塔(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处理器代码不变。

要求

  1. 创建 adapter/persistence/file_user_repository.go
  2. 实现 CreateFindByID 方法(FindAll 可选)。
  3. main.go 中简单替换 userRepo 的实例化代码即可完成切换。

练习3:引入依赖注入工具(进阶)

使用上一节课学习的 wire 工具,将 main.go 中的手动依赖注入自动化。你需要为 InMemoryUserRepositoryCreateUserUseCaseUserHandler 编写 wire 的 Provider 和 Injector。

常见错误

  1. 违反依赖规则:在 domainusecase 包中导入了 adapterinfrastructure 的包(如导入 gormgin)。这是最严重的错误,破坏了架构的核心价值。
  2. 过度设计:对于非常简单的应用(如一个CLI工具或微型服务),强制套用完整的整洁架构层次可能导致不必要的复杂性。理解原则比机械套用结构更重要。
  3. 忽略测试优势:整洁架构的最大好处之一是易于测试。初学者常常不编写针对用例层的单元测试。通过为用例编写测试(使用 InMemoryUserRepository 作为模拟实现),可以验证核心业务逻辑的正确性。
  4. 错误处理不当:适配器层(如HTTP处理器)应该将技术性错误(如“数据库连接失败”)转换为对用户友好的错误信息,并避免将核心层的错误细节(如SQL查询)暴露给最终用户。

小结

本节课我们学习了如何将整洁架构的理念应用于Go项目:

  • 核心是依赖规则:让业务逻辑位于最内层,不依赖任何外部框架或工具。
  • 通过接口(端口)进行解耦:用例层定义它需要什么,适配器层提供具体实现。
  • 分层结构清晰domain -> usecase -> adapter,每一层都有明确的职责。
  • 为可维护性和可测试性奠基:更换数据库或Web框架变得容易,核心业务逻辑可以通过模拟对象进行充分测试。

整洁架构不是银弹,但它为我们构建复杂、长期维护的Go应用程序提供了一个强大而清晰的蓝图。掌握它,将帮助你写出更健壮、更灵活的代码。在下一课中,我们将学习如何使用Docker Compose来编排这种分层架构下的多服务应用。

练习编辑器

go
Loading...

继续学习

完成本课后,建议继续学习下一课「Docker Compose 编排」 以巩固所学知识。