architecture-layering-and-wiring

分层与组装根

YouTeacher 的内容服务——负责博客文章、分类、文件附件——建立在 DDD + Clean Architecture 之上。整套目录结构只为一条规则服务:依赖永远只指向内层,朝着 domain 收敛。

四层与依赖规则

源码在 src/ 下分成四个环:

  • domain——业务规则、不变式、实体行为、值对象校验。不依赖任何外部东西。没有 Prisma,没有 Fastify,没有 DTO。
  • application——编排 domain 的 use case,协调聚合,掌管事务边界。只依赖 domain,通过 port 触达基础设施。
  • infrastructure——实现这些 port 的适配器:Prisma 仓储、S3/MinIO 存储、Redis 缓存、鉴权与日志。
  • interfaces——HTTP 入口(Fastify 插件、路由、Zod schema)。每个路由解析输入、调用一个 use case、序列化响应。

项目把这条规则标为 CRITICAL:interfaces → application → domain,infrastructure 通过实现 application 的 port 挂在它下面。domain 不依赖任何东西;application 只依赖 domain;interfaces 从不直接碰 infrastructure。CLAUDE.md 甚至逐条列出典型违规——在路由里聚合数据、把业务规则塞进仓储、把 PrismaClient import 进 domain 实体——这些正是这条规则要防的失败模式。

Port 与 adapter

这是依赖倒置的落地:高层代码依赖抽象,具体适配器在组装时才选定。

有个值得一提的细节:port 并不都放在同一处。StoragePortIdGenerator 声明在 application/ports/——比如 StoragePort 是一个聚焦的接口,只有 uploadFeaturedImageuploadAttachmentdeletedownload,返回一个朴素的 { url, key },S3/MinIO 的实现由 infrastructure 提供。而 PostRepository 接口是从 domain@/domain/post)里 import 的。也就是说仓储契约归 domain 所有,存储和 id 生成的契约则外移一环,落在 application 层。接口隔离是刻意的——仓储 port 不带任何上传方法。

infrastructure 适配器保持"笨"

PrismaPostRepository implements PostRepository,只干一件事:纯数据访问,加上 Prisma 记录与 domain 实体之间的映射。它私有的 toDomain 方法通过 Post.reconstitute / Attachment.reconstitute 重建 Post(及其 Attachment 子实体),因此 Prisma 类型绝不越过仓储边界。save 在单个 Prisma 事务里做 upsert 加附件同步,遇到连接池超时会重试后再放弃——这是基础设施层的韧性,不是业务规则。这里没有 domain 逻辑;诸如"这篇文章是否已公开上线"之类的判断属于 domain 实体,不属于查询。

组装根

接线发生在 bootstrap/,与它所组装的各层分开。

  • bootstrap.tsbootstrapDatabase)是持久化的组装根:校验数据库 URL,确保 schema 存在,然后构造 Prisma 仓储——post、category、page——给每个注入同一个 PrismaClient。它返回一个装着这些仓储的朴素对象,供应用其余部分使用。
  • createApp.tscreateApp)搭建 Fastify 实例:注册 CORS、一个能容忍空 body 的自定义 JSON content-type 解析器(这样无 body 的 POST /publish 也能解析)、service-auth 插件、health 与 env-check 端点、一个给路径加 API base 前缀的 rewriteUrl 钩子,以及一个记录日志并返回结构化 404 的 not-found handler。

组装根的价值在于构造集中在一处。因为契约由 domain 定义、由 infrastructure 实现,每个仓储实现都可互换,而接线层是唯一知道插进来的是哪个具体适配器的代码。

本节内容

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →