CloudNotes之领域建模篇:领域模型简介

CloudNotes领域模型还是相对简单的,并不一定需要采用面向领域驱动的设计方法来解决CloudNotes的领域问题。但出于以下几个方面的原因,我还是采用了面向领域驱动的方式来开发CloudNotes:

  1. 领域驱动是企业级应用开发的一种指导性模型,以领域模型作为软件开发的中心,符合解决问题的基本思路
  2. 现有的企业级应用开发框架对面向领域的开发模式支持得越来越好,如果选用这种方式,可以在CloudNotes中更好地利用这些框架的最新功能,为系统开发寻求新的机遇
  3. 自己对领域驱动设计相对比较熟悉,而且也维护了一套自己研发的DDD开发框架Apworks。在CloudNotes中直接复用该框架,可以大大减小开发投入,缩短开发周期
  4. 温故而知新,选用DDD来指导CloudNotes开发,可以获取到有关DDD的更多信息

接下来,让我们一起对CloudNote的领域模型作些简单的了解。

基本模型

如果你使用的是Visual Studio 2013/2015旗舰版(Ultimate Edition),那么在打开CloudNotes解决方案后,你可以在CloudNotes.Design项目下,找到CloudNotesModel.classdiagram类图设计文件:

image

双击该文件,可以打开CloudNotes的领域模型设计视图。到目前为止,CloudNotes的领域模型如下:

image

咱们暂且不考虑C# class、aggregate root等这些UML构造型,这些内容我会在接下来的文章中详细介绍(顺带会介绍Visual Studio对于模型设计与自动化代码产生的支持)。从该图可以看出,CloudNotes的领域模型还是相对简单的。基本上可以分为三个大部分:笔记、用户以及客户包(ClientPackage)。或许你会考虑,是否可以将这些部分看成是DDD的界定上下文(Bounded Context)呢?

界定上下文(Bounded Context)

是的,我们可以考虑将CloudNotes的领域模型划分为三个界定上下文:

  • 笔记界定上下文:管理系统中所有的笔记内容
  • 用户认证与授权上下文:管理系统账户、角色以及权限
  • 客户包管理上下文:管理针对所有客户端平台的升级包

从DDD角度看,由于模型被划分为多个上下文,而且上下文中的子模型也都是高内聚的(界定的),因此有可能会产生概念或语义上的二义性,而这又是通用语言(Ubiquitous Language)所不能容忍的。比如,一个经典的例子就是银行系统里“Account” 的概念:一个提供在线服务的银行系统,简单地说可以粗略地分为两个界定上下文:银行业务以及在线服务。对于银行业务而言,Account表示客户的银行账户,而对于在线服务而言,Account则又表示客户的在线登录账号,而且一个在线登录账号下可以承载多个银行账户,好比招商银行一网通账号可以有多个银行卡账户和信用卡账户等。那么在做系统设计的时候,遇到Account概念时,如何保证团队交流的准确性呢?因此,需要在领域模型中有一个能够解除这种二义性的“组件”,负责帮助团队人员在整个领域模型的理解与交流上保持一致。这种“组件”就是平时常说的“上下文映射(Context Map)”。有关界定上下文以及上下文映射的详细介绍,可以参考这篇文章:Strategic Domain Driven Design with Context Mapping

当应用程序所需处理的领域变得很大时,引入界定上下文是很有必要的,从实践角度考虑,使用界定上下文不仅可以在一个相对封闭的范围内,使用一个相对较小的子领域模型,而且还可以从一定程度上提升应用程序的性能:比如某个操作只会发生在系统的某个子领域内部,又如较小的领域模型能够减少系统的处理开销。在Entity Framework 代码先行(Code-First)的开发过程中,可以很好地引入界定上下文的概念,以将复杂的领域模型划分成多个相对较小的简单的领域模型,不仅在模型设计还是在性能上,都有着很好的表现。有关这个话题的更多内容,可以参考我之前翻译的一篇文章:Entity Framework模型在领域驱动设计界定上下文中的应用

CloudNotes的领域模型相对简单,而且虽然目前后台采用的是Entity Framework,但使用Entity Framework并不是必须的,今后有可能会换成其它的数据持久化系统(比如MongoDB),因此也没有按照上面所述的方式应用界定上下文的相关概念。

实体键

由于CloudNotes使用了Apworks框架,因此CloudNotes领域模型中的实体都是使用Guid作为实体键的。这一问题就要引入对Apworks框架的讨论。在Apworks框架中,我们可以看到以下代码:

public interface IEntity
{
    /// <summary>
    /// Gets or sets the identifier of the entity.
    /// </summary>
    Guid ID { get; set; }
}

在Apworks中,实体键是Guid类型的。有关实体键的类型选择,在Vaughn Vernon的《实现领域驱动设计》一书中,有相关的描述。概括起来,大致有以下几种情况:

  • 用户指定实体键:这种方式最为直观:用户在界面上直接提供某个值,作为实体的键值。但通常而言,实体键值应该是不可变(immutable)的,于是,当用户为某个实体选定了实体键值后,一般是不允许修改的。但这样做又显得跟标准的用户操作流程不相符:我自己输入的东西,凭什么不让我改?另一种情况就是,用户可以修改实体键的值,但这种修改或许还会带来更多的影响,例如某个实体键发生变化时,由该实体发起的所有事件数据中针对该实体的引用也要发生改变。当然,如果应用程序需要保证实体键的可读性时,我们就不得不设计一套合理的机制,来确保可读性和可行性不会发生冲突
  • 应用程序产生实体键: 很多应用程序会通过应用程序来产生实体键,比如通过应用程序开发框架提供的Guid/UUID生成算法来产生实体键。使用Guid不仅简单而且相对比较高效,更重要的是,它不需要依赖于应用程序以外的任何外部机制,比如持久化系统等。此外,正如《实现领域驱动设计》一书中所述,有些对性能要求很高的系统,会在一个独立的服务器上(或者有可能是一个后台的服务上)缓存一定数量的ID值,使用Guid就不会产生因ID服务器崩溃或重启而出现ID值重复或不连续的情况,因为Guid的生成程序始终能够保证ID值的唯一性。另一方面,如果需要通过应用程序来产生实体键值,那么我们就需要完美地解决ID值的同步问题,而且还需要考虑性能问题,更进一步,企业级应用系统往往都是由多个应用程序组成的,是否,以及如何保证实体键在这些应用之间的统一,也是一件不容易的事情
  • 借助持久化机制产生实体键:这种情况非常常见,例如ORM系统会借助关系型数据库为实体设置键值。这也是一种简单方便的ID值获取方式,因为可以借用持久化机制来获得ID值缓存的功能,而且还能保证ID值的唯一性。但这往往也并不高效,毕竟需要借助外部系统,持久化机制的性能表现,也将对应用程序本身的性能造成影响甚至成为性能瓶颈。对于大型分布式企业级应用解决方案而言,也存在ID值的同步和统一的问题,例如应用程序A和B采用了不同的持久化机制,然而却无法保证原本应该保持一致的实体键值
  • 外部界定上下文指定实体键:这种方式并不常见,在大型的企业级应用系统解决方案中可以用到,但也并不一定会采用这种方式。它是通过领域事件的方式,在界定上下文之间进行信息传递,比如本地界定上下文(Local Bounded Context)通过订阅来自外部界定上下文的领域事件,并通过事件中的信息,以在本地界定上下文中还原实体,于是ID值也一并被还原过来,以便表示当前实体与外部界定上下文中另一对象是同一实体

很显然,Apworks采用的就是上述Guid的方式,这样做实现起来比较简单高效,而且Guid值类型的特性,在绝大多数存储系统中都能很好地支持。当然它也有一些弊端,比如可读性差,以及在某些存储系统中性能并不算太好等。无论如何,就Apworks框架本身而言,应该提供一套实体键产生的框架,以便开发人员能够扩展ID值的产生机制,而不应该将ID值类型限定在Guid之上。这在老版本的Apworks中是有类似机制的,只是在考虑到装箱、拆箱带来的性能问题后,就在新的版本中,把这种机制给取消了,也就演变到目前的这种情形。今后我还是会继续改进这部分的设计的。

总结

CloudNotes的领域模型相对简单,实现上采用了Apworks框架,并借用了Visual Studio Ultimate的Architecture功能,对领域模型进行可视化设计和自动化代码生成。或许在今后领域模型会进一步修改,或者逐渐变大,或许也会由此而采用一些新的技术,并产生新的解决方案。到目前为止,CloudNotes的领域模型规模还是很小的,本系列文章也会首先针对当前的这个模型来进行介绍。

posted @ 2015-02-22 20:46  dax.net  阅读(3304)  评论(4编辑  收藏  举报