DDD与整洁架构中值对象与原始类型的边界及应用疑问
值对象与原始类型的边界划分指南
核心原则
领域层必须完全使用值对象/实体,绝对不能直接暴露原始类型——这是保持领域模型纯度、封装业务规则的核心要求。应用层作为领域层的入口,也应该遵循这个原则,仅在适配外部系统的边界层做原始类型和值对象的转换。
1. 应用服务/CQRS命令:优先使用值对象
应该选择第二种实现:
data class RegisterNewUserCommand( val username: Username, val rawPassword: RawPassword, val email: Email)
原因:
- 提前校验拦截:值对象本身封装了业务规则(比如
Username限制长度、禁止特殊字符,Email校验格式),在构造命令时就能触发校验,避免无效数据流入领域层。如果用String,你需要在应用服务或领域层重复编写校验逻辑,容易遗漏或不一致。 - 语义明确:
username: Username比String更清晰地表达了这个参数的业务含义,避免把任意字符串误传为用户名,降低协作时的理解成本。 - 统一规则入口:所有涉及用户名的校验逻辑都集中在
Username类里,后续规则变更只需修改这一处,不用到处找代码修改。
2. 应用层与基础设施层的端口:用值对象定义接口
应该选择第一种端口定义:
interface CouponsRepository { fun findByCode(couponCode: CouponCode): Coupon? }
原因:
- 保持领域边界清晰:Repository端口是应用层定义的,属于领域模型的一部分,用值对象能保证调用者(领域服务/应用服务)只需要传递符合业务规则的
CouponCode,不用关心底层存储的细节。 - 适配逻辑隔离:基础设施适配器(比如JPA实现类)负责处理值对象到原始类型的转换,示例如下:
class JpaCouponsRepository : CouponsRepository { override fun findByCode(couponCode: CouponCode): Coupon? { return entityManager.find(Coupon::class.java, couponCode.value) } }
转换逻辑被限制在适配器内部,领域层完全不用感知原始类型的存在。
最终疑问解答:应用层及领域层应仅使用值对象和实体吗?
是的,应用层和领域层应该优先且尽可能使用值对象和实体,仅在以下场景使用原始类型:
- 完全没有业务规则的通用参数(比如分页的
page: Int,但如果有分页大小限制,还是应该封装成PageSize值对象); - 与外部系统交互的适配层(比如HTTP控制器接收请求时用原始类型,然后转换为值对象传递给应用服务;数据库适配器把值对象转成原始类型存储)。
本质上,原始类型属于“无业务含义的通用类型”,而值对象是“带有业务规则的领域类型”——在领域和应用层用值对象,才能让代码真正体现业务逻辑,而不是一堆无意义的字符串/数字。
内容的提问来源于stack exchange,提问作者user19111862
相关产品推荐
相关产品推荐

