《柒柒架构》DDD领域驱动设计--领域模型(一)

4 篇文章 0 订阅
本文深入介绍了DDD领域的核心概念,包括分层架构、聚合、实体、值对象、聚合根等,并通过代码示例展示了如何在Java中实现。此外,讨论了领域服务、仓储模型以及缓存策略,强调了事务管理和效率优化的重要性。最后,提出了差异化更新策略,以减少不必要的资源消耗。
摘要由CSDN通过智能技术生成

前言

你好! 在读这篇 《柒柒架构》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领域模型的其他模块,并且深入的探讨这个仓储如何在实际业务中发挥作用。

欢迎大家点赞收藏,记得加关注啊,我会带你分享更多《柒柒架构》的内容。

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值