《柒柒架构》DDD领域驱动设计--领域模型(四)
前言
前面我们跳过了值对象的部分,是因为,值对象的设计其实并不影响DDD的核心功能。而且在值对象的设计上,一千个读者有一千个哈姆雷特。因此在这篇文章中,我讲下我自己对值对象的理解,当然也只是作为参考,各位在真正使用DDD的值对象时,还需根据自身的情况及需要,设计自己的值对象。
值对象的定义(Value Object)
从任何其他事物发展而来,初级的形成或生长的早期阶段,非Entity的值对象。 就好像 Integer、String 是所有编程语言的Primitive一样,在 DDD 里,VO 可以说是一切模型、方法、架构的基础,而就像 Integer、String 一样, DVO又是无所不在的。
我们看看严格的VO定义是什么样子的:
这个例子是将Entity某一属性,使用JAVA对象进行包装,并且将其行为及其验证逻辑进行封装在一起。然后由多个值对象来组装成Entity。
所以,Entity和VO是多对多的关系,每个Entity是由多个值对象组装得到。DDD严格定义,希望Entity每个属性(多个关联属性,比如地址:省、市、区、街道等)都定义成值对象,分别将值对象的属性、行为、验证进行封装,保证每个值对象的定义是合法的,这样Entity中的VO就可以保证,一定是符合逻辑的,无需再Entity中再去验证栏位属性的合法性。
因此值对象有如下行为特征:
- 将隐性的概念显性化
- 将隐性的上下文显性化
- 封装多对象行为
及其特点: - 不可变性
- 是一个完整的概念整体,拥有精准定义
- 使用业务域的原生语言
- 是业务域的最小组成部分、可构建复杂组合
我的理解
实际操作起来,按照VO的严格定义,代码虽说会很规整,但是会对开发带来很大的工作量及开发阻力
值对象的目的:是将实体的栏位进行业务含义校验,确保一个合理的属性值或者一组属性值
因此为了满足这个目的,我们可以选择针对一些部分属性组合成值对象,在属性上强制做验证,如上述例子。
保证Entity属性的合法性
我们在使用springboot对入参进行验证的时候,都是使用@Valid注解结合DTO上的@Length等注解完成栏位验证的,示例如下:
如果不添加@Valid注解,是不会进行栏位验证的,而且只能在方法的参数上使用。
那为了满足,在Entity的属性上对其赋值的严格合法性,不依赖这些限制。我使用了Aspectj进行set方法的切入,在对Entity属性值进行赋值时,验证@Length等相关配置,这样就无需添加@Valid,且没有必须作为方法参数的限制。
业务验证
第二个问题是,每个属性的验证现在可以满足了,多栏位的联合校验怎么处理呢,注解是无法处理这种情况的。下面我就介绍下,柒柒架构中,多属性的联合校验如何处理的。
业务验证的目的是要保证实体或者聚合内,属性的业务属性必需保证合法性,同时也是对值对象业务含义验证的一种辅助手段
- 定义业务验证注解
public @interface ValidFailMemo {
String value() default "验证失败";
}
标记一个对象是 业务验证对象。其Value值,是业务验证失败时,失败的memo信息。
- 定义业务验证接口
@ValidFailMemo
public interface DddValidation<T extends Aggregate> {
boolean isValid(T t);
}
业务验证对象必须实现该接口,并且实现isValid方法。
3. 应用启动时,扫描所有bean,做聚合与业务验证类的映射
@Component
public class DddValidationManager {
private Map<Class<? extends Aggregate>, List<DddValidation<Aggregate>>> validationMap = new HashMap<>();
@PostConstruct
private void init() {
final String[] beanDefinitionNames = SpringContextUtil.getApplicationContext().getBeanDefinitionNames();
for (String beanName : beanDefinitionNames) {
Object beanName1 = SpringContextUtil.getBean(beanName);
if (beanName1 == null) {
continue;
}
if (DddValidation.class.isAssignableFrom(beanName1.getClass())) {
Class aClass = (Class) ((ParameterizedType) beanName1.getClass().getGenericInterfaces()[0]).getActualTypeArguments()[0];
List<DddValidation<Aggregate>> dddValidations = validationMap.get(aClass);
if (dddValidations == null) {
dddValidations = new ArrayList<>();
}
dddValidations.add((DddValidation) beanName1);
validationMap.put((Class<? extends Aggregate>) aClass, dddValidations);
}
}
}
- 定义验证方法
public List<ValidResult> valid(Aggregate aggregate) {
List<ValidResult> validResults = new ArrayList<>();
List<DddValidation<Aggregate>> dddValidations = validationMap.get(aggregate.getClass());
if (dddValidations == null || dddValidations.size() == 0) {
return validResults;
}
for (DddValidation<Aggregate> dddValidation : dddValidations) {
boolean valid = dddValidation.isValid(aggregate);
if (valid == false) {
Class<? extends DddValidation> dddValidationClass = dddValidation.getClass();
ValidFailMemo annotationsByType = dddValidation.getClass().getAnnotation(ValidFailMemo.class);
String value = annotationsByType.value();
validResults.add(new ValidResult(dddValidationClass, value));
}
}
return validResults;
}
3和4结合组成了DddValidationManager 对象,定义了初始化及验证时的方法。
5. 具体验证类
@ValidFailMemo("聚合根与实体的聚合值不一致")
@Component
public class AggregateKeyMustSame implements DddValidation<CustomInfo> {
@Override
public boolean isValid(CustomInfo customInfo) {
String idValue = customInfo.idValue();
String positionIdValue = customInfo.getPosition().getUserId();
if (idValue.equals(positionIdValue)) {
return true;
} else {
return false;
}
}
}
目录结构:
只需要按照上述Demo形式定义业务验证对象,并添加到Spring容器中即可,柒柒架构会自动扫描所有bean并添加到以Aggregate为key的map中。
那么柒柒架构是在何时进行处理业务验证的呢?
public interface BaseRepository {
/**
* @param aggregate,c 传入的聚合class类型
*/
@SneakyThrows
static <T extends Aggregate> void save(Aggregate aggregate, Class<T> c) {
DddValidationManager dddValidationManager = SpringContextUtil.getBean(DddValidationManager.class);
List<DddValidationManager.ValidResult> valid = dddValidationManager.valid(aggregate);
if (valid == null || valid.size() == 0) {
String className = c.getSimpleName();
Repository repository = (Repository) SpringContextUtil.getBean(className.substring(0, 1).toLowerCase().concat(className.substring(1)) + "RepositoryImpl");
if (repository == null) {
repository = (Repository) SpringContextUtil.getBean("generalRepositoryImpl");
}
repository.save(aggregate, c);
} else {
throw new Exception(valid.toString());
}
}
}
在使用BaseRepository 的save方法前,会去做相应的验证,如果业务验证失败,则不会执行save方法,保证了聚合属性的逻辑一致性。
小结
- 使用严格的DDD进行VO的设计,会比较繁琐,对开发带来很大的工作量及开发阻力
- 值对象是为了保证属性值的合法性,对有关联的属性进行组装,并且包含值对象的相关行为
- 我对值对象进行了简化,使用注解方式对Entity单个属性进行栏位校验
- 使用业务验证对象,对联合属性进行业务校验
这样就减少了对VO的过多设计,否则按照严格的VO设计会很复杂,且涉及到DO、PO、DTO的转换时,也是非常负责,因此考虑到这些因素,我不建议对VO进行过度复杂的设计。
当然,这仅仅代表个人观点,VO的设计见仁见智,各位在使用DDD的时候,可以根据自身的具体情况自行设计。