抛弃概念,浅浅谈谈DDD

领域驱动设计的原理抽象且难以理解,概念众多且晦涩难懂,在这段时间的学习过程中有不小的阻力,在本文,我想抛开所有的概念,来聊一聊这段时间的学习后,我对领域驱动设计的学习感受。

模型是对业务的抽象

这里是一段真实业务场景下的代码,为了更好的进行理解,我们对业务场景进行简化:

用户将本地文件上传到个人云盘,其中文件源数据是在用户本地上传的,我们需要在逻辑上将文件保存到用户个人云盘中:

scss 复制代码
 public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
        // 参数校验
        checkSceneParam(sceneDTO);
        checkNodeParams(nodeDTO);
    
        // 参数前置准备
        sceneDTO = convertAndSetParamsBeforeUpLoad(sceneDTO, nodeDTO);

        // 查询数据库,确认个人云盘对应的节点(技术语言)
        Long fromAliId = Long.valueOf(sceneDTO.getFrom());
        Long toAliId = Long.valueOf(sceneDTO.getTo());
        NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

        // 根据个人云盘查询信息进行参数填充
        convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);

        // 文件名编码(技术语言)
        if (chatScene != ChatScenceTypeEnum.PERSON_CLOUD.getValue() && RegexpUtil.isContainIllegalChar(file.getNodeName())) {
            file.setNodeName(UrlCodingUtil.encode(file.getNodeName()));
        }

        //检查OSS文件源数据(技术语言)
        checkOss(chatScene, nodeDTO)

        // 根据OSS数据,参数准备
        prepareNodeBeforeInsert(chatScene, nodeDTO);
     
        // 文件安全送审(业务语言)
        fileSafeManager.submitSafeScan(nodeDTO);

        // 文件数据插入数据库(技术?业务?语言)
        cloudNodeRepository.insertFile(nodeDTO);
 }

类似的代码在我们的应用中很常见,当然更多的,这些逻辑会被拆解打散,分布在一个个的service与manager中,当我们把这些打散的逻辑梳理到一起,就构成了上面的例子。

针对例子分析,可以发现,其实整个处理逻辑是按照面向过程的方案,一步步推到下来的,这就导致了两个非常明显的问题:

  1. 冗余的数据准备与填充(查询个人云盘信息填充一次,查询文件源数据再填充一次)。
  2. 整条链路上,技术与业务操作混成一团,反复切换。

而这些,就会使我们对这段代码逻辑处理过程的理解产生困难。

其实,当我们对业务场景进行分析,整个业务链条是很简单的:

  1. 用户上报文件信息
  2. 文件信息储存到个人云盘
  3. 文件安全送审

这其中,只涉及了两个主要对象和一个服务:文件对象和个人云盘对象、安全审核服务。我们按照这个思路重新编排代码:

scss 复制代码
 public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
        // 创建文件对象和个人云盘对象
        File file = createFile(nodeDTO, sceneDTO, params);
        Clouddisk clouddisk = createClouddisk(nodeDTO, sceneDTO, params);
     
        // 安全服务扫描文件
        safeService.asynScan(file);

        // 将文件保存到个人云盘
        clouddisk.save(file);
 }

File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    checkOss(chatScene, nodeDTO);
    file = prepareNodeBeforeInsert(chatScene, nodeDTO);
    return file;
}

Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // 查询数据库,确认个人云盘对应的节点(技术语言)
    Long fromAliId = Long.valueOf(sceneDTO.getFrom());
    Long toAliId = Long.valueOf(sceneDTO.getTo());
    NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

    // 根据个人云盘查询信息进行参数填充
    return convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);
}

两者相比,在核心处理链路上,可能还是后者要清晰一些。

抽象模型,是为了最终用业务语言代替技术语言,降低代码语言的混乱度与复杂度,用业务而不是技术指导代码设计。而抽象出的模型,就是面向对象中的对象,在领域驱动设计中,就是Entity、Object Value或Service。

工厂类与存储库的引入

核心处理链路上,我们的业务逻辑已经清晰很多了,但是,问题仍然存在:对象创建本身可能也有自己的业务或技术规则,在上面的例子中,这些规则并没有完全与主链路解耦。

我们可以借助工厂类将业务与数据解耦:

csharp 复制代码
 public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
     // 创建文件对象和个人云盘对象
     File file = FileFactory.createFile(nodeDTO, sceneDTO, params);
     Clouddisk clouddisk = ClouddiskFactory.createClouddisk(nodeDTO, sceneDTO, params);
 
     // 安全服务扫描文件
     safeService.asynScan(file);

     // 将文件保存到个人云盘
     clouddisk.save(file);
 }
scss 复制代码
Class FileFactory {
   public File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // 查询OSS数据源
    checkOss(chatScene, nodeDTO);
    // 数据填充
    file = prepareNodeBeforeInsert(chatScene, nodeDTO);
    return file;
   } 
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // 查询数据库,确认个人云盘对应的节点(技术语言)
    Long fromAliId = Long.valueOf(sceneDTO.getFrom());
    Long toAliId = Long.valueOf(sceneDTO.getTo());
    NodeDTO privateRootDir = filePrivateChatDirComponent.getAndCreatePrivateRootDir(fromAliId, toAliId, true);

    // 根据个人云盘查询信息进行参数填充
    return convertAndSetParamsBeforeUpload(nodeDTO, privateRootDir, fromAliId);
  }
}

我们的工厂类就承担了原材料获取(查询数据)与拼装(实例化对象)两个职责,在这个基础上,再引入存储库对数据查询进行封装,对工程屏蔽原材料的生产过程,进一步清晰逻辑:

typescript 复制代码
Class FileFactory {
   public File createFile(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // 查询OSS数据源
    ossObj = ossRepository.getOssObject(nodeDTO, privateRootDir, fromAliId);
    // 数据填充
    return convertAndSetParamsBeforeUpload(nodeDTO, ossObj);
   } 
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
    // 查询数据库,确认个人云盘对应的节点(技术语言)
    cloudNode = cloudNodeRepository.getCloudNode(nodeDTO, privateRootDir, fromAliId);
    // 根据个人云盘查询信息进行参数填充
    return convertAndSetParamsBeforeUpload(nodeDTO, cloudNode);
  }
}

Interfect OssRepository {
    public Object getOssObject(NodeDTO nodeDTO, SceneDTO sceneDTO, String params);
}

Interfect CloudNodeRepository {
    public Object getCloudNode(NodeDTO nodeDTO, SceneDTO sceneDTO, String params);
}

工厂与存储库是为创建业务实例而生的,能够对主逻辑链路屏蔽实例创建的额外规则。

层级间的依赖关系

业务实例+工厂+存储库,就构成了一个最基本的领域结构(或者说业务结构,业务对象是什么,业务对象从何处而来)。

回到云盘的例子,我们的代码其实还是没有满足上面的分层结构,对于Factory和Repository,有NodeDTO和SceneDTO等没有业务含义的对象乱入,而我们的终极目标,是在一个业务领域内,没有任何与之业务含义无关的定义或语义出现,因此,我们需要在应用层屏蔽调进入业务领域的无关对象:

scss 复制代码
 public void buildSendFileRelation(NodeDTO nodeDTO, SceneDTO sceneDTO, String params) {
     // 创建文件对象和个人云盘对象
     File file = FileFactory.createFile(nodeDTO.getId(), nodeDTO.getParentId());
     Clouddisk clouddisk = ClouddiskFactory.createClouddisk(sceneDTO.getALiId);

     // 安全服务扫描文件
     safeService.scan(file);

     // 将文件保存到个人云盘
     clouddisk.save(file);
 }
typescript 复制代码
// Entity VO Service
Class File {
    //
}

Class Clouddisk {
    //
}

Class SafeService {
   public void scan(File file) {
       //
   } 
}

// Factory
Class FileFactory {
   public File createFile(Long id, Long parentId, String ossKey) {
    Object ossObj =  ossRepository.getOssObject(ossKey);
    file = new File(id, parentId, ossObj);
    return file;
   } 
}

Class ClouddiskFactory {
  public Clouddisk createClouddisk(Long fromAliId) {
    Object cloudNode = cloudNodeRepository.getCloudNode(fromAliId);
    Clouddisk clouddisk = new Clouddisk(fromAliId, cloudNode);
    return clouddisk;
  }
}
// Repository
Interfect OssRepository {
    public Object getOssObject(Long id, Long parentId, String ossKey);
}

Interfect CloudNodeRepository {
    public Object getCloudNode(Long fromAliId);
}

