一. 前言
你好!在读这篇 **《柒柒架构》DDD领域驱动设计--领域模型** 之前,首先你需要先去了解DDD的相关知识,网上相关的内容比较多,很容易找的到。了解了最基本的DDD概念后再来看这篇文章。
这篇文章主要是以代码的方式实现DDD架构,让你更直观的去理解DDD的理念,在实际项目开发中能更好的运用DDD理念进行代码设计及开发。
**文章原创、纯手打,文中DDD的理解很多都是个人理解,如要引用请注明出处。**
二.DDD分层架构
在这里,我先简单介绍下DDD的分层架构。现在主流的DDD架构是分成四层架构:用户接口层、应用层、领域层、基础层。

1、User Interface为用户接口层(或表示层),负责向用户显示信息和解释用户命令。这里指的用户可以是另一个计算机系统,不一定是使用用户界面的人。
2、Application为应用层,定义软件要完成的任务,并且指挥表达领域概念的对象来解决问题。这一层所负责的工作对业务来说意义重大,也是与其它系统的应用层进行交互的必要渠道。应用层要尽量简单,不包含业务规则或者知识,而只为下一层中的领域对象协调任务,分配工作,使它们互相协作。它没有反映业务情况的状态,但是却可以具有另外一种状态,为用户或程序显示某个任务的进度。
3、Domain为领域层(或模型层),负责表达业务概念,业务状态信息以及业务规则。尽管保存业务状态的技术细节是由基础设施层实现的,但是反映业务情况的状态是由本层控制并且使用的。领域层是业务软件的核心,领域模型位于这一层。
4、Infrastructure层为基础实施层,向其他层提供通用的技术能力:为应用层传递消息,为领域层提供持久化机制,为用户界面层绘制屏幕组件,等等。基础设施层还能够通过架构框架来支持四个层次间的交互模式。
传统的四层架构都是限定型松散分层架构,即Infrastructure层的任意上层都可以访问该层(“L”型),而其它层遵守严格分层架构。在这里我也建议开始使用DDD进行代码架构设计时,尽量严格遵守该模式。
这篇文章主要介绍领域层,在DDD中最核心的模块。
三.领域层概念模型

这幅图在第一篇文章中有个全景视图。这是其中涉及到的概念。
1.聚合
- 在一个业务流程中,他是一个必须强强关联的一组业务数据及业务操作,其业务操作必须满足事务性。比如用户信息和用户角色关联信息,一般来说在数据库存储中,这会被定义到两张物理模型,但是这两张表的操作一般是同时进行且要保证事务性的。如果没有相应用户信息了,继续保持用户和角色的映射关系也是没有意义的。因此,如何找到聚合,最简单的方式就是从用户角度(或者用户界面设计的角度),考虑哪些数据是需要一起操作处理的。
2.实体
- 严格意义上来讲,一个实体可以映射到多张物理模型,即比如上面聚合的例子,你可以把用户和用户关系定义成一个实体,也可以分开看出两个实体。我也思考的很久,考虑到代码架构的设计的方便以及操作的简易性,这边建议,最好实体和物理模型做一一映射。
- 一个聚合会包含1到多个实体,多个实体必须在一个事务内完成相应操作,如聚合中的举例。
3.值对象
- **为了更清晰的介绍领域模型,这一部分我会在后续的文章中,单独拿出来讲解,因此值对象部分在该文章中先跳过**
4.聚合根
- 主流的介绍DDD的文章及图书,基本上都是建议将一个聚合中最核心的实体作为聚合根,比如上述例子中的用户信息。由该实体管理聚合内其他实体的状态、及其他聚合的事件触发。
- 但是我这边给出了另一种更好理解的处理。我这边建议仅将该聚合核心实体的实体ID作为聚合根,实体作为该聚合的实体,所有的实体都通过该实体ID(也是聚合ID)进行聚合。这样也很理解,用户信息和用户角色关联信息都是通过用户ID进行关联、处理的。订单信息和订单中的商品列表信息,也是根据订单ID来进行关联的。因此可以使用核心实体的ID作为聚合ID,核心实体ID的属性作为该聚合的索引属性。
好,到此为止,最基本的概念讲完了,我们看下具体怎么定义。
定义聚合:
public interface Aggregate extends Serializable {/*** 聚合键** @return*/String idKey();/*** 聚合键的值** @return*/Object idValue();}@Data@Builder@NoArgsConstructor@AllArgsConstructorpublic class CustomInfo implements Aggregate {private User user;private Position position;public void handle1() {System.out.println("aggregate aop test");this.user.testAop();}@Override//标识聚合IDpublic String idValue() {return this.user.getUserId();}@Override//标记聚合属性public String idKey() {return "userId";}}
定义实体:
public interface Entity extends Serializable {}@Data@Builder@AllArgsConstructor@NoArgsConstructorpublic 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@NoArgsConstructorpublic 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;}
本地缓存&分布式缓存
由于仓储对聚合的操作是整体的操作,即对聚合的查询、对聚合的更新、对聚合的删除,但是聚合又有多实体聚合的性质,因此为了保证处理更高效,不能每次操作聚合都去持久化数据库中获取一次聚合对象,因此需要把聚合进行缓存。
下面是缓存对象的定义:
@Datapublic 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@Slf4jpublic 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");}}@Overridepublic void attach(String id, Aggregate aggregate) {cacheObject.save(id, aggregate);}@Overridepublic void detach(String id) {cacheObject.remove(id);}@Overridepublic DiffUtil.AggregateDiff detectChanges(String id, Aggregate aggregate) {Aggregate snapshot = cacheObject.find(id);DiffUtil.AggregateDiff diff = DiffUtil.diff(aggregate, snapshot);return diff;}@Overridepublic 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:
@Configurationpublic 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")@Lazypublic 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;}}
所以,我们知道,仓储对象真正持久化之前,都需要先去变更缓存对象,根据监测当前对象和缓存对象的差异,进行差异化变更。
仓储的差异化更新
我们知道了,我们对聚合的所有操作,都被凝聚成了三个操作,查、改、删。我们上面也定义了仓储的接口、缓存对象的定义,那么如何根据这些如何实现对聚合的更新呢?
首先,如果每次操作都对聚合中所有实体进行更新,是可以完成相关业务逻辑的,但是这将及其耗费资源,比如说订单下面有几十个商品,我仅仅是修改了订单中其中一个商品信息,如果我要将订单及其相关的商品都进行更新,明显是很违法逻辑的。
因此我们要找到业务操作对聚合状态的更新与之前聚合状态的差异,仅对聚合进行局部更新即可。
下面是仓储对象的具体实现:
@Datapublic abstract class RepositorySupport implements Repository {@Autowiredprivate 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@Transactionalpublic 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@Transactionalpublic 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@Transactionalpublic 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领域模型的其他模块,并且深入的探讨这个仓储如何在实际业务中发挥作用。
欢迎大家点赞收藏,记得加关注啊,我会带你分享更多《柒柒架构》的内容。




