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

Clean Architecture架构下非业务实体类POST请求数据的最佳建模方式

Clean Architecture下POST请求数据的最佳建模方案

这绝对是Clean Architecture落地时最容易纠结的问题之一——很多人都会在请求数据和业务实体的边界上犯难。咱们紧扣Clean Architecture的核心原则来拆解你的三个方案,找到最优解:

首先得明确Clean Architecture的核心:依赖方向永远向内,内层(业务实体、用例)不关心外层(API、数据库)的细节,所有对外的契约都应该由内层定义,外层只负责适配。而你说的POST请求数据,本质是API层传递给用例层的「输入数据契约」,它完全不属于业务实体范畴——业务实体是核心领域概念,而请求数据只是跨层传输的载体。

接下来逐个分析你的方案:

方案一:独立的UserRequest「实体类」

你觉得这个方案从业务视角不合理,完全正确!因为它根本就不是「业务实体」——把它放到实体层,等于让核心业务层依赖了外层的API输入格式,直接违反了Clean Architecture的依赖规则。这种方案直接Pass。

方案二:带Optional[id]的复合型User实体

这个方案的问题在于模糊了业务实体的语义:在业务逻辑里,一个合法的User必然有唯一id(创建完成后就存在);而创建请求中的「待创建用户」还没有id,这是两个完全不同的概念。把它们混在同一个实体类里,会让User实体承担了不属于它的职责,还会导致业务逻辑里出现大量不必要的空值判断,破坏了实体的严谨性,同样不符合Clean Architecture对业务实体纯粹性的要求。

方案三:在创建用例中定义UserRequest

这才是踩对了方向,但可以再优化得更贴合Clean Architecture的规范:

在Clean Architecture里,用例层的输入输出都应该是数据传输对象(DTO)——它们是用例对外暴露的契约,只负责传递数据,不包含任何业务逻辑。正确的做法是:

  • 在用例层定义CreateUserInputDTO(或者你习惯叫UserRequest也可以),作为CreateUserUseCase.execute()的参数,只包含创建用户必需的firstname和lastname;
  • API层接收HTTP请求后,把请求数据映射成这个DTO,再传递给用例;
  • 用例执行完成后,返回一个CreateUserOutputDTO(比如包含创建后的用户id和基本信息),API层再把它转换成HTTP响应。

这么做的好处一目了然:

  • 严格遵守依赖规则:用例层定义输入输出契约,API层依赖用例层,而不是反过来;
  • 业务实体保持纯粹:User实体始终是完整的业务概念,id非空,只承载核心业务逻辑;
  • 解耦API和业务:如果以后API请求格式变了(比如新增middlename字段),只需要修改API层的映射逻辑和用例的输入DTO,完全不会影响核心的User实体。

给你补个更清晰的代码示例:

用例层代码

# 用例专属的输入DTO,仅用于传递创建用户的请求数据
class CreateUserInputDTO:
    firstname: str
    lastname: str

# 用例专属的输出DTO,返回创建完成后的用户信息
class CreateUserOutputDTO:
    id: int
    firstname: str
    lastname: str

# 核心业务实体,保持纯粹的业务语义
class User:
    id: int
    firstname: str
    lastname: str

    def __init__(self, id: int, firstname: str, lastname: str):
        self.id = id
        self.firstname = firstname
        self.lastname = lastname
        # 这里可以添加业务规则校验,比如名字长度限制
        self._validate()

    def _validate(self):
        if not self.firstname.strip() or not self.lastname.strip():
            raise ValueError("Firstname and lastname cannot be empty")

class CreateUserUseCase:
    def __init__(self, user_repository):
        self.user_repository = user_repository

    def execute(self, input_dto: CreateUserInputDTO) -> CreateUserOutputDTO:
        # 生成用户ID(或由数据库自动生成)
        new_user_id = self.user_repository.generate_next_id()
        # 创建完整的User实体(此时ID已确定)
        new_user = User(new_user_id, input_dto.firstname, input_dto.lastname)
        # 保存到数据库
        self.user_repository.save(new_user)
        # 转换为输出DTO返回
        return CreateUserOutputDTO(
            id=new_user.id,
            firstname=new_user.firstname,
            lastname=new_user.lastname
        )

API层代码(以FastAPI为例)

from fastapi import FastAPI, HTTPException
from use_cases.create_user import CreateUserInputDTO, CreateUserUseCase, UserRepositoryImpl

app = FastAPI()
# 初始化依赖(实际项目中可依赖注入)
user_repo = UserRepositoryImpl()
create_user_use_case = CreateUserUseCase(user_repository=user_repo)

@app.post("/users", response_model=dict)
def create_user(firstname: str, lastname: str):
    try:
        # 将API请求参数映射为用例输入DTO
        input_dto = CreateUserInputDTO(firstname=firstname, lastname=lastname)
        # 调用用例执行创建逻辑
        output_dto = create_user_use_case.execute(input_dto)
        # 转换为API响应格式
        return {
            "id": output_dto.id,
            "firstname": output_dto.firstname,
            "lastname": output_dto.lastname
        }
    except ValueError as e:
        raise HTTPException(status_code=400, detail=str(e))

最后总结一下:这类POST请求数据本质是用例层的输入DTO,应该定义在用例层(或用例的专属目录下),作为用例与外层(API层)之间的数据契约。这种方式既符合Clean Architecture的依赖规则,又能保持业务实体的纯粹性,是最规范的落地方式。

内容的提问来源于stack exchange,提问作者Stéphane Point

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.04.30 23:57:31