从去年10月份开始,学了几个月的领域驱动设计(Domain Driven Design,简称DDD)。主要是学习领域驱动设计之父Eric Evans的名著:《Domain-driven design:领域驱动设计:软件核心复杂性应对之道》,以及另外一本Martin Flower的《企业应用架构模式》,学习到了不少关于如何组织业务逻辑方面的知识。另外,在这个过程中也接触到了一些开源的架构和一些很好的思想。如:命令查询职责分离(Command Query Responsibility Segregation,简称CQRS),事件驱动架构(Event Driven Architecture,简称EDA),以及四色原型和DCI架构,等等。前面这些知识对我来说是非常宝贵的财富,可以说我能进淘宝,很大程度上也是因为我学习了前面这些知识的原因。

在介绍我设计的框架之前,我想先探讨一下以往我们都是如何思考设计OO的系统的。大家都知道,真正的对象应该是不仅有属性,而且有行为的。并且大家也有另外一个共识,那就是为了完成某个任务,各个对象应该会相互协作共同完成这个任务。之前我们在设计一个系统时,往往会先设计好各个对象,明确他们的职责,在这个过程中,还会考虑如何建立对象之间的关系(依赖、关联、聚合、组合),在这些关系的影响下,我们会认为对象之间应该有主从关系、依赖关系,等等。然后我们所做的这些设计最终的目的是为了能让对象之间能够通过相互协作来共同完成某个任务。这种方式最核心的设计特色是,我们会通过”对象引用“的方式来实现对象之间的各种关系。这种方式很好,并且我们也已经完全习惯了从对象的职责以及它们之间的关系的角度去设计对象。但这仅仅体现了一个哲学思想,那就是“物体之间通过直接作用完成某个任务”。

我觉得任何两个对象之间的交互有两种形式:1)直接作用,即对象A引用一个对象B,然后A调用B提供的某个方法,以此来完成两个对象之间的协作;2)间接作用,对象A不引用对象B,仅仅包含了一个对象B的唯一标识,当它要和对象B协作时,会发送一个消息给对象B,然后对象B收到该消息后做出响应,从而实现两个对象之间的协作;不管是哪种方式,他们最终的效果是一致的,都可以实现两个对象之间的交互并最终完成某个任务。那么这两种方式各自的优缺点在哪里呢?我个人觉得对于对象引用的方式,其好处就是简单、直观、容易理解,很符合我们平时的设计习惯。但坏处是什么呢?我个人觉得这种方式是形成对象耦合的根本原因,对象A对对象B存在了紧密的耦合,也许你会说,在间接作用的方式下,对象A不也会保留一个对象B的唯一标识吗?没错,但要知道保留引用和保留唯一标识的耦合强度是不一样的。前者的耦合强度更大,因为持有另外一个对象的引用就意味着可以直接操作该对象,而仅仅持有另外一个对象的唯一标识则不行,必须先根据该唯一标识获取另外一个对象,然后再操作它。而对于发送接受消息的方式,它的好处和坏处是什么呢?其实正好和前者相反,即不简单、不直观、不容易理解,容易让大家觉得有过度设计的嫌疑,而好处则是能够将两个对象之间的耦合度降到最低。

好了,有了前面这些介绍之后,我想可以引出我所设计的这个框架的设计思想了。

既然在对象直接作用的思路下设计软件的各种原则、模式,以及各种最佳实践已经很多了,如SOLID五大设计原则、GRASP九大OO设计原则、Gof的23种设计模式、各种更大的模式如MVC、MVP、MVVM,等等。所以,我也就不用去费功夫去研究了,直接利用前辈的研究成果就行了。但我发现在对象间接作用的思路下设计软件的各种原则或框架似乎还不够多。当然也有很多大家都很熟悉了,比如Observer设计模式,按照这个设计模式设计出来的.NET框架中的事件和委托的机制,还有比如一些第三方的开源框架如事件总线,事件驱动架构,等等。

思考到这里,再结合自己最近不断学习DDD的背景下,我脑子里有了一个奇特的想法!那就是:是否可以搞一个事件驱动的领域模型实现框架从而可以让我们从消息和行为的角度去设计对象呢?

