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

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

上一篇文章简单讨论了领域层在Kratos中的使用,主要涉及引入领域层,将数据层和业务层之间的解耦,接下来讨论一个稍微全面一点的例子,在此基础上引入外部Api(主要是易变部分)的领域层下的情况。

我们同样可以通过依赖倒置适配器模式实现统一治理:


一、升级后的领域层架构

internal/
├── domain(biz)                  # 核心领域层
│   ├── user.go             # 用户聚合根
│   ├── payment.go          # 支付领域服务接口
│   ├── repository.go       # 数据仓储接口
│   └── gateway.go          # 新增:外部服务网关接口
├── service                 # 应用服务层
│   └── order_service.go    # 组合领域能力
├── data                    # 数据层实现
│   ├── user_repo.go        # 实现domain.Repository
│   └── mysql/              # 数据库相关
└── infra                   # 基础设施层
    ├── payment             # 支付网关实现
    │   ├── stripe_adapter.go  # 实现domain.PaymentGateway
    │   └── mock_adapter.go    # 测试用Mock
    └── api                  # 其他API适配器
        └── sms_client.go     # 实现domain.Notifier

二、关键组件设计

1. 定义外部服务网关接口(领域层)
// domain/gateway.go
type PaymentGateway interface {
    Charge(amount Money) (TransactionID, error)
    QueryStatus(txID TransactionID) (PaymentStatus, error)
}

type Notifier interface {
    SendSMS(phone PhoneNumber, msg string) error
}
2. 实现API适配器(基础设施层)
// infra/payment/stripe_adapter.go
type StripeAdapter struct {
    client *stripe.Client
    config StripeConfig
}

func (s *StripeAdapter) Charge(amount domain.Money) (domain.TransactionID, error) {
    // 转换领域模型→API协议
    params := &stripe.ChargeParams{
        Amount:   toStripeAmount(amount),
        Currency: s.config.Currency,
    }
    
    // 调用易变API
    charge, err := s.client.Charge(params)
    if err != nil {
        return "", domain.NewPaymentError(err)
    }
    
    return domain.TransactionID(charge.ID), nil
}
3. 业务逻辑调用(领域服务)
// domain/payment.go
type PaymentService struct {
    gateway PaymentGateway
    repo    PaymentRepository
}

func (s *PaymentService) Process(order *Order) error {
    // 使用网关接口(不感知具体实现)
    txID, err := s.gateway.Charge(order.Total)
    if err != nil {
        return fmt.Errorf("支付失败: %w", err)
    }
    
    order.TransactionID = txID
    return s.repo.Save(order)
}

三、应对API变更的防御措施

1. 版本化适配器
infra/payment/
├── v1/
│   └── stripe_adapter.go   # 旧版API实现
└── v2/
    └── stripe_adapter.go   # 新版API实现

通过工厂模式切换版本:

func NewStripeGateway(version string) domain.PaymentGateway {
    switch version {
    case "v2":
        return v2.NewAdapter()
    default:
        return v1.NewAdapter()
    }
}
2. 动态字段映射
// infra/payment/field_mapper.go
type FieldMapper struct {
    mappings map[string]FieldRule
}

func (m *FieldMapper) Transform(req interface{}) map[string]interface{} {
    // 根据配置转换字段名和格式
    return applyMappings(req, m.mappings)
}

// 配置示例(可热更新)
var stripeMappings = map[string]FieldRule{
    "amount": {
        Source: "total_cents", // API字段名
        Convert: func(v any) any {
            return v.(int) * 100 // 单位转换
        },
    },
}
3. 断路器模式
// infra/payment/circuit_breaker.go
type CircuitBreakerGateway struct {
    gateway domain.PaymentGateway
    breaker *gobreaker.CircuitBreaker
}

func (c *CircuitBreakerGateway) Charge(amount domain.Money) (domain.TransactionID, error) {
    resp, err := c.breaker.Execute(func() (interface{}, error) {
        return c.gateway.Charge(amount)
    })
    if err != nil {
        return "", err
    }
    return resp.(domain.TransactionID), nil
}

四、开发流程优化

1. 并行开发模式
Phase 1: 协议设计
  领域工程师 ───┐
               ├─ 定义领域接口
  API工程师 ───┘

Phase 2: 实现
  领域工程师 ───► 编写领域逻辑测试
  API工程师 ───► 实现网关适配器

Phase 3: 集成
  测试工程师 ───► 验证契约一致性
2. 测试策略
// 契约测试(验证适配器符合领域接口)
func TestStripeAdapter_Contract(t *testing.T) {
    adapter := infra.NewStripeAdapter()
    testify.Implements(t, (*domain.PaymentGateway)(nil), adapter)
}

// 黄金文件测试(捕获API变更)
func TestStripeResponses_GoldenFiles(t *testing.T) {
    testCases := []struct{
        Name     string
        Response string // 保存的API响应样例
    }{
        {"success", "testdata/stripe_success.json"},
    }
    
    for _, tc := range testCases {
        t.Run(tc.Name, func(t *testing.T) {
            golden := loadGoldenFile(tc.Response)
            result := parseAPIResponse(golden)
            
            // 对比结构变化
            assert.Equal(t, golden, toJSON(result))
        })
    }
}

五、架构优势验证

变更影响对比
变更类型 传统方式影响范围 领域层方案影响范围
API路径变更 修改所有调用点 修改单个适配器
字段名变更 全局搜索替换 更新字段映射配置
响应结构变更 重写解析逻辑 修改响应转换器
新增错误码 需要修改业务逻辑 在适配器层统一处理
量化收益
  • 开发效率:初期Mock使联调等待时间↓80%
  • 测试稳定性:核心业务测试不受API变更影响
  • 维护成本:API变更处理时间↓90%

六、实施示例

1. 依赖注入配置
// wire.go
func initOrderService() *service.OrderService {
    wire.Build(
        // 领域服务
        domain.NewPaymentService,
        
        // 基础设施实现
        infra.NewStripeGateway,
        data.NewUserRepository,
        
        // 应用服务
        service.NewOrderService,
    )
    return &service.OrderService{}
}
2. 动态切换实现
// 根据环境选择适配器
func getPaymentGateway() domain.PaymentGateway {
    if config.IsTest() {
        return infra.NewMockGateway()
    }
    return infra.NewStripeGateway(config.API.Version)
}
3. 错误处理统一化
// infra/payment/error_adapter.go
func wrapAPIError(err error) error {
    if apiErr, ok := err.(*stripe.Error); ok {
        switch apiErr.Code {
        case stripe.ErrorCodeRateLimit:
            return domain.ErrRateLimited
        default:
            return domain.ErrPaymentFailed.WithCause(err)
        }
    }
    return err
}

通过这种设计,领域层成为抵御外部变更的稳定层,外部API的频繁变更被限制在基础设施层内,核心业务逻辑始终保持干净、可测试的状态。

下一篇讨论在设计数据定义和API的请求响应,是否应该自定义领域层模型(而不是第三方或proto文件生成的pb.Model或者pb.Request、pb.Response) Golang Kratos 系列:领域层model定义是自洽还是直接依赖第三方(三)


网站公告

今日签到

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