DDD分层架构之我见

前面介绍了应用程序框架的一个重要组成部分——公共操作类,并提供了一个数据类型转换公共操作类作为示例进行演示。下面准备介绍应用程序框架的另一个重要组成部分,即体系架构支持。你不一定要使用DDD这样的架构,使用单层架构和普通三层架构一样可以,不过你如果希望获得更进一步的复用性和封装度,使用更加面向对象的技术是必经之程。

我在2010年以前还在使用古老的ASP.NET WebForm和原始的Ado.Net。之前我有个观念:.NET技术发展太快,跟着微软屁股后面跑太累,所以只使用它一些原始的东西,自己封装一下也能满足工作上的需求。对于像Linq这样的技术只是随便看了下,特别是当时很多人告诉我Linq已死,千万别学,我当时很喜欢这样的言论,因为不学习新知识就有了充分的理由。

到了2010年,我有一次上博客园,浏览了一些文章,发现充满各种缩写和名词,什么Dto、工作单元一类,我才知道新一代的.Net技术已经开始普及,我已经Out了。之后我开始学习EF和MVC,在刚开始接触EF的时候,我从一些博客了解到,为了发挥EF的威力,必须使用DDD进行设计。为了扫清拦路虎,我购买了几本DDD的书来学习,学习过程中才发现面向对象和敏捷开发才是关键,于是开始大量购书,一发不可收拾,四年时间买了接近两百本后,终于把基础补起来一点。

DDD的核心思想是描述如何使用面向对象的方法对业务领域建模,怎样获得更好的领域模型。虽然看了不少DDD的资料,但还是感觉它异常抽象,另外面向对象的思想也很难进步,可能和前些年的编程习惯有关,已经习惯于从数据的角度考虑问题,形成了思维定势。

DDD虽然抽象,但它还是提供了一些技术上的支持。大部分人都是从DDD分层架构入手来进行学习和实践,当然,DDD并不是分层架构,分层架构只是DDD的一个技术构造。下面简单介绍一下我对DDD分层架构的理解,由于我使用DDD的时间不长,我所描述的观点都是我自己的一些开发经验,不一定正确,欢迎各位高手批评指正,共同进步。

DDD分层架构总体上和三层架构相似,不过对各层提出了更具体的职责和构造块。我也经常与一些在使用DDD分层架构的朋友交流,我问他们DDD分层架构与普通三层架构有何区别,大部分人都感觉差不多,除了一些名词术语有所变化。如果你也是这个感觉,那么可能本文对你是有帮助的,因为我明显感觉出它们之间有所不同。

DDD分层架构与传统三层架构示意图如下。

(领域驱动设计分层架构示意图)

(传统三层架构示意图)

DDD分层架构与传统三层架构最重要的区别可能是重心不同,即传统三层架构的重心在业务逻辑层,而DDD分层架构的重心在领域层。

面向对象设计的核心是基于业务概念建模,并映射到代码中,这样的好处是减轻程序员将业务概念转换到技术的负担,因为更容易理解。传统三层架构虽然也把业务概念转换到实体层的Model对象,但实体层只是一个辅助设施,这些Model只是用来装数据的容器,作用并不显著。DDD分层架构把领域层提到核心地位,这些Model成为业务逻辑的一个主要放置场所。

使用DDD分层架构的第一个好处就是业务逻辑高度内聚到领域层,换句话说,如果有逻辑问题,找领域层就对了。对于这一点,有些人认为传统三层架构也可以,找业务逻辑层不是一样吗?这可能是大多数对领域驱动设计分层架构认识无法突破的关键。

虽然你可以按照分层架构的要求,把全部业务逻辑都写到BLL层,但你无法精确定位你需要的业务逻辑究竟处于什么位置,换句话说,你需要业务逻辑的一个唯一访问点。由于你无法轻易找到业务逻辑的访问点,所以产生冗余代码就再所难免,一段相同或相似的冗余代码会在多个地方产生,从而导致可维护性的降低。通过强制约束代码和目录规范以及提取公共方法可以缓解部分问题,但要从根本上解决,你还得向面向对象求救。

那么,哪里是业务逻辑最好的落脚点,最直观,最容易被大家想到的唯一访问点在哪呢?比如你要处理一个订单,让你到其它地方去找处理订单的代码,你自然找起来困难。那么如果这段代码处于订单实体的内部,情况就大不相同了,你可以在最短的时间内找到它。在领域实体中内聚业务逻辑,可以为你创建一个业务逻辑的唯一访问点。大家以后需要某个逻辑的时候,先看看实体中有没有自己需要的,这样就能显著降低代码冗余,从而更好维护。

所以,我的第一条DDD使用经验就是,使用充血模型,将业务逻辑尽量放到领域实体中。充血模型有很多争论,不过你大可不必理会别人的说法,自己实践才能出真知。用得不爽,你后面不用就是了,对你基本没啥影响。目前我使用充血模型,感觉它主要的问题是,如果采用分布式架构,比如中间采用WCF远程调用,需要通过一层专门的DTO来进行传输,而且需要增加一个远程外观的服务,会导致工作量上升。

当把充血模型用起来以后,下一步是要把聚合用起来。聚合这个概念很好理解,就是包含关系。在UML中有两种包含关系,第一种叫聚合,表示比较弱的包含关系,聚合内部的东西在外面可以直接访问。第二种叫组合,即组成聚合,是很强的包含关系,表示外部的对象由内部的多个子对象组成,内部的子对象在外面不能直接访问,必须通过外部的对象间接引用。DDD虽然用了聚合这个词,但它表示UML中的组成聚合,所以它把外部的对象称为根,即聚合根,要访问内部对象,必须先访问聚合根。

概念上的理解,除了能吹吹牛以外,没多大帮助。我在刚接触DDD的时候,也能理解聚合的概念,说起来一样口沫横飞,但真正用起来过了差不多一年。除了我反应比较迟钝以外,还有一个原因是被之前以数据为中心的思维定势所束缚。

我也经常下载一些DDD的Demo来学习,但是这些例子大多都非常简单,所以我主要还是依靠看书和自己摸索。我刚开始的用法是一个表对应一个领域实体,每个领域实体对应一个仓储。我在使用的过程中,隐隐发现哪里不对,但是无法找出具体的原因。经过大半年,我也使用DDD开发了几个简单的项目,逐步积累了一些经验,在一次看书的时候,我突然领悟到我的DDD用法主要毛病是依赖关系混乱,而解决这些依赖关系的手段就是聚合。

聚合的主要影响是显著减少仓储数量,以及集中管理高度依赖的相关实体。把高度相关的实体内聚到一个聚合中,可以把这些依赖关系封装到一个更小的空间,外部只与聚合根打交道,与聚合内部子对象的依赖关系就会明显降低。一个聚合对应一个仓储,而不是一个实体对象一个仓储,可以减少仓储数量,从而进一步降低依赖关系。

后面我重新阅读了一些博客和书籍,发现别人其实都说清楚了,只是自己当时看过去没有理解而已,这真是纸上得来终觉浅 绝知此事要躬行。

  我的第二条DDD使用经验是,把高度相关的实体封装到聚合中,为每个聚合根创建一个仓储。

观察上面的DDD分层架构示意图,会发现领域层只依赖于应用程序框架服务,仓储采用了接口分离模式将实现和接口分离到不同的程序集,领域层中只包含仓储的接口,这个设计让领域层非常纯净,和外部的依赖关系降到最低。这对我们意味着什么?更低的依赖让我们可以方便的对业务逻辑进行单元测试,特别是采用了TDD方式的话,这一点将显得尤其重要。我们可以在单元测试中使用模拟框架对仓储以及外部依赖进行模拟测试,从而大幅度提升业务逻辑的稳定性和健壮性。

另外,观察传统三层架构,业务逻辑层一般直接依赖数据访问层,让单元测试变得困难,从而转向更粗粒度的集成测试。

通过上面的分析,可以看到采用DDD分层架构可以获得比传统三层架构更好的可复用性、可维护性、可测试性等。

当然不可能把所有业务逻辑全部放入领域实体中,有些功能需要操作多个实体,或者需要使用某些设计模式,这时候需要使用领域服务。这里的要点是尽量把领域服务的操作委托给领域实体,因为这样业务逻辑可以更加集中。

DDD分层架构还有一些构造块,我会在后面的文章详细介绍。如果没有介绍到的,说明我还处于学习和摸索阶段,还没有多少心得,等我有些经验以后再告诉大家。

现在来总结一下。

   使用DDD分层架构有哪些好处

  • 帮你更集中的管理业务逻辑。

  • 帮你降低各层间,以及各业务模块间的依赖关系。

  • 帮你更方便的进行单元测试。

我的DDD分层架构使用经验

  • 使用充血模型,将业务逻辑尽量放到领域实体中,领域实体为业务逻辑提供一个唯一访问点。

  • 不能放入领域实体的逻辑,尽量放到领域服务,总之,业务逻辑应该高度内聚到领域层。

  • 把高度相关的实体封装到聚合中,为每个聚合根创建一个仓储。

最后,提醒一下,我们使用一些DDD分层架构构造块,可能并不算真正用上了DDD。但是,我们的目标是使用DDD吗?不是,我们的目标是把业务逻辑做得更稳定,更好维护。所以不用在意自己使用的技术正不正宗,标不标准,只要比以前更好,就应该坚持下去。

应用程序框架实战