有了这个框架,我们可以:1)通过消息实现领域模型中各个领域对象之间的交互,或者说是通信及协作;2)通过消息实现领域模型和外界的交互,如领域模型的使用者和领域模型之间的交互,一般这个使用者是应用层;还有比如领域模型和数据持久层的交互。带着这个问题,我试图去寻找目前已有的框架来实现我的想法,但遗憾的是,我找不到,所以只能自己开发。想到这里,我其实挺担心的,因为我很有可能已经走火入魔了,因为我要走的设计道路很可能是个死胡同或不归路,或者说不是一条真正能很好的解决软件设计的路,不然我怎么会找不到这样的框架呢?但不管怎样,还是先试试再说吧!反正我的大脑放在那里不用也是浪费。就这样,带着这样的目标和思路,我开始一步步设计我的框架了。

经过了三个月的设计、编码、测试、分享、讨论、重构的循环过程。到目前为止,总算初步实现了自己当初的目标,现在唯一差的就是在真正的实际项目中使用了。但幸好已经写了两个不同层次的Demo用来验证我的框架了。

http://files.cnblogs.com/netfocus/EventBasedDDDExample.rar

该Demo包含了框架的源代码和Demo文件,基于VS2010开发,因为需要用到.NET4.0中的一些特性,如逆变和协变。源代码打开后,EventBasedDDDExample.PresentationLayer是启动项目,直接F5就可以运行。该Demo为了重点突出领域模型的设计,特意采用内存作为数据持久层,去掉了应用层,并且用控制台应用程序作为UI层,这样就方便大家运行Demo。该项目包含了四个演示的例子,前面两个例子演示了如何利用我的框架实现特定的业务场景(一个是银行转账的例子,另一个是论坛中发帖发回复删除回复的例子)。具体功能参见源代码。

http://files.cnblogs.com/netfocus/ProductName.rar

该Demo是一个比较真实的项目,也是用VS2010开发。前身是我之前开发过的一个蜘蛛侠论坛,现在用我最新的框架来实现这个论坛。但由于时间有限,UI层还没开发好,但应用层、领域层、持久层已经开发好。因此大家在查看源代码时,不要去看UI层的设计,因为我还没开发好。而应该去看其他几层的设计!大家从我这个Demo中,可以看到如何将经典的领域驱动四层分层架构和我的框架集成。相信这对大家非常具有实用价值。然后关于项目的命名空间,我也要解释下。假设现在有一个公司要做一个项目,我觉得比较好的项目命名方式为:以CompanyName.ProductName作为前缀,基础类库命名为Common,产品中的某个子应用模块,则可以命名为CompanyName.ProductName.Modules.Forum,CompanyName.ProductName.Modules.Blog,等。然后每个模块还可以根据模块的分层设计分出不同的Project,比如论坛的应用层可以命名为:CompanyName.ProductName.Modules.Forum.ApplicationService,等。由于我做的只是一个展示架构的Demo,所以没有用具体的CompanyName,ProductName。我觉得在开发阶段我们可以不使用最后的名字,到了最后项目快完成时再做统一全局替换即可。

下面介绍一下我的框架的设计思想:

