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

