领域驱动设计(游戏框架)——从理论到实践的复盘

什么是领域驱动设计领域驱动设计(Domain-driven design)简称DDD,网上有很多关于DDD的介绍,也有许多专门的书籍去讲述。(推荐一本领域驱动

什么是领域驱动设计

  • 领域驱动设计(Domain-driven design)简称DDD,网上有很多关于DDD的介绍,也有许多专门的书籍去讲述。(推荐一本领域驱动设计(精简版)(提取码:52dx))总结一下,领域驱动设计是一套应对复杂软件系统分析和设计的面向对象建模方法论。闭上眼睛回想一下,什么是核心域、子域,什么是限界上下文,聚合根和实体有什么区别,值对象又是什么。如果你能快速的回想起这些是什么个概念,相信你已经对DDD有了个大致的了解。

实践出真知

  • 领域驱动设计是从后端演变而来,既然它是为了应对复杂的软件系统,那么像我们在应对大型的复杂游戏,在前端开发框架的设计上是否可以借鉴呢?我们把最核心的业务,即我们的游戏业务,用DDD作为指导思想,进一步进行划分。

版本1.0

  • 战略设计 从战略设计开始,我们规定在拿到策划案的开始,并不是就开始进行业务编程。按照DDD的思想,战略设计是需要领域专家的参与,但是在游戏业务中,我们认为我们的一线开发人员跟我们的策划爸爸们,他们就是最好的领域专家。他们熟悉核心的业务流程,是产品的把控者。战略设计最重要的一点是划分出限界上下文,比如在游戏业务中,我们可以通过策划案的一些概念,划分出英雄养成系统、活动、战斗等。战略设计相对来说是比较容易的,这一块在游戏业务中我们也不会太去强调
  • 战术设计 主要是从上下文内部,分析内部名词的组织关系
    • 名词建模法。名词建模法是一个很好的方法,把一个限界上下文的东西,有条理的划分出一个聚合内的东西。具体方法是从策划案中提取关键的名词,并将名词规整到一个个的聚合。通过分析合并聚合或者拆分出更详细的聚合(这里的划分可能会随着迭代不断地演进)。

      接下来就可以得到我们的聚合根和实体了。一般来说我们是把一个聚合里面最核心的一个实体(名词)作为我们的聚合根。比如说一个英雄的聚合,他可能包含有英雄这个实体,然后还有英雄所持有的道具,英雄所能影响的城池等。我们可以很明显的把英雄这个实体提升为我们的聚合根,其他的就相应的作为实体了。也可以参考四色建模法,本质上是一样的,只是这里删减了一些步骤,让开发更快的切入到业务中。
  • 建模绘制 这里的建模绘制其实是战术设计的一部分,也是最重要的一部分。通过模型图,我们可以从数据(model)出发,更加聚焦于我们的核心业务,而不至于被UI的表现所影响。这里借助了Visual Paradigm这个工具来绘制我们的模型图。模型图上要把我们的实体、聚合根、聚合根之间的关系体现出来。为什么选择Visual Paradigm来帮助我们,原因是这个工具可以一键生成C#的类壳,我们在绘制完模型图后可以直接导出代码。通过与服务端策划一起,构建模型图,商讨出网络协议(Proto、json等),伴随着这个过程又不断调整我们的领域模型,最终有个基础版本,生成我们的业务代码架构。(如下图)

迭代与进化

浮于上层的架构必然无法成为一个好的架构,也没有一套绝对的架构可以适用于所有的场景。基于版本1.0我们在公司内部进行推广,在持续了几个迭代后,通过DDD讨论大会上,我们总结与反思,列出了几个开发的痛点:

  • 领域层一般包含实体、值对象、领域服务、聚合、聚合根、仓储、工厂等概念,因为涉及的概念众多并且持久化需要配合CQRS + ES 模式,而且掌握起来有相当门槛
  • 领域设计流程臃肿,VP使用成本代价高
  • DDD建议的充血模型,在游戏客户端业务上比较难以充血,更多的人偏向于丝血模型,甚至是贫血模型。

当然也得到了一些优点的反馈:

  • 战略设计可以对整个系统有个大的把控,在前期策划会时可以提出一些程序的建议和疑问,进行交流解答,更加详细的确认需求
  • DDD带来的后期维护,细化复杂系统的优势
  • 从数据出发理解业务需求,不至于被UI界面影响设计。

版本2.0

摒弃不足,基于版本1.0我们进行了优化和改进。

  • 首先我们保留DDD的战略设计,调整DDD的战术设计。我们保留了前期对于整个需求分析的头脑风暴,依旧倡导大家从数据出发去理解需求
  • 聚合内实体采用贫血模型。通过分析我们发现客户端的业务,或者说养成这个品类的游戏在客户端这一块确实很难进行充血。大量的游戏数据都上传到服务端,客户端只是进行简单的数据映射,进行表现,甚至于有一些是纯表现项的东西。
  • 引入领域层事件总线(DomainEventBus),取消领域服务(DomainService)。领域层事件总线可以实现修改数据直接派发事件,由应用层直接监听进行逻辑响应或者视图映射,避免了臃肿的响应流程。而我们在实践中发现,在领域服务的业务中也只是对数据的操作,那么为什么不能由聚合根暴露操作借口来进行操作呢?这里有的小伙伴会说,领域服务也可以用来组织多个聚合,进行多个聚合甚至是多个限界的数据操作。这边我们也是出于在实践过程中发现,编码人员很难区分应用服务和领域服务,并且我们遵循一个原则:那就是对数据的读取应该是随意的,对数据的更改应该是谨慎的。既然我们已经规范了只能由聚合根暴露接口来进行数据更改,那么就可以不要领域服务来进行协调,因为领域服务的业务太单调了,单调到可以由应用层来组织。当然框架依旧保留了领域服务的接口,开发人员可以选择性的去使用。
  • 规范化数据对象设计。取消vp工具,而采用ProcessOn。取消标准的领域建模图,而采用ER图进行分析。强调三范式,基于三范式来进行实体和聚合的划分,减少上手的学习成本。

总结

没有通用的框架设计,只有实用的框架设计。我们在版本2.0的迭代后,确实对开发上起到了一个更好的正向的作用。当然也在这个过程中提出了一些疑问,比如说我们的领域仓储是否有必要,是不是可以把我们的聚合变成我们的DataModel,直接静态进行调用(现在是以IoC注入的形式)。依然有很多不足,希望后面能够随着迭代慢慢优化。