近期一直在从软件概念完整性和工程实现的角度,思考DDD中各个模型的概念及用法,终于在清明节期间,完成了全部对象模型的概念一致性描述思路,在未来的一段时间, 我将逐步将个人建模的想法,以DDD的概念形式从不同的视角呈现给大家,形成一系列的文章. 在此先对大家常用到对象进一个汇总的分析,作为领域设计系列文章的第二篇.
领域实体对象
这里讲述实体对象本身并不确切,因为在面向对象世界里,并不存在单一的值实体和完整实体的区分, 值实体本质上只是完整领域在某个特定的约束下的snapshot. 对于领域建模而言,整个概念范围内通常并不应该存在简单独立的实体对象,而总是应当将对象及交互作为统一体进行整体描述.因此在本章节,在描述对象实体的时候,我们不免要针对对象相关的内部及外部交互进行各方面的涉及,这是自然而又不可避免的.
1. DO (Domain Object)
首先,我认为DO不是一个简单的对象,在所有领域模型中,DO都被定义为包含状态和行为的对象, 完整的DO应当代表一个领域的全部状态和行为,以及对状态行为的变迁的管理. 对应的, 我们应当存在相应的抽象工厂,或工厂化方法,用于协助构建领域. 因此,在领域建模中, 应至少包含三个部分, 领域对象, 构建工厂, 构建序列. 领域对象用于系统运行时状态, 构建工厂仅应当实现领域对象的构建,并在构建完成后结束自身的职责, 而构建序列则作为指导工厂实现领域对象动态化配置的工序清单,需要独立去实现.
从软件概念完整性的角度分析,领域对象自身是具有自我包含且自我状态维护的Closure. 任何外部的行为都不应当直接改变领域对象的内部结构,这应当也是领域的天然特性,与面向对象的封装概念具有天然的一致性吻合.因此,领域需要完善对自身状态的管理职能.
2. VO (View Model, View Object)
关于VO我觉得是存在歧义最大的部分, 领域对外的每一个表现,都是一个VO, VO应当是领域对象在某一个侧面的表现形式, 称为View Object本身并不确切,更准确的说,应当是ViewModel, ViewModel反映了领域的对外数据表现格式及规格.如果要严格的定义ViewObject的话,那么ViewObject应当是领域在某个时刻其自身内部状态,在特定的ViewModel上的Snapshot. 有关VO的更多解释将放在领域交互部分进行讲述.
3. DTO, (Data Transfer Object)
这并不一定是一个Java对象,在领域设计中,应当牢记一个准则:”如无必要,勿增实体”, 当前基于面向对象的抽象建模的产物已经足够复杂了,因此在没有必要的时候,我们毋须刻意的为切合某一理论或模型而刻意去增加什么实体. DTO是什么? 典型的讲,DTO仅仅表示数据模型在传输过程中的一种表示形式,属于分层通信模型中representation层的范畴,如果是采用JAX-WS,那么它就是一个符合SOAP协议的xml格式数据流,如果采用rest风格的传输,DTO会根据实际情况不同而表现为XML,JSON,TEXT-PLAIN,在直接的Java mothod,Invocation中,可能DTO表现为Pojo, 而在RMI条件下,可能这一切又是对编程人员透明的serializable javaObject,亦或者在二进制的传输协议上使用OCTET-STREAM, 在领域设计的过程中, 切忌对DTO进行过度设计和编程,DTO应当根据领域通信方式的不同而自然而然的出现,在信道的两端进行encoding和decoding操作,这是一组完整的介质,协议,算法和数据的组合,有时候,为了保证数据传输的完整性,以及对传输可靠性及容错的要求,在DTO传输层,可能会增加对外部屏蔽的重传,校验等机制,在多数情况下,这一层一般会依赖于专门的第三方技术实现. Data Transfer 在领域中对其他的环节应当是透明的, 这样,我们在集群及虚拟化技术中,将分布于多个服务器上的应用最终组合成完整的云, 关于DTO在实际实现中为什么是必要的,将会在领域交互部分进行说明.
4. PO(Persistent Object), PO并不是与数据库表一一映射的实体对象,相反PO应当反应领域在持久化过程中,需要维护事务原子性的最小单元, 也就是说,每一次与持久化层进行交互过程,我们都应保证交互对象本身是符合最小完整性一致性依赖的. 通过领域业务执行序列,我们可以对一系列持久化单元操作进行再现,恢复,和逆向操作. 因此, PO的设计应当是根据业务需求而进行拆解,而不是根据数据表格式进行定义,是可以保证业务ACID,通过依赖其他技术和过程,从而实现系统数据的最终一致性以及可靠性的最小粒度单元. 在这种设计理念下,我们在领域驱动的设计理念及现实业务中,保证事务一致性的实际需求间通过折中达成了最终一致,为未来的技术实现扫清了设计概念层的障碍.
另外,PO本身切忌与数据库实体对象直接耦合, 从PO到最终的持久化实现,应当具备独立的物理层架构设计,以及完整的对象到存储的mapping技术,甚者很多对象是不需要持久化到RDB中的,从PO到DB表的直接映射,从复杂的设计逻辑来讲,应该是罕见的特例.
领域基础之Repos
Repos,作为领域设计中不可或缺的一部分,是整个工程设计中不可或缺的一环.在此仅就Repos的设计理念及原则进行简要的论述,具备性能及独立性要求的具体实现应当根据每个项目及领域的具体约束来确定,那应当是针对每个具体案例所进行的针对性开发.
Repos设计的一般性原则
首先, Repos在概念设计上应当是抽象的,是隔离领域概念设计和领域持久化具体实现的抽象层. 因此Repos的接口层设计应当是实现无关的.
其次, Repos在接口设计上,除必要的业务完整性,原子性,一致性,持久化要求外,还应当对接口的使用频率,数据吞吐量,和响应时长等性能指标及其他非业务指标指标提出明确的规格.
Repos设计的概念隔离性说明
在领域设计的过程中,必须严格的界定领域的范围和其Repos内部的实体概念,Repos的内容对于每个独立的领域都必须是领域内部私有的, ,跨领域的Repos访问在整个系统中是需要绝对禁止的. 实践证明,跨领域的Repos访问最终会导致整个系统的概念混乱和运行时崩溃,是领域设计过程中,必须绝对避免的行为.
当系统设计中,如果出现了需要进行跨领域Repos访问的需求时,有(但不限于)以下解决方案:
1. 重构系统.重新定义领域的概念和边界,从而使整个系统在新的需求加入的时候,依然保证整体的完整性和领域间清晰的隔离及交互规则. 从而形成新的系统状态,这叫做系统的完整性迁移.
2. 重新定义领域交互, 将需要进行跨域Repos访问的业务从概念上拆解为领域内部的部分实体和领域间的交互两个部分,通过对单一概念的细粒度拆解,最终保证两个领域间的隔离性,和整体概念上的一致性.(这一方法适合短期,且后续延伸变更不强的需求, 并且通常会出现系统在拆解部分的非功能性约束被违背的现象,因此,在理想的设计过程中,应当首选方案1).
Repos实现在性能与优雅之间的思考
在理想的世界里,如果计算足够快,存储吞吐量足够大到满足任何业务的需求,那么通常关于性能的优化是没有意义的,但现实往往没有理想那么丰满. 所以在追求设计尽量优雅的前提下, 通常我们的实现要向性能等其他需求进行妥协,这边有了这一段的思考.
数据存储的设计首先要满足概念正确性和完整行的要求,违背业务正确的存储没有任何实际的意义. 这是存储实现的第一原则.
在存储实现过程中,根据性能及其他方面的要求不同, 我们通常会在纯粹的概念设计中加入基于场景的倾向, 比如偏向读取的存储,偏向高吞吐量的存储设计, 面向高可靠的存储实现,面向严格的事务保证的实现, 通常一个领域的存储是在多个方面的业务需求间的折中,好的设计会尽可能满足多个目标的非业务边界需求,在这个方面,没有直接可以拿来的方案,一切都是各种技术根据应用场景需求而进行的灵活的组合,这里就需要大家在思考Repos设计中的绝对边界条件,充分发挥自己的创造力,而这里通常没有捷径可走,历史上每一个优雅的实现,都是杰出程序员思想的结晶,幸运的是,前路已经有无数的成功或失败的案例可以参考.
在这一章中,本应涉及到缓存,及存储的实际应用,但因为需要在领域交互篇内,针对领域交互的边界进行相关性论述,因此也挪到了后续章节.
领域交互
在领域驱动的设计方案里,不应当有开发的领域不受约束. 所谓的开放领域,实际上是无数个未经组织和优化的独立的子领域的无序集合,而在这样未经设计和规划的集合里,往往孕育着evils. 所以,在谈及领域交互之前, 再一次强调,如果你不是万能的上帝,那么将所有的一切都纳入受控的领域范围,是作为渺小的人类对抗邪恶的唯一的手段,切记切记.
领域概念交互,再论ViewModel和ViewObject.
VO真的仅仅封装了指定页面的数据么?
在公司的Confluence上引用的文章对VO(View Object)做了如下解释:
视图对象,用于展示层,它的作用是把某个指定页面(或组件)的所有数据封装起来。
这个定义本身是错误的,它给我进行领域完整性思考的过程带来了很大的干扰,以至于我在思考整个领域设计的过程中间阶段甚至抛弃了VO的概念,因为从定义和所处的层次而言,上文定义的VO本质上是一个领域对另一个领域概念的入侵,因此其本质上,上文所述的VO仅仅是另一个领域的内部对象的部分状态的一个snapshot实例,因此,在最初的领域设计原型中,我摒弃了VO的概念.
VO概念的重定义.
在摒弃作为上文定义的VO之后的领域概念思路中,一切就都水到渠成了,Manager,Provider,DataTransfer等等概念在完整的领域建模的程序实现中一一呈现了其自然拥有的角色和功能边界,而此时,ViewModel,ViewObject的概念也自然而然的浮出水面. 从领域建模的概念完整性角度来讲, ViewModel和ViewObject应当以如下方式进行定义:
ViewModel是领域与外部领域交互边界的规格表示,它定义了领域间交互的数据组织形式和数据的格式约束.
ViewObject 是领域与外部领域交互的过程中,任意一次交互的规则的一个状态实例,表示的是领域自身在交互临界状态基于ViewModel的Snapshot.
本质上讲,View是属于Domain的外部视图,是Domain自身状态的外部表现,在系统交互过程中属于表示层(representation)层的概念,而非对其他领域的入侵. 而上一节的定义中,直接将ViewModel具化到网页项目中的页面数据及组件封装,与我们当前所描述的VO之间,其实还欠缺了PageDomain模型设计,PageDomain与BusinessLogicDomain intercommunication的详细设计,以及PageRepos设计等很多领域建模过程, 进而导致了业务模型与页面模型两个领域的强耦合和页面自身领域模型的开放性. 因此从严格的意义上讲, ViewObject本身作为视图对象没有错误,它仅仅是狭隘的表达了ViewObject作为一个抽象概念在某一个特定领域的具体应用.并且在既有的系统设计模型和结构上,因为领域对象设计上的缺失,最终将人们误导到了一个错误的设计方向上,这种领域设计上的错误,应该是我们目前所遇到的系统问题的根源所在,关于如何正确的进行商城领域设计,以及如何定义领域模型和领域交互,已经超出了本文的范围,我在系统设计上已经有了一个大概的思路,但在具体实现上,还有一些编程的细节和设计模式上如何去实现方面的问题需要去处理,在此不再延伸. 有关VO应当如何去定义和实现,因牵扯到后续多个对象,因此放在下文合适的章节内.
领域设计的现实约束,DataTransfer和DTO的现实意义
DataTransfer在领域设计中并不是必须的
首先,在概念层领域设计中,不应当出现DataTransfer的概念, 领域应当以其内的全部实体,交互为主体,以标准外部交互接口,ViewModel来表示领域的外部可见性和可交互性,在底层通过Repos来管理领域状态的持久化,在整个设计过程,DataTransfer是体现不出价值的.
为什么在现实中离不开DataTransfer?
DataTransfer的出现,本质上是分布式计算的的需求推动的,从根本上讲,如果不存在分布式计算,不存在冗余集群,负载均衡和高可用性,那么在领域交互中,就没有DataTransfer概念存在的必要,而现实中,以上特性在我们的系统中,都真实的存在,因此,DataTransfer在实际的系统中是必须的.
DataTransfer的模型的完整性说明
DataTranfer的模型寄宿环境,Service,Agents结构说明
在谈及DataTransfer之前,必须要定义DataTranfer模型设计的宿主, 领域模型的 Service, Agent(Manager,Provider,Or MethodInvocationAgents)交互模型.
为了实现分布式和冗余集群以及领域隔离等程序的实现约束,在实际的系统实现中, 领域通常会划分为2个部分, 完整的领域模型宿主服务和对服务请求的(远程)代理.
Service可以是一个独立的虚拟机实现,也可以是一组虚拟机的集群共同实现一个完整的领域,在小规模的项目中,多个领域的实现可以集成到单一的宿主内,进行逻辑隔离,以上多种实现方案在领域设计的概念上是一致的, 但在实现上,会导致Service及Agents均表现为不同的形式.
在最小化的设计中,Service和Agents是集成到单一宿主中的,从现实意义来讲,就是全部的系统领域全部寄宿在同一个Java虚拟机中,在这种条件下,Agents可以直接实现对Service的Method Invocation, 这是全部实现中,唯一不需要DataTransfer结构的应用场景.
不幸的是,在我们当前的系统中,采用的就是这种典型的最小化系统模型.因此,在当前的系统中,DataTransfer是完全不必要出现的,我们如果一定要抽象出这一逻辑结构的话,那么它唯一的功能就应当是对Service标准接口的direct Invocation来实现一个伪Data Transfer,并保证Invoke的上下两层所使用的ObjectModel是完全一致的, 从分层隔离的角度来讲,其他任何工序的添加,必然会带来系统整体架构的概念混乱或冗余以及功能入侵.实际上,这种错误普遍的存在于我们当前的项目当中.
DataTransfer的功能范围和实现和系统意义
DataTransfer的整体由Sender,Receiver,transport protocol和model representation四部分组成. 它在隔离的Agents,Service之间形成一个系统领域概念透明的数据传输通道,其职能主要是保证数据的传输过程,符合数据传输的外部要求.
DataTransfer模块必须且只应当完成以下功能:由发送端将需要从Service/Agent发送的数据以TransferModelRepresentation的规格进行编码,并依赖其底层的应用或传输协议实现,将编码结果传输到Receiver,Receiver对接受的数据根据TransferModelRepresentation协议进行解码,并转换为Agent规定的新的数据模型(ViewModel,或者Partial of the View Model的对象或者对象的部分状态,具体的转换要根据实际的领域交互设计来最终确定. 但DataTransfer模块的功能界限是清晰的,这在所有的架构模型中均保持一致,没有例外.
然而,例外总是发生在没有例外之后, 在需要进行广播或多播的数据传输模型中,可能我们会遇到基于网络路由的多播技术实现的高性能实时冗余集群数据传输架构,这在目前来讲,可能我们在相当长的一段时间内都不会遇到,因此不对此情况进行额外的建模和分析.
从使用性上讲,DataTransfer模型,在实际的编程中具有如下意义:
1. 通过与Agents和Service配合,DataTransfer模型彻底实现了不同领域间的解耦,为实现虚拟化技术和SOA的实现提供了有力的技术支撑.
2. 在领域建模中,向上层屏蔽了不同的介质上进行数据传输的复杂性,对所有的领域交互提供了统一的交互模式.
3. 通过DataTransfer模型,降低了不同技术实现间进行交互的难度.这与Agents模型和Service实现有更直接的关系,因此将在后续章节详细描述.一般的来讲, DataTransfer模型并没有定义数据传输两端的技术实现方式,而仅仅强调了数据模型在概念上的一致性,这是跨技术领域系统实现的一个有力保证.
关于DataTransfer的表现形式:可以表现为异步的Command, Provider独立形式,也可以表现为hang out的Method Invocation形式.纯异步的结构设计将大大增加系统整体的设计复杂性,并引发过多的领域同步问题,这将给业务设计,体系结构设计和最终编码实现都带来大量的额外工作和实现压力.因此,无意义的异步交互需要尽量避免,以维持领域整体交互的清晰和简洁.
在绝大多数情况下,设计良好的direct method Invocation已经足够满足当前我们业务的需求.因此,绝大多数开发及设计人员应当将精力更多的放在如何更有效的进行领域的设计和实现过程的性能优化方面.完全异步的系统设计形式,以目前国内程序员的水平来讲,能够有效的进行系统性把握和构建的并不是很多,因此,除非是有足够的理由和相当的异步和并发设计经验,并对进行模型有足够把握的程序员,不建议进行过多的异步结构设计,即使如上所述,为了保持系统的简洁,也不建议仅仅为了展示技术而引入过多不必要的异步结构.
领域交互之Service的建模及Agents的实现
在领域交互中, DataTransfer,Agents和Service技术想结合,实现了不同领域之间从概念层到物理层的解耦. 如果在简单的direct method Invocation模式中,这些概念往往可以简化为一个对外的调用接口,而在实际的领域交互中,正是通过对接口调用过程的扩展来向调用方屏蔽接口具体的实现过程.
Agents的概念模型
从概念上讲,Agents应当完成以下两个必要功能和不止一个可选功能:
1. 设计应包含某一领域针对另一领域的公开API和ViewModel模型的实现.
2. 应当包含对DataTransfer模型的封装,进而实现领域内通信对外的透明性.
3. (可选)对于复杂的Service和Agents交互模型,可能Agents还需要承担远程调用缓存,客户端领域View集成,负载均衡等复杂的功能. 一般的来讲, service或service集群应当包含领域的完整概念,而agents可以在不破坏service领域概念完整性的前提下,因为性能或集群分布式等需要,提供客户端的缓存及业务集成等辅助功能,这将根据具体的业务及service架构方式来决定,每个模型的选择应该是自然而贴切的,有关agent的具体UML或Java实现样例,将是后续某个文章的一个主题,在此不过多展开.
Service建模的常见形式及对应的Agents的实现:
单一服务模式
在这种模式下,整个领域的全部业务集中在一个容器中实现,没有并行冗余,也没有分布式业务,所有对外的视图都体现在service层对外的开放的API和Service端ViewModel层上.
单一服务模式下的Agents设计
根据系统最小实体集合思路,Agents的实现根据领域交互方式的不同,本质上分为两类:
小型单一容器系统 (Model 1):
对于全部领域模型都存在于一个容器中的应用系统,Agents包括DataTransfer均可以省略,在全部领域间使用direct method invocation, 也可以使用spring进行注入来实现不同领域间的解耦,这是Agents设计中最简单也是最常用的实现方式,这种程序,如果未来需要进行拓展,仅需要根据领域交互接口,实现一个相同的远程调用Agents,再封装Agents和service之间的DataTransferModel,即可实现向(Model 2)的平滑过渡,进而实现面向分布式和SOA的扩展,因此,对于快速开发的项目,没有必要为了日后的扩展而进行过多的冗余组件编码,仅仅在设计过程中严格遵循领域设计的规则,即可为日后的水平扩展和垂直扩展打好基础.
实现Agents和Service隔离的系统 (Model 2):
在这种编码模型中,Agents与Service遵循相同的数据模型协议进行独立的实现. 两者可以是同一种技术比如Java,然后在Agents和Service之间共同引用相同的ViewModel模型,进行系统在开发过程的解耦.在系统集成时,通过动态注入和direct Method invocation进行Agents和Service之间的方法调用和集成. Agents和Service也可以采用完全不同的实现, 这就需要使用一些跨平台,跨语言的技术,比如在Java语言中封装c语言的实现来调用c的底层库. 或者通过javascript over xmlhttp协议来调用平台无关的http开放服务. 在这种条件下领域设计过程应当明确的定义数据模型和数据内容在 Agents和Service之间的概念及含义的对等性.并在DataTransfer的入口和出口的Encoding和Decoding过程,进行概念对等转义.
在技术对等的Agents和Service之间,采用同一个DataModel来应用于DataTransfer,和ViewModel是可行的. 如果在未来的可能情况中,如果出现Agents层的变化,我们不需要改变DataTransfer的representation和protocol和Service端的Sender和Receiver实现,仅需要改变Agents端的DataTransfer的Encoding和decoding实现,即可实现Agents层的技术迁移.
同样,我们也可以仅通过改变服务端的Encoding和Decoding来实现Agents向不同的服务的动态重定向,以及向Agents屏蔽Service端实现技术的变迁, 从而使系统平滑的向(Model 3)进行平滑技术迁移.
在Model 2模型中, DataTransfer技术在Agents和Service间是透明的,这本质上利用了数据隧道的概念,向领域模型屏蔽了传输的实现细节,而程序员必须考虑不同的实现技术在非业务行的各种规格,以便做出正确的技术选型和程序设计决策.
实现Agents和Service不对等隔离的系统 (Model 3):
通常,现实业务在Model 2的基础上,随着时间的变迁,会发生服务端或客户端的不一致性变更,这样最终会导致Model 2 模式自然的演变成Model 3 模式, 在Model 3中, 系统是可运行的, 但随着时间的迁移,最总系统将走向概念完整性不可维护, 因此,对于绝大多数项目而言, Model 3是一个项目生命的终点, 当我们的项目在Model 3渡过了生命的黄金时刻,我们需要做的就是 Refactoring. 因为此时,继续的维护产生的成本将远远超过实际成本.
基于完全一致服务的应用冗余集群(Model 4)
在该模型下,系统中领域的全部Service均具有该领域业务的全部职能,即全部服务是业务一致的.在该模型下,我们可能会在Agents端加入动态的Service注册和负载均衡检测,高可用性方案等机制,从而避免单一节点down机对系统整体的影响.
从Model 2到Model 4的技术迁移中,需要在Agents端加入动态服务注册机制,如何要实现具有复杂策略的负载均衡和实时的高可用性检测,可能还需要加入Agents与多个Service间的性能交互机制, 因此,从Model 2 向Model 4的迁移也是平滑的, 并不需要在项目建设之出,付出过多的精力去为未来而设计.
Model X:
根据业务需求的变化和物理实现的不同,通过对领域完整模型采用不同的设计理念, 领域最终实现可能会发展为Model X混合的多维架构组合体, 这些都已经超出了本文所要阐述的思考范围,也超出了对程序员所要掌握的一般性技术要求,在此不再过多的举例.
无处不在的缓存
在写本文之前,我一直在迟疑缓存究竟应当存在于Provider之前还是之后,在彻底理清领域交互概念之后,我释然了. 缓存并不是必须存在于Provider的哪一边的.
准确的来讲, 缓存如果存在于Provider的外部,那么缓存本质上就是另一个领域的状态元素的一个非持久化表现形式,或持久化单元的内存表示形式.
而当缓存存在于Provider内部而对外部不可见时, 那么缓存本质上代表了领域对象实现中为非业务性需求而进行的体系结构优化.
因此,这两者都可以存在,并且在概念上存在本质的不同, 这里可能我又被最初的设计模型引入了错误的方向.
总结
本文是基于对公司当前既有的领域对象框架的不断思考而得出的一篇概述性文字. 根据我个人的理解, 针对当前使用的系统框架, 本文已经从系统概念完整性的角度针对绝大多数层次和结构类型给出了比较准确和严谨的功能和交互定义,也对大家常用的对象实体给出了相对于Confluence上面的文字更为全面的概念定义,和陷阱剖析.
比较遗憾的是, 本文没有针对DomainModel以及领域Model的交互给出具体的实例性参考,在写本文的时候曾考虑过以PageDomain和LogicDomain为例,来进行具体的Domain对象建模的实际举例,但在实现的设计模式细节上,还存在一些不太成熟的方面,因而最终放弃了.
同时,也没有针对Agents(Manager,Provider)的建模给出多样化的实例,很想写一些基于类指纹识别算法的Agents Service交互模型,或者基于负载均衡的高可用性模型,或者用于面向测试的DefaultAgentsModel的实现样例,这些可能都会是接下来,领域设计后续篇章的内容.