领域模型的组成元素:领域服务(Domain Service)+领域对象(Domain Object)+领域事件(Domain Event)+中央事件处理器(Event Processer)。

  1. 领域服务:这个元素和Evans提到的领域服务一致,主要目的也是用来完成单个领域对象不能完成的职责,如银行转账操作;
  2. 领域对象:这个元素和Evans提到的Entity很类似,也有一个唯一标识,但和Evans中的概念也有不同的地方。比如Evans中的Entity为了保持领域模型的完整性,有聚合的概念,即Aggregate。另外还有值对象的概念,即Value Object。但在我的设计中,领域模型中的所有的对象都是平等的,没有任何聚合或关联的概念,也没有值对象的概念。所有的领域对象都通过事件来进行交互协作,从而达到完成各种任务的目的。
  3. 领域事件:这个元素在整个领域模型中最重要,就好比是人体的血液或神经。它是领域模型内部各个领域对象之间通信以及领域模型和外部通信时传递的信息的载体。通过领域事件,我们可以“串”连任何两个领域对象,从而达到让他们相互协作的目的。所谓的串联就是,一个对象发出消息,另外一个对象接收消息并做出正确响应。值得一提的是,我这里提到的事件不仅仅是通知别人发生了什么,而是泛指所有可能的通信情况。比如告诉别人我要什么(我想干什么),告诉别人我将要做什么,等等。也就是说,事件有可能带有一定的目的性,即有可能会指定应该由哪个对象去响应该事件。也许在你看来这已经不是标准的事件了,因为标准的事件应该是不可能知道会由哪些人回去响应该事件的。没错,它就是一个不标准的事件。我前面已经提到了,我这里的事件指对象之间通信的载体。而通信的情况是非常多的,肯定不只局限于告诉别人我发生了什么,还有非常多其他的情况。最后还有一点需要特别指出的是,事件发出去并响应后,有可能会有响应结果。关于这个问题,一般有两个实现方式:利用事件的回调函数实现;让事件响应函数提供返回值,然后在事件完全响应完成后,从事件对象中取出可能的返回值。我认为这两种方式都可以,我的框架采用的是后者。
  4. 中央事件处理器:这个元素只做一件事情,那就是处理某个传进来的事件。怎么处理?就是根据当前事件获取所有可能的响应者,然后调用每个响应者的响应函数执行每个响应。

另外,领域模型与外界如何交互呢?还是通过上面所提到的事件,当外界需要领域模型做什么事情时,就发送一个在领域模型中已经定义好的事件,然后领域模型或其他人(比如持久层)就会响应该事件了。当领域模型发生了什么或想要外界提供什么数据时,也是通过发送事件,然后外界就会响应,从而为领域模型提供必要的支持,如持久化支持。通过上面的分析,似乎可以看出我们已经找到银弹了,即找到了一种单一的模式可以用来解决所有的对象交互与协作的问题了?应该不是这样,但我自己没发现不知道这种设计的问题出在哪里,所以非常期望大家能多给我些意见。还是那句话,我希望我们每个中国人都是一个不盲目相信权威并敢于怀疑权威并能积极去思考和将自己的思考转化为生产力的人,而不只是一个仅仅会使用外国人写出来的框架的人。

这篇文章说的全部是思想或思考心得,接下来我会具体分析我上面提到的两个Demo的具体设计。但我真的很希望大家能重视思想,重视自己的思考过程,并且要敢于去将自己的思想转化为具体的成果,如框架。我们来这个地球上走一趟,如果仅仅只是会用别人写出来的东西,那不是很可惜?但如果你根据自己的思想写出了几个能让别人用的东西出来,那不是非常好吗?那才是很有意义和价值的事情。

补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵。不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好!

