记录DDD的学习

介绍

传统的MVC架构模型中,Model包含所有的数据逻辑,View包含前端界面,Controller负责控制数据如何展示,容易造成以下问题:

  • Controller层非常臃肿
  • 面向数据库表编程
  • Model是一个纯数据的贫血模型
  • Model之间的关系复杂
  • 不方便测试

DDD是Eric Evans在2003年出版的《领域驱动设计:软件核心复杂性应对之道》(Domain-Driven Design: Tackling Complexity in the Heart of Software)一书中提出的具有划时代意义的重要概念,是指通过统一语言、业务抽象、领域划分和领域建模等一系列手段来控制软件复杂度的方法论。

其中包含战略设计和战术设计:

  • 战术设计
    • 代码结构规范
      • 实体、值对象、聚合
      • 工厂
      • 仓储
    • 架构模式
      • CQRS
      • EDA
      • 分层
  • 战略设计
    • 思想
      • 统一语言
      • 业务为核心
    • 原则
      • 限界上下文
      • 领域模型隔离
    • 规范
      • 领域间的关系
      • 上下文映射

概念

  • 领域,一个领域本质上可以理解为就是一个问题域,只要是同一个领域,那问题域就相同。所以,只要我们确定了系统所属的领域,那这个系统的核心业务,即要解决的关键问题、问题的范围边界就基本确定了。从字面上的能够知道,领域其实就是我们的范围,而范围实际上就是我们的边界,范围定义下来之后,我们根据这个范围去计划我们要做的事情。这就是领域驱动设计。
    • 比如你现在要去做一个电商网站,那么我们的进货,优惠规则,物流仓储,销售报表,这些直接跟业务相关的东西都归属于领域,而所谓领域驱动设计就是需要预先把领域所涉及到的数据,流程以及业务规则搞清楚,然后再通过面向对象的方式去建立一个模型,这个模型就叫做领域模型。我们再去选择合适的技术去对他进行实现。
  • 限界上下文,在DDD中,解系统可以映射为一个个限界上下文,限界上下文就是软件对于问题域的一个特定的、有限的解决方案。
  • 子领域
    • 核心域,指领域中最核心的部分,通常对应企业的核心业务(如商品等…)
    • 支撑域,是一种特殊的子域,是指为了实现核心业务而不得不开发的业务所对应的相关知识的集合(如促销活动等…)
    • 通用域,是另一种特殊的子域,对应的是业界已经有成熟方案的业务(登陆,push,短信通知等)
  • 贫血模型,对象里只有get和set方法,或者包含少量的CRUD方法,所有的业务逻辑都不包含在内而是放在业务层。
  • 充血模型,大多业务逻辑和持久化放在Object里,业务层只是简单封装部分业务逻辑以及控制事务、权限等。
  • Data Object(DO、数据对象),DO仅仅作为数据库物理表格的映射,不能参与到业务逻辑中。为了简单明了,DO的字段类型和名称应该和数据库物理表格的字段类型和名称一一对应。
  • Entity(实体对象),是我们正常业务应该用的业务模型,它的字段和方法应该和业务语言保持一致,和持久化方式无关。
  • DTO(传输对象),主要作为Application层的入参和出参,比如CQRS里的Command、Query、Event,以及Request、Response等都属于DTO的范畴。DTO的价值在于适配不同的业务场景的入参和出参,避免让业务对象变成一个万能大对象。 ppPc0Ld.png
  • 值对象,一种特殊的领域模型,不可变,通过值判断同一性。
  • 聚合和聚合根,聚合是用来确保这些领域对象在实现共同的业务逻辑时,能保证数据的一致性。每个聚合内部有一个外部访问聚合的唯一入口,称为聚合根。每个聚合中应确定唯一的聚合根实体。
  • 工厂(Factory),在设计模式中我们通过工厂实现多态对象、单例对象或某种需要特殊构造的对象的实例化。在 DDD 中,当我们需要一个新的实体的时候,我们就需要通过工厂来生成实体,保证实体创建即一致(区别于直接NEW对象)。
  • 仓储(Repository),主要有两个核心功能:持久化实体,将实体持久化到底层的数据库中;重建实体,实体持久化后当我们需要再次使用的时候需要从数据库的数据重建实体
  • 防腐层,用于做适配器、增加缓存、逻辑兜底、方便测试、实现功能开关

案例分析

设计一个转账服务:

  • 用户可以通过银行网页转账给另一个账号,支持跨币种转账。
  • 同时因为监管和对账需求,需要记录本次转账活动。

原始代码如下, ppP6CH1.png 上面代码存在以下问题:

  • 可维护性
    • DB结构变化影响服务
    • 中间件或者依赖库改动会影响代码变更
    • 第三方服务API接口变化,造成服务不稳定
  • 可扩展性
    • 数据格式兼容性差
    • 业务逻辑无法复用
  • 可测试性
    • 设施搭建困难
    • 耦合度高

改造后代码如下: ppPyxc4.png Entity是拥有ID的域对象,除了拥有数据之外,同时拥有行为。 Repository负责Entity对象的存储和读取,具体实现里完成数据库存储的细节。

ppP6FN6.png 抽象第三方服务和中间件,增加防腐层,被依赖的外部系统可能包含不合理的数据结构、API、协议或技术实现,如果对外部系统强依赖,会导致我们的系统被"腐蚀"。

ppP6iAx.png 用Entity封装单对象的有状态的行为,包括业务校验

ppP6pu9.png 用对象封装跟实体无关的无状态计算逻辑

ppPyzjJ.png 用Domain Service封装多对象逻辑

ppP69BR.png

最后

DDD一方面使用分而治之的思想,引入划分领域、限界上下文、模块分层、划分聚合在不同层次、不同粒度来降低问题的复杂度。另一方主张聚焦领域逻 辑,通过不同手段来减少业务和技术的耦合。因此DDD只是大部分软件设计思想一种,软件设计的本质都是为了高内聚低耦合。但是DDD并不是万能的,不 是所有业务开发场景都适合用DDD。有些简单业务场景不使用DDD反而更恰当。因为DDD有较高的学习门槛,需要整个团队形成统一认识和协同,需要相应 的编码规范和架构落地。因此学习和落地DDD时要时刻记住自己的出发点是为了应对现在或者将来的复杂业务领域而来。不必太拘泥于某些点是否遵守了 DDD原则,如果觉得用了DDD部分理念比没有用好一点点,也值得迈出这一步。