面向对象分析与设计,即Object Oriented Analysis and Design(OOA\D)
领域驱动设计,即Domain Driven Design(DDD)
四色原型:MI原型、Role原型、PPT原型、Description原型
DCI架构:Data Context Interaction
CQRS架构: 命令查询职责分离原则,即Command Query Responsibility Segregation
以下是DCI的核心思想:
对象扮演某个角色进入场景,然后在场景中进行交互,场景的参与者就是对象所扮演的角色;
一个对象可以扮演多个角色,一个角色也可以被多个对象扮演;
对象的属性和行为分为:A:核心属性和行为,这些属性或行为是不依赖于任何场景的;B: 场景属性和行为,对象通过扮演某个角色进入某个特定场景时拥有的属性或行为,一旦对象离开了这个场景,不再扮演了这个角色后,这些场景属性或行为也就不再属于该对象了;比如人有核心的属性和行为:身高、体重、吃饭、睡觉,然后当人扮演教师的角色在教室里上课时,他则具有上课的行为,一旦回到家里,就又变成了一个普通的人;比如一个物品,在生产时叫产品,在销售时叫商品,坏了的时候叫废品,它在不同阶段扮演不同的角色所具有的属性是不一样的;
场景的生命周期,场景是一个时间与空间的结合,可以理解为某个活动;一旦活动结束,则场景也就消失;
DCI中的D可以理解为DDD中的领域模型;场景中交互的是角色,而不是领域实体。场景属于DSL的思考层面,更接近于需求和用例。而领域也是伟大的出现,但是不能为了领域而领域,为什么呢?因为场景是大哥用例是大哥。领域的存在是为了控制固定概念的部分,这样在某种成度上控制了一定的复杂性和提高了可控性,而DCI则解决了可变性和需求的问题。从某种意义上来说,“领域层(在DCI中可能不会太凸显领域层,不如OLD DDD那么凸显)” 是为了DCI架构服务的。
角色是人类的主观意识,用于对象分析和设计阶段,但是在运行阶段,角色和对象实体是一体的,软件运行过程中只有对象,只是这些对象在参与某个活动时扮演了某个角色而已;
DDD的在对象设计方面的最大贡献之处在于其实体、值对象,以及聚合边界的三个部分,通过这三个概念,我们可以将对象的静态结构设计好。
领域对象所包含的属性必须是只读的,只读的含义是一旦对象被创建好,则只有对象自己才能修改其属性,属性的类型可能是基本数据类型或值类型,即ValueObject;
领域模型设计时不应考虑ORM等技术性的东西,而应该只专注于业务,不要让你的领域模型依赖于技术性的东西;
领域对象的属性和方法设计时要完全根据业务的含义和需要来进行,不要动不动就把每个属性定义为get;set,这会导致领域模型的不安全;
仓储(Repository)不是解决让领域模型不依赖于外部数据存储的唯一方式,我觉得还有更优雅的方式那就是事件驱动;
设计领域模型时不要考虑分层架构方面的东西,因为领域模型与分层架构无关;
不要认为领域模型可以做任何事情,比如查询。领域模型只能帮你处理业务逻辑,你不要用它来帮你做查询的工作,那不是它擅长的领地,因为它的存在目的不是为了查询;CQRS的思想就是指导我们:命令和查询因该完全分离,领域模型适合处理命令的部分,而查询可以用其他任何的不依赖于领域模型的技术来实现,甚至可以直接写SQL也可以;
分析领域模型及其对象之间的交互时,要分清什么是交互的参与者,什么是交互的驱动者,通常情况下,比如人是交互的驱动者,而人在系统中注册的某个帐号所扮演的角色就是交互的参与者;比如我用A的图书卡去图书馆借书,则我是借书活动的驱动者,而A的图书卡对应的帐号所扮演的借书者(Borrower)角色就是借书活动的参与者;
以图书管理系统中的借书和还书的场景进行说明:
借书场景:某个人拿着某张借书卡去图书馆借书;
还书场景:某个人拿着某张借书卡去图书馆还书;
根据四色原型的分析方法,我们可以得出:某个“人”以图书借阅者的角色向图书馆借书。从这里我们可以得出三个角色:1)借阅者(Borrower);2)被借的图书(BorrowedBook);3)图书馆。那么这三个角色的扮演者对象是谁呢?其实这是问题的关键!
1)是谁扮演了借阅者这个角色?很多人认为是走进图书馆的那个人,其实不是。 人所持的图书卡对应的那个人才是真正的借阅者角色的扮演者;试想张三用李四的图书卡借书,借书的是谁?应该是李四,此时相当于李四被张三操控了而已;当然这里假设图书馆不会对持卡人和卡的真正拥有者进行身份核对。所以,借阅者角色的扮演者应该是借书卡对应的帐号(借书卡帐号本质上是某个人在图书馆里系统中的镜像)。那么图书卡帐号和借阅者角色有什么区别?图书卡帐号是一个普通的领域对象,只包含一些核心的基本的属性,如AccountNumber,Owner等;但是Borrower角色则具有借书还书的行为;
2)是谁扮演了被借的书这个角色?这个问题比较好理解,肯定是图书了。那图书和被借的图书有什么区别吗?大家都知道图书是指还没被借走的还是放在书架上的书本,而被借的书则包含了更多的含义,比如被谁借的,什么时候借的,等等;
3)为什么图书馆也是一个角色?图书馆只是一个地点,它不管有没有参与到借书场景中,都叫图书馆,并且它的属性也不会因为参与到场景中而改变。没错!但是他确实是一个角色,只不过它比较特殊,因为在参与到借书场景时它是“本色演出”,即它本身就是一个角色;举两个其他的例子你可能就好理解一点了:比如教室,上课时是课堂,考试时是考场;比如土地,建造房子时是工地,种植粮食时是田地,是有可能增加依赖场景的行为和属性的。
有了场景和角色的之后,我们就可以写出角色在场景中交互的代码了。我们此时完全不用去考虑对象如何设计,更不用考虑如何存储之类的技术性东西。因为我们现在已经清晰的分析清楚1)场景参与者;2)参与者“做什么”;
领域驱动设计中存在3种对象 DataTransformObject、ViewModel、DomainModel
DataTransformObject(DTO) :DTO是表现层与application层传递的对象,此对象不包含行为,只是包含属性。此类传递到application后会通过automapper等类似的工具转化为DomainModel,因为DomainModel包含重要的业务逻辑,上层是不能之间访问到DomainModel。
ViewModel:存在于表现层,如表现层使用asp.net mvc 此时的model 就是ViewModel,负责联系view和controller,一是view向controller提交数据,二是controller向view发送展示数据,这时的ViewModel只是数据的封装,不包含行为。负责表现端的数据校验。同样也需要 automapper的介入 注意:此时的viewmodel 就是数据传输对象 下图能够清晰地表示出这个意思(facade 就是表现层)
DomainModel:是整个软件、整个系统的核心部分。整个核心的业务逻辑都在这里。单独的讲DomainModel是没有意义的,聚合、领域服务才是核心。通常领域层都要有 unitofwork 来保证聚合内部的事务性,同样聚合之间的事物作为外部事物,在application层体现。
领域模型是对领域内的概念类或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
认为领域模型它是一个分析模型,帮助系统分析人员、用户认识现实业务的工具,描述的是业务中涉及到的实体及其相互之间的关系,它是需求分析的产物,与问题领域相关。领域模型是需求分析人员与用户交流的有力工具,是需求分析人员与用户共同理解的概念,是彼此之间交流的语言。而数据模型是系统设计、实现的一部分,描述的是对用户需求在数据结构上的实现,仅此而已。当然数据模型中的概念模型设计与领域模型类似,缺乏的是实体之间更广泛的关系描述。
通常大家会考虑数据怎么存放的问题,我的理解是领域模型设计期间不用考虑数据的存放问题,只考虑业务描述中涉及的实体以及实体之间的关系。
实体之间的关系,很多书都讲了,无非是泛化、依赖和关联,关联又分了一般关联、聚合、组合等等,我这里就不列了。
设计步骤编辑
领域模型设计是需求分析的关键步骤。它帮助用户及需求分析人员建立业务概念,确定用户业务的问题域,系统涉及的业务范围等等。
领域模型设计的步骤为:
用UML提供的方法和图例进行领域模型设计、确定模型之间的关系 [4] ;
种类编辑
失血模型
简单来说,就是domain object只有属性的getter/setter方法,没有任何业务逻辑。
贫血模型
简单来说,就是domain ojbect包含了不依赖于持久化的领域逻辑,而那些依赖持久化的领域逻辑被分离到Service层。Service(业务逻辑,事务封装) –> DAO —> domain object
这种模型的优点:
1、各层单向依赖,结构清楚,易于实现和维护
2、设计简单易行,底层模型非常稳定
这种模型的缺点:
1、domain object的部分比较紧密依赖的持久化domain logic被分离到Service层,显得不够OO
2、Service层过于厚重
充血模型
充血模型和第二种模型差不多,所不同的就是如何划分业务逻辑,即认为,绝大多业务逻辑都应该被放在domain object里面(包括持久化逻辑),而Service层应该是很薄的一层,仅仅封装事务和少量逻辑,不和DAO层打交道。
Service(事务封装) —> domain object <—> DAO
这种模型的优点:
1、更加符合OO的原则
2、Service层很薄,只充当Facade的角色,不和DAO打交道。
这种模型的缺点:
1、DAO和domain object形成了双向依赖,复杂的双向依赖会导致很多潜在的问题。
2、如何划分Service层逻辑和domain层逻辑是非常含混的,在实际项目中,由于设计和开发人员的水平差异,可能导致整个结构的混乱无序。
3、考虑到Service层的事务封装特性,Service层必须对所有的domain object的逻辑提供相应的事务封装方法,其结果就是Service完全重定义一遍所有的domain logic,非常烦琐,而且Service的事务化封装其意义就等于把OO的domain logic转换为过程的Service TransactionScript。该充血模型辛辛苦苦在domain层实现的OO在Service层又变成了过程式,对于Web层程序员的角度来看,和贫血模型没有什么区别了 [5] 。
胀血模型
基于充血模型的第三个缺点,有同学提出,干脆取消Service层,只剩下domain object和DAO两层,在domain object的domain logic上面封装事务。
domain object(事务封装,业务逻辑) <—> DAO
似乎ruby on rails就是这种模型,他甚至把domain object和DAO都合并了。
该模型优点:
1、简化了分层
2、也算符合OO
该模型缺点:
1、很多不是domain logic的service逻辑也被强行放入domain object ,引起了domain ojbect模型的不稳定
2、domain object暴露给web层过多的信息,可能引起意想不到的副作用。
在这四种模型当中,失血模型和胀血模型应该是不被提倡的。而贫血模型和充血模型从技术上来说,都已经是可行的了。但是我个人仍然主张使用贫血模型。其理由:
1、参考充血模型第三个缺点,由于暴露给web层程序拿到的还是Service Transaction Script,对于web层程序员来说,底层OO意义丧失了。
2、参考充血模型第三个缺点,为了事务封装,Service层要给每个domain logic提供一个过程化封装,这对于编程来说,做了多余的工作,非常烦琐。
3、domain object和DAO的双向依赖在做大项目中,考虑到团队成员的水平差异,很容易引入不可预知的潜在bug。
4、如何划分domain logic和service logic的标准是不确定的,往往要根据个人经验,有些人就是觉得某个业务他更加贴近domain,也有人认为这个业务是贴近service的。由于划分标准的不确定性,带来的后果就是实际项目中会产生很多这样的争议和纠纷,不同的人会有不同的划分方法,最后就会造成整个项目的逻辑分层混乱。这不像贫血模型中我提出的按照是否依赖持久化进行划分,这种标准是非常确定的,不会引起争议,因此团队开发中,不会产生此类问题。
5、贫血模型的domain object确实不够rich,但是我们是做项目,不是做研究,好用就行了,管它是不是那么纯的OO呢?其实我不同意firebody认为的贫血模型在设计模型和实现代码中有很大跨越的说法。一个设计模型到实现的时候,你直接得到两个类:一个实体类,一个控制类就行了,没有什么跨越。
面向对象架构模式之:领域模型(Domain Model)
领域模型是对领域内的概念类或现实世界中对象的可视化表示。又称概念模型、领域对象模型、分析对象模型。它专注于分析问题领域本身,发掘重要的业务领域概念,并建立业务领域概念之间的关系。
面向领域对象设计的原则简单的说就是起名字定职能,对象定领域,找关系做关联。俗话:起名字,画圈,画线。
领域模型是为了解决问题,所谓领域就对应一个问题或者主题。
领域解决了再解决表述和表述持久化的问题。
先来完成最简单的部分,即找关系。也就是说,按照所谓的关系,我们来重构 事务脚本 中的代码。上篇“你在用什么思想编码:事务脚本 OR 面向对象?”中同样的需求,如果用领域模式来做的话,我们大概可以这样设计:
image
概念
编辑
业务对象模型(也叫领域模型 domain model)是描述业务用例实现的对象模型。它是对业务角色和业务实体之间应该如何联系和协作以执行业务的一种抽象。业务对象模型从业务角色内部的观点定义了业务用例。该模型为产生预期效果确定了业务人员以及他们处理和使用的对象(“业务类和对象”)之间应该具有的静态和动态关系。它注重业务中承担的角色及其当前职责。这些模型类的对象组合在一起可以执行所有的业务用例。
核心元素
编辑
业务角色显示了一个人承担的一系列职责。业务实体表示使用或产生的可交付工件、资源和事件。业务用例实现显示了协作的业务角色和业务实体如何执行某个工作流程。使用以下几种图来记录业务用例实现: 图显示参与的业务角色和业务实体。活动图,其中泳道显示业务角色的职责,而对象流显示如何在工作流程中使用业务实体。 序列图描述业务角色和业务主角之间交互的详细情况,并显示如何在业务用例执行过程中访问业务实体。
业务对象模型将结构的概念和行为的概念结合了起来。
它是一个纽带工件,用于对业务关系进行清晰的表述,表述方式与软件开发人员的思考方式类似,同时仍保留一些纯粹的业务内容。将我们所知道的有关业务的信息按照对象、属性和职责进行了合并。
它探索业务领域知识的本质,所采用的方式使我们能够从对业务问题的思考转变到对软件应用程序的思考上来。
它是一种确定需求的方法,使需求能够为待建信息系统使用,并得到该系统的支持。
确定业务对象定义、对象间关系、对象名称和对象间关系名称的流程使我们能够以一种能被业务领域专家理解和验证的精确方式来表达业务领域知识。
领域模型
领域模型
命名
编辑
对每个业务角色和实体进行命名,要求名称能够表示对象的职责。
一个好的名称通常是名词或动词的名词形式, 每个名称都必须是唯一的。避免使用发音或拼写类似的词以及同义词作为名称,可能需要用好几个单词来组成一个明确的、无需额外说明的名称。
对象
编辑
当您研究参与业务中不同用例的业务角色和业务实体时,可能会发现某些对象如此相似,以致于实际上是一个类。即使不同的业务用例没有相同的要求,类是之间也可能相似到足以被视为一个相同现象的程度。如果是这种情况,您应该将相似的类合并在一起。这就产生了一个业务角色或业务实体,它拥有足以满足不同业务用例要求的关系、属性和操作。
因此,多个业务用例可以对同一个类有不同的要求。对于业务角色来说,如果有些雇员有能力担当所描述的一组角色,那么同样还要有一些比较灵活可以胜任多个职位的雇员。这会使您的业务更加灵活。
模型
编辑
在业务对象模型中,业务角色代表雇员将担当的角色,而业务实体则代表雇员将处理的对象。一方面,可以使用业务对象模型来确定业务雇员将如何进行交互,以产生业务主角所期望的结果。另一方面,系统用例模型和设计模型指定了业务的信息系统。
业务建模和系统建模解决不同的问题,其抽象程度也不一样。所以一般而言,信息系统不应该直接出现在业务模型中。
另一方面,雇员作为业务角色来使用信息系统,实现相互之间的通信、与主角的通信以及对业务实体信息进行访问。所有的链接、关联关系或属性都有某个潜在的信息系统对其进行支持。
这两类建模环境有以下关系:
作为特定业务角色的雇员与信息系统的一个系统主角相对应。如果建立的信息系统使该雇员在业务用例中的所有工作都得到一个系统用例的支持,则他最有可能得到最好的支持。 另外,如果业务用例规模大、生存期长或者合并了多个独立领域中的工作,信息系统用例将可以支持业务角色的操作。 雇员工作的对象(建模为业务实体)常在信息系统中得到表现。在信息系统的对象模型中,这些业务实体作为实体类出现。业务实体之间的关联关系和聚合关系常常使设计模型中实体类之间产生对应的关联关系和聚合关系。 因此,系统用例访问并操作设计模型中的实体类,这些实体类代表由被支持业务用例访问的业务实体。最后,直接使用业务信息系统的业务主角也成为信息系统的系统主角。 当确定对支持业务的信息系统的需求时,这些关系十分关键。
VO、DTO、DO、PO的概念、区别和用处
VO、DTO、DO、PO的概念、区别和用处
作者: Johnny.Liang 发布时间: 2015-06-02 18:47 阅读: 20593 次 推荐: 29 原文链接 [收藏]
上一篇文章作为一个引子,说明了领域驱动设计的优势,从本篇文章开始,笔者将会结合自己的实际经验,谈及领域驱动设计的应用。本篇文章主要讨论一下我们经常会用到的一些对象:VO、DTO、DO和PO。
由于不同的项目和开发人员有不同的命名习惯,这里我首先对上述的概念进行一个简单描述,名字只是个标识,我们重点关注其概念:
概念:
VO(View Object):视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
DTO(Data Transfer Object):数据传输对象,这个概念来源于J2EE的设计模式,原来的目的是为了EJB的分布式应用提供粗粒度的数据实体,以减少分布式调用的次数,从而提高分布式调用的性能和降低网络负载,但在这里,我泛指用于展示层与服务层之间的数据传输对象。
DO(Domain Object):领域对象,就是从现实世界中抽象出来的有形或无形的业务实体。
PO(Persistent Object):持久化对象,它跟持久层(通常是关系型数据库)的数据结构形成一一对应的映射关系,如果持久层是关系型数据库,那么,数据表中的每个字段(或若干个)就对应PO的一个(或若干个)属性。
用户发出请求(可能是填写表单),表单的数据在展示层被匹配为VO。
展示层把VO转换为服务层对应方法所要求的DTO,传送给服务层。
服务层首先根据DTO的数据构造(或重建)一个DO,调用DO的业务方法完成具体业务。
服务层把DO转换为持久层对应的PO(可以使用ORM工具,也可以不用),调用持久层的持久化方法,把PO传递给它,完成持久化操作。
对于一个逆向操作,如读取数据,也是用类似的方式转换和传递,略。
VO与DTO的区别
大家可能会有个疑问(在笔者参与的项目中,很多程序员也有相同的疑惑):既然DTO是展示层与服务层之间传递数据的对象,为什么还需要一个VO呢?对!对于绝大部分的应用场景来说,DTO和VO的属性值基本是一致的,而且他们通常都是POJO,因此没必要多此一举。但不要忘记这是实现层面的思维,对于设计层面来说,概念上还是应该存在VO和DTO,因为两者有着本质的区别,DTO代表服务层需要接收的数据和返回的数据,而VO代表展示层需要显示的数据。
用一个例子来说明可能会比较容易理解:例如服务层有一个getUser的方法返回一个系统用户,其中有一个属性是gender(性别),对于服务层来说,它只从语义上定义:1-男性,2-女性,0-未指定,而对于展示层来说,它可能需要用“帅哥”代表男性,用“美女”代表女性,用“秘密”代表未指定。说到这里,可能你还会反驳,在服务层直接就返回“帅哥美女”不就行了吗?对于大部分应用来说,这不是问题,但设想一下,如果需求允许客户可以定制风格,而不同风格对于“性别”的表现方式不一样,又或者这个服务同时供多个客户端使用(不同门户),而不同的客户端对于表现层的要求有所不同,那么,问题就来了。再者,回到设计层面上分析,从职责单一原则来看,服务层只负责业务,与具体的表现形式无关,因此,它返回的DTO,不应该出现与表现形式的耦合。
理论归理论,这到底还是分析设计层面的思维,是否在实现层面必须这样做呢?一刀切的做法往往会得不偿失,下面我马上会分析应用中如何做出正确的选择。
VO与DTO的应用
上面只是用了一个简单的例子来说明VO与DTO在概念上的区别,本节将会告诉你如何在应用中做出正确的选择。
在以下才场景中,我们可以考虑把VO与DTO二合为一(注意:是实现层面):
当需求非常清晰稳定,而且客户端很明确只有一个的时候,没有必要把VO和DTO区分开来,这时候VO可以退隐,用一个DTO即可,为什么是VO退隐而不是DTO?回到设计层面,服务层的职责依然不应该与展示层耦合,所以,对于前面的例子,你很容易理解,DTO对于“性别”来说,依然不能用“帅哥美女”,这个转换应该依赖于页面的脚本(如JavaScript)或其他机制(JSTL、EL、CSS)。
即使客户端可以进行定制,或者存在多个不同的客户端,如果客户端能够用某种技术(脚本或其他机制)实现转换,同样可以让VO退隐。
以下场景需要优先考虑VO、DTO并存:
上述场景的反面场景
因为某种技术原因,比如某个框架(如Flex)提供自动把POJO转换为UI中某些Field时,可以考虑在实现层面定义出VO,这个权衡完全取决于使用框架的自动转换能力带来的开发和维护效率提升与设计多一个VO所多做的事情带来的开发和维护效率的下降之间的比对。
如果页面出现一个“大视图”,而组成这个大视图的所有数据需要调用多个服务,返回多个DTO来组装(当然,这同样可以通过服务层提供一次性返回一个大视图的DTO来取代,但在服务层提供一个这样的方法是否合适,需要在设计层面进行权衡)。
DTO与DO的区别
首先是概念上的区别,DTO是展示层和服务层之间的数据传输对象(可以认为是两者之间的协议),而DO是对现实世界各种业务角色的抽象,这就引出了两者在数据上的区别,例如UserInfo和User(对于DTO和DO的命名规则,请参见笔者前面的一篇博文),对于一个getUser方法来说,本质上它永远不应该返回用户的密码,因此UserInfo至少比User少一个password的数据。而在领域驱动设计中,正如第一篇系列文章所说,DO不是简单的POJO,它具有领域业务逻辑。
DTO与DO的应用
从上一节的例子中,细心的读者可能会发现问题:既然getUser方法返回的UserInfo不应该包含password,那么就不应该存在password这个属性定义,但如果同时有一个createUser的方法,传入的UserInfo需要包含用户的password,怎么办?在设计层面,展示层向服务层传递的DTO与服务层返回给展示层的DTO在概念上是不同的,但在实现层面,我们通常很少会这样做(定义两个UserInfo,甚至更多),因为这样做并不见得很明智,我们完全可以设计一个完全兼容的DTO,在服务层接收数据的时候,不该由展示层设置的属性(如订单的总价应该由其单价、数量、折扣等决定),无论展示层是否设置,服务层都一概忽略,而在服务层返回数据时,不该返回的数据(如用户密码),就不设置对应的属性。
对于DO来说,还有一点需要说明:为什么不在服务层中直接返回DO呢?这样可以省去DTO的编码和转换工作,原因如下:
两者在本质上的区别可能导致彼此并不一一对应,一个DTO可能对应多个DO,反之亦然,甚至两者存在多对多的关系。
DO具有一些不应该让展示层知道的数据
DO具有业务方法,如果直接把DO传递给展示层,展示层的代码就可以绕过服务层直接调用它不应该访问的操作,对于基于AOP拦截服务层来进行访问控制的机制来说,这问题尤为突出,而在展示层调用DO的业务方法也会因为事务的问题,让事务难以控制。
对于某些ORM框架(如Hibernate)来说,通常会使用“延迟加载”技术,如果直接把DO暴露给展示层,对于大部分情况,展示层不在事务范围之内(Open session in view在大部分情况下不是一种值得推崇的设计),如果其尝试在Session关闭的情况下获取一个未加载的关联对象,会出现运行时异常(对于Hibernate来说,就是LazyInitiliaztionException)。
从设计层面来说,展示层依赖于服务层,服务层依赖于领域层,如果把DO暴露出去,就会导致展示层直接依赖于领域层,这虽然依然是单向依赖,但这种跨层依赖会导致不必要的耦合。
对于DTO来说,也有一点必须进行说明,就是DTO应该是一个“扁平的二维对象”,举个例子来说明:如果User会关联若干个其他实体(例如Address、Account、Region等),那么getUser()返回的UserInfo,是否就需要把其关联的对象的DTO都一并返回呢?如果这样的话,必然导致数据传输量的大增,对于分布式应用来说,由于涉及数据在网络上的传输、序列化和反序列化,这种设计更不可接受。如果getUser除了要返回User的基本信息外,还需要返回一个AccountId、AccountName、RegionId、RegionName,那么,请把这些属性定义到UserInfo中,把一个“立体”的对象树“压扁”成一个“扁平的二维对象”。笔者目前参与的项目是一个分布式系统,该系统不管三七二十一,把一个对象的所有关联对象都转换为相同结构的DTO对象树并返回,导致性能非常的慢。
DO与PO的区别
DO和PO在绝大部分情况下是一一对应的,PO是只含有get/set方法的POJO,但某些场景还是能反映出两者在概念上存在本质的区别:
DO在某些场景下不需要进行显式的持久化,例如利用策略模式设计的商品折扣策略,会衍生出折扣策略的接口和不同折扣策略实现类,这些折扣策略实现类可以算是DO,但它们只驻留在静态内存,不需要持久化到持久层,因此,这类DO是不存在对应的PO的。
同样的道理,某些场景下,PO也没有对应的DO,例如老师Teacher和学生Student存在多对多的关系,在关系数据库中,这种关系需要表现为一个中间表,也就对应有一个TeacherAndStudentPO的PO,但这个PO在业务领域没有任何现实的意义,它完全不能与任何DO对应上。这里要特别声明,并不是所有多对多关系都没有业务含义,这跟具体业务场景有关,例如:两个PO之间的关系会影响具体业务,并且这种关系存在多种类型,那么这种多对多关系也应该表现为一个DO,又如:“角色”与“资源”之间存在多对多关系,而这种关系很明显会表现为一个DO——“权限”。
某些情况下,为了某种持久化策略或者性能的考虑,一个PO可能对应多个DO,反之亦然。例如客户Customer有其联系信息Contacts,这里是两个一对一关系的DO,但可能出于性能的考虑(极端情况,权作举例),为了减少数据库的连接查询操作,把Customer和Contacts两个DO数据合并到一张数据表中。反过来,如果一本图书Book,有一个属性是封面cover,但该属性是一副图片的二进制数据,而某些查询操作不希望把cover一并加载,从而减轻磁盘IO开销,同时假设ORM框架不支持属性级别的延迟加载,那么就需要考虑把cover独立到一张数据表中去,这样就形成一个DO对应多个PO的情况。
PO的某些属性值对于DO没有任何意义,这些属性值可能是为了解决某些持久化策略而存在的数据,例如为了实现“乐观锁”,PO存在一个version的属性,这个version对于DO来说是没有任何业务意义的,它不应该在DO中存在。同理,DO中也可能存在不需要持久化的属性。
DO与PO的应用
由于ORM框架的功能非常强大而大行其道,而且JavaEE也推出了JPA规范,现在的业务应用开发,基本上不需要区分DO与PO,PO完全可以通过JPA,Hibernate Annotations/hbm隐藏在DO之中。虽然如此,但有些问题我们还必须注意:
对于DO中不需要持久化的属性,需要通过ORM显式的声明,如:在JPA中,可以利用@Transient声明。
对于PO中为了某种持久化策略而存在的属性,例如version,由于DO、PO合并了,必须在DO中声明,但由于这个属性对DO是没有任何业务意义的,需要让该属性对外隐藏起来,最常见的做法是把该属性的get/set方法私有化,甚至不提供get/set方法。但对于Hibernate来说,这需要特别注意,由于Hibernate从数据库读取数据转换为DO时,是利用反射机制先调用DO的空参数构造函数构造DO实例,然后再利用JavaBean的规范反射出set方法来为每个属性设值,如果不显式声明set方法,或把set方法设置为private,都会导致Hibernate无法初始化DO,从而出现运行时异常,可行的做法是把属性的set方法设置为protected。
对于一个DO对应多个PO,或者一个PO对应多个DO的场景,以及属性级别的延迟加载,Hibernate都提供了很好的支持,请参考Hibnate的相关资料。
四色原型是诞生于90年代,被广泛使用的一种系统分析方法,如Borland的Together架构师版,准确地说,是由Peter Coad 和 Mark Mayfield首先提出[Coad92],然后由David North拓展[Coad95-97]
前面已经说过,域建模是整个软件系统的龙头,在现代Java技术如JiveJdon3.0开始之前,我们都是需要领域建模,也就是在UML中画出类图,然后标记上类图四种关系(关联、依赖、继承和实现),但是这些只是UML图的表面,只是一种画图技巧,就象CAD画图一样,你可能没有被告知:这个类图是怎么出来的?为什么选用关联而不是依赖,这些实际都属于分析领域的知识,而四色图可以说为我们这种分析提炼提供了一种模板或分析框架,这样我们可以按图索骥去分析每个陌生的系统,我们拥有强大的分析方法工具。
moment-interval archetype
这是一个很重要的原型,重要在于时间概念上:某个时刻(moment)或一段很短时间(interval)内. 意味在某个时刻发生的事情因为业务要求或合法性原因需要跟踪;或者过一段时间以后,应该是很短的时间,可以帮助我们寻找到它。
卖东西是在某个时刻发生的,它有发生日期和时间。租赁行为是在一段时间内发生,从开始出租和归还所租物品;预定也是持续一段时间,什么时候预定;什么时候过期等。
这些我们都使用moment-interval原型来表达,UML图如下:
Moment-intervals是和组件模型捆绑在一起,代表了组件模块关注的核心和灵魂,在一个Model中,Moment-intervals经常封装的是最关键的方法,为让其显目,moment-interval的UML图我们使用粉红颜色表示。
时刻-时段原型(Moment-Interval Archetype)
表示事物在某个时刻或某一段时间内发生的。
使用红色表示。简写为MI。
1.2. 描述原型(Description Archetype)
表示资料类型的资源,它可以被其它原型反复使用,并为其它原型提供行为(用作方法的参数)。
使用蓝色表示。简写为DESC。
1.3. 参与方-地点-物品原型(Part-Place-Thing Archetype)
表示参与扮演不同角色的人或事物。
使用绿色表示。简写为PPT。
1.4. 角色原型(Role Archetype)
角色是一种参与方式,它由人或组织机构、地点或物品来承担。
使用黄色表示。简写为Role。
MI类似DDD中的Service,但与DDD的Service不同之处在于,MI是充血模型,Service是失血模型。
比如“一次销售”就是一个MI,“一次销售”是一段时间(从开单到审批)内的业务活动。
2.2. PPT
PPT是指一个具体的,可以作为单个个体(每个单个个体之间有唯一标识符)被识别、区分出来的对象。
2.3. Desc
Desc是PPT的抽象概念,它是PPT的特性的总结,每一个PPT都属于一个(种)Desc。
比如“一台电脑”就是一个PPT(每台电脑都有唯一编号),而“一个硬盘”也是一个PPT(每个硬盘也有唯一编号)。
但是“硬盘”就是Desc(硬盘是每个硬盘的泛指,也是每个硬盘的类型)。并且硬盘是可以再分类的,IDE硬盘和SCIS硬盘等,也就是说,Desc可以是一个树形结构。
注意,以上的理解是错误的,正确的理解请参照《四色原型札记(一)》,下图是纠正后的表示:
2.4. Role
Role是PPT在参与业务行为时的身份,PPT不直接与MI打交道,PPT必须拥有指定的角色(Role),才能使用MI操作业务。
Role存在的作用是为了隔绝MI直接使用PPT。
2.5. 举例
比如,“人”就是一个Desc,而“亚洲人”、“黄种人”也是一个Desc,亚洲人和黄种人都属于“人”这个Desc的子Desc,即Desc可以是一个树结构。
具体到每一个人的时候,“张三”、“李四”就是一个PPT了。因为张三和李四是独一无二的,他们都有唯一标识符可以被识别、区分(比如身份証号、指纹等,视不同的系统需求采用不同的唯一标识)。
2.6. 总结
如果以PPT为中心,那么以上概念可以总结为:什么东西(人或事物)通过什么方式(身份)进行什么操作(业务)。
即当 PPT是Role时,可以调用MI。例如当张三是学生时,可以去上课。
如果以Desc为中心,那么以上概念可以总结为:什么什么类型的东西进行什么操作(业务)。
即MI调用Desc。例如人都可以睡觉。
规则1:PPT不能直接与MI打交道,它必须通过Role或者Desc才能操纵MI。
为什么要隔绝MI直接访问PPT?
如果MI直接访问PPT会带来以下问题:
PPT如果直接参与MI,那么PPT就会拥有MI环境中的属性,比如电脑在维修时必须记录维修结果,在销售时必须记录售价,那么PPT随着MI的增加会不断地膨胀,每增加一种MI,PPT就要修改一次。
两个MI之间业务是完全不同的,PPT中有些属性对某一个MI来说,根本是无用的。例如电脑的价格,对维修来说是无用的。
MI直接使用PPT,还会带来MI之间的资料隔绝性问题。有些PPT的属性对某一MI是不允许访问的,如果MI直接使用PPT,那么就无法保密资料。
例如,电脑的折扣可能是保密的,不应该让维修知道。
增加Role之后,上述问题迎刃而解:
每增加一种PPT,就相应地增加一个Role,凡是与此MI相关的属性,都放在Role中。这样既避免了PPT的频繁修改,也避免了资料访问的问题。
可以这么描述Role:只有当事物(PPT)具有某个身份(Role)时,它才拥有与业务(MI)相关的属性(字段和方法)。
特征描述的模板: