Golang Kratos 系列:业务分层的若干思考(一)

发布于:2025-06-25 ⋅ 阅读:(20) ⋅ 点赞:(0)

在使用 Kratos 框架开发云服务的过程中,渐渐理解和感受到“领域层”这个概念和抽象的强大之处,它可以将业务和存储细节解耦、将业务和开发初期频繁变更的API结构,让Mock单元测试变得更加容易、对细节的变化更鲁棒。让业务代码摆脱技术细节依赖,使系统变更成本与业务复杂度解耦,领域层将领域层的放置位置应当遵循 “靠近使用者” 原则。以下是常见的分层方案和决策依据:


一、推荐架构(领域层独立)

.
├── internal
│   ├── domain   # 核心领域层(独立包)
│   │   ├── user.go        # 聚合根/值对象定义
│   │   ├── repository.go  # 仓储接口
│   │   └── service.go     # 领域服务  
│   │
│   ├── service  # 应用服务层
│   │   └── user.go        # 依赖domain包
│   │
│   └── data     # 数据层
│       └── user_repo.go   # 实现domain.Repository

二、分层决策依据

方案 优点 缺点 适用场景
领域层独立 1. 双向解耦
2. 明确分层界限
1. 多一个包目录
2. 需严格依赖管理
中大型项目
复杂业务逻辑
领域层在service 1. 减少包数量
2. service直接使用
1. data层需反向依赖service
2. 易产生循环引用
小型项目
快速原型开发
领域层在data 1. 数据层自包含 1. service层被迫依赖data
2. 破坏分层架构
不推荐

三、具体实施示例

1. 独立领域层(推荐)
// internal/domain/user.go
package domain

type User struct {
    ID   UserID
    Name string
}

type UserRepository interface {
    FindByID(context.Context, UserID) (*User, error)
}

// internal/service/user.go
package service

type UserService struct {
    repo domain.UserRepository // 依赖抽象
}

// internal/data/user_repo.go
package data

type userRepo struct {
    db *gorm.DB
}

func (r *userRepo) FindByID(ctx context.Context, id domain.UserID) (*domain.User, error) {
    // 实现domain.Repository
}
2. 领域层在service(备选)
// internal/service/domain/user.go
package service

type User struct {
    ID   UserID
    Name string
}

type UserRepository interface {
    FindByID(context.Context, UserID) (*User, error)
}

// internal/data/user_repo.go
package data

import "project/internal/service"

type userRepo struct {
    db *gorm.DB
}

func (r *userRepo) FindByID(ctx context.Context, id service.UserID) (*service.User, error) {
    // 需要依赖service包
}

四、关键设计原则

  1. 依赖方向不可逆

    高层模块(service) → 领域抽象(domain) ← 实现细节(data)
    
    • 永远保持 data 依赖 domain,而不是相反
    • 禁止 domain 导入 dataservice 的任何代码
  2. 接口所有权

    • 领域接口(如 Repository)由 领域层定义
    • data 层仅负责实现这些接口
  3. 类型系统控制

    // 正确定义(领域层拥有类型)
    type UserID string
    
    // 错误定义(data层定义类型)
    type User struct {
        gorm.Model // 基础设施细节泄露
    }
    
  4. 依赖注入管理

    // wire.go 中明确依赖关系
    func newService(repo domain.UserRepository) *service.UserService {
        return &service.UserService{repo: repo}
    }
    

五、Kratos 特化建议

1. 项目结构优化
.
├── api              # proto定义
├── internal
│   ├── biz          # 领域层(原domain)
│   │   ├── user.go
│   │   └── repo.go  # 仓储接口
│   ├── service      # 应用服务
│   └── data         # 数据实现
└── pkg             # 通用领域组件
2. 接口定义示例
// internal/biz/repo.go
package biz

type UserRepo interface {
    Save(context.Context, *User) error
    FindByID(context.Context, int64) (*User, error)
}

// internal/data/user.go
package data

type userRepo struct {
    data *Data
}

func (r *userRepo) FindByID(ctx context.Context, id int64) (*biz.User, error) {
    // 转换data -> biz
}
3. Wire 注入配置
// internal/data/data.go
func NewData() (*Data, func(), error) {
    // 初始化数据库等
}

// internal/data/user.go
func NewUserRepo(data *Data) biz.UserRepo {
    return &userRepo{data: data}
}

// internal/service/service.go
func NewService(repo biz.UserRepo) *Service {
    return &Service{repo: repo}
}

六、何时打破原则

在以下场景可考虑将领域模型放在 service 层:

  1. 原型验证阶段:快速迭代时减少目录切换
  2. 简单CRUD服务:无复杂业务规则
  3. 独立微服务:服务边界即领域边界

但需保证:

  • data 层不包含业务逻辑
  • 随时可以重构为独立领域层

七、架构验证检查表

  1. 编译时验证

    # 确保data不依赖service
    grep -r "import.*service" internal/data/
    
  2. 循环依赖检测

    go mod graph | awk '{print $1}' | sort | uniq -c | sort -nr
    
  3. 接口所有权检查

    # 确认Repository接口定义在domain/biz
    ls internal/domain/repo.go  
    
  4. 类型定义位置

    # 核心类型不应包含gorm标签
    grep -r "gorm:" internal/domain/
    

通过这种分层,您将获得:

  • 明确架构边界:各层职责清晰
  • 测试友好:可轻松Mock仓储实现
  • 演进自由:数据源切换不影响业务逻辑
  • 团队协作:领域专家可独立设计模型
  1. 附录
    Kratos 官方架构中与传统DDD概念的对应
Kratos 结构 传统DDD概念 核心职责
internal/biz 领域层 (Domain) 定义聚合根/实体/值对象/领域服务
internal/data 基础设施层 (Infra) 实现仓储接口、外部API适配
internal/service 应用层 (Application) 协调领域对象、处理业务流程

在下篇文章我们讨论在同时对容变API和数据层同时应如领域层隔离的案例。Golang Kratos 系列:业务分层的若干思考(二)


网站公告

今日签到

点亮在社区的每一天
去签到