业务模型(领域层)不应该依赖于应用层定义的对象或提供的能力,业务模型在自己的业务域中应该是闭环的,自给自足的。

然而,业务实例在创建的过程中,肯定是要使用到其他服务或者数据库提供的数据的,那该如何做到自给自足呢?这也是领域内Repository的另一个作用,Repository在领域内仅提供接口,不提供实现,领域内的数据诉求依靠接口实现闭环,而接口的具体实现,交给领域外去做。

基础层与领域层的分界线

领域层是一块业务概念的集合,其不应该对基础能力层有依赖关系(领域层就像一颗心脏,基础能力就像是能量获取的渠道,动物通过进食,植物通过光合作用,但是心脏不应该受进食或者光合作用的影响,他需要的只是外界往血管中输入血液与营养)。

回到我们的例子,文件Entity有一个文件owner字段,当我们调用基础能力层的具体实现完成文件保存到云盘的动作时,文件不关心自己的信息是保存到DB还是OpenSearch,文件也不关系自己的owner字段会保存到数据库的owner字段还是extend信息中,因此,上述所有的与实现细节相关的逻辑都应该从领域模型中抽离,而落在基础能力层。

以文件转发为例,文件对象:

vbnet 复制代码
Class File {
    Long id;
    String name;
    Long OwnerId;
}

文件转发:

ini 复制代码
public void forwardFile(Long fileId, Long toUser) {
    File originalFile = FileFactory.createFile(fileId);
    File newFile = FileFactory.createFile(originalFile.getName(), toUser);
    Clouddisk clouddisk = ClouddiskFactory.createClouddisk(toUser);

    clouddisk.save(newFile);
}
arduino 复制代码
Class FileFactory {
   public File createFile(Long fileId) {
       // 一些其他操作,可能是多个Repository查询结果的拼装
    return  fileNodeRepository.getFileNode(fileId);
   } 
}

Interfect FileNodeRepository {
    public File getFileNode(Long fromAliId);
}
scss 复制代码
class FileNodeRepositoryImpl impl FileNodeRepository {
    public File getFileNode(Long fromAliId) {
        Condition condition = creatCondition(fromAliId);
        Do obj = findInDB(condition);
        return covertDoToFile(obj);
    }
}

这样,就剔除了领域层对基础能力层的强依赖。

业务模型对于外界数据的汲取只应该强依赖自己为自己提供的抽象接口,至于接口在"外界"的实现方式亦或是副作用,应该被业务领域排查在外。

小结

在全文,没有从概念上解析DDD到底是什么,也没有对贫血、充血模型,聚合、聚合根、领域消息等概念有过多的提及与深究。我这段时间来对DDD学习的最深感受一个是概念过多,抽象且难以理解,另一个是千人千面,大家看法不一。

我目前觉得,领域驱动设计的本质不是概念的堆叠,也不是设计模式的套用,领域驱动设计的本质是业务驱动设计,好的领域模型,其代码实现应该是趋近于自然语言的,能够被内行的非技术人员读懂的领域代码,才是好的领域设计,至于具体的技术实现,应该被排除在领域之外。

相关推荐
java小吕布23 分钟前
Java中的排序算法:探索与比较
java·后端·算法·排序算法
Goboy1 小时前
工欲善其事,必先利其器;小白入门Hadoop必备过程
后端·程序员
李少兄1 小时前
解决 Spring Boot 中 `Ambiguous mapping. Cannot map ‘xxxController‘ method` 错误
java·spring boot·后端
代码小鑫1 小时前
A031-基于SpringBoot的健身房管理系统设计与实现
java·开发语言·数据库·spring boot·后端
徐小夕2 小时前
Flowmix/Docx 多模态文档编辑器:V1.3.5版本,全面升级
前端·javascript·架构
Json____2 小时前
学法减分交管12123模拟练习小程序源码前端和后端和搭建教程
前端·后端·学习·小程序·uni-app·学法减分·驾考题库
monkey_meng2 小时前
【Rust类型驱动开发 Type Driven Development】
开发语言·后端·rust
落落落sss2 小时前
MQ集群
java·服务器·开发语言·后端·elasticsearch·adb·ruby
Smile丶凉轩2 小时前
微服务即时通讯系统的实现(客户端)----(1)
微服务·云原生·架构
大鲤余3 小时前
Rust,删除cargo安装的可执行文件
开发语言·后端·rust