上一篇文章简单讨论了领域层在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定义是自洽还是直接依赖第三方(三)