You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

DDD服务与仓储设计咨询:博客系统场景下的实现困惑

嘿,我来帮你梳理下DDD视角下这个博客系统里服务和仓储的设计思路——刚好我之前做过类似的场景,应该能帮你理清头绪!

第一步:先明确聚合根的划分(DDD的核心基础)

在DDD里,聚合根是划分领域边界的关键,先搞清楚这几个实体的归属:

  • Post是聚合根:评论的生命周期完全依赖于Post(删除Post时评论必须跟着删除),所以评论应该作为Post聚合的子实体,不需要单独的聚合根。
  • 标签的两种情况:
    • 如果是全局可复用的标签(比如系统里的标签库,多篇Post可以共用同一个标签),那标签是独立的聚合根;
    • 如果只是Post专属的标签(比如每篇Post自己的标签,不与其他Post共享),那标签直接作为Post聚合的一部分即可。
      通常博客的标签是可复用的,所以我们按“标签是独立聚合根”来设计。
第二步:仓储层的设计(只聚焦聚合根)

DDD里仓储的职责是只负责聚合根的持久化,所以:

  • 只需要定义PostRepository和TagRepository接口(DDD推荐面向接口编程,具体实现交给ORM框架比如Doctrine),不需要单独的CommentRepository——因为评论属于Post聚合,跟着Post一起被查询和保存。
  • 仓储接口的核心方法:
    • PostRepositoryInterface:
      • findById(int $postId): Post:查询指定ID的Post聚合,必须包含其下的所有评论
      • save(Post $post): void:保存/更新整个Post聚合(包括新增、修改评论)
    • TagRepositoryInterface:
      • findByPostId(int $postId): array:查询与指定Post关联的所有标签实体
第三步:服务层的设计(应用服务做协调,领域逻辑在实体)

你的postService属于应用服务,它的职责是协调仓储和领域实体,处理业务流程,但核心业务逻辑要放在聚合根(Post实体)里:

  • 应用服务不直接处理业务规则,比如“添加评论”的逻辑应该在Post实体里,而不是服务层
  • postService->find($postId)的核心流程:
    1. 调用PostRepository获取完整的Post聚合(带评论)
    2. 调用TagRepository获取该Post关联的标签
    3. 把领域实体转换成DTO(数据传输对象)返回给控制器(避免直接把实体暴露给视图层,保证领域边界)
具体代码示例(伪代码)

仓储接口定义

// PostRepository 接口
interface PostRepositoryInterface {
    /**
     * @throws PostNotFoundException
     */
    public function findById(int $postId): Post;
    public function save(Post $post): void;
}

// TagRepository 接口
interface TagRepositoryInterface {
    public function findByPostId(int $postId): array;
}

Post聚合根(包含评论子实体)

class Post {
    private int $id;
    private string $title;
    private string $content;
    /** @var Comment[] */
    private array $comments = [];

    // 构造函数初始化Post的基础属性
    public function __construct(int $id, string $title, string $content) {
        $this->id = $id;
        $this->title = $title;
        $this->content = $content;
    }

    // 核心业务逻辑:添加评论(在实体里保证规则,比如内容不能为空)
    public function addComment(string $content, User $author): void {
        if (empty(trim($content))) {
            throw new InvalidArgumentException("Comment content cannot be empty");
        }
        $this->comments[] = new Comment($content, $author, new DateTimeImmutable());
    }

    // 获取评论列表(对外只读)
    public function getComments(): array {
        return $this->comments;
    }

    // 其他getter方法
    public function getId(): int { return $this->id; }
    public function getTitle(): string { return $this->title; }
    public function getContent(): string { return $this->content; }
}

class Comment {
    private string $content;
    private User $author;
    private DateTimeImmutable $createdAt;

    public function __construct(string $content, User $author, DateTimeImmutable $createdAt) {
        $this->content = $content;
        $this->author = $author;
        $this->createdAt = $createdAt;
    }

    // getter方法
    public function getContent(): string { return $this->content; }
    public function getAuthor(): User { return $this->author; }
    public function getCreatedAt(): DateTimeImmutable { return $this->createdAt; }
}

应用服务PostService

class PostService {
    public function __construct(
        private PostRepositoryInterface $postRepository,
        private TagRepositoryInterface $tagRepository
    ) {}

    /**
     * @throws PostNotFoundException
     */
    public function getPostDetail(int $postId): array {
        // 1. 获取Post聚合(包含评论)
        $post = $this->postRepository->findById($postId);

        // 2. 获取关联标签
        $tags = $this->tagRepository->findByPostId($postId);

        // 3. 组装DTO返回(避免直接返回实体,隔离领域层和表现层)
        return [
            'post' => [
                'id' => $post->getId(),
                'title' => $post->getTitle(),
                'content' => $post->getContent(),
                'comments' => array_map(function(Comment $comment) {
                    return [
                        'content' => $comment->getContent(),
                        'author_name' => $comment->getAuthor()->getName(),
                        'created_at' => $comment->getCreatedAt()->format('Y-m-d H:i:s')
                    ];
                }, $post->getComments())
            ],
            'tags' => array_map(function(Tag $tag) {
                return [
                    'id' => $tag->getId(),
                    'name' => $tag->getName()
                ];
            }, $tags)
        ];
    }
}

控制器调用

function showPost($postId) {
    try {
        $postDetail = $this->postService->getPostDetail((int)$postId);
        // 把$postDetail传给视图层渲染,展示Post内容、评论和标签
        $this->render('post/show.html', $postDetail);
    } catch (PostNotFoundException $e) {
        // 处理Post不存在的情况
        $this->render('errors/404.html', ['message' => $e->getMessage()]);
    }
}
几个关键注意点
  • 控制器永远不要直接访问仓储,所有持久化操作都通过服务层和仓储层完成,保证领域边界的清晰
  • 聚合根内部维护自身的业务规则,比如添加评论时的内容校验,避免把业务逻辑散落在服务层或控制器
  • 如果你的标签是Post专属的(不复用),可以直接把标签列表放在Post实体里,仓储查询Post时一起返回,不需要单独的TagRepository

内容的提问来源于stack exchange,提问作者user2939345

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.22 08:19:27