请教关于DDD中,聚合根新媒体概念与实践和相关实践的一些疑惑

交流与勘误可
DDD社区官网上一篇关于聚合设计的几个原则的简单讨论:文章地址:,该地址中包含了一篇关于介绍如何有效的设计聚合的一些原则,共3个pdf文件。该文章中指出了以下几个聚合设计的原则:
聚合是用来封装真正的不变性,而不是简单的将对象组合在一起;
聚合应尽量设计的小;
聚合之间的关联通过ID,而不是对象引用;
聚合内强一致性,聚合之间最终一致性;
上面这几条原则,作者通过一个例子来逐步阐述。下面我按照我的理解对每个原则做一个简单的描述。
聚合是用来封装真正的不变性,而不是简单的将对象组合在一起这个原则,就是强调聚合的真正用途除了封装我们本身所关心的信息外,最主要的目的是为了封装业务规则,保证数据的一致性。在我看来,这一点是设计聚合时最重要和最需要考虑的点;当我们在设计聚合时,要多想想当前聚合封装了哪些业务规则,实现了哪些数据一致性。所谓的业务规则是指,比如一个银行账号的余额不能小于0,订单中的订单明细的个数不能为0,订单中不能出现两个明细对应的商品ID相同,订单明细中的商品信息必须合法,商品的名称不能为空,回复被创建时必须要传入被回复的帖子(因为没有帖子的回复不是一个合法的回复),等;
聚合应尽量设计的小这个原则,更多的是从技术的角度去考虑的。作者通过一个例子来说明,该例子中,一开始聚合设计的很大,包含了很多实体,但是后来发现因为该聚合包含的东西过多,导致多人操作时并发冲突严重,导致系统可用性变差;后来开发团队将原来的大聚合拆分为多个小聚合,当然,拆分为小聚合后,原来大聚合内维护的业务规则同样在多个小聚合上有所体现。所以实现了既能解决并发冲突的问题,也能保证让聚合来封装业务规则,实现模型级别的数据一致性;另外,回复中的一位道友“”提到,聚合设计的小还有一个好处,就是:业务决定聚合,业务改变聚合。聚合设计的小除了可以降低并发冲突的可能性之外,同样减少了业务改变的时候,聚合的拆分个数,降低了聚合大幅重构(拆分)的可能性,从而能让我们的领域模型更能适应业务的变化。
聚合之间通过ID关联这个原则,是考虑到,其实聚合之间无需通过对象引用的方式来关联;
首先通过引用关联,会导致聚合的边界不够清晰,如果通过ID关联,由于ID是值对象,且值对象正好是用来表达状态的;所以,可以让聚合内只包含只属于自己的实体或值对象,那这样每个聚合的边界就很清晰;每个聚合,关心的是自己有什么信息,自己封装了什么业务规则,自己实现了哪些数据一致性;
如果通过引用关联,那需要实现LazyLoad的效果,否则当我们加载一个聚合的时候,就会把其关联的其他聚合也一起加载,而实际上我们有时在加载一个聚合时,不需要用到关联的那些聚合,所以在这种时候,就给性能带来一定影响,不过幸好我们现在的ORM都支持LazyLoad,所以这点问题相对不是很大;
你可能会问,聚合之间如果通过对象引用来关联,那聚合之间的交互就比较方便,因为我可以方便的直接拿到关联的聚合的引用;是的,这点是没错,但是如果聚合之间要交互,在经典DDD的架构下,一般可以通过两种方式解决:1)如果A聚合的某个方法需要依赖于B聚合对象,则我们可以将B聚合对象以参数的方式传递给A聚合,这样A对B没有属性上的关联,而只是参数上的依赖;一般当一个聚合需要直接访问另一个聚合的情况往往是在职责上表明A聚合需要通知B聚合做什么事情或者想从B聚合获取什么信息以便A聚合自己可以实现某种业务逻辑;2)如果两个聚合之间需要交互,但是这两个聚合本身只需要关注自己的那部分逻辑即可,典型的例子就是银行转账,在经典DDD下,我们一般会设计一个转账的领域服务,来协调源账号和目标账号之间的转入和转出,但源账号和目标账号本身只需要关注自己的转入或转出逻辑即可。这种情况下,源账号和目标账号两个聚合实例不需要相互关联引用,只需要引入领域服务来协调跨聚合的逻辑即可;
如果一个聚合单单保存另外的聚合的ID还不够,那是否就需要引用另外的聚合了呢?也不必,此时我们可以将当前聚合所需要的外部聚合的信息封装为值对象,然后自己聚合该值对象即可。比如经典的订单的例子就是,订单聚合了一些订单明细,每个订单明细包含了商品ID、商品名称、商品价格这三个来自商品聚合的信息;此时我们可以设计一个ProductInfo的值对象来包含这些信息,然后订单明细持有该ProductInfo值对象即可;实际上,这里的ProductInfo所包含的商品信息是在订单生成时对商品信息的状态的冗余,订单生成后,即便商品的价格变了,那订单明细中包含的ProductInfo信息也不会变,因为这个信息已经完全是订单聚合内部的东西了,也就是说和商品聚合无关了。
实际上通过ID关联,也是达到设计小聚合的目标的一种方式;
聚合内强一致性,聚合之间最终一致性这个原则主要的背景是:如果用CQRS+Event Sourcing的架构来实现DDD,那聚合之间因为通过Domain Event(领域事件)来实现交互了,所以同样也不需要聚合与聚合之间的对象引用,同时也不需要领域服务了,因为领域服务已经被Process(流程聚合根)和Process Manager(流程管理器,无状态)所替代。流程聚合根,负责封装流程的当前状态以及流程下一步该怎么走的逻辑,包括流程遇到异常时的回滚处理逻辑;流程管理器,无状态。负责协调流程中各个参与者聚合根之间的消息交互,它会接受聚合根产生的domain event,然后发送command。另外一方面,由于CQRS的引入,使得我们的domain只需要处理业务逻辑,而不需要应付查询相关的需求了,各种查询需求专门由各种查询服务实现;所以我们的domain就可以非常瘦身,仅仅只需要通过聚合根来封装必要的业务规则(保证聚合内数据的强一致性)即可,然后每个聚合根做了任何的状态变更后,会产生相应的领域事件,然后事件会被持久化到EventStore,EventStore用来持久化所有的事件,整个domain的状态要恢复,只需要通过Event Sourcing的方式还原即可;另外,当事件持久化完成后,框架会通过事件总线将事件发布出去,然后Process Manager就可以响应事件,然后发送新的command去通知相应的聚合根去做必要的处理;
上面这个过程可以在任何一个CQRS的架构图(包括enode的架构图)中找到,我这里就不贴图了。enode中对经典的转账场景用这种思路实现了一下,有兴趣可以去下载,然后看一下其中的BankTransferSample这个例子就清楚了。另外,因为事件的响应和Command的发送是异步的,所以,这种架构下,聚合根的交互是异步的;
需要再次强调的一点是,聚合如果只需要关注如何实现业务规则而不需要考虑查询需求所带来的好处,那就是我们不需要在domain里维护各种统计信息了,而只要维护各种业务规则所潜在的必须依赖的状态信息即可;举个例子,假如一个论坛,有版块和帖子,以前,我们可能会在版块对象上有一个帖子总数的属性,当新增一个帖子时,会对这个属性加1;而在CQRS架构下,domain内的版块聚合根无需维护总帖子数这个统计信息了,总帖子数会在查询端的数据库独立维护;
从聚合和哲学的角度思考,为什么需要状态?聚合的角度首先,什么是状态?很简单,比如一个商品的库存信息,那么该库存信息有一个商品的数量这个属性,表示当前商品在库存中还有多少件;那么我们为什么需要记录该属性呢?也就是为什么需要记录这个状态呢?因为有业务规则的存在。以这个例子为例,因为存在“商品的库存不能为负数”这样的一个业务规则,那这个规则如果要能保证,首先必须先记录商品的库存数量;因为商品的库存数量是会随着商品的卖出而减少的,而减少就是通过:Product.Count = Product.Count - 1这样的逻辑运算来实现;这个逻辑运算要能运行的前提就是商品要有库存信息。从这个例子我们不难理解,一个聚合根的很多状态,不是平白无辜设计上去的,而是某些业务规则潜在的要求,必须要设计这些状态才能实现相应的业务规则;这样的例子还有很多,比如银行账号的余额不能小于0,导致我们的银行账号必须要设计一个当前余额的属性;
另外一个原因是,看起来像是废话,呵呵。就是:因为我们关心这些信息,所以需要设计在当前聚合上;比如,以一个论坛的帖子为例,作为一个帖子,我们通常都会关心帖子的标题、描述、发帖人、发帖时间、所属版块(如果论坛有版块这个概念的话);所以,我们就会在帖子聚合根上设计出这些属性,以表达我们所关心的这些信息的状态;
哲学的角度下面在从偏哲学的角度表达一下对象的概念吧:
人类永远无法认识完整的事物,因为我们认识到的总是事物的某一方面。我们所说的对象实际上是客观事物在人头脑里的反应,而事物则是不因人的认识发生改变的客观存在。同样一根铁棒,在钢材生产厂家看来,它是成品;在机械加工厂家看来,它是原料;在废品站看来,他是商品。成品、原料、商品,这三者拥有不同的属性,有本质的不同。为什么同一事物在不同人的眼里就截然不同了呢?这是因为我们总是取对我们有用的方面来认识事物。当这根铁棒作为商品时,它的原料属性依然存在,只是我们不关心了。
所以,总结出来就是,因为我们关心一个对象的某些方面,所以我们才会为他设计某些状态属性;
关于聚合的设计的一些思考上面只是简单提到,聚合的设计应该多考虑它封装了哪些业务规则这个问题。下面我想再多讲一点我的一些想法:
关于GRASP九大模式中的最重要模式:信息专家模式还是以论坛的帖子为例,创建一个帖子时,有一个业务规则,那就是帖子的发帖人、标题、描述、所属板块(如果论坛有板块这个概念的话)都不能为空或无效的值,因为这些信息只要有任何一个无效,那就意味着被创建出来的帖子是无效的,那就是没有保证业务规则,也就没办法谈领域模型的数据一致性了;如果像以往的三层贫血架构,那帖子只是一个数据的载体,不包含任何业务规则,帖子会先被构造一个空的帖子对象出来,然后我们给这个空帖子对象的某些属性赋值,然后保存该帖子对象到数据库;这种设计,帖子对象只是一个数据的容器,它完全控制不了自己的状态,因为它的状态都是被别人(如service)去修改的;这样的设计,相当于是没有把业务规则封装在业务对象内部,而是转移到了外部service中,虽然这样通常也没问题,事实上我们大部分人都一直在这么干,因为这样干写代码很随意,也很高效,呵呵。
GRASP九大模式中有一个面向对象的模式叫信息专家模式,不知道大家有了解过没有,该模式的描述是:将职责分配给拥有执行该职责所需信息的对象;这个模式告诉我们,如果一个对象负责维护一些信息,那它就有职责维护好这些信息。体现到对象的属性上,那就是这个对象的属性不能被外部随便更改,对象自己的属性必须自己负责维护修改。构造函数和普通的方法都会改变对象的状态,所以,我们对构造函数和对象普通的公共方法,都要秉持这个原则;这点非常重要,否则,如果像贫血模型那样,那对象就不叫对象了,而只是一个普通的容纳数据的容器而已,和数据库里的一条记录也无本质差别了。实际上,在我看来,这也是DDD中的聚合区别于贫血模型中的实体的最大的地方。聚合不仅有状态,还有严格维护好自己状态的各种方法,包括构造函数在内;而贫血模型,则只有状态,没有行为;
关于DDD中一个领域对象是否是聚合根的考虑这个问题,没有非常清晰的放之四海而皆准的确定方法,我的想法是:
首先从我们对领域的最基本的常识方面的理解去思考,该对象是否有独立的生命周期,如果有,那基本上是聚合根了;
如果领域内的一个对象,我们会在后台有一个独立的模块去管理它,那它基本上也是聚合根了;
是否有独立的业务场景会去创建或修改一个对象;
如果对象有全局唯一的标识,那它也是聚合根了;
如果你不能确定一个对象是否是聚合根的的时候,就先放一下,就先假定它是聚合根也无妨,然后可以先分析一下你已经确定的那些聚合根应该具体聚合哪些信息;也许等你分析清楚其他的那些聚合的范围后,也推导出了你之前不确定是否是聚合根的那个对象是否应该是聚合根了呢。
关于一个聚合内应该聚合哪些信息的思考
把我们所需要关心的属性设计进去;
分析该聚合要封装和实现哪些业务规则,从而像上面的例子(商品库存)那样推导出需要设计哪些属性状态到该聚合内;
如果我们在创建或修改一个对象时,总是会级联创建或修改一些级联信息,比如在一个任务系统,当我们创建一个任务时,可能会上传一些附件,那这些附件的描述信息(如附件ID,附件名称,附件下载地址)就应该被聚合在任务聚合根上;
聚合内只需要值对象和内部的实体即可,不需要引用其他的聚合根,引用其他的聚合根只会让当前聚合的边界模糊;
关于如何更合理的设计聚合来封装各种业务规则的思考这一点在最上面的几个原则中,实际上已经提到过一点,那就是尽量设计小聚合,这里的出发点主要是从技术的角度去思考,为了降低对公共对象(大聚合)的并发修改,从而减小并发冲突的可能性,从而提高系统的可用性(因为系统用户不会经常因为并发冲突而导致它的操作失败);关于这一点,我还想再举几个例子,来说明,其实要实现各种业务规则,可以有多种聚合的设计方式,大聚合只是其中一种;
比如,帖子和回复,大家都知道一个帖子有多个回复,没有帖子,回复就没有意义;所以很多人就会认为帖子应该聚合回复;但实际上不需要这样,如果你这样做了,那对于一个论坛来说,同一个帖子被多个人同时回复的可能性是非常高的,那这样的话,多个人同时回复一个帖子,就会导致多个人同时修改同一个帖子对象,那就导致大家都回复不了,因为会有并发冲突或者数据库事务的等待超时,因为大家都在修改同一个帖子聚合根;实际上如果我们从业务规则的角度去思考一下,那可以发现,其实帖子和回复之间,只有一个简单的规则,那就是回复一旦被创建,那他所对应的帖子不能被修改即可;这样的话,要实现这个规则其实很简单,把回复作为聚合根,然后把帖子传入回复聚合根的构造函数,然后回复保存帖子ID,然后回复将帖子ID设置为不允许外部修改(即可),这样我们就实现了这个业务规则,同时还做到了多人同时推一个帖子回复时,不会对同一个帖子对象就并发修改,而是每个回复都是并行的往数据库插入一条回复记录即可;
所以,通过这个例子,我们发现,要实现领域模型内的各种业务规则,方法不止一种,我们除了要从业务角度考虑对象的内聚关系外,还要从技术角度考虑,但是不管从什么角度考虑,都是以实现所要求的业务规则为前提;
从这个例子,我们其实还发现了另外一件有意义的事情,那就是一个论坛中,发表帖子和发表回复是两个独立的业务场景;一个人发表了帖子,然后可能过了一段时间,另一个人对该帖子发表了回复;所以将帖子和回复都设计为独立的很容易理解;这里虽然帖子和回复是一对多,回复离开帖子确实也没意义,但是将回复设计在帖子内没任何好处,反而让系统的可用性降低;相反,像上面提到的关于创建任务时同时上传一些附件的例子,虽然一个任务也是对应多个附件信息,但是我们发现,人物的附件信息总是随着任务被创建或修改时,一起被修改的。也就是说,我们没有独立的业务场景需要独立修改任务的某个附件信息;所以,没有必要将任务的附件信息设计为独立聚合根;
ENode框架对聚合设计和聚合之间交互的支持enode提供了一个基于DDD+CQRS+Event Sourcing+In Memory+EDA这些技术的应用开发架构;
enode在框架层面就限制了一个command只能修改一个聚合根,这就杜绝了我们使用Unit of Work的模式来以事务的方式来一次性修改多个聚合根;
enode提供了可靠的原子操作和并发冲突检测机制,来保证对单个聚合的操作的强一致性;
enode提供了可靠的事件机制,来保证我们的domain中的聚合之间数据交互可以通过事件异步通信的方式来实现聚合之间的最终一致性;如果有些复杂业务场景是一个流程,那我们可以通过Process+Process Manager的思想来实现流程状态的跟踪和流程的流转;
enode因为基于domain event,所以,我们的聚合根不需要引用,每个聚合根只需要负责自己的状态更新,然后更新完后产生相应的domain event即可,这本质就是就是实现了:Don’t Ask, Tell这个设计原则;
enode提供了可靠的事件发布机制,可以确保command side和query side的数据最终一定是一致的;
enode提供了in memory的设计,使得我们的domain可以非常高效的运行,持久化事件不需要事务,获取聚合根直接从in memory获取;
enode提供了很多设计,可以让我们最大化的对不同的聚合根实例做并行操作,从而提高整个系统的吞吐量;
使用enode,将会迫使你思考如何设计聚合,如何通过流程实现聚合之间的异步交互;迫使你思考如何定义domain event,将领域内的状态更改显式化;迫使你将外部对领域的各种操作显式化,即定义出各种command;迫使你将command side和query side的数据分离和架构分离,技术分离。减少的是,我们不必再设计unit of work,不必设计domain service,不必让聚合设计各种非第一手的冗余的统计信息;
如果您有任何想法或问题需要讨论或交流,可进入发表您的想法或问题。
本文作者:汤雪华
原文链接:/netfocus/p/3307971.html
版权归作者所有,转载请注明出处下次自动登录
现在的位置:
& 综合 & 正文
首先,需要根据需求建立一个初步的领域模型,至少要识别出领域对象和领域对象之间的关系(可以是没有方向的关联关系)。这些领域对象只应该放在领域层中。如果存在应用职责,可以识别出应用类。它们用来协调领域对象,只负责提出问题,本身并不解决问题。解决问题是领域层的职责。这些应用类将被放在应用层中。
接下来分析领域模型,识别出实体对象和值对象。如果是实体对象,最好判断其标识的组成与生成方式。然后,再细致地分析关联关系,确定关联关系的游历方向。要注意几种特殊的关联关系。如果是多对多,则要看是否可以转换为有限定的一对多。对于循环引用关系,则需要特别注意。可以考虑利用查询(利用资源库)来解除其中一方的引用关系。如果要继续维护这种循环引用,需要考虑参与循环引用的多个对象之间的不变量。如果是双向关联,很可能是组成聚合的两个对象,但要判断哪个是聚合根。此时,彼此的构造函数是双重委派的,但其中非根对象的创建应该封装在根对象的构造函数(或者工厂)中。
之后,是划分聚合的边界,找到聚合根。放在聚合中的实体对象通常存在两个特点:1)不需要对它进行全局访问;2)它与聚合根有非常紧密的联系。如果无法确定聚合根,且又不是某个聚合范围中的对象,可以暂时当作聚合根对象来处理。
需要考虑资源库(Repository)对象了。通常,我们只需要对聚合根考虑资源库。主要是从查询的角度来判断资源库(这里存在查询方法的定义,需要哪些查询关键字,或者利用Criteria)。接着考虑对象的创建。需要考虑几个方面。一个是对象的构造函数参数,尤其是聚合根对象,要考虑聚合中的对象保持不变量。同时,还需要考虑标识的问题。第二要考虑是否需要工厂来封装对象的创建;第三需要考虑是否需要复制对象,即是否采用原型模式。如果需要复制,则要考虑聚合中的对象,哪些需要创建新的对象,哪些需要复制。
接下来,需要对模型进行进一步的细化。例如重点考虑循环引用。同时,还要分析是否需要对象的抽象和多态,以及是否需要考虑服务对象。
最后,是对模块的划分。最好不要根据模式来分类,将实体对象、值对象、资源库等对象分别放在一起,而应该按照业务逻辑的内聚性对对象进行模块划分。
注意,在识别领域模型时,对于业务规则和约束来说,需要利用规格(Specification)模式对这些进行封装。另外,切忌将与业务和业务规则相关的职责放到应用层中。应用层应该是薄薄的一层。当然,在整个DDD设计过程中,还需要不断迭代、精化和重构。
终于到了该说说领域驱动设计的时候了。我们在这场关于代码质量的讨论中,从代码可读性开始,讨论了代码复用性、设计模式,然后探讨了职责驱动设计。代码可读性是对代码质量最基本的要求,可惜我们仍有做得不够的(即使那些开发程序很多年的老程序员)。代码复用是提高代码质量的最初级阶段,但是在一个多人开发的项目团队中,围绕代码复用值得讨论的问题依然非常多,它依然是一个非常复杂的问题,甚至有时它不再仅仅是一个技术问题,而是一个管理问题。唉,提高代码质量的道理漫漫兮同志们要上下而求索。一个比较成功的保证代码质量的管理模式就是代码复查。让一些有经验的程序员定期去复查那些初级程序员的代码,指导他们的开发,被认为是成功的,但也代价巨大的。
然而,在这场关于代码质量的讨论中,我认为,最终的终极目标毫无疑问应当是“领域驱动设计”。领域驱动设计可以快速而根本地提高我们的代码质量,举一个最近发生的一件事情也许可以深刻地说明这一点。前不久,我将一个开发任务交给了我的一个手下。一周后,当我对他的代码进行复查的时候,我惊呆了。我甚至不能提出任何的建议来优化他的代码。随后,我花了半个小时的时间与他一起进行了一次领域模型分析,将他开发的这个模块用领域模型绘制了一个草图。随后的数日,他照着这个图纸重新进行了编码。当我再次复查他的代码时,我忍不住笑了。在短短的一周时间内能让一个人的代码质量判若两人,这不得不说是领域驱动设计带给我们的震撼。
但是,在领域驱动设计之前,我用大量篇幅讲解了职责驱动设计。职责驱动设计是领域驱动设计的理论基础,领域驱动设计是职责驱动设计的最佳实践。领域驱动设计要求我们以领域模型作为我们分析与开发的核心,为什么?因为我们的设计应当与现实世界保持“低表示差异”。领域驱动设计强调所有的领域对象应当以现实世界作为模板,为其定义和分配行为,为什么?因为我们的设计应当以职责为中心,按职责分配行为,分配行为的原则可以参照“信息专家”模式。领域驱动设计并不是横空出世的,而是在职责驱动设计的基础之上发展的。理解职责驱动设计可以促进我们对领域驱动设计的理解,然而非常遗憾的是,它却长期游离于我们的视线之外。
低表示差异与领域模型
我在前面的“职责驱动设计”部分已经讨论了“低表示差异”。用一句简短的话说,在我们的分析设计中,软件世界始终应当与现实世界保持“低表示差异”。如何保持低表示差异呢?答案就是领域模型分析。
领域驱动设计,其名称中,将“领域(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和值对象。同时,为了让开发人员更加专注地去思考那些领域问题,而不为其它技术细节所分析,也许以下方式不失为一个最佳实践之一:
领域模型被映射到了软件框架的BUS层中。领域模型中的每个概念类,在BUS层中都有对应的XxxBus,并且包含了这个概念类中的所有行为(函数)。
领域模型中的每个概念类都映射成了软件系统中的一个值对象。这个值对象包含了概念类的所有数据(即那些属性),以及各概念类之间的关系。但是一个值对象不一定完全对应一个数据库中的表(比如具有继承关系的值对象)。特别注意,《领域驱动设计》中提到的值对象与ORM中的值对象并不是一个概念,书中的实体也与这里的实体不完全是一个概念。
软件系统中的UI尽量与值对象保持一致,即,页面上表单中的控件ID尽量与值对象中的属性保持对应,并通过诸如DWR的技术,将UI与BUS能够直接交互,简化过去繁杂的service层操作。
使用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认为,我们对业务领域的认识是一个逐渐深刻的过程。随着认识的逐渐深刻,我们应该在一些合适的时机去重构我们的设计,甚至软件系统已经设计完成并交付使用以后。这当然要求我们拿出我们的勇气与魄力。在完成一次重构以后,相应的设计文档也应当同步更新。
当我们完成了以上这些领域模型的维护工作,一旦有新的开发工作,有新人参与项目的时候,快速熟悉系统并适应工作就应当是顺理成章的事情了。而我在《软件开发的轮回》中提到的那些痛苦的经历就将不再会出现。
也许以上的描述还不够直观,表述得还不够清晰。后面我会通过一个实例详细阐释这样的一个开发过程。
&&&&推荐文章:
【上篇】【下篇】}

我要回帖

更多关于 devops的概念与实践 的文章

更多推荐

版权声明:文章内容来源于网络,版权归原作者所有,如有侵权请点击这里与我们联系,我们将及时删除。

点击添加站长微信