范文健康探索娱乐情感热点
投稿投诉
热点动态
科技财经
情感日志
励志美文
娱乐时尚
游戏搞笑
探索旅游
历史星座
健康养生
美丽育儿
范文作文
教案论文
国学影视

领域驱动设计(DDD)实践之路(第二篇)

  在领域驱动里面,infrastructure作为基础设施,是提供技术细节的模块。需要强调的是,很多人会误以为infrastructure就是传统的DAO层,其实infrastructure包括但不限于DAO层,比如文件处理,三方调用,使用缓存,发送异步消息等具体的技术细节实现都存在于infrastructure层。那么技术细节是什么呢。在我们看来,技术细节包含以下特征与业务知识无关与我们常说的技术实现方案相关这里可能说得比较抽象,举几个例子来理解。
  案例1:我们的实体需要持久化(存储),所以我们需要提供存储的实现。领域层的repository.save等方法提供了持久化接口约定,对于infrastructure来说,如何实现这个方法的代码,就是技术细节。那么我们如何实现这个过程呢?自然是选择缓存,OSS存或者数据库存。如果选择数据库,则进而需要选择orm框架,配置...,实现repository.save的接口,这些都属于持久化所需的技术细节代码。
  案例2:我们的应用需要导出资产包相关的excel形式数据,那么当导出资产包数据时,文件领域模块提供了导出的统一接口,资产领域模块提供了资产包的适配接口,而导出excel的代码需要使用easyExcel或者POI等第三方框架,属于技术细节代码。
  案例3: 接案例2,为了实现导出时所需的excel排版格式,排版本身的格式与业务有关,比如在我们的业务场景下,我们导出调解明细(我们项目特定的一个领域模型)的时候,只需要按照常见的导出方式即可,而导出资产明细(我们项目特定的一个领域模型)则需要解析拼接所有的动态数据列,合并显示每条数据不同的动态列,而这一切是由业务决定的。根据业务不同有不同的排版要求这一点体现了资产域需要提供文件域的导出策略,调解域也需要实现文件域的导出策略。这些都属于描述业务信息的约定,而这些约定的具体实现比如怎么把实体的那一个属性映射到excel的哪一行哪一列,则属于技术细节。这种区分方式显性化了业务的概念,同时又将实现放在了基础设施层,提供了一定的解耦性。
  说完了infrastructure的技术细节的定义,我们接下来聊几个在采用DDD研发模式下,infrastructure层开发过程中经常会遇到的一些问题及我们的解决方案。小数据量系统的save的实现细节Insert还是Update
  为了让业务逻辑和代码实现解耦,在repository的约定中,我们通常用"save保存"代替我们通常说的"insert(插入)","update(更新)"这样的技术术语,以屏蔽技术细节。这样带来的一个副作用是,在save时就需要根据策略判断调用insert还是update,我们使用的策略是根据id是否是空决定,即我们所有的实体对象都有一个属性,类型为Id类的子类,id对象的属性(数据库里面实际存放的id值)可能为null,但是id对象,本身不会为null,根据这个对象可以判断当前实体id是否为空。对象关系体现数据库关联
  对于聚合场景,子实体是需要知道聚合根的id的,因为在存储到数据库时可能需要以外键的方式存储对象间的映射关系。
  然而,在具体实现中,我们认为,实体之间的对象关系才是标识两个实体之间关系的方式,而不是id,所以生成实体时,先通过对象引用关联对象,表明聚合和实体之间的关系,在保存到数据库的时候,通过实体生成数据库映射类的时候就可以知道当前数据的id是否为空,同时又能知道当前数据之间的关系。
  对象之间的关系在1:1聚合保存的时候可能体现不明显,但是当1:N或者N:N批量保存聚合的时候,作用就比较明显了。在我们的系统中发起调解业务就需要批量保存调解批次。代码如下(欢迎吐槽,拥抱进步)for (MediationBatch mediationBatch : mediationBatchList) {     // 得到聚合根MediationBatch的数据库ORM类(DO)     MediationBatchDO batchDO = MediationTransfer.toMediationBatchDO(mediationBatch);     List recordList = MediationTransfer.toMediationRecordDO(mediationBatch).stream()         .peek(record -> record.setOperatorId(user.getAccountId()))         .peek(record -> record.setOperatorTenantId(user.getTenantId().getTenantId()))         .collect(Collectors.toList());     if (mediationBatch.getId().isNew()) {         batchDO.setId(idGenerator.getBatchId());         recordList.stream().peek(record -> record.setBatchId(batchDO.getId()))             .forEach(record -> record.setId(idGenerator.getBatchRecordId()));         insertBatchList.add(batchDO);         insertRecordDOList.addAll(recordList);     } else {         updateBatchList.add(batchDO);         updateRecordDOList.addAll(recordList);     } }
  通过这种方式就解决了批量插入不能返回id,同时又能继续复用id.isNew()判断是否为新数据的方式(这里我们没有创建entity基类,所以判断放在了Id上)。
  以上方法提供了批量保存时如何区分是新增还是更新。下面我们来谈谈我们项目内提供的插入和更新模板代码。批量保存性能优化
  对于领域来说,save是基本的保存代码。方法传入的参数往往是一个存在于内存中的聚合根对象,有时包含全量的子实体,VO和全量的字段,而在插入场景,对批量请求我们希望支持批量插入,减少对数据库的IO频率,在更新场景下,我们希望减少update时的更新字段的数量(只更新需要更新的字段),这有助于减少数据库IO次数、binlog大小和mysql数据库索引变更带来的开销,所以是非常有必要的。因此对于infrastructure来说,可以提供统一的定制化模板方便repository定制化更新字段的方法快速实现。
  由于我们的系统使用的是mybatisplus的ORM方案,所以我们根据api和mysql的批量语句开关提供了一个批量插入和批量更新的Mapper基类,其中insertBatchSomColumn是mybatisplus自带的,updateBatchById则是我们实现的,文档链接如下https://mp.toutiao.com/profile_v4/graphic/preview?pgc_id=7062223527654916621通过这种方式可以轻松地提供定制化更新某几列的sql,减轻sql编写负担。业务决定技术方案的策略实现细节
  这一次要讲的其实就是上面提到过的excel导入导出的案例。对于我们的系统来说,具有资产域,文件域,调解域等。其中资产域、调节域等三个域需要导入导出excel。但是我们在设计的时候认为文件的操作属于文件域的概念,所以应当由文件的domain提供功能。但是很明显,具体的导入导出的策略根据数据的不同是可以变化的。所以针对这种情况,我们回归到领域驱动的实现的本质------面向对象技术来思考这个问题的优雅解法。以导入为例excel的操作属于文件操作,所以属于文件域,所以文件域提供了一个接口ExcelParser ,来表明文件域提供了excel的解析能力各个业务域有自己的具体的操作策略,所以各个域提供了自己的文件域的子接口,来表明自身域在excel的导入这件事上,资产与调解提供了解析能力AssetDetailExcelParser 和MediationResultExcelParser 。随后在infrastructure层,在deal.excel包(deal代表业务处理技术细节实现,excel代表excel相关业务)下提供了实现类和相关的其他类。
  代码如下package xxx.domain.file;  /**  * 解析Excel,得到对象  *  * @author  * @version   */ public interface ExcelParser extends SimpleStrategy {     /**      * 解析excel对应的文件,得到对象输出      *      * @param url excel的路径对象,使用{@link FileIO}进行解析得到InputStream再处理      * @return 输出解析得到的对象      */     List parse(FileRecordUrl url);      /**      * key,能够处理的数据类型,      * 同时也是{@link com.antgroup.antchain.unifyx.base.common.strategy.registrar.StrategyFactory}      * 的路由key。      *      * @return {@link ExcelParser#parse(FileRecordUrl)}的元素类型      */     Class key();      @Override     default void putKey(Set keys) {         keys.add(key());     }      @Override     default Class<? extends Strategy> strategyGroup() {         return ExcelParser.class;     } }package xxx.domain.assetdetail;  /**  * 解析资产明细excel  * @author   * @verson Id:ParaseExcel.java v0.1 2021年10月28日16:26  */ public interface AssetDetailExcelParser extends ExcelParser {      @Override     default Class key(){         return ExportAssetDetailExcelVO.class;     }  }package xxx.domain.mediation.batch;  /**  * 调解回填excel解析器  *  * @author   */ public interface MediationResultExcelParser extends ExcelParser {     @Override     default Class key() {         return MediationResultVO.class;     } }package xxx.infrastructure.deal.excel;  /**  * 调解回填excel解析器  *  * @author   * @verson Id:ParseAssetMediationExcelImpl.java v0.1 2021年11月02日11:27  */ @Component public class MediationResultExcelParserImpl implements MediationResultExcelParser {      @Autowired     FileIO fileIO;      @Override     public List parse(FileRecordUrl url) {         InputStream inputStream = fileIO.openFile(url);         try {             return EasyExcelFactory.read(inputStream, new Sheet(1, 3, MediationResultDTO.class))                     .stream().map(item -> (MediationResultDTO) item)                     .map(item -> MediationResultVO.create(                             new MediationBatchId(Long.parseLong(item.getMediationBatchId())),                             new MediationDetailId(Long.parseLong(item.getMediationDetailId())),                             MediationDetailStatus.convert(item.getMediationStatus()),                             item.getMediationNote())                     ).collect(Collectors.toList());         } finally {             fileIO.closeFile(inputStream);         }     } }
  上面4份代码是domain的,最下面的是infrastructure的,这里我们只讲infrastructure的(但是我个人认为领域分层后还是需要整体考虑的,所以才会贴上domain的代码)。这里的代码实现上与具体的技术有关,使用了EasyExcel,所以在实现上的时候,我们认为这个代码应当是一个技术细节,对于domain层来说不需要感知如何实现。因此放在了infrastructure。当前代码所在的包表明了在infrastructure的概念,这是业务流程中的excel处理的部分。继承关系是为了domain中的子概念接口赋能。
  这是我们对于跨域业务逻辑的处理办法。简单、小巧的领域事件发送功能实现细节
  为了保证各领域模型间的解耦,我们经常通过最轻量级的领域事件的方式实现,而不是类似metaq,msgbroker这样的异步分布式消息中间件。领域事件的发送有很多的实现方案,我们倾向于直接使用spring的功能,因为我们需要同步保证事务。但是spring的event发送需要继承ApplicationEvent而领域事件我们又希望独立于spring的event体系,所以我们通过对spring的了解发现了spring已经提供了PayloadApplicationEvent 可以实现这种功能实现上和其他的spring的event一致,获取我们自己定义的event的方法如下@Override public void onApplicationEvent(PayloadApplicationEvent event) {     Assert.assertEquals(event.getPayload().getClass(), TimeoutEvent.class);     System.out.println(event); }
  这里的getPayload()可以获取到我们放进去的领域事件TimeoutEvent大数据量聚合加载的解决方案
  在任何系统中都会有批处理的业务。可能是批处理聚合,可能是批处理聚合内的实体类。这里说一下我之前遇到的一个帖子(jdon)上的讨论。帖子上说的是有一个排班业务,一条班表数据作为聚合存在着每日排班子实体,每日排班下又存在着排班明细子实体,当日期逐渐增加时一条排班需要加载好几年的数据用于生成聚合,而实际上则仅仅只需要计算最近几周的数据。这里存在两点问题聚合器是否必须加载完整如何实现这种部分加载
  第一点自然不用多说,技术实现以提供业务功能为核心是我一直以来的主张。所以当数据量可能会不断增大的情况下不用加载完整自然是必须的(哪怕内存存储的下也应当尽可能少的消耗)。第二点来说帖子的一位回复者倾向于DomainService提供专门的适配方法,用于加载几周的数据。
  我们的系统中存在一个有一些类似的业务。我们的系统需要每隔几分钟就运行一次批处理任务,获取所有已经过期的调解明细,并且设置为过期。调解明细属于调解批次的聚合,所以我们有同样的需求。
  我们在此提供一种我们的实现,供参考。
  repository提供iter(LocalDateTime):Iterator<>方法,直接在调解明细上过滤数据(正确的业务逻辑),返回一个迭代器,方便逐个处理聚合的业务iter返回的聚合里面,由于调解批次中的调解明细的数据量可能一次就会加载上千条,而调解批次本身不会受到调解明细的状态影响,所以这里我们也是加载部分实体组装聚合。具体代码如下LocalDateTime now = LocalDateTime.now(); Iterator iterator = repository.iter(now); // ... while (iterator.hasNext()) {     MediationBatch batch = iterator.next();     timeoutAssetDetailIdList.addAll(batch.timeout(now));     // 批量更新 timeoutAssetDetailIdList }
  repository的实现根据面向对象原则,仅仅提供如何查询过滤数据库数据public Iterator iter(LocalDateTime mediationDetailDeadlineMin) {     LambdaQueryWrapper wrapper = new LambdaQueryWrapper<>();     wrapper.le(MediationRecordDO::getDeadline, mediationDetailDeadlineMin);     return new BatchIterator<>(new MediationIteratorUnit(wrapper, detailMapper, batchMapper)); }
  迭代器的实现提供了迭代职责实现/*  * Ant Group  * Copyright (c) 2004-2021 All Rights Reserved.  */ package com.antgroup.antchain.donpa.infrastructure.cache;   import java.util.Collections; import java.util.Iterator; import java.util.List; import java.util.function.Supplier;  /**  * 批量迭代器  *  * @author  * @version   */ public class BatchIterator implements Iterator {     private final Supplier> batchSupplier;     private boolean hasNext = true;     private Iterator localCacheIterator = Collections.emptyIterator();      public BatchIterator(Supplier> batchSupplier) {         this.batchSupplier = batchSupplier;         loadCache();     }      @Override     public boolean hasNext() {         // 更新hasNext         loadCache();         return hasNext;     }      @Override     public T next() {         return localCacheIterator.next();     }      private void loadCache() {         if (!localCacheIterator.hasNext()) {             List dataList = batchSupplier.get();             if (dataList.isEmpty()) {                 hasNext = false;             }             localCacheIterator = dataList.iterator();         }     } }
  至此实现了批处理加载聚合的逻辑,同时可以提供聚合的部分加载(需要注意业务的正确性不会因为聚合的不完全加载而产生问题)。
  最后总结一下领域驱动最初的初衷根据我的理解和论坛上其他人的讨论我们都认为是为了回归面向对象,提供更简单更易维护的方式解决业务。可以根据产品文档和业务语义拆解,根据场景模拟的方式分辨当前代码实现到底是技术还是业务,业务我们可能不熟,但是我们可以反向从更熟悉的技术入手采用排除法分辨。针对不同域之间概念交叉形成的业务,可以采用接口继承或组合的形式提供概念的结构,如果最终需要技术细节实现,则在infrastructure中实现。对于对象职责分离后,产生的底层方法无法适配优化性能的问题,在业界更容易被接收的做法就是domain的repository等接口提供适配的方法。其实这也提醒了我们编写领域驱动代码时,对象职责分离不代表我们开发人员的逻辑也是割裂的。我们需要用全局视角写出一个个独立的对象逻辑,这一点我认为是更为重要的一点。对于尽可能少加载对象的时候,我们可以根据面向对象原则,infrastructure实现时屏蔽底层实现逻辑,为domain提供通用的但是性能更好的实现方案
重磅!苏炳添成小米品牌代言人雷军他说他喜欢小米手机中国新闻小米MIX4将于8月10日晚上七点半发布,时隔三年,这款小米顶级旗舰终于来了!但你以为关注点就只有这些?就在刚刚(8月10日1000),小米创办人雷军突然又宣布了一条重腾讯公司威胁一个普通百姓的勇气从何而来?不久前,一个普通的头条作者因为写了一篇帖子王者荣耀是谁的荣耀?,结果收到了腾讯公司的两次律师函,态度强硬地要求作者撤销帖子,并向腾讯赔礼道歉,否则诉诸法律手段!一个市值过千亿的平台小米全球第二!三年内争第一!雷军的梦想,柳传志早就实现过在小米的第2个十年的开始,雷军以我的梦想,我的选择为主题进行演讲,并汇报了小米的最新战绩和新品!全球第二!话说,这个成绩不容易!并且,雷军说,三年内做到第一,没问题!雷军作为第一代小米十年了!雷军又立下三年做到第一的梦想雷军演讲金句1小米当前任务是要站稳全球第二。2我们会努力工作,让IPO的投资者至少赚一倍。3最好的投资就是投资自己。4创办小米的同时接手金山,当初不理智选择背后四个字情义无价。5小京东滴滴醒悟?主动退出社区团购社区团购是一个烧钱场,不烧钱,你就进不了这个市场,没有足够的钱,你就不配进入这个市场。社区团购和曾经的外卖平台抢占市场非常相似,只是比当时的外卖要更加激烈。因为现在的资本实力比之过不要妄想大而不倒!人民日报再次点名阿里,事情越闹越大了在过去的20年间,腾讯和阿里由于各自业务的主攻方向不同,从而在民众的口碑中呈现两极分化。相对而言,阿里主攻电商和网购的思路,比起腾讯的王牌支柱产业游戏来说,更容易让人接受。再加上时2021开学季好物推荐男女生宿舍幸福感数码好物指南转眼间还有半个多月就又是一年的开学季了,许多校园里又将迎来新面孔。许多学生朋友们在去大学校园都会憧憬他们美好的校园生活,可能许多学生朋友们都是第一次离家去到新的城市。那么该带点什么荣耀X20Max突然被宣布,数亿花粉始料未及,幸福来得太突然8月12日晚上,是荣耀新品发布会,此次发布会为大家带来了荣耀Magic3系列,还有荣耀X20手机。值得一提的是,在荣耀X20发布阶段,荣耀官方表示,荣耀X20Max将要到来,它是一横向对比!TCL98英寸智屏和华为98英寸智慧屏哪个更强?然而随着智屏的发展,智屏的屏幕被越做越大,画质越来越清晰,音响也越来越好。把巨幕智屏搬回家,似乎也成为当前的一种必然趋势和潮流。放在五年前,这样一台超大巨幕智屏最便宜的价格都在10阿里丑闻背后,是大公司与公众的割裂作者第一财经吴洋洋王姗姗陈锐淘鲜达业务线员工王成文(花名曲一)涉嫌性侵女下属的公司丑闻在8月7日登上微博热搜后,对于阿里巴巴的舆论声讨迅速膨胀。声讨的出发点,是公众对男上司恶意灌酒在地球上用什么方式可以看到黄道?为什么?笔者虽然是数学老师,但也是天文爱好者,对回答这个问题很感兴趣,谈一下自己看法,不当之处,留言点评探索。什么是黄道我们古代常说的黄道吉日就是指的黄道!所谓黄道吉日,就是太阳对地球的影
天堂里的百鸟林在我以前的脑海认知里,天堂一定会是在天上,地狱也一定会是在地下!然而,这只是一种字面上的错误理解!真正的天堂和地狱,不是存在地球上,而是和地球并行,高于地球空间的一个星体,现在这个不会塌房不用工资的虚拟数字人是一本万利的好生意?文卡里娜编辑卡里娜在元宇宙系列文章中,第一篇我们探讨了构建元宇宙经济系统的核心技术NFT风口上的数字藏品,本文我们将进入元宇宙另一个重要组成部分虚拟数字人。虚拟数字人同样是元宇宙的一加7pro关于RAMBOOST卡顿问题解决本人当年新机发布入手一加7pro6128版本,屏幕丝滑操作流畅,两年后开始出现卡顿,很是影响体验,这才关注运存,发现平均使用5。7G总共5。7G,闲置90m,内存使用高达99,如此(社会)利用App诈骗成为电信网络诈骗主要犯罪手段之一新华社北京4月14日电(记者王思北)记者14日从国家网信办获悉,近年来,利用App进行诈骗已成为电信网络诈骗案件的主要犯罪手段之一,约占整体案发量的六成。其中,网络兼职刷单快速贷款创维电视发布三款全通道120Hz高刷产品中证网讯(记者张军)4月13日,创维电视发布全通道120Hz高刷新品,包括壁纸电视系列Q53ProQ53守护者G53电竞级画质大师A33。公司表示,三款新品都具备全通道120Hz高折叠屏手机销量排行榜,华为MateX2屈居第二,OPPO一骑绝尘随着折叠屏手机技术逐渐成熟良率提升明显,经过多年的技术沉淀,2022年将成为折叠屏手机销量爆发元年。当前除了三星华为这两家厂商外,小米OPPO荣耀等手机大厂也纷纷入局折叠屏手机市场小米科技进军汽车行业最新消息,小米汽车在北京工厂所在的地块已经开工建设,该地块正在进行整理,工程工程量已完成过半。日前发布的北京市2022年重点工程计划中明确,小米汽车将选址北京经济技术开发区马驹桥镇搭载无人泊车系统的威马W6的实际体验5月29日30日,威马汽车超进化体验营北京站暨W6无人驾驶挑战赛在北京朝阳公园举行,现场可以看到很多开着其他品牌新能源产品的车主过来参加活动。现场活动分为三个环节,车辆的场地动态驾盛视科技2021年净利1。79亿同比下滑20。12董事长瞿磊薪酬89。2万挖贝网4月13日,盛视科技(002990)近日发布2021年度报告,报告期内公司实现营业收入1,126,809,481。64元,同比增长20。50归属于上市公司股东的净利润179,Apple13pro玩几分钟特别烫,这是为什么?感谢您的阅读!手机出现发烫,它的原因是什么?我们来了解一下。一款手机出现发烫的原因其实有多种。比如说手机本身的系统优化程度不高,它有可能会让手机出现比较严重的发热问题,特别是你在进联想柳传志做了什么导致形象崩塌?我看,主要是侵吞了一些国有财产。比如那八万平方米的国有土地。这个就是柳传志形象崩塌的主要原因。因为,国有资产哪可是全国人民的血汗啊!一,联想从100的国有资产到最后仅剩20左右,其