Java EE/J2EE面向对象实战之道

板桥里人 http://www.jdon.com 2006/9/28(转载请保留)

OO思维

  经常看到不少人抱怨Java EE/J2EE中配置太复杂,烦琐,不简单易学,其实所谓简单易学是取决于你是否有OO思维方式。

  分层架构是面向对象OO在企业软件中应用的标志,目前一个企业软件系统包括表现层、业务层和持久层,那么分层架构和OO关系是如何?

  表现层的界面表单中通常是一些离散数据,也就是单个字段数据,通过Struts等框架提供ActionForm以及标签库,将这些单个字段数据封装起来和业务层的Domain Model进行了映射,因此,表现层的主要编程工作就是映射配置。

  持久层是将Domain Model对象保存到数据库中,过去使用JDBC,我们要逐个打开这些Model对象,然后每个字段逐个 保存到数据库中,如果说表现层框架是实现离散数据封装,那么持久层实现的是反方向:拆封。Hibernate是一个持久层O/R mapping框架, 也就是在对象和关系数据库之间进行映射的框架,EJB的CMP也是类似道理,因此,持久层的主要编程工作也是映射配置。

  表现层和持久层这种配置工作就如同打包邮寄一样:你首先要将你的单件用一个箱子包装起来,达到目的地,这个箱子被打开,单件被逐步取出。表现层和持久层这样做的目的是保证中间业务层完全面向对象,保证业务层完全是和一个个对象模型打交道。

  在一个真正面向对象的系统中,表现层和持久层是为了将非对象化的数据转为对象。因此,在先进的JavaEE/J2EE架构中,表现层和持久层的主要工作就是配置工作,而且主要是映射mapping的配置。

  下面的问题就是:如何解决映射配置简单而且易用,如果拥有正确的指导配置的思维,那么配置工作就容易简单多, 否则,就倍感配置复杂。 那些感觉Java配置复杂的人其实他并没有完整的OO思维。为什么这么说呢?以ORM(Hibernate)配置简易方式说明:

配置的简要之道

  首先,配置是映射XML配置,顾名思义,也就是在两者之间做协调,牵线搭桥,说白了,就是做红娘,但和做红娘又有些区别,做红娘可以要求双方做些改变,互相迁就,但是做映射配置,则不能这样,因为那样做就可能做出和需求要求不一样的东西。

  配置的简要之道就是:围绕对象模型进行配置;而不是围绕数据表进行配置。

  以持久层映射配置来说:存在Domain Model对象和关系数据表,如果感觉在两者之间配置映射很困难,双方做些改变,但是有可能 需求不答应,你一旦为协调而作出的改变可能偏离需求实现的目标,最后作出的系统面貌全非,根本不是客户所需要的。

  那么怎么办?很显然,紧扣需求,反映需求的那一方坚决不要变动,那么Domain Model和关系数据表哪一方反映需求呢?按照OO分析,当然 是Domain Model,Model对象我们是依据Evans Model等模型驱动设计MDD概念设计出来,他们是需求的代表。

  很显然,我们的映射配置必须顺着Model对象这个思维来配,对于名词式的Model,关联无外乎是其主要关系,当然还有继承,因此,象Hibernate 这些映射配置语法也是面向这些主要对象关系的。

  表现层配置也是同样的道理,需要将Domain Model配置成界面表单,在实际中,我们有可能采取的是通过界面收集需求,因此,这个映射配置过程也是考验Model对象是否提炼正确与否,有可能发现Model不能实现一些界面需求功能,这时反过来必须修改我们的Model,而不是仅仅在表现层这个技术层面做些补救措施就糊弄过去。

  Java EE/J2EE系统开发过程 敏捷的迭代是必然的。没有一个天才能够一步到位提炼出兼顾界面和数据表以及需求的统一模型出来。

  总之,完成一个真正面向对象的Java EE/J2EE系统,必须抓住领域建模和具体框架熟练配置两点,只有这样才能保证Java项目成功实施。最关键的是提炼出反映出业务系统的领域模型:Domain Model,完成业务建模后,就是依赖Struts/Hibernate等配置分配将Model 映射到界面和数据库,其实就是将业务模型移植到计算机领域并能够正确运行。

高聚合和低关联

  如果一个系统都被设计成相互没有任何不包含的单个对象,很显然是不能正确反映实际需求的,万事万物都是有其部分组成的,例如窗户由玻璃和框架组成,人是由胳膊 腿等身体部分组成,现实世界中,事物之间总是存在关系,聚合和组成是最常见的。

  例如订单,一个订单Orders中由客户名称和地址,订购的产品品种和数量,客户名称和地址我们可以抽象为Customer来代表,产品我们使用Product来代表,由于一个订单中可能订购了多个产品,很显然,一个订单对象中应该有多个Product对象,而且每个Product的数量不一样,我们将Product和其数量再抽象包装成OrderLine订单条目对象,这样,订单中包含多个订单条目,而且订单条目只有依赖某个订单,是其组成部分,是一种强聚合关系,不是普通的聚合或关联关系。而Customer和Order之间是一种聚合关系,如果订单没有客户信息,就不成为订单了。
订单


  下面再以用户User这个对象为例,用户User可能拥有很多动态属性,一些属性需要运行时动态确定,用户和动态属性是一个整体和部分的聚合关系;每个用户都必然属于某个部门,因此,用户和部门属性对象之间也是一个整体和部分的聚合关系,这两种聚合关系不同之处在于:前者一个用户可能有多个动态属性,是1:N关系;部门Dept和用户User之间是1:N关系,一个部门中可能有多个用户,反过来说,对于用户User来说:它和部门Dept之间是N:1关系。

model

  通过以上建模过程,我们基本搞清楚两件事:这个领域中存在哪些模型对象?按照Evans的DDD理论,哪些是实体,哪些是值对象;然后我们必须搞清楚那些聚合关系,他们是整体部分的关系,用来共同组成一个完整对象的。

持久层Hibernate聚合实现

  在持久层我们需要做的主要工作就是将上述Domain Model 进行持久化映射配置,以User为例,User是一个实体,我们配置User.hbm.xml如下:

<hibernate-mapping>
  <class name="sample.model.User" table="testuser">
    <id name="userId" type="java.lang.String" >
    <generator class="assigned"/>
  </id>

  <property name="username" type="java.lang.String">
    <column name="name" />
  </property>

  <!--表示和部门Dept之间是一种多对一关系 -->
   <many-to-one cascade="save-update" name="dept"
     class="sample.model.Dept" column="categoryId" />

  <!-- 表示和用户属性UserPropperty之间是一种1对多关系-->
  <bag name="userProps" inverse="true" cascade="all" >
    <key column="userId" />
    <one-to-many class="sample.model.UserProperty" />
  </bag>  
</class>



</hibernate-mapping>

  在User的映射配置文件中,我们很自然地表达了上节Model之间的聚合关系,通过Hibernate配置,我们将模型对象之间的关系可以持久化保存到数据库中了,也就是可以永久维持这种关系,实际上,现实世界中也是这样的,部分和整体的关系是一直存在,除非这个整体这个对象不存在,而且修改部分对象内部值,必须通过整体这个对象。

  在user配置中,我们并没有去做任何关系数据表testuser的设计和设定,因为我们知道,当User.hbm.xml配置完成后,这个J2EE系统部署发布到J2EE容器中时,Hibernate会根据这个配置自动创建数据表testuser,数据表的建立已经是一个部署调试阶段的、技术层面的具体工作。

Hibernate重要的父子关系

  Hibernate在处理User和UserProperty这样一对多的父子关系时,具体实现起来要有一些具体细节必须注意,而且Hibernate2和Hibernate3两个版本是不一样的:

  当我们需要只通过一句话session.save(user)或session.update(user)就能完成User和它其中多个Userproperty都能自动保存或更新时(必须指定cascade="all" 或save-update),尤其是update(user)更新时,其子集合userProps属性中可能有一些Userproperty是修改过的,一些Userproperty则是新增的,对于新增要使用insert语句;而对于修改则使用update语句,当我们笼统地调用一句update(user)时,那么Hibernate是如何判断这个user中子集合中哪些是修改?哪些是新增的?

  Hibernate是通过主键来判断的,也就是说,通过UserProperty的主键来判断该对象是修改?还是新增。最关键的是:这个主键必须由Hibernate自动产生,如果你想自己指定子对象UserProperty主键,那么就有可能很多麻烦,这个麻烦是出其的麻烦,无法判断具体原因。所以,在简单方便的道路上迈错一步就是万丈深渊。下面是UserProperty的映射配置:

<hibernate-mapping>

  <class name="sample.model.UserProperty" table="userprops">
    <id name="propId" type="java.lang.String" >
      <generator class="uuid.hex"/><!-- 不能为assigned-->
    </id>  
  <property name="name" />  
  <property name="value" />

  <!-- 为提升性能而设定 -->
  <many-to-one name="user" column="userId" not-null="true"/>
</class>




</hibernate-mapping>

  注意:以上配置只适合Hibernate 3.0以上版本,如果是Hibernate 2,那么必须在 :

<id name="propId" type="java.lang.String" >

中加入unsaved-value="null",而且这个值是null还是0或-1,取决你的主键类型:

