Enterprise Persistence Design
Introduction
本文将介绍以下内容:
1. 企业应用中的持久层设计
2. Transparent Persistence的意义
3. Hibernate给我们带来了什么
4. Domain Model Design
5. Spring又能做些什么
阅读本文前, 如果你缺乏O/R Mapping的了解,建议参考O/R Mapping 基本概念.
History
个人对面向对象以及模式比较感兴趣, 也算是坚实支持者之一.长期以来也写了不少有关面向对象和设计模式的文章, 不过由于无法用于实践一直心存疑惑, 特别是在有数据库应用的场合下更是不知如何摆脱关系型数据对于面向对象的束缚. 另人失望的是, 在.Net社区从未见过类似文章让我得窥其貌. 既然不能寄望于人,只能自己潜心研究,希望能有所领悟. 这两周一直在查看相关书籍, 终有所心得, 特写下此文, 望与大家互相探讨, 互相学习.
对于企业应用最成熟的领域自然是J2EE.尤以EJB为代表,下面就从它谈起.
EJB中负责persistence的是Entity Bean. Entity Bean就相当于一个Active Record,它自身就包含了Save, Load方法,也就是persistence logic.从职责分配的角度来看,你已经会觉得不太合理.如果利用BMP来实现persistence你还需要自己去实现相关的persistence logic, CMP虽然由于容器的支持,使得 persistence的实现不再需要用户太费神,但是不管怎么说persistence logic还是包含在Entity之中了. 问题还不仅如此,如何从数据库中找到你所需对象?你要实现Entity的Home接口来完成查找的功能,看看吧Entity做了多少的事?
Entity Bean做了这么多的事, 似乎是劳苦功高应该能完成自己的任务了,不过Entity Bean可真是吃力不讨好? 为什么呢? 他把自己该做的事忘了. Entity在很多时候成为了一个只有数据没有行为的"哑对象", 那么行为也就是业务逻辑哪去了?它们往往被放到XXXEntityManager中去了.
这样的模型带来了什么问题?
1. 它破坏了面向对象的封装的特性, 在这样的模型下, Entity必然要把它所有的属性暴露出来.
2. 既然Entity没有行为,必然无法利用到多态的特性, 那么在XXXEntityManager中一旦涉及区分Entity类型的逻辑, 就会出现很多if else语句,繁琐不说,也不利于扩展,维护.
既然封装,多态都没了, 那也就不存在面向对象了. 所谓的类也就成为了一些个数据和函数的包装体,是彻彻底底的结构化模型在面向对象中的复辟. Martin 给它起了一个名字,叫Transaction Script.
既然XXXEntityManager的任务就是围绕XXXEntity的数据做操作为什么不直接让XXXEntity自己做?
Entity不是不想做, 问题就在于此时此刻它多了一份责任, 它要负责自身的persistence , 试想一下如果Entity中存在业务逻辑, 那么在逻辑中必然会频繁的更改其自身的属性.而为了使得这些更改能体现到数据库中.那么所有的业务逻辑中就经常需要显式(explicit)的Save, Update. 在这种情况下, 这样包含persistence logic的业务逻辑一旦脱离了EJB容器,根本无法测试.
由此我们发现Entity Bean做了很多不该做的事情, 同时该做的事情它却没有做.由此在责任分配的原则下,分以下三点讨论:
-
1. 不该做的不做. Separate persistence from business
-
2. 该做的要做. Implement business
-
3. 自己独力不能做的交给别人做. Delegate, Façade
Main Content
1. 不该做的不做.
DAO模式提供了某种程度的persistence logic的分离, 但是上面提到的问题在DAO模式下是否得到了解决呢? 没有! 传统的DAO存在和EJB同样的问题, 它比Entity Bean好的地方在于利用strategy模式将DAO从Entity Bean中抽象出来,由专门的类来实现DAO. 因为Entity Bean只包含了DAO的接口.这样使得面对各种不同的数据源时能够不破坏Entity Bean的内部实现.
由此可见,要避免上述的问题必须将业务逻辑和persistence logic完全的分离.为什么我们早没有这么做? Technical Knock Out. 当时的技术达不到.要想实现完全的分离, 我们需要一种透明的persistence 机制.
何谓透明?
让我们再来看看之前的问题:
如果Entity中存在业务逻辑, 那么在逻辑中必然会频繁的更改其自身的属性.而为了使得这些更改能体现到数据库中.那么所有的业务逻辑中就经常需要显式(explicit)的Save,Update.
其实问题就在于避免在业务逻辑中,显式的采取有关persistence的操作.我们需要一种机制------它能够在完成一个业务逻辑后自动的检测到对象状态所发生的变化.随后在事务提交时,将发生的变化保存到数据库中.这里突然出现了事务的概念,别急在后面还会有解释.如果有了这么一种机制, 我们就应该可以实现业务逻辑和persistence logic完全分离.
谁提供了这么一种transparent persistence的机制? Hibernate!
在Hibernate的支持下,persistence 并不由Entity自己负责. Hibernate会监视对象的变化.所有的更新只要在事务提交的时候就会保存到数据库中, 不需要在业务逻辑中显式得去一次次调用DAO中得方法来完成数据库同步得目的, 也就是实现了业务逻辑和persistence logic的完全分离.并且Hibernate还支持级连(Cascade)操作,这使得在你的模型设计良好的情况下,save,update的操作最少. 当然由于Hibernate的引入, 现在的DAO需要重新设计了.
先来看看以往DAO所支持的操作. Save() Update() Load() 这三个是最起码的. 现在呢? 你可以去掉Update(). 很奇怪吗? 前面已经提到了, Hibernate可以自动检测到对象状态的改变,并且在事务提交的时候存入数据库. 也就是说Update这个操作其实可以交由事务来完成了. 何时实现事务呢? 别急还在后面.
现在总结一下Hibernate给我们带来的好处.
1. Transparent persistence自动检测变化,以致DAO可以和Entity完全分离.某些时候DAO可以不必实现update的方法.
2. 级联操作使得save语句可以减到最少.
3. Detached Object可以在某些情况下使得DTO消失.(DTO是一个经常让人比较心烦的东西)
4. Hibernate提供了实现细粒度(fine-grained)的对象的方便机制.
5. Hibernate提供了Lazy-Load的支持.
*4. 5. 在本文中并没有提到, 不过作为掌握Hibernate必须了解的内容,我还是在此列出.
2. 该做的要做.
既然利用Hibernate提供的Transparent Persistence,我们实现了将Persistence Logic从Business Logic中分离. 这样我们就可以在Entity中编写相关的业务逻辑.此时我们把这个Entity称之为Domain Object. 封装多态的特性可以在Domain Object中得到很好的体现.
现在我们已经确定将在Domain Object中包含Business Logic, 随之而来有下面几个问题:
1. Domain Object中的Business Logic的性质是否都一样?
2. 是不是所有的Business Logic都由Domain Object来完成? 哪些Business Logic不是由Domain Object完成的?是否还需要Service, Manager?
前面我们一直强调在Business Logic中经常会改变对象的属性,从而混杂有Persistence Logic. 但是有些Business Logic并不改变对象的属性, 它们只是读取属性,以完成一些工作.一个很常见的例子就是计算工资.在这种Business Logic中本来就不涉及到Persistence Logic, 它也无需Hibernate监视对象的状态改变, 无需事务的包装.我们应该把这种Business Logic和改变对象状态的Business Logic区分开来.
如果所有的需求,只需要一个个独立的对象来完成, 那么可能仅仅依靠Domain Object我们就可以完成任务. 但是很多情况下, 有些任务需要很多没有关联(Association)的对象之间进行协作才能完成. 这个时候光依靠Domain Object是无法完成任务,如果要靠你说靠谁(这里也说明如果你能组织好Domain Object的关系是有可能仅仅依靠它就搞定的,但是有的时候很难避免这种情况), 所以就需要一个上层来组织它们. 这个上层的作用非常类似于J2EE中的Session Bean.从事过J2EE开发的人都知道,在J2EE中是不鼓励Entity Bean和Entity Bean直接关联. 因此这个关联协作的工作就交给了Session Bean. 这里也是同样的道理. 我们需要一个Service, 一个Façade.
需要注意的是: 不是说所有关于多个对象的业务逻辑都要放到Façade中去实现, Domain Object中的业务逻辑同样可能由多个对象协作完成.只不过这里的多个对象是Domain Object的属性.它们之间存在明显的关联(Association)关系.可以通过对象之间的关联定位到,而不需要使用到DAO来查找某个对象. 正因为这点,不要因为Hibernate提供了HQL,我们就到处用它, 一来在Domain Object中不应该使用有关DAO的操作, 二来在Domain Model中提倡的是通过关联定位对象,而不是频繁的查找.HQL的用途不应该被曲解.HQL应该更多的用在报表等等批量数据输出的地方.(不过这里是DataSet的强项)
3. 自己独力不能做的交给别人做.
“别人”就是上面所提到的Façade. 上文提到了仅仅依靠Domain Object, 有些业务逻辑将不太方便实现,所以引出了Façade的概念.让我们来看看Façade具体做的哪些事:
1. 实现需要多个没有直接关联的Domail Object协作的Business Logic (CooperatorOperation)
2. 对涉及更新数据库的业务逻辑进行事务的包装. (PlaceOrder)
3. 为DAO中的Persistence Logic提供包装. (Update)
4. 在Domain Object(Business Logic)和DAO(Persistence Logic)被分开后再次组合,组合的目的是为了1.
在引入Façade后, 暴露给上层的是不是只有Façade? 我个人觉得未必, 我的想法是将Façade和Domain Object都暴露给上层,DAO隐藏起来. 由于Hibernate 具有Reassociate Detached Object的功能.此时的Domain Object在适当的情况下可以充当DTO的作用.
基于这样的考虑,让我们来看看Façade应该如何封装它下面的Domain Object和DAO.
因为Hibernate虽然有自动检测变化的能力, 但最终需要通过事务的提交才能将变化真正的反映至数据库中. 既然把事务的包装的任务放在了Facade这里解决,那么一切有关对象状态变化需要反映到数据库中的业务逻辑都需要经过Façade的包装. 这里面包括了Domain Object中的业务逻辑(PlaceOrder)和Façade中多个Domain Object合作的业务逻辑(CooperatorOperation).只要它们涉及Persistence,就需要在Façade做事务包装. 当然只读的业务逻辑可以不必包装.
这样包装后, 上层接触到将是Domain Object的只读业务逻辑, 和Façade的所有业务逻辑以及经过Façade包装后的DAO中的Persistence Logic. 需要注意Façade绝不只一个. All Done!
Conclude
以上模型有什么优点?
注意Domain Layer那部分,你会发现这里你不需要关心任何其他的事情, 你所需要关心的就是业务逻辑. 这是最让我觉得心动的地方. 同时你会注意到Domain Layer和Persistence Layer没有任何的联系,这样大大提高了业务逻辑的可测性. 并且如果你设计良好,组织好类与类之间的关系,让Domain Object将承担起主要的责任, Façade尽量只做一层简单的包装. 这样我们就可以关注于领域模型, 还较少的考虑其他技术方面的细节.
目前的不足主要集中在Façade和DAO这里,这里需要考虑很多技术上的细节.Facade与一些对象关联,产生的依赖关系可以设计成动态注入, 事务的编写很费事, 数据库连接的打开和关闭等等麻烦的事.
And More
What About Spring?
由于Façade对DAO有明显的依赖关系,依赖注入的任务可以依靠Spring的IoC框架. Hibernate异常的处理支持不够通用,.Spring提供了一套通用的异常处理模板,还有支持数据库连接关闭等一些方便Hibernate使用的模板.Spring还对事务的处理提供了强大的配置功能, 避免了很多不必要的代码,维护起来也很简单.
写在最后的话
这篇文档的缺点在于没有实例的支持,显得过于理想化,过于理论化. 所以希望在以后能开发一个示例,(没有实例的支持也是我的遗憾,毕竟一个涉及面如此之广的例子不是那么容易设计和实现出来的) 也好对其中涉及的技术细节做进一步的解释. 不过理想化,理论化的东西也不是完全没有意义, 它可以起到更好的指导作用, 让我们在实施的时候尽量往较好的方向努力, 也即更加明确目标. 还有本文涉及内容实在太多, 有待以后文章不断就某些专题进行补充.
至于本模型的适用范围, 以及OO的好坏, 这些个人都有自己的见解. 可能你觉得本文对你毫无价值, 那也是正常之事, 只是我更希望在评论中更多的针对主题(领域模型).而不要发散的太广.
关于.Net的平台下表现层(UI Layer)的设计, 以及如何和当前的持久层(Persistence Layer)相结合, 是本人的尚未了解的领域, 希望听到你的声音.
以上有关Hibernate的介绍同样大多适用于.Net下的NHibernate, 但是Spring.Net和Spring差的还比较远, 比如对事务的支持, 对NHibernate的支持都没有. 不过.Net下的Castle项目组倒是对事务和NHibernate的有所支持.希望有人能给予指引性的介绍. (绿叶?)
参考资料:
<<Expert One on one J2EE Development Without EJB>>
<<Patterns of
<<Core J2EE Patterns>>
<<Domain-Driven Design>>
<<Hibernate in Action>>
另外javaeye论坛中的一些观点也是参考和借鉴的资料, 尤其是Robbin兄的一些见解.
All these resources are Highly Recommended