← 返回博客
go2026-09-12 22:13:151 分钟 · 389 0

DDD 分层落地:用 Gin 写出领域模型

把贫血的三层(handler/service/repo)升级成 DDD 分层,讲清 entity、聚合、领域服务、仓储接口怎么映射到 Gin 项目,业务规则沉到领域层。

#gin#DDD#领域驱动

1. 三层的问题

第 5 篇的三层(controller/service/repo)容易写成贫血模型:service 里全是 getter/setter 搬运,业务规则散落各处,改一个规则要翻好几个文件。DDD 把规则收进领域对象。

2. 实体和值对象

领域对象自己带行为,不是纯数据:

type Order struct {
    ID     string
    Items  []OrderItem
    Status OrderStatus
}

func (o *Order) Total() int {
    sum := 0
    for _, it := range o.Items {
        sum += it.Price * it.Qty
    }
    return sum
}

func (o *Order) Pay() error {
    if o.Status != Pending {
        return ErrAlreadyPaid
    }
    o.Status = Paid
    return nil
}

TotalPay 是订单自己的事,放订单上,谁改金额逻辑都不会漏。

3. 聚合根

一组对象当整体一致性边界,只暴露聚合根。订单和订单项是聚合,外部通过 OrderItem,不能直接改 item:

func (o *Order) AddItem(p int, q int) error {
    if o.Status != Pending {
        return ErrLocked
    }
    o.Items = append(o.Items, OrderItem{Price: p, Qty: q})
    return nil
}

4. 领域服务

跨多个实体的规则放领域服务,不属于某一个实体:

type PricingService struct{ rules []DiscountRule }

func (s *PricingService) Apply(o *Order) {
    for _, r := range s.rules {
        r.Apply(o)
    }
}

5. 仓储接口

仓储只管持久化,定义在领域层,实现在基础设施层(第 5 篇的 repo 就是):

type OrderRepo interface {
    Save(*Order) error
    FindByID(string) (*Order, error)
}

Gin handler 通过构造函数注入 OrderRepo 实现,领域层不依赖具体数据库。

6. 映射到 Gin

func (h *OrderHandler) Create(c *gin.Context) {
    o := h.repo.Find(id)
    o.AddItem(price, qty)
    o.Pay()
    h.repo.Save(o) // 领域行为先跑,最后才落库
    OK(c, o)
}

handler 薄,领域对象厚,业务规则都在 Order 里可测。

7. 上线清单

不是所有项目都上 DDD,简单 CRUD 用三层够了。聚合根是一致性边界,外部只通过它改内部。领域层不依赖框架和数据库,规则可单测。仓储接口定义在领域层、实现在外层。别为了 DDD 把简单东西搞复杂。

下一篇讲分布式事务,跨服务的数据怎么一致。

相关推荐

本文为原创文章,采用CC BY-NC-SA 4.0协议授权,转载请保留署名与原文链接。原文链接:https://www.wxbuluo.com/article/202