在UI层使用Domain逻辑的一些探讨
今年做了两个基于Rich Domain Model的系统, 如何在UI层使用业务逻辑,公司之前的系统在这上面的处理上让人非常不爽,自己重新设计了一套还是觉得有点别扭,拿出来给感兴趣的人探讨下。
先给出系统逻辑架构简图
逻辑结构:
UI层使用WinForm,和后台间传递DTO(DTO后面还会详细介绍);
Business 层逻辑上包含Façade层和Domain层,Façade不是简单的对应于Façade模式,还兼容了Biz Flow Control, DTO Assemble(将Domain对象转换成DTO对象或者相反)等Application Service 方面的内容和逻辑,Business层只有Façade对UI层可见;
Persistence层负责和数据库打交道,因为不是重点就不多作介绍了。
我们规定UI层只能使用无行为的DTO对象,通过分析,我们发现大部分的界面都可以使用和Domain对象内容一致的DTO对象,比如 Domain里面有Order->OrderItem这么个Domain对象的聚合体,在UI的某个界面往往也正好只需要同样的数据聚合体。所以我 们划分出EntityDTO和FlatDTO的概念,EntityDTO和Domain对象一一对应,只包含Domain对象的数据,DTO Assembler有工具可以自动完成这两者的映射,FlatDTO就是任意数据内容的组合(和PEAA里面的DTO的介绍一致)。
物理结构:
系统UI和Business层物理分离的,通过Remoting通信,公司之前的这种Remoting结构的系统一
直存在两个问题,1)因为WinForm提供了丰富的界面操作,有的人为了使用后台和业务相关的逻辑频繁的调用Remoting对象,完全不考虑效率,大
部分操作背后都进行了若干次的Remoting调用,2)有的人考虑到了效率,把很多不访问到后台资源的检查和逻辑都写到UI层,这样导致逻辑分散难于管
理难于重用。(我承认UI必不可少得包含一些逻辑,比如非空输入检查,这种检查往往UI和Domain都需要做的,但有些业务特别是计算还是只由
Domain去处理好些)
为了缓解上述两个问题,我考虑了另外一种稍微有点怪异的结构。因为UI层对RemoteFaçade依赖于实现,而非倚赖于接口,所以 我们的后台组件必须全部部署到客户端(ok,我知道这样不好,或许我会写篇文章来介绍这其中的权衡),也就是说,如果不使用到后台资源,我们完全可以在客 户端直接使用Domain,所以我们设计了LocalFacade和RemoteFacade两个组件.RemotingFacade都是 Remoting对象,UI层如果访问RemoteFacade对象的话,肯定是要访问后台数据库,后台文件之类的资源;而LocalFacade是普通 对象,如果UI访问它,则LocalFacade首先将DTO转化成DomainObject,然后使用Domain逻辑,而这一切都在客户端处理,速度 肯定比Remoting快得多(稍后才看到EJB的Local和Remote接口,不知道它的两种接口是否可以同时使用)。
这样处理最明显的好处就是,从逻辑结构上来讲,所有和业务对象本身相关的检查、计算等所谓的业务逻辑都可以全部驻留在Domain里面,而不会分散到UI层。另外一个好处是,如果客户端已经拿到了检查或计算所必要的相关数据,就不用费力的往服务端再跑一趟了。
看到这里,细心的人可能有个疑问,为什么要使用EntityDTO,而不可以把Domain对象直接暴露给UI,这样就可以省掉 EntityDTO的维护,EntityDTO和Domain映射的维护以及映射导致的效率损失(里面有很多反射操作)。Ok,这确实是我也看着不爽或许 以后会改过的地方,之前的一些想法包括,1)UI层不应该直接使用Domain的细粒度接口(实际上因为WinForm提供的丰富的界面操作,以及大量的 业务检查,细粒度接口的访问的概率倒是挺大的),2)Domain对象反转倚赖于Persistence层的接口,因为我们认为Domain在进行某些检 查或者计算时,它所需要的数据可能还在数据库里面,所以需要domain自身直接去访问Persistence层,这种访问是只读的,也就是说,写数据还 是必须由Façade层调用Persistence接口去实现。这样做实际上并非必要的,也可以通过Façade层获取到所有Domain逻辑所需要的数 据,这样Domain就可以与persistence完全隔离,不过这样的domain逻辑给人感觉不连贯,相关的讨论很多这里就不多说了。因为 Domain里面的行为可能回访问到数据库,所以,为了避免在UI误用了这些接口,我们提供了LocalFacade,LocalFacade由后台对 Domain完全清楚的开发人员开发,可以减少这种误用的可能性,当然这个理由很牵强,毕竟通过测试就可以避免这个问题了。
在使用的过程中,还碰到另外一个头疼的问题,比如判断某个定单是否已经发送,可以通过Order.Status = ORDER_STATUS_SENDOUT来判断,但是UI人员建议提供Order.IsSend()的行为,这个要求如果在后台的话,无可厚非,毕竟 IsSend()的判断逻辑很可能到后面不仅仅是对一个状态的判断,但是如果使用行为的话,就需要调用LocalFacade接口,也就是说 OrderLocalFacade需要有IsOrderSent( OrderEntityDTO order)的接口,然后在接口里面还需要把EntityDTO转化成DomainObject(要么用N多的反射自动映射,效率降低;要么手动映射,维 护成本巨大),然后才能通过Domain的行为来进行IsSend()的检查,LocalFacade暴露如此细粒度的接口,其维护的成本和发射的效率损 失都是让人很难接受的,要知道,客户端这样的需求实际上是非常多的。
总之,权衡是件很头疼的事,尤其是使用分布式的架构,也难怪MartinFlower,Rob Johnson等人都在极力呼吁不要使用分布式的架构。上述的讨论主要是针对C/S、Remoting的这种架构,在B/S架构里面根本无须 LocalFacade和RemotingFacade的区分,但是也会面临Web层如何使用Domain逻辑的问题,再加上AJAX的引入,如何解决 JavaScript里面直接包含业务逻辑的问题,应该比我这里列出的问题更严峻。
最后,我考虑到另外两种方案,以后或可尝试,1)后台使用Rich
Domain,前台使用Manager。Manager都是静态的方法,专门在客户端处理业务逻辑。优点是效率提高了,不用维护LocalFacade,
不用把Domain部署到客户端,缺点是后台的业务逻辑会在前台大量的重复,无法使用到OO的继承多态所带来的灵活性。2)前台不包含逻辑,所有需要访问
到业务逻辑的地方都走一趟服务器,这要求更好的设计利用DTO,尽量减少在一个操作背后往返服务器的次数,优点是开发人员爽了,缺点是客户不爽了。好象又
回到我最早提出的两个问题上,唯一不同的是,之前的系统在这方面都没有做特别的分析和约束,不同的开发人员在不同的地方都可能采用不同的方式,整个UI层
使用Domain逻辑显得很混乱,需要把这种无意识的状态变成有意识的状态。
-----------
看了age0的回复,感觉误解有点大,抱歉介绍得不清楚,补充一份概要的部署图,注意客户端和服务端的Domain是同一组件,在客户端运行的代码,通过人为控制不会去访问到服务端的资源(虽然domain里面有对存储层的反转依赖)