[转] DDD领域驱动设计框架分享的更多相关文章

  1. 基于事件驱动的DDD领域驱动设计框架分享(附源代码)

    原文:基于事件驱动的DDD领域驱动设计框架分享(附源代码) 补充:现在再回过头来看这篇文章,感觉当初自己偏激了,呵呵.不过没有以前的我,怎么会有现在的我和现在的enode框架呢?发现自己进步了真好! ...

  2. 浅谈我对DDD领域驱动设计的理解

    从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能够在线上也能销售自己的产品 ...

  3. C#进阶系列——DDD领域驱动设计初探(二):仓储Repository(上)

    前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repository,为什么Repository会有这么大的争议,博主认为主要原 ...

  4. C#进阶系列——DDD领域驱动设计初探(四):WCF搭建

    前言:前面三篇分享了下DDD里面的两个主要特性:聚合和仓储.领域层的搭建基本完成,当然还涉及到领域事件和领域服务的部分,后面再项目搭建的过程中慢慢引入,博主的思路是先将整个架构走通,然后一步一步来添加 ...

  5. DDD 领域驱动设计-谈谈 Repository、IUnitOfWork 和 IDbContext 的实践(转)

    http://www.cnblogs.com/xishuai/p/ddd-repository-iunitofwork-and-idbcontext.html 好久没写 DDD 领域驱动设计相关的文章 ...

  6. (转载)浅谈我对DDD领域驱动设计的理解

    原文地址:http://www.cnblogs.com/netfocus/p/5548025.html 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来 ...

  7. DDD领域驱动设计的理解

    DDD领域驱动设计的理解 从遇到问题开始 当人们要做一个软件系统时,一般总是因为遇到了什么问题,然后希望通过一个软件系统来解决. 比如,我是一家企业,然后我觉得我现在线下销售自己的产品还不够,我希望能 ...

  8. DDD领域驱动设计仓储Repository

    DDD领域驱动设计初探(二):仓储Repository(上) 前言:上篇介绍了DDD设计Demo里面的聚合划分以及实体和聚合根的设计,这章继续来说说DDD里面最具争议的话题之一的仓储Repositor ...

  9. DDD领域驱动设计初探(四):WCF搭建

    前言:前面三篇分享了下DDD里面的两个主要特性:聚合和仓储.领域层的搭建基本完成,当然还涉及到领域事件和领域服务的部分,后面再项目搭建的过程中慢慢引入,博主的思路是先将整个架构走通,然后一步一步来添加 ...

随机推荐

  1. 为linux系统实现回收站

    在linux系统中,经常采用"rm *"或"rm -r *"操作删除一下文件,但是有时某些文件并不是我们想要删除的,但是已经被删除.很多时候都是悲剧的,数据是难 ...

  2. 转-Tomcat 8 安装和配置、优化

    https://github.com/judasn/Linux-Tutorial/blob/master/Tomcat-Install-And-Settings.md Tomcat 8 安装 Tomc ...

  3. jquery ajax自定义分页组件(jquery.loehpagerv1.0)原创

    简单的两个步骤截可调用 <script src="<%=basePath%>/resources/js/jquery-1.7.1.min.js"></ ...

  4. 【react】利用prop-types第三方库对组件的props中的变量进行类型检测

    1.引言--JavaScript就是一个熊孩子   1.1对于JSer们来说,js是自由的,但同时又有许多让人烦恼的地方.javascript很多时候就是这么一个熊孩子,他很多时候并不会像C和java ...

  5. VirtulBox虚拟机搭建Linux Centos系统

    简要说明 该文章目的是基于搭建hadoop的前置文章,当然也可以搭建Linux的入门文章.那我再重复一下安装准备软件. 环境准备:http://pan.baidu.com/s/1dFrHyxV  密码 ...

  6. 从点击到呈现 — 详解一次HTTP请求

    一般来说,很多的参考资料上面都会说,http 是一个基于请求/响应的工作模式,然后画出一张浏览器和服务器的 b/s 结构图,再画上两个箭头,表示请求和响应,应该说这么解释是易懂的,一般也是够清楚的,但 ...

  7. oracle的神奇化学反应(行转列+获取表字段)

    橘子+汽水=橘子汽水,∑(゚Д゚ノ)ノ好无聊!!! 火鸡+烤架=烤火鸡,ლ(´ڡ`ლ)还不错. wm_concat()+表字段查询=(✪ω✪)会是啥呢? wm_concat()函数,该函数可以把列值以 ...

  8. Js调用exe程序方法(通过URL Protocol实现网页调用本地应用程序)

      1.使用记事本(或其他文本编辑器)创建一个protocal.reg文件,并写入以下内容 Windows Registry Editor Version 5.00 [HKEY_CLASSES_ROO ...

  9. OC语言中如何在便利构造器中利用便利初始化进行初始化

    因为利用便利初始化在便利构造器中进行初始化,所以要利用便利初始化的声明及实现部分,可与前篇做比较: 1. 主函数部分: 2. 接口部分: 3. 实现部分: 4. 打印结果: 感兴趣的朋友们可自己与前面 ...

  10. python 用户交互

    #coding=utf8 name = input("name:") age = int(input("age:")) job = input("jo ...