基于DDD与Repository模式的带图博客系统实现疑问
DDD与Repository模式适配问题解答
需求回顾
- 支持博客(含图片)的创建、列表、查看
- 允许查看所有博客中的图片,且能单独更新图片标题
- 禁止单独增删改图片,仅能在博客上下文内操作图片的增删
当前实现的问题
你的实现不符合DDD与Repository模式规范,核心问题如下:
- 重复定义领域实体:在
blogpost和photo两个仓库中分别定义了Photo结构体,属性不一致(前者无BlogPostID,后者有),会导致领域模型混乱、数据不一致,维护成本极高。 - 错误将Photo设为聚合根:根据需求,图片不能被单独增删改,仅能依附于博客存在,说明
Photo是BlogPost聚合内的实体,而非独立的聚合根。给Photo单独设置Repository,等于默认它拥有独立的生命周期,违反了需求中的业务规则,也破坏了DDD的聚合边界。
修正方案
1. 统一领域实体
将BlogPost和Photo实体统一放在领域层(而非仓库层),确保模型唯一且一致:
// pkg/domain/blogpost.go package domain import "errors" type BlogPost struct { ID string Name string Author string Content string Photos []Photo } type Photo struct { ID string Caption string URL string BlogPostID string // 持久化用的外键,领域模型中通过BlogPost聚合关联 } // 给Photo添加修改标题的方法,封装业务规则(如标题非空校验) func (p *Photo) UpdateCaption(newCaption string) error { if newCaption == "" { return errors.New("图片标题不能为空") } p.Caption = newCaption return nil }
2. 仅保留BlogPost作为聚合根的Repository
BlogPost作为唯一聚合根,其Repository负责整个聚合的增改查,确保图片的增删操作只能在博客上下文内完成:
// pkg/repository/blogpost/repository.go package blogpost import ( "context" "your-project/pkg/domain" ) type Repository interface { Create(ctx context.Context, post domain.BlogPost) (domain.BlogPost, error) Update(ctx context.Context, post domain.BlogPost) (domain.BlogPost, error) List(ctx context.Context) ([]domain.BlogPost, error) GetByID(ctx context.Context, id string) (domain.BlogPost, error) GetPhotoByID(ctx context.Context, photoID string) (domain.Photo, error) // 用于查询图片所属博客 }
3. 处理图片的列表与标题更新需求
图片列表:用查询服务实现
因为图片列表是查询需求,不修改聚合状态,DDD允许查询模型与领域模型分离,可单独创建图片查询服务:
// pkg/repository/photo/query_service.go package photo import ( "context" "your-project/pkg/domain" ) // 注意:不要命名为Repository,避免误解为聚合根仓库 type QueryService interface { ListAll(ctx context.Context) ([]domain.Photo, error) ListByBlogPostID(ctx context.Context, postID string) ([]domain.Photo, error) }
图片标题更新:通过聚合根或领域服务实现
必须确保更新操作在博客聚合的边界内完成,避免直接操作图片:
// pkg/domain/service/blogpost_service.go package service import ( "context" "errors" "your-project/pkg/domain" "your-project/pkg/repository/blogpost" ) type BlogPostService struct { blogRepo blogpost.Repository } func NewBlogPostService(repo blogpost.Repository) *BlogPostService { return &BlogPostService{blogRepo: repo} } func (s *BlogPostService) UpdatePhotoCaption(ctx context.Context, photoID string, newCaption string) error { // 1. 查询图片所属的博客 photo, err := s.blogRepo.GetPhotoByID(ctx, photoID) if err != nil { return err } post, err := s.blogRepo.GetByID(ctx, photo.BlogPostID) if err != nil { return err } // 2. 在博客聚合内找到目标图片,调用其标题更新方法 updated := false for i := range post.Photos { if post.Photos[i].ID == photoID { err := post.Photos[i].UpdateCaption(newCaption) if err != nil { return err } updated = true break } } if !updated { return errors.New("未找到指定图片") } // 3. 更新整个博客聚合 _, err = s.blogRepo.Update(ctx, post) return err }
总结
你的初始实现违反了DDD聚合边界与Repository模式的核心规则,修正后通过统一领域实体、仅保留BlogPost为聚合根、用查询服务处理图片列表、通过领域服务+聚合根处理标题更新,既满足业务需求,又符合DDD规范。
内容的提问来源于stack exchange,提问作者Aaditi Jain
相关产品推荐
相关产品推荐

