首先把图书馆系统的背景说明一下吧﹕公司每个成员通过局域网登录图书管理系统﹐然后预借书籍﹐图书管理员收到预借信息后﹐核准借阅﹐并通知借书人前来领书﹐告知相关事项。
领域模型的价值不在于它的设计优美(它只是一些对象﹐最重要的也就是对象之间的关系)﹐而在于它体现了系统的核心价值。什么是系统的核心价值呢?我想我们的图书馆系统和华尔街的一个商业系统本质的区别应该不是用了什么语言﹐OO还是过程﹐用了什么数据库。因为这两个系统使用什么语言﹐采用什么方式﹐利用什么DBMS﹐都是不能本质区别这两个系统的价值的。
那什么才是系统真正的价值呢?
那就是﹕系统能为使用提供什么服务﹐以及提供的质量(即如何服务﹐这也是系统内部的我们要做的事情)。
那什么体现的系统的服务呢?系统的运行方式﹐系统的运行过程﹐系统的业务逻辑。
以数据库为中心的开发方式﹐将所有业务逻辑都放在了数据库﹐并结合系统的代码来保证业务逻辑的实现。
而面向对象﹐则其表现点则是直接在对象本身上﹐在于对象之间真正的交互过程﹐结果也是保留在对象的属性和对象与对象的关系中。
从以上方式可以看出﹐无论以数据库为中心的开发如何的OO﹐如何多的设计模式﹐架构体系如何优美﹐它始终离不开数据库。它的OO只是一种手段﹐而非目的。(大家别激动﹐我始终没有提这种方式不好﹐只是实现系统的两种方法而已)
面向对象则不同﹐它将逻辑直接存在于对象上﹐这与现实情况是吻合的﹐这也是面向对象引起这么大热潮的根本原因(我想﹐不明白这一点﹐您会在ORM,AOP等等方面不知其所以然的)。
领域模型是一种思维﹐是一种方法,是在系统分析阶段使用﹐而不是程序员在自己的代码中进行纯设计时的工具。我们不是为了OO而领域﹐不是为了最终要新增数据库而领域﹐这也是为什么在没有理解领域模型本质时﹐使用它进行片断式的代码编写﹐得不到任何好处的原因。想想你在编码中弄出的那些用户﹐图书对象﹐它在系统中的位置是什么?您不能否认它就是为了让新增数据库时更方便吧?
领域模型的建立决不是像如何实现某些纯软件逻辑而进行的纯软件设计。(我这里提出的纯软件设计﹐指的是如何在设计中体现扩展性﹐灵活性﹐可维护性而进行的设计)。而是为了能够分析系统提供的服务而产生的一种思维结果。
系统分析员在接手一个系统后﹐首先要做到的事情就是得出系统的服务和服务场景。也就是我们经常所讲的用例(use case)
可惜的是﹐很多使用者一直将用例的作用和价值没弄清楚。我想如果没有真正理解用例的作用时﹐您的用例分析将会一直停留在和别人Demo系统之上﹐提供一個漂亮的圖形好說話而已﹐而不会对我们的系统开发有任何实质作用。
用例表示的是使用系统的一个场景﹐其本质在于详细描述了系统用户(actor)与系统是如何交互的﹐以及交互的后果是什么﹐详细而完善的用例将指导您进行系统开发的全过程?
还以图书管理系统的借书为例﹐这是用例的文本描述(也是真正的用例)﹕
系统﹕显示图书馆的所有书籍类别
用户﹕浏览并选择一个书籍类别
系统﹕显示这个书籍类别的所有书籍
用户﹕浏览并选择一本书籍
系统﹕显示书籍的详细信息
用户﹕借阅书籍
系统﹕办理预借手续
用例后果﹕产生一份预借记录。并将预借记录与书籍关联。
领域模型如何提取的具体办法就不讲了﹐其实在基本的面向对象的书籍中都有提到过﹐一个最简单的方法就是从语句中的名词找出领域对象。
经分析﹐这里的领域模型有﹕图书馆﹐书籍类别﹐书籍﹐预借记录(所有类别﹐所有书籍﹐详细信息这些也是名词﹐但是经过分析﹐它们分别是属于图书馆﹐书籍类别﹐书籍的属性﹐而不是领域模型)
领域模型中最重要的是领域模型之间的关系(当然还有属性)﹐我们的系统的逻辑也是体现在领域对象与对象之间的关系上(如您借了一本设计模式的书﹐在系统中就是设计模式这本书对象与借阅记录对象关联)。
得到领域模型后﹐再根据用例﹐我们分析出领域模型对象的交互过程。
请注意﹐这里的显示****,并没有UI过程﹐实际上就是返回这个对象的某个属性﹐至于如何显示﹐那是UI层的故事。
根据”借阅”用例的描述﹐我们不难得到用例的业务过程的代码表示了﹕
借阅记录 aaa = new 借阅记录
设计模式.借阅记录 = aaa;
很简单﹐就是产生一个借阅记录对象﹐并将它与书籍对象关联﹗
依照迭代过程﹐我们对所有的用例都完成这样的建模过程。系统也就完成了。
在不断的迭代过程中﹐有很多属性呀﹐过程呀﹐都会逐步地由浅至深﹐最终完全符合了用户的要求。
在完成这些过程之后﹐一个系统的领域模型就建造完毕。所有系统的本质属性都体现在这些领域模型的活动过程之中。
一个系统要变更业务逻辑﹐我们只要针对领域模型作变化即可﹐再也不需要抱怨变化了。
终于要考虑系统其它部分了。
数据库﹐日志﹐邮件通知﹐当然还有UI(在之前的评论中有详细描述﹐感兴趣的朋友可以去找找这几天的评论)
这些都属于系统核心以外—领域模型以外的东西。
这些模块与领域模型的接触在哪里呢?当然不能在领域模型内部﹐您不能在借阅逻辑中加入这些对象。
只有一个地方﹐那就是边界。
系统边界﹐即域控制器﹐即上面顺序图的”系统”对象所处的位置﹐系统对象除了与领域模型﹐用户打交道以外﹐它还会与系统的其它模块交互。如持久化系统(您不能因为客户由sql server要求转向oracle而去更改业务逻辑吧)﹐日志系统(您不希望修改日志规则而影响业务逻辑吧)﹐信息通知系统(您不能因为用户要求由邮件通知改为短信通知就修改领域模型吧)。
将领域模型独立于持久化系统的真正原因﹕系统业务逻辑与如何存储数据以便使系统正常持续运行无任何关系。
当然﹐还有设计原则也会支持这种做法。持久化系统依赖领域模型时﹐只有领域模型本身变动才需要修改持久化过程﹐反之则不然﹐这正是依赖倒置原则的一个体现。
我稍微写一下域控制器的代码实现过程吧﹕
Public void 借阅()
{
//这个借阅处理者是纯粹的软件对象﹐其存在的目的就是将借阅的实际处理过程独立出来
借阅处理者 处理者 = new 借阅处理者(当前书籍﹐当前登录人姓名);
Bool successful = 处理者.借阅() //這個方法主要就是上面的那2行代碼
If(successful){
持久化系统.add(当前书籍);
日志系统.add日志(当前当籍,”借阅”)
邮件系统.发送邮件(当前书籍.当前借书人姓名)
}
}
Void 持久化系统.add(书籍 当前书籍)
{
借阅关系 Bbb = new 借阅关系
Bbb.id = 产生ID()
Bbb.图书ID = 当前书籍.id;
Bbb.借阅人 = 当前书籍.借阅记录.借阅人姓名
Bbb.借阅时间 = 当前书籍.借阅记录.借阅时间
Bbb.Save();
}
其它模块处理过程类似。
從這里可以看出﹐業務過程是系統的核心﹐其它模塊都是依賴于它而存在。
如果有ORM﹐它使用的地方就是这里了。
在现实系统中﹐我在if(successful)这里作了一些纯软件设计﹐如利用C#具有Event特性﹐将借阅方法后公开出一个事件﹐这样我在再要添加什么外围模声时﹐只要响应事件就可以了﹐不需要再来动这个方法
另外一个不容回避的问题﹐统计报表﹐虽然循环内存中的对象﹐理论上完全可以抓出所有的数据。
但是对于这一类问题﹐我就不麻烦领域对象了(其实它与领域模型﹐业务逻辑也真的没有关系)﹐而是将它独立实现﹐直接调用持久化模块﹐下sql抓资料。
还有一个实际的问题﹐就是对象的加载。理论上﹐每次系统重启后﹐都要将所有内存中的对象全部还原﹐构建领域环境。
然而对于大部分实际运用的系统﹐这是不可能的﹐如这里的图书馆对象﹐它可能有上千本书﹐因此一次加载肯定不行﹐我使用的方法是﹐利用Proxy﹐继承领域模型﹐延迟加载。关于这一点﹐就属于纯设计问题了﹐如何实现﹐欢迎大家有兴趣再探讨﹗
(請教一下大家用什么方法發表blog呀﹐我每次從word中貼過來好痛苦)
------------------------------
2011.1.22补录
此篇内容已过时
领域驱动真实实践时还会遇到很多问题
目前我的看法是:使用数据库,才是真正的问题解决之道