<id name="propId" type="java.lang.String" unsaved-value="null">

  是不是感觉Hibernate2太麻烦了!在Hibernate3中,就没有这个规定了,所以,如果当初使用Hibernate2来实现J2EE的oo简洁实现之道,还存在技术上的困难和难点。

  Hibernate2和Hibernate3在处理父子关系上,还有一个不同就是lazy设定上:Hibernat2缺省lazy是false,当通过load将User获取以后,在session关闭以后,你可以直接通过user.getUserProps()方法获得其中子集合;而Hibernate3则不行了,缺省lazy是true,在session关闭情况下,只有两种方式获得子集合:

  1. Open session in view,也就是在表现层一直打开持久层的session,这不但违背分层不干扰原则,而且造成数据库连接一直打开,一旦出错,有可能造成内存泄漏死机等问题。

  2.在load父对象User时,调用Hibernate.initialize(user.getUserProps());强行装载所有的子对象,这样问题是:我们再也无法通过简单一句load生成父对象User及其所有内部部分,而无须照顾其内部关系。

  板桥实践中总结方法是:根据当初EJB CMP的读取模式,采取JDBC来读取整个User及其部分子集合,缺点也是必须在Dao语句中打开User(破坏封装),根据其内部结构从数据库中获取数据,这样的好处是:我们可以使用统一的Hibernate模板来进行任何一个模型的持久化(不必为每个模型写一套DAO实现类),而无须关心其内部结构了。

  注意:Spring+Hibernate采取的是Open session in view方案,这也是这种架构在系统复杂时发生性能问题一个原因,J道性能板块有多个这样的求救贴。

  使用Hibernate映射配置另外一个注意点就是:使用双向关系可以提高性能,但是Evans DDD告诉我们,建模时尽量搞 单向关系,不要用双向,这两者有矛盾之处,实际中,我们如果使用Hibernate作为持久层框架,那么就采取双向,性能很重要啊,否则后果很严重,这种设计和性能不匹配也是目前面向对象领域需要解决的另外一个问题。

  通过在子对象UserPropery配置中引入many-to-one ,然后在父对象User配置中规定inverse="true" 来实现双向,Hibernate会通过和insert或update一条SQL语句完成关系设定。

表现层Struts聚合实现

  前面我们完成了Hibernate的映射配置,下面是表现层的映射配置,这是使用标签库来实现,我们使用Struts的标签库来实现:在界面主要实现下图效果:

javaeeSample

  当进行用户User资料增删改查时,需要一个如图录入页面,部门是通过下来菜单选择,用户属性UserPropery是通过一行行属性名称和属性值输入的,主要是在user.jsp中完成:

<html:form action="/userSaveAction.do" method='post'>
<html:hidden property="action" />

<!-- 下拉菜单选择部门,通过使用Struts的Action串联,产生deptListForm新ActionForm-->

<html:select property="dept.deptId" >
<logic:notEmpty name="deptListForm" >
<html:optionsCollection name="deptListForm" property="list" value="deptId" label="name"/>
</logic:notEmpty>
</html:select>
<br>
UserId:<html:text property="userId" />
<br>
Username:<html:text property="username" />

<table>
<tr><td>属性Id</td><td>属性名称</td><td>属性值</td></tr>

<tr><td>
<html:hidden property="userProp[0].propId" />
</td><td>
<html:text property="userProp[0].name" />
</td><td>
<html:text property="userProp[0].value" />
</td></tr>

<tr><td>
<html:hidden property="userProp[1].propId" />
</td><td>
<html:text property="userProp[1].name" />
</td><td>
<html:text property="userProp[1].value" />
</td></tr>

<tr><td>
<html:hidden property="userProp[2].propId" />
</td><td>
<html:text property="userProp[2].name" />
</td><td>
<html:text property="userProp[2].value" />
</td></tr>

</table>

<br><input type='submit' value='submit'></input>
</html:form>

  相应的UserActionForm和User Model内容差不多,不同之处:为接受多个动态属性的输入,需要设定一个特定的方法:

public class UserForm extends ModelForm {

  .....   
  public UserProperty getUserProp(int index) {
    return (UserProperty)((List)userProps).get(index);
  }
  .....   

}

  增删改查和批量查询根据JdonFramework的简化可迅速配置实现,这里不再描述,整个项目的代码结果如下图:也就是10个类左右,而且都是和业务有关,简要,扣主题,整个案例代码是免费自由下载,作为JdonFramework应用源码下载之一的sample

Java项目

总结

  一个真正面向对象的JavaEE或J2EE系统,应该是一个围绕领域模型的多层架构,以面向对象OO思维进行领域模型提炼和重构,继续以OO思维进行表现层和持久层的配置实现,才能寻找到一条Java系统快速有效高质量的解决之道。

相关文章:

Java实用系统开发指南

面向对象与领域建模

当前Java项目开发中几种认识误区

实战DDD(Domain-Driven Design领域驱动设计)

领域模型驱动设计(DDD)之模型提炼

数据库时代的终结

更多OO思维专题

可伸缩性

 

讨论