《柒柒架构》DDD领域驱动设计--领域模型(一)
前言
你好! 在读这篇 《柒柒架构》DDD领域驱动设计–领域模型 之前,首先你需要先去了解DDD的相关知识,网上相关的内容比较多,很容易找的到。了解了最基本的DDD概念后再来看这篇文章。
这篇文章主要是以代码的方式实现DDD架构,让你更直观的去理解DDD的理念,在实际项目开发中能更好的运用DDD理念进行代码设计及开发。
文章纯手打,可能部分很小的概念摘自网上资源,文中DDD的理解很多都是个人理解,如要引用请注明出处。
DDD分层架构
在这里,我先简单介绍下DDD的分层架构。现在主流的DDD架构是分成四层架构:用户接口层、应用层、领域层、基础层。
1、User Interface为用户接口层(或表示层),负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
2、Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
3、Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
4、Infrastructure层为基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。
传统的四层架构都是限定型松散分层架构,即Infrastructure层的任意上层都可以访问该层(“L”型),而其它层遵守严格分层架构。在这里我也建议开始使用DDD进行代码架构设计时,尽量严格遵守该模式。
这篇文章主要介绍领域层,在DDD中最核心的模块。
领域层概念模型
这幅图在第一篇文章中有个全景视图。这是其中涉及到的概念。
聚合
- 在一个业务流程中,他是一个必须强强关联的一组业务数据及业务操作,其业务操作必须满足事务性。比如用户信息和用户角色关联信息,一般来说在数据库存储中,这会被定义到两张物理模型,但是这两张表的操作一般是同时进行且要保证事务性的。如果没有相应用户信息了,继续保持用户和角色的映射关系也是没有意义的。因此,如何找到聚合,最简单的方式就是从用户角度(或者用户界面设计的角度),考虑哪些数据是需要一起操作处理的。
实体
- 严格意义上来讲,一个实体可以映射到多张物理模型,即比如上面聚合的例子,你可以把用户和用户关系定义成一个实体,也可以分开看出两个实体。我也思考的很久,考虑到代码架构的设计的方便以及操作的简易性,这边建议,最好实体和物理模型做一一映射。
- 一个聚合会包含1到多个实体,多个实体必须在一个事务内完成相应操作,如聚合中的举例。
值对象
- 为了更清晰的介绍领域模型,这一部分我会再后续的文章中,单独拿出来讲解,因此值对象部分在该文章中先跳过
聚合根
- 主流的介绍DDD的文章及图书,基本上都是建议将一个聚合中最核心的实体作为聚合根,比如上述例子中的用户信息。由该实体管理聚合内其他实体的状态、及其他聚合的事件触发。
- 但是我这边给出了另一种更好理解的处理。我这边建议仅将该聚合核心实体的实体ID作为聚合根,实体作为该聚合的实体,所有的实体都通过该实体ID(也是聚合ID)进行聚合。这样也很理解,用户信息和用户角色关联信息都是通过用户ID进行关联、处理的。订单信息和订单中的商品列表信息,也是根据订单ID来进行关联的。因此可以使用核心实体的ID作为聚合ID,核心实体ID的属性作为该聚合的索引属性。
好,到此为止,最基本的概念讲完了,我们看下具体怎么定义。
定义聚合
public interface Aggregate extends Serializable {
/**
* 聚合键
*
* @return
*/
String idKey();
/**
* 聚合键的值
*
* @return
*/
Object idValue();
}
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class CustomInfo implements Aggregate {
private User user;
private Position position;
public void handle1() {
System.out.println("aggregate aop test");
this.user.testAop();
}
@Override
//标识聚合ID
public String idValue() {
return this.user.getUserId();
}
@Override
//标记聚合属性
public String idKey() {
return "userId";
}
}
定义实体
public interface Entity extends Serializable {
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class User implements Entity {
@Length(min = 2, max = 10)
private String userId;
private String userName;
private int cash;
public void testAop() {
System.out.println("entity aop test");
}
}
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class Position implements Entity {
private String userId;
private String positionId;
private String positionName;
}
充血模型及贫血模型
贫血模型:
定义对象的简单的属性值,没有业务逻辑上的方法,我们平时做control-service-repository三层架构时,基本上都是贫血模型
充血模型
充血模型也就是我们在定义属性的基础上(贫血模型的基础上),将该实体的业务逻辑也定义在该对象中,也就是该实体既有属性的定义也包含业务逻辑的定义。如上述例子中聚合的handle1方法及User对象中testAop方法定义。
设计原则
- 和本实体相关的所有业务操作需要,需要将其业务逻辑定义在该实体中
- 在一个聚合跨实体的业务操作,需要将其业务逻辑定义在该聚合中
领域上下文
在一个业务中,比如用户、岗位、用户和岗位关联关系的操作中,用户有对用户及用户岗位关联关系的操作,还有对岗位的单独操作。明显将岗位放在以用户ID为聚合的聚合中不合适。但是将其拆分到其他微服务中也是非常不合适的,因此岗位在这里,它和用户信息、用户岗位关联信息同属于用户管理上下文,岗位信息是另外一个聚合。
- 因此微服务的拆分最小单位是领域上下文
- 一个领域上下文可以有1到多个聚合
- 一个聚合可以有1到多个实体,核心实体的实体ID作为该聚合的聚合根
领域服务
那么又有一个问题,我有一个事务需要同时修改用户信息和岗位信息,这个操作我要怎么处理呢?这就涉及到另外一个概念:领域服务。
- 一个领域上下文中跨聚合的操作,统一将其放在该领域上下文的领域服务对象中
- 聚合的批量操作,是DDD的弱项,关于此类的处理,将放到后面说明,此篇文章将不涉及聚合批量操作
- 一个领域上下文的同一个聚合的指定多聚合操作,比如用户A向用户B转账的情形
则一个领域上下文,大概是这个样子:
小结
对业务的操作,总共有三种类型:
- 单实体内的业务操作
- 单聚合内的业务操作,涉及多个实体
- 领域上下文领域服务的业务操作,涉及多个聚合
因此一个业务逻辑该定义到什么位置,应该是很清晰了。
仓储模型
由于我还没有讲应用层,但是在这里我需要预热一个原则:从应用层到领域层的每一个操作,都是每一个不同的事务。也就是说上面小结的三种操作,如果是从应用层调用,都必须在一个事务中。
从聚合的角度来看,所有的操作都是对聚合的查、改、删,即find、save、remove。这个目前可能还不太好理解,需要读者好好去理解下。在已知获取了当前聚合对象的情况下,该聚合对象里不仅有该聚合的属性,还有聚合的业务操作(上面有介绍),当我对聚合进行业务处理时,实质上就是对该聚合的属性进行变更,也可以说是对该聚合的状态进行变更,因此对该聚合仅有查、改、删的操作,这好理解了吧。
所以一般一个业务要对某个聚合进行操作的话,首先需要找到相应的聚合,每个聚合都有不同的聚合ID,因此聚合必须提供一个通过聚合ID操作聚合的操作。
仓储设计
仓储就是对聚合进行持久化的对象,因此聚合的操作适合仓储操作独立进行的。
仓储操作单元是聚合,仓储操作单元是聚合,仓储操作单元是聚合,重要的问题说三遍。且仓储属于领域层,不是基础层,仓储对象只有三个操作:查、改、删。
我们定义仓储对象接口:
public interface Repository {
/**
* 通过ID寻找Aggregate。
* 找到的Aggregate自动是可追踪的
*/
Aggregate find(Object id, Class c) throws Exception;
/**
* 将一个Aggregate从Repository移除
* 操作后的aggregate对象自动取消追踪
*/
void remove(Object id, Class c) throws Exception;
/**
* 保存一个Aggregate
* 保存后自动重置追踪条件
*/
void save(Aggregate aggregate, Class c) throws Exception;
}
本地缓存&分布式缓存
由于仓储对聚合的操作是整体的操作,即对聚合的查询、对聚合的更新、对聚合的删除,但是聚合又有多实体聚合的性质,因此为了保证处理更高效,不能每次操作聚合都去持久化数据库中获取一次聚合对象,因此需要把聚合进行缓存。
下面是缓存对象的定义:
@Data
public abstract class AggregateContext {
public CacheObject cacheObject;
public abstract void attach(String id, Aggregate aggregate);
public abstract void detach(String id);
public abstract Aggregate find(String id);
public abstract DiffUtil.AggregateDiff detectChanges(String id, Aggregate aggregate);
}
可以看出缓存同样具有查询、更新、删除操作,每次聚合业务操作完,仓储对象进行相应处理时,需要对比业务操作后的聚合对象与缓存的聚合对象的差别,因此还有一个detectChanges查询变更操作。
下面是缓存对象的实现:
@Data
@Slf4j
public class AggregateContextImpl extends AggregateContext {
public void afterInit() {
boolean mapCacheFlag = this.getCacheObject().getCacheMap() != null;
boolean redisCacheFlag = this.getCacheObject().getJedisCluster() != null;
if (mapCacheFlag == true) {
log.info("DDD领域模型缓存使用的是: mapCache");
} else if (redisCacheFlag == true) {
log.info("DDD领域模型缓存使用的是: redisCache");
}
}
@Override
public void attach(String id, Aggregate aggregate) {
cacheObject.save(id, aggregate);
}
@Override
public void detach(String id) {
cacheObject.remove(id);
}
@Override
public DiffUtil.AggregateDiff detectChanges(String id, Aggregate aggregate) {
Aggregate snapshot = cacheObject.find(id);
DiffUtil.AggregateDiff diff = DiffUtil.diff(aggregate, snapshot);
return diff;
}
@Override
public Aggregate find(String id) {
return cacheObject.find(id);
}
}
其中CacheObject的定义如下:
public class CacheObject {
private String appName;
private int cacheExpiresTime;
private Map<String, Aggregate> cacheMap;
private JedisCluster jedisCluster;
public Aggregate find(String id) {
if (this.cacheMap != null) {
return cacheMap.get(id);
} else {
byte[] bytes = jedisCluster.get((appName + "_" + id).getBytes());
if (bytes != null) {
Aggregate t = SerializeUtil.unSerialise(jedisCluster.get((appName + "_" + id).getBytes()));
jedisCluster.expire(id.getBytes(), cacheExpiresTime);
return t;
} else {
return null;
}
}
}
public void save(String id, Aggregate t) {
if (this.cacheMap != null) {
cacheMap.put(id, SerializationUtils.clone(t));
} else if (jedisCluster != null) {
jedisCluster.setex((appName + "_" + id).getBytes(), cacheExpiresTime, SerializeUtil.serialise(t));
}
}
public void remove(String id) {
if (this.cacheMap != null) {
cacheMap.remove(id);
} else if (jedisCluster != null) {
jedisCluster.del((appName + "_" + id).getBytes());
}
}
}
从代码片段可以看出,柒柒架构支持本地缓存及redis缓存两种,分别是用于单机模式和分布式部署模式。在本地调试时,建议将其配置为map,如果部署到生成,将其配置为redis。
属性配置信息:
cache:
appName: casebasemaintain #缓存DDD聚合前缀id,防止redis存储对象的名字一致时,对数据进行覆盖,仅redis有效
type: map #可选--- map:适用于单机模式,redis:适用于集群模式
cacheExpiresTime: 240 #过期时间,单位 秒
cacheExpiresSize: 10000 #该属性仅对map时有效
# redis:
# nodes: localhost:6379
# password: Tsfmdl#2019
# connectionTimeout: 1000
# soTimeout: 1000
# maxAttempts: 3
# clientName: DDDcache
# pool:
# maxTotal: 8
# maxIdle: 8
# minIdle: 0
javaConfig:
@Configuration
public class CacheConfiguration {
/**
* 系统默认缓存TTL时间:4分钟
*/
@Value("${cache.cacheExpiresTime:240}")
private int cacheExpiresTime;
@Value("${cache.cacheExpiresSize:10000}")
private int cacheExpiresSize;
@Value("${cache.appName:public}")
private String appName;
@Bean(value = "aggregateContextImpl", initMethod = "afterInit")
@Lazy
public AggregateContext aggregateContextImpl(@Qualifier("cacheObject") CacheObject cacheObject) {
AggregateContext aggregateContextImpl = new AggregateContextImpl();
aggregateContextImpl.setCacheObject(cacheObject);
return aggregateContextImpl;
}
@Bean("cacheMap")
@Lazy
@ConditionalOnExpression("#{'map'.equals(environment['cache.type'])}")
public Map cacheMap() {
ExpiringMap<String, String> map = ExpiringMap.builder()
.maxSize(cacheExpiresSize)
.expiration(cacheExpiresTime, TimeUnit.SECONDS)
.variableExpiration().expirationPolicy(ExpirationPolicy.ACCESSED).build();
return map;
}
@Bean
@Lazy
@ConditionalOnExpression("#{'redis'.equals(environment['cache.type'])}")
@ConfigurationProperties(prefix = "cache.redis")
public RedisProperties redisProperties() {
return new RedisProperties();
}
@Bean
@Lazy
@ConditionalOnExpression("#{'redis'.equals(environment['cache.type'])}")
@ConfigurationProperties(prefix = "cache.redis.pool")
public GenericObjectPoolConfig baseObjectPoolConfig() {
return new GenericObjectPoolConfig();
}
@Bean("jedisCluster")
@Lazy
@ConditionalOnExpression("#{'redis'.equals(environment['cache.type'])}")
public JedisCluster jedisCluster() {
Set<HostAndPort> nodes = (Set) Arrays.stream(redisProperties().getNodes().split(",")).map((item) -> {
return new HostAndPort(item.split(":")[0], Integer.parseInt(item.split(":")[1]));
}).collect(Collectors.toSet());
String password = redisProperties().getPassword();
int connectionTimeout = redisProperties().getConnectionTimeout();
int soTimeout = redisProperties().getSoTimeout();
int maxAttempts = redisProperties().getMaxAttempts();
String clientName = redisProperties().getClientName();
return new JedisCluster(nodes, connectionTimeout, soTimeout, maxAttempts, password, clientName, baseObjectPoolConfig());
}
@Bean("cacheObject")
@Lazy
@ConditionalOnBean(name = "cacheMap")
public CacheObject cacheObjectMap() {
CacheObject cacheObject = new CacheObject();
cacheObject.setCacheExpiresTime(cacheExpiresTime);
cacheObject.setAppName(appName);
Map cacheMap = cacheMap();
if (cacheMap != null) {
cacheObject.setCacheMap(cacheMap);
}
return cacheObject;
}
@Bean("cacheObject")
@Lazy
@ConditionalOnBean(name = "jedisCluster")
public CacheObject cacheObject() {
CacheObject cacheObject = new CacheObject();
cacheObject.setCacheExpiresTime(cacheExpiresTime);
cacheObject.setAppName(appName);
JedisCluster jedisCluster = jedisCluster();
if (jedisCluster != null) {
cacheObject.setJedisCluster(jedisCluster);
}
return cacheObject;
}
}
所以,我们知道,仓储对象真正持久化之前,都需要先去变更缓存对象,根据监测当前对象和缓存对象的差异,进行差异化变更。
仓储的差异化更新
我们知道了,我们对聚合的所有操作,都被凝聚成了三个操作,查、改、删。我们上面也定义了仓储的接口、缓存对象的定义,那么如何根据这些如何实现对聚合的更新呢?
首先,如果每次操作都对聚合中所有实体进行更新,是可以完成相关业务逻辑的,但是这将及其耗费资源,比如说订单下面有几十个商品,我仅仅是修改了订单中其中一个商品信息,如果我要将订单及其相关的商品都进行更新,明显是很违法逻辑的。
因此我们要找到业务操作对聚合状态的更新与之前聚合状态的差异,仅对聚合进行局部更新即可。
下面是仓储对象的具体实现:
@Data
public abstract class RepositorySupport implements Repository {
@Autowired
private AggregateContext aggregateContext;
/**
* 这几个方法是继承的子类应该去实现的
*/
public abstract void onInsert(Entity entity, Class c);
public abstract List<Entity> onSelect(Map<String, Object> searchMap, Class c);
public abstract void onUpdate(Entity entity, Map<String, Object> searchMap, Class c);
public abstract void onDelete(Map<String, Object> searchMap, Class c);
@Override
@Transactional
public Aggregate find(Object id, Class c) throws Exception {
String aggregateContextId = c.getSimpleName() + "_" + id;
Aggregate t = this.aggregateContext.find(aggregateContextId);
//找到对应的Entity的id,用来查询
if (t != null) {
return t;
} else {
Aggregate aggregate = (Aggregate) c.newInstance();
BeanMap beanMap = BeanMap.create(aggregate);
Field[] fields = c.getDeclaredFields();
Map<String, Object> mapEntity = new HashMap<>();
for (Field f : fields) {
if (f.getType().getSimpleName().equals("StaticPart")) {
continue;
}
Class fieldC = f.getType();
String fieldName = fieldC.getSimpleName();
fieldName = fieldName.substring(0, 1).toLowerCase().concat(fieldName.substring(1));
Map<String, Object> searchMap = new HashMap<>();
searchMap.put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, aggregate.idKey()), id);
if (fieldName.contains("List") || fieldName.equals("list")) {
mapEntity.put(fieldName, this.onSelect(searchMap, f.getType()));
} else {
List<Entity> entities = this.onSelect(searchMap, f.getType());
if (entities != null && entities.size() > 0) {
mapEntity.put(fieldName, this.onSelect(searchMap, f.getType()).get(0));
}
}
}
if (mapEntity.size() == 0) {
return null;
} else {
beanMap.putAll(mapEntity);
if (aggregate != null) {
this.aggregateContext.attach(aggregateContextId, aggregate);
}
return aggregate;
}
}
}
@Override
@Transactional
public void remove(Object id, Class c) throws Exception {
Aggregate aggregate = this.aggregateContext.find(id.toString());
Map<String, Object> searchMap = new HashMap<>();
if (aggregate != null) {//正常缓存查询不到会去查数据库
searchMap.put(aggregate.idKey(), aggregate.idValue());
} else {
Aggregate t = (Aggregate) c.newInstance();
searchMap.put(CaseFormat.LOWER_CAMEL.to(CaseFormat.LOWER_UNDERSCORE, t.idKey()), id);
}
Field[] fields = c.getDeclaredFields();
for (Field f : fields) {
if (f.getType().getSimpleName().equals("StaticPart")) {
continue;
}
this.onDelete(searchMap, f.getType());
}
String aggregateContextId = c.getSimpleName() + "_" + id;
this.aggregateContext.detach(aggregateContextId);
}
@Override
@Transactional
public void save(Aggregate aggregate, Class c) throws Exception {
String aggregateContextId = c.getSimpleName() + "_" + aggregate.idValue();
Aggregate aggregateCache = this.aggregateContext.find(aggregateContextId);
if (aggregateCache == null) {//如果缓存失效,需要先恢复缓存
find(aggregate.idValue(), c);
}
DiffUtil.AggregateDiff aggregateDiff = this.aggregateContext.detectChanges(aggregateContextId, aggregate);
switch (aggregateDiff.getState()) {
case UNTOUCHED:
case ADDED:
case REMOVED:
case CHANGED:
List<DiffUtil.EntityDiff> entityDiffsChanged = aggregateDiff.getEntityDiffs();
for (DiffUtil.EntityDiff entityDiff : entityDiffsChanged) {
switch (entityDiff.getState()) {
case ADDED:
this.onInsert(entityDiff.getToEntity(), entityDiff.getToEntity().getClass());
break;
case CHANGED:
this.onUpdate(entityDiff.getToEntity(), BeanMap.create(entityDiff.getFromEntity()), entityDiff.getToEntity().getClass());
break;
case REMOVED:
this.onDelete(BeanMap.create(entityDiff.getFromEntity()), entityDiff.getFromEntity().getClass());
break;
}
}
break;
default:
break;
}
this.aggregateContext.attach(aggregateContextId, aggregate);
}
}
小结
到目前为止,领域对象还仅仅讲了一部分内容,但是由于篇幅比较大,我会在后续章节继续讲解DDD领域模型的其他模块,并且深入的探讨这个仓储如何在实际业务中发挥作用。
欢迎大家点赞收藏,记得加关注啊,我会带你分享更多《柒柒架构》的内容。