Skip to content

Latest commit

 

History

History
117 lines (82 loc) · 7 KB

浅谈ddd在前端的应用(介绍).md

File metadata and controls

117 lines (82 loc) · 7 KB

浅谈DDD在前端的应用(介绍)

前言

先引用Angular官网的一段话

Angular 把组件和服务区分开,以提高模块性和复用性。 通过把组件中和视图有关的功能与其它类型的处理分离开,你可以让组件类更加精简、高效。 理想情况下,组件的工作只管用户体验,而不用顾及其它。 它应该提供用于数据绑定的属性和方法,以便作为视图(由模板渲染)和应用逻辑(通常包含一些模型的概念)的中介者。

从上文中我们可以看到,在我们使用前端框架编程的时候不应该只看到组件,还有包括不限于模型服务(service)等概念,现在就来讲讲DDD中的领域模型。

领域,指的特定行业或者场景下的业务逻辑。

什么是领域模型

DDD 中的模型是指反应 IT 系统的业务逻辑和状态的对象,从具体业务(领域)中提取出来,故又名领域模型。 通过对实际业务出发,而非马上关注组件、页面设计。通过识别出固定的模式,并将这些业务逻辑的承载到模型上,模型负责处理业务逻辑,并表达当前的系统状态。

我们做的某某系统,实际上是对现实世界中的映射。

为什么要建模

  • 提取心智模型,显性化,让不同的人对业务的理解达成一致;
  • 归类复用,避免重复的工作,让人可以关注更高层面的事务。

编写业务代码并不是创造新的业务,只不过是将现实中的业务逻辑在计算机系统中实现。 按照面向对象设计的思想,例如系统是一个电子餐厅。现实餐厅中的实体,应该映射到系统中,用于承载业务。例如服务员、顾客、订单、餐桌、菜品,这些虚拟的实体表达了系统的状态,在某种程度上就能指代系统,这就是模型。 如果找到了这些元素,就很容易设计出软件。即使一个不开发软件,现实中也具备其业务模型。

使用领域驱动设计还有一个好处,我们可以通过隔离这些技术细节,先将业务逻辑建模,然后再完成技术实现,因为业务模型已经建立,那么剩下的技术细节就是响应用户操作和划分组件。

怎么建模:贫血模型 vs 充血模型

先来看看领域驱动中的两种模型

贫血模型

贫血模型是指使用的领域对象中只有setter和getter方法,它虽然比较直接,但是我们很难知道对象的变更过程(为什么要变,变得条件是什么,怎么变得,变成什么样了)。 当我们使用贫血模型的时候,业务逻辑都在组件中,我们就很难了解对象状态的变更过程,当组件之间的数据存在互相流动时,就更难理解对象状态的变更过程了。同时,组件中持有大量对象的状态处理逻辑,也会出现很多重复的代码。

缺点:逻辑集中在视图层,导致视图层太过厚重,面向事务脚本。容易导致将逻辑全部写在组件中,组件应该专注于视图层。

充血模型

充血模型充血模型将大多数业务逻辑放在领域对象中,C层只是完成对业务逻辑的封装、权限等的处理。比较符合面向对象,可以让视图层变得很薄。

在DDD中一般是鼓励使用充血模型

其中,实体(Entity)是领域模型的核心,一个Entity是对单个业务进行描述的完整信息集合。Entity包含了业务对象的能力,本质上就是描述这个业务都拥有哪些特征,以及内部的约束逻辑。 在前端语境下,领域模型在代码层面,就是一个class类。而这个Model类需要包含对实体的描述,需要实现事件系统,必要的时候需要实现服务(用静态属性即可),举个例子:

class Good {
  type = ''
  price = 0

  #events = []
  
  increasePrice(num) {
    this.price += num
    this.#events.forEach((item) => {
      const { type, fn } = item
      if (type === 'priceIncreased') {
        fn()
      }
    })
  }

  onPriceIncreased(fn) {
    this.#events.push({
      type: 'priceIncreased',
      fn,
    })
  }

  static create(goodData) {
    const { type, price } = goodData
    const instance = new this()
    instance.type = type
    instance.price = price
    return instance
  }
}

这是一个针对Good的纯业务模型,不涉及任何和界面相关的信息。它包含了实体信息type, price, increasePrice,也包含了事件系统#event,还包含了用于创建Good实例的服务create。在VM中,它可能被使用:

const good = Good.create({
  type: 'ball',
  price: 12.5,
})

good可能被用于界面上某处的渲染vm.good = good,此时的good和Good已经脱离了模型,成为运行时的状态,可以作为状态在视图中使用了。某些情况下,一个模型的实例可能包含另外一个模型的实例,此时,模型之间就产生了依赖关系。例如

class Shop {
  goods = []
  addGood(data, count) {
    for (let i = count; i --;) {
      const good = Good.create(data)
      this.goods.push(good)
    }
  }
}

这是一个商铺模型,一个商铺中可能包含一堆商品,通过addGood方法向商铺中添加商品,该店铺的商品都被放在goods属性中。但是这里有一个问题,如果我们要从商铺下架一个商品怎么办?可以在Good中加一个下架方法吗,这样就不需要调用Shop的方法,而是可以直接对Good实例进行操作?虽然实践层面这是可行的,但是这是错误的设计。Clean Architecture 提供了一种有关数据分层的设计,外层实体依赖内层实体,对于内层的实体,不需要知道外层实体对它的需求。如果在设计时,内层实体还要考虑外层如何使用它,那么这是“不干净”的架构。对于领域模型而言,它所要描述的是自身所拥有的特征和能力,描述时,不需要,也不应该考虑外部环境,它被谁使用,如何使用,对于它本身而言并不需要关系。领域模型是对业务对象的纯粹描述,和业务运行过程中的环境无关。

补充

真正自然的“领域模型”应该是这样的,如果对象的某些行为在任何场景都是通用的,那么就放在领域中去,将其绑定,这是尊重“共性”的约束;如果对象的某些依赖于具体的场景,那么则在具体的场景中注入相应的行为,赋予对象相应的角色,这是尊重“个性”的自由。所以,对象的行为该不该放入“领域模型”,我们要先分析一下这些行为是对象所固有的,还是依赖于场景的,如果是固有的,即是共性的,就放入领域模型(domain),如果不是则延迟在具体的场景(service)中注入,赋予其角色的个性(DCI)。

总结

并不是说DDD必须原模原样的在前端实现。DDD是一个很重的概念,最佳实践都是针对后端的,前端可能并不完全适配,因此,以DDD为设计思想,在前端特殊性做出一些调整,才是明智的选择。