协议层代码必须置于adapters或interfaces层,http/grpc/websocket等协议处理(路由、绑定、响应)不得进入usecase或domain;dto与domain实体须显式单向转换,时间字段用clock接口抽象,json/protobuf/db tag严禁出现在domain中。

协议层代码必须放在 adapters 或 interfaces 层,否则 domain 和 usecase 会随 HTTP/gRPC 协议变更而被迫修改——这不是分层问题,是依赖方向错了。
HTTP handler 为什么不能写在 usecase 包里
常见错误是把 http.HandlerFunc 直接定义在 usecase 目录下,或者让 usecase 接收 *http.Request 或返回 http.ResponseWriter。这会导致:业务逻辑被绑定到 HTTP 生命周期(比如 request.Context 被透传进 domain 方法),后续换成 WebSocket 或 CLI 就得重写整个 usecase。
- controller 层(如
adapters/http/user_controller.go)才负责解析id路径参数、调用CreateUserUsecase.Execute()、再把结果写成 JSON - usecase 接口只接收纯数据结构(如
CreateUserInput{Email: string, Name: string}),返回CreateUserOutput或 domain 错误(如ErrEmailAlreadyExists) -
http.StatusConflict这类状态码只出现在 controller 的if err == ErrEmailAlreadyExists { w.WriteHeader(http.StatusConflict) }中
DTO 与 domain 实体之间必须显式单向转换
很多人图省事,在 domain struct 上直接加 json:"id" 或 db:"created_at" tag,甚至让 DTO 嵌套 domain struct 指针。结果是 protocol 层字段一变,domain 就得跟着改,违反了“domain 独立于序列化格式”的原则。
- domain 实体(如
User)字段只能是基础类型、自定义枚举或 domain 内定义的值对象(如type Email string) - 时间字段不直接用
time.Time,而是通过Clock接口注入:u.CreatedAt = c.Now(),其中c Clock是 controller 构造时传入的 - DTO(如
UserHTTPResponse)只存在于adapters/http/dto/,且必须手动映射:dto := UserHTTPResponse{ID: u.ID, Email: string(u.Email)},不复用 struct,不嵌套指针
gRPC 和 WebSocket 怎么适配同一套 usecase
gRPC 不是“换个协议就行”,它要求 .proto 生成代码和 server 实现都严格隔离;WebSocket 更麻烦——它不是请求-响应模型,而是长连接+事件驱动,但核心规则不变:协议状态归 adapter 管,业务动作仍走 usecase。
-
.proto文件和生成代码放adapters/grpc/pb/,服务实现(UnimplementedUserServiceServer)放adapters/grpc/,仅调用usecase接口 - WebSocket 连接管理器(
ConnectionManager)放在adapters/ws/,它持有用户连接状态,但所有业务动作(如 “发送消息”、“拉取历史”)仍通过调用SendMessageUsecase执行 - 不要在
ws层做领域判断(比如检查用户是否在线),那是usecase的职责;adapter 只负责把message_event转成SendMessageInput并触发
最易被忽略的是时间抽象和错误边界——Clock 接口没注入,domain 就悄悄依赖了 time.Now();usecase 返回了 errors.New("not found") 而非 domain 定义的 ErrUserNotFound,controller 就无法精准映射 HTTP 状态码。这些细节不卡死,Clean Architecture 就只是目录名游戏。











