一堂如何提高代码质量的培训课 之 领域驱动设计
终于到了该说说领域驱动设计的时候了。我们在这场关于代码质量的讨论中,从代码可读性开始,讨论了代码复用性、设计模式,然后探讨了职责驱动设计。代码可读性是对代码质量最基本的要求,可惜我们仍有做得不够的(即使那些开发程序很多年的老程序员)。代码复用是提高代码质量的最初级阶段,但是在一个多人开发的项目团队中,围绕代码复用值得讨论的问题依然非常多,它依然是一个非常复杂的问题,甚至有时它不再仅仅是一个技术问题,而是一个管理问题。唉,提高代码质量的道理漫漫兮同志们要上下而求索。一个比较成功的保证代码质量的管理模式就是代码复查。让一些有经验的程序员定期去复查那些初级程序员的代码,指导他们的开发,被认为是成功的,但也代价巨大的。
然而,在这场关于代码质量的讨论中,我认为,最终的终极目标毫无疑问应当是“领域驱动设计”。领域驱动设计可以快速而根本地提高我们的代码质量,举一个最近发生的一件事情也许可以深刻地说明这一点。前不久,我将一个开发任务交给了我的一个手下。一周后,当我对他的代码进行复查的时候,我惊呆了。我甚至不能提出任何的建议来优化他的代码。随后,我花了半个小时的时间与他一起进行了一次领域模型分析,将他开发的这个模块用领域模型绘制了一个草图。随后的数日,他照着这个图纸重新进行了编码。当我再次复查他的代码时,我忍不住笑了。在短短的一周时间内能让一个人的代码质量判若两人,这不得不说是领域驱动设计带给我们的震撼。
但是,在领域驱动设计之前,我用大量篇幅讲解了职责驱动设计。职责驱动设计是领域驱动设计的理论基础,领域驱动设计是职责驱动设计的最佳实践。领域驱动设计要求我们以领域模型作为我们分析与开发的核心,为什么?因为我们的设计应当与现实世界保持“低表示差异”。领域驱动设计强调所有的领域对象应当以现实世界作为模板,为其定义和分配行为,为什么?因为我们的设计应当以职责为中心,按职责分配行为,分配行为的原则可以参照“信息专家”模式。领域驱动设计并不是横空出世的,而是在职责驱动设计的基础之上发展的。理解职责驱动设计可以促进我们对领域驱动设计的理解,然而非常遗憾的是,它却长期游离于我们的视线之外。
低表示差异与领域模型
我在前面的“职责驱动设计”部分已经讨论了“低表示差异”。用一句简短的话说,在我们的分析设计中,软件世界始终应当与现实世界保持“低表示差异”。如何保持低表示差异呢?答案就是领域模型分析。
领域驱动设计,其名称中,将“领域(Domain)”这个词放在了最显著的位置上,为什么呢?因为它的理论核心就是领域。在需求分析和设计阶段使用领域模型与客户进行软件需求的讨论。在这个阶段,领域模型是最重要的一个验收成果,没有完成领域模型分析,这个阶段就永远不算结束。在软件开发阶段采用领域模型作为核心设计图纸指导设计开发。领域模型怎样设计则我们的软件系统就怎样设计,软件系统中的最主要软件类都是源自领域模型中定义的领域对象。在运行维护及二次开发阶段,领域模型就如同房屋建筑中的设计图纸,它成为运行维护人员和二次开发人员熟悉和理解软件系统的核心线索。总之,在领域驱动设计中,领域模式成为最最核心的内容。所以我们应当首先理解什么是领域模型。
领域模型是对现实世界中某个业务领域的抽象。我们设计的软件不是对所有现实世界的模拟,而是对某个领域的模拟,譬如财务领域、税务领域、企业管理领域等等。这个领域我们称之为“业务领域”,而在这个领域里工作,并熟悉掌握这个领域中所有知识的人我们称之为“领域专家”。我们的分析和设计人员对业务领域的熟悉和理解的程度,往往决定我们的软件是否满足客户需求,也往往就决定了我们的软件是否成功。领域驱动设计理论要求我们在需求分析阶段必须非常深入地理解业务领域,采用的方式就是领域模型分析。同时,在这样一个过程中,应该有领域专家参与,甚至成为分析设计中的一个成员。
过去我们使用用例模型与领域专家交流,直到现在我们依然还在这样做。用例模型分析是我们分析设计的方法之一,但现在我们又有了一个新的强有力的工具,那就是领域模型分析。与用例模型比较,领域模型更加直观,可以更加立体地描述现实世界。如果说过去的需求设计文档是二维世界,用例模型只是二维半,领域模型才是真三维世界。领域模型是一大堆的类图,它描述的是业务领域中的各个事物,以及事物与事物之间的关系。
从业务领域中获取知识
说了这么多东西,现在让我们来点儿实在的东西吧:如何进行领域模型分析?建立领域模型需要从业务领域中获取素材。获取领域模型所需素材通常有两个途径:与领域专家的现场交流会中获得,和从用例模型的各个流程中提取名词或名词短语获得。我们将这些获取的素材经过加工,形成我们在领域模型中的一个又一个的类,这些类我们称之为概念类。现在的问题是,哪些应当成为领域模型中的概念类呢?如果我引用一堆定义和准则,并不能让你清楚明了,也许一个生动的比喻更能够让你理解深刻。需求分析有时候就像一部部动画剧,而那些枯燥乏味的概念,纷繁复杂的流程,在这些动画剧中似乎都突然活了,个个都有语言有性格。在这些动画剧中扮演的所有角色,就是我们需要的概念类。而他们做的所有动作,就是用例模型中的所有流程。
1)在业务讨论会中绘制领域模型
运用我曾经一篇文章中的实例来更加生动地描述这样一个过程吧:
在一个阳光明媚的下午,我们一个个西装革履、精神抖擞地来到了客户的办公现场。在一个明亮的会议室里,宽大深褐色的椭圆木桌旁已经聚集了十来个业务人员。看到我们进来,大家握手问候。相继就座后,互相介绍,往来寒暄,唠唠家常。共同的家乡,或熟或不怎么熟的某个人,都可能成为拉近彼此关系的理由。逐渐,一切开始进入正题。客户开始絮絮叨叨的描述自己的需求,而我们则在紧张的做着记录,时不时问一些问题,表明我们的立场,抒发我们的建议。在这样一个过程中,客户会描述他们的每一个业务,会讲解每个业务的流程,他们会讲出一些业务领域的专业词汇(尽管有些你当时还不太懂)。在这样一个过程中,作为需求分析员,你应当非常注意业务流程中的一些关键词汇,你应当(在当时或者过后)将它们提取出来,通过询问客户,弄清楚他们的定义,以及相互之间的关系。而这些词汇就是建立领域模型的开始。
这样的讨论会不可能是一次两次,而是数次。在这样的讨论会中,也许一支笔和一摞白纸会非常有用。在这样的讨论会中,你可以迅速将从客户那里理解的各种概念和知识,立即在白纸上画出一个又一个的草图。那些关键词汇被绘制成了一个个的概念类和它的属性(如果确实需要),用线条迅速标注出相互之间的关系。在你绘制的时候,客户会在不断地给你指正,或者说出了更多的业务知识。一张张的草图成为了你与客户交流的工具,也是最初始的领域模型。
这是一个财务软件的业务讨论会,一个业务人员正在跟我讲付款单是怎样制作成凭证的。“每张付款单都有一个商品明细,每个商品明细都有它的价格、数量和金额。”他指着一张付款单向我解释着。从这句话,我可以提出一些关键信息:付款单、商品明细、价格、数量和金额。付款单与商品明细是一对多关系,并且商品明细聚合在付款单中。每个商品明细都有价格、数量和金额,也就是说,价格、数量和金额是商品明细的属性,这都很清楚。紧接着,他下面的讲解就不是那么清楚容易了。“如果按照一张单据生成一张凭证,那么每张付款单生成一张凭证。单据中的每个明细在凭证中生成一条借方分录和一条贷方分录。将付款单中的付款科目作为借方科目,将付款单结算方式对应的结算方式科目作为贷方科目。现结的付款单在采购发 票中已制作凭证了,因此不再单独制作凭证。非预付的付款单不制作凭证,而是其执行付款核销以后,在核销单中制作凭证。”经过对以上语言的分析,我们可以绘制以下关系:一张凭证包含多个分录,是内聚关系。分录分为借方分录和贷方分录两种。一条商品明细对应一条借方分录和一条贷方分录。借方分录中包含“借方科目”属性,对应付款单中的付款科目;贷方分录中包含“贷方科目”属性,对应的是付款单中的一个什么科目。在这里,你可能对客户的某些描述不明白,因此要他做出解释。原来客户预先制订了一个规则,付款单中的结算方式分布对应了一个结算方式科目。OK,你在绘制的图形中,把结算方式科目作为关联类,将结算方式和贷方科目进行了一个关联。这样,“付款单生成凭证”这样一个场景的领域模型就绘制出来。
2)归纳和整理领域模型
在现场讨论会中,可能一些关键的概念被你忽略掉了。也可能一些关键性的关系被你忽略,或者在草图上并没有很好地表达,甚至存在谬误和矛盾。随着你事后的分析和整理,你从用例模型的流程描述中提取出了更多的概念。同时,随着你对问题的一步一步深入理解,你开始重构你起初的领域模型。在Evans的《领域驱动设计》中,他用大量的篇幅和实例描述和讲解了这样一个过程。另外一个重要的概念是,深入理解和重构领域模型不仅仅是在软件需求分析的阶段完成,它贯穿了整个软件开发的周期。按照迭代软件开发的思想,我们绝不能企图在需求分析阶段完成所有的分析(那是瀑布的思想)。随着我们对业务领域的深入理解,重构和精化领域模型贯穿整个“开发—维护—再开发”的过程中。而这也正符合了现代软件开发业的发展需求(我参与的项目已经经历了快5个年头,每年都在经历着新的开发)。
经过了这样的、有领域专家参与的、反复讨论与整理的过程,我们对业务领域理解将越来越深入,而我们设计的领域模型将越来越贴近现实世界中事物的本质。运用这样的领域模型图纸去开发我们的软件,毫无疑问我们已经成功了一半。(制作领域模型的更多细节见我的相关博客,我也会写更多的文章讨论)
运用领域模型开发软件
曾经有个笑话是这样说的:大师们站的高度都是非常高的,高到什么程度?他们都是生活在太空中的。追随大师是一个高风险的职业,为什么?一不小心就能让我们因缺氧而死掉。这个笑话非常深刻的道出了追随大师的关键,那就是怎样“着陆”,也就是如何“落地”。一个高深的理论,如果不能指导我们的实际工作,那么这个理论是没有价值的,领域驱动设计也是一样。下面我们来讨论一下如何运用领域模型指导我们的软件开发。
1)领域模型在我们的软件框架中扮演的是什么角色
首先第一个要解决的问题是,领域模型在我们的软件框架中,特别是时下最常见的Spring+Hibernate框架中扮演的是什么角色。我们不妨先看看Evans是怎样分层的。在书中,Evans将系统分为用户界面层(表示层)、应用层、领域层(模型层)和基础结构层。从他对各个层的表述我们不难看出,用户界面层(表示层)就是前端界面,应用层即是Service层,基础结构层即是DAO、工具类,以及其它的技术支持类。从这个角度来说,Evans在他的书中所说的领域层,在我们的框架中就应当是业务逻辑层(BUS),但事实并不是这样简单。在我们现在的框架中,数据与业务逻辑处理被分离了,举例说吧:
在一个员工信息管理系统中,领域模型可能包含了一个员工类,并且在该类中包含了那些诸如员工编号、姓名、性别、职务等属性。除此以外,一个员工类肯定也包含了诸如“新增员工”、“修改员工资料”之类的行为。领域模型如此,那么软件设计时会是怎样呢?
在设计一个员工信息管理系统时,它必然包含一个“员工BUS”的类,用于执行诸如“新增员工”、“修改员工资料”之类的行为。那么,那些员工的相关属性被放在哪里呢?它们并没有放在“员工BUS”类中,而是“员工”值对象中(注意:这里的值对象不是DDD中的那个值对象,而是ORM,或者说hibernate中的那个值对象)。领域模型的员工类,在软件系统中被分离为了“员工BUS”类和“员工”值对象类。
正是因为这种数据与业务逻辑处理的分离,令一些人产生了误解,错将领域类对应成了Hibernate对象(希望他正在看这里)。没错,领域模型对应的是BUS层,但部分内容被分离到了值对象中。
记得数年前还有PO和VO的争论,但现在再也没有了。按照现在软件设计的思想,从UI一直到数据库,数据格式变得合成一体了。什么意思呢?页面上的表单是什么样子,提交到后台的值对象就是什么样子,最后持久化成数据库表就是什么样子。按照这样的设计思想,页面上表单中的控件ID、值对象中的属性、数据库表中的字段,都命名成了一致的名称。这样的设计大大简化了程序代码,但因为表单与值对象长得一个模样,也使得一些人误以为领域类对应的是UI。
2)运用领域模型开发的软件系统应当是这样的
不论怎样,我认为,运用领域模型开发软件,应当是以领域模型为中心,即以领域模型为蓝本进行开发,就如同建筑图纸与盖楼一样。领域模型中的某个概念类,在软件设计时应当映射成对应的BUS和值对象。同时,为了让开发人员更加专注地去思考那些领域问题,而不为其它技术细节所分析,也许以下方式不失为一个最佳实践之一:
a. 领域模型被映射到了软件框架的BUS层中。领域模型中的每个概念类,在BUS层中都有对应的XxxBus,并且包含了这个概念类中的所有行为(函数)。
b. 领域模型中的每个概念类都映射成了软件系统中的一个值对象。这个值对象包含了概念类的所有数据(即那些属性),以及各概念类之间的关系。但是一个值对象不一定完全对应一个数据库中的表(比如具有继承关系的值对象)。特别注意,《领域驱动设计》中提到的值对象与ORM中的值对象并不是一个概念,书中的实体也与这里的实体不完全是一个概念。
c. 软件系统中的UI尽量与值对象保持一致,即,页面上表单中的控件ID尽量与值对象中的属性保持对应,并通过诸如DWR的技术,将UI与BUS能够直接交互,简化过去繁杂的service层操作。
d. 使用BasicDao这样的通用代码来处理数据库持久化操作,将值对象直接扔给insert()、update()、delete()、load()函数,摒弃了过去为每个业务设计DAO的设计;采用hql配置文件的方式,将系统需要查询的语句全部放在配置文件中,然后使用统一的find()函数执行查询,满足各种各样的查询要求。
采用这样一个设计框架好处多多。首先它大大简化了软件开发的内容,过去繁杂的service层和DAO层统统被砍掉,仅仅保留下BUS层和UI层(当然必须有诸如DWR的强大框架和诸如BasicDao自开发的超轻量平台的支持)。我始终认为,每增加一段代码,就增加了一份程序出错的机会。因此我总是不遗余力地试图简化代码,甚至到了发指的地步。
其次,系统的层次划分会非常清晰。UI层就是前端的一堆jsp、html和js,BUS就是一堆业务逻辑操作程序(不包含任何诸如hql的数据持久化代码),hql配置文件可以支持多配置文件,因此被分为了“员工管理”配置文件、“部门管理”配置文件、“薪金管理”配置文件。。。。。。
此外,我不得不说,世界终于变得清静了。因为这样一个框架,程序员从那么多羁绊中解脱出来了,他们终于可以全心全意地、以领域模型为中心、仔仔细细地开始考虑那些领域问题了。
在这样一个框架中,每个BUS都有它们自己的职责,这种职责被清清楚楚地标注在各自的注释中。从此,系统开始以职责为中心设计系统了。
3)运用领域模型开发的一个简短实例
也许一个实例是最说明问题的,让我们来举一个项目评审系统的例子吧。
在进行一次评审前首先要制定一个评审计划。在这份计划中,要详细定义此次评审的评审人、评审材料。显然,在领域模型中,评审计划是一个重要的概念,而评审人与评审材料是聚合在评审计划下的。随后是在评审过程中制作评审表。每个评审人都要对评审材料制作评审表。最后,评审组织者根据评审人的意见制作评审报告。
在这样一个需求下,我们应当怎样设计“制作评审表”的业务呢?在领域模型中,“制作评审表”应当是“评审表”的职责,也就是它所拥有的行为。因此,我们创建一个“评审表BUS”,并包含“制作评审表”的函数。随后,我们开始编写“制作评审表”的代码。在这里,我们首先要获取“评审者”和“评审材料”。由于这两部分是聚会在评审计划下的,毫无疑问我们应当调用“评审计划”获取“评审者”和“评审材料”(这里的“评审计划”即可以设计成“评审计划BUS”,也可以设计成“评审计划”配置文件)。然后,我们通过前端与用户交互,最终从前端获得用户填写的评审表,利用dwr直接形成“评审表”值对象,在“保存评审表”中调用通用DAO,持久化“评审表”。
在这样的设计过程中,首先当然是设计领域模型了。在完成了领域模型的设计以后,应当是按照领域模型设计BUS和生成值对象(实际工作中可以先生成数据库再生成值对象)。随后开始编写BUS中的各个方法。在编写过程中,应当将某个方法合理地进行分解,根据职责去调用其它类中的方法(正如评审表去调用评审计划获取评审人和评审材料一样)。通过这样,功能被合理地分配到BUS的各个类中,保证了功能组织的高度内聚。
另一个开发中可能出现的问题这里不得不提。按照理想的领域驱动设计的流程,首先应当是需求分析人员分析和设计出领域模型,然后由开发人员照着领域模型设计开发。但是,由于各种各样的原因,实际情况可能并不总是这样。很多时候,开发人员可能没有得到领域模型而仅仅只有需求文档。这样的情况并不意味着开发人员可以摒弃领域模型而直接开始编码。在编码前,一个简短的领域模型分析和绘制领域草图,就是如同砍柴前的磨刀,是一个必不可少的步骤(这也是领域驱动设计与以往开发模式重要的不同点之一)。
领域模型维护与二次开发
前面,我分别讲述了分析人员运用领域模型分析和开发人员运用领域模型设计。在这两部分,我不断强调运用草图快速进行领域模型分析。开发过程总是忙碌而紧凑的,运用草图快速进行领域模型分析可以大大简化我们分析的过程,提高设计开发的效率。但是,这并不意味着我们可以随意处理这些分析草图。正如建筑设计图是建筑设施运行维护的重要资料,领域模型以及其它资料也是软件系统运行维护的重要资料。因此,我认为,这些分析设计草图应当妥善保管,并且在设计开发完成以后,应当专门进行归纳整理,为今后的运行维护和二次开发提供帮助。
另外,前面我提到,Evans的领域驱动设计,一个非常重要的思想就是持续地精化。Evans认为,我们对业务领域的认识是一个逐渐深刻的过程。随着认识的逐渐深刻,我们应该在一些合适的时机去重构我们的设计,甚至软件系统已经设计完成并交付使用以后。这当然要求我们拿出我们的勇气与魄力。在完成一次重构以后,相应的设计文档也应当同步更新。
当我们完成了以上这些领域模型的维护工作,一旦有新的开发工作,有新人参与项目的时候,快速熟悉系统并适应工作就应当是顺理成章的事情了。而我在《软件开发的轮回》中提到的那些痛苦的经历就将不再会出现。
也许以上的描述还不够直观,表述得还不够清晰。后面我会通过一个实例详细阐释这样的一个开发过程。
上一篇: DES算法实例详解