UML软件工程组织

J2EE体系结构设计
作者: 务实
目前大多数企业采用J2EE技术的结构设计与解决方案。对于我们学习和研究J2EE体系结构来说,了解与掌握J2EE体系结构的设计方法及一些常用模式是必须的;模型-视图-控制(model-view-control,简称MVC)结构是目前最常见的J2EE应用所基于的体系结构,MVC主要适用于交互式的Web应用,尤其是存在大量页面及多次客户访问及数据显示;相比较而言,一个工作流体系结构更多应用于过程控制和较少交互的情况下;除了体系结构外,J2EE的设计模式对我们解决应用系统的设计也有很大的帮助。
一、J2EE的模型-视图-控制(MVC)体系结构

模型-视图-控制结构是交互式应用程序广泛使用的一种体系结构。它有效地在存储和展示数据的对象中区分功能模块以降低它们之间的连接度,这种体系结构将传统的输入、处理和输入模型转化为图形显示的用户交互模型,或者换一种说法,是多层次的Web商业应用;MVC体系结构具有三个层面:模型(Model)、视图(View)和控制(Controller),每个层面有其各自的功能作用,MVC体系结构如下:

图1  MVC 体系结构

   模型层负责表达和访问商业数据,执行商业逻辑和操作。也就是说,这一层就是现实生活中功能的软件模拟;在模型层变化的时候,它将通知视图层并提供后者访问自身状态的能力,同时控制层也可以访问其功能函数以完成相关的任务。

    视图层负责显示模型层的内容。它从模型层取得数据并指定这些数据如何被显示出来。在模型层变化的时候,它将自动更新。另外视图层也会将用户的输入传送给控制器。

    控制层负责定义应用程序的行为。它可以分派用户的请求并选择恰当的视图以用于显示,同时它也可以解释用户的输入并将它们映射为模型层可执行的操作;在一个图形界面中,常见的用户输入包括点击按钮和菜单选择。在Web应用中,它包括对Web层的HTTP GET和POST的请求;控制层可以基于用户的交互和模型层的操作结果来选择下一个可以显示的视图,一个应用程序通常会基于一组相关功能设定一个控制层的模块,甚至一些应用程序会根据不同的用户类型具有不同的控制层设定,这主要是由于不同用户的视图交互和选择也是不同的。

在模型层、视图层和控制层之间划分责任可以减少代码的重复度,并使应用程序维护起来更简单。同时由于数据和商务逻辑的分开,在新的数据源加入和数据显示变化的时候,数据处理也会变得更简单。

二、J2EE设计模式

一个设计模式描述了对于特定设计问题被验证的解决方案,它综合了所有开发者对这个问题所在领域的知识和见解;同时也是对于常见问题的可重用方案,它们一般适用于单个问题,但是组织在一起就可以提供整个企业系统的解决方案。下面我们列举八种常用于J2EE平台的设计模式,并对每种模式作简单的介绍,便于大家学习、理解与灵活应用。

1、前控制器

   前控制器(front controller)主要提供一种可以集中式管理请求的控制器,一个前控制器可以接受所有的客户请求,将每个请求递交给相应的请求句柄,并适当地响应用户。

   前控制器也是表示层的设计模式,它的出现主要是由于表示层通常需要控制和协调来自不同用户的多个请求,而这种控制机制又根据不同的需要,可能会集中式控制或分散式控制。换句话说,就是应用系统需要对于表示层的请求提供一个集中式控制模块,以提供各种系统服务,包括内容提取、视图管理和浏览,如果系统中没有这种集中式控制模块或控制机制,每个不同的系统服务都需要进行单独的视图处理,这样代码的重复性就会提高,致使系统开发代价提高;同时,如果没有一个固定模块管理视图之间的浏览机制,致使其浏览功能下放于每个不同的视图中,最终必将使得系统的可维护性受到破坏;本文中我们主要讨论的是集中式控制模块,而不是分散式控制,因为前者更适合于大型的应用系统。

  基于上面所说的问题,研究人员提出了前控制器的设计模式。在这种模式中,控制器提供一个处理不同请求的控制点,这里的处理工作包括安全事务、视图选择、错误处理和响应内容的生成;通过将这些处理工作集中在一点进行,大大地减低了Java代码量,同时这种方法也可以减少在视图模块的程序逻辑,保证了在不同请求之间可以重用大量的逻辑代码。通常,控制器都是和一个分派组件联合工作的,分派组件主要是用于视图管理和浏览,也就是为用户选择下一个应该显示的视图,并同时提供对于相关显示资源的控制。分派组件可以包含在控制器之内,或是在另外一个单独的组件中;虽然前控制器模式推荐对于全部的请求使用统一处理,但是它也没有限制在一个系统中只能具有一个控制器,在系统中的每个层次都可以具有多个控制器,并且映射至不同的系统服务,下图2显示了前控制器的类图。

图2 前控制器的类图

 

   图3显示了前控制器的序列图,表示一个控制器如何处理相关的请求。

图3前控制器序列图

下面我们来讨论一下图3的各个组件。

2、控制器

   控制器(controller)是负责处理各种客户请求的控制点,并可以将一定的职能(如用户认证等)下放给帮助类。

  (1)分派组件(Dispatcher)。一个分派组件主要是用于视图的管理和浏览,为用户选择下一个可以显示的视图,并管理相关的显示资源;分派组件可以在一个控制器内运行,或者作为一个单独的组件与控制器协同工作;开发人员可以在分派组件中实现静态的视图分派技术,或是复杂的动态分派。

  (2)帮助类(Helper)。帮助类负责帮助一个视图或控制器来完成其处理工作,因此,帮助类具有多项职责,包括收集数据、存储中间数据模型等;另外,帮助类也可以在保证数据完整性和准确性的情况下,为不同显示需求修改数据模型;也就是说,根据用户的请求,帮助类可以向视图提供未经处理的原始数据,或是已经格式化后的Web内容,一个视图同时可以和多个帮助类协同工作,而后者通常是由JavaBeans和标签(tag)实现的。

3、视图    

   视图(view)负责向用户显示信息,而帮助类则负责支持视图的工作,即打包和建立相应的数据模型,下面我们介绍几种可以实现控制器的方法。

1)基于Servlet前控制器

这种方法建议使用servlet来实现一个控制器,尽管在语法上相差无几,但是它比使用JSP来实现要优越一些;因为控制器所进行的请求处理,多数都是与程序运行和控制流动相关的,这些处理工作虽然与显示模式相关,但是实际上是逻辑独立的,所以它们更适合在servlet中实现,而不是JSP技术中;使用这种方法也存在一些弱点,比如说servlet无法使用JSP运行环境的资源,如请求参数等,但是这个弱点也不是不能解决的,我们可以在servlet中建立相关的句柄来访问同样的资源,当然其代码会变得繁琐一点。

2)基于JSP的前控制器

  这种方法建议使用JSP页面实现控制器,尽管语法上相同,但是Servlet方案要比其优越一些;因为控制器所处理的逻辑一般都不是有关显示模式的,所以在JSP页面中实现控制器似乎有点风马牛不相及;使用这种方法也不利于开发团队的角色和职责的分配,即软件开发人员需要在负责显示逻辑的JSP页面中修改请求处理的代码,通常,这种工作都是相当复杂的,尤其考虑整个JSP页面的编程、编译、测试和调试错误。

3)控制器之中的分派组件

  如果分派组件没有较多功能,开发人员可以在控制器实现该组件。

4)基础前端

  基于使用servlet实现前控制器,这种方案建议实现一个控制器作为基础类,这样其他的控制器可以在其之上扩展;这个基础类可以包含一些通用的逻辑实现,它的子类就会重载这些实现代码,这种方法也有一定的缺陷,当有许多子类继承这个基础类,并大量地重用代码时,那么就有可能出现一个类的改变会影响到所有子类的情况。

5)用过滤器实现前控制器

    过滤器提供了与用户请求的中心处理相类似的功能,也就是说,控制器的一些功能可以由过滤器来实现,这种方案的过滤器主要负责处理请求的截取和解释,而不是请求的处理和响应的生成;通常可以为应用系统提供一个核心控制点,以处理所有的系统服务和程序逻辑,核心控制也就表明了所有的请求都可以简单地被跟踪和记录,从而方便各种服务功能的实施;当然,它也存在一些缺点,一个核心控制点的小问题可能会引发系统的崩溃,但在应用系统的实际开发中,这并不是个问题,因为通常我们都会在同一个层面上实现多个控制器,从而避免了这个缺陷;在控制器中,开发人员可以很方便地实现一个检查安全机制的组件,从而可以在最外层屏蔽对系统的恶意访问,另外使用控制器也会提高系统模块的可重用性,尤其在控制器同时使用帮助类的时候。

4、视图帮助

   视图帮助(View helper)是属于表示层的设计模式,一个视图帮助可以包含相关视图中的数据访问和内容显示的逻辑,并可以精炼简化视图;显示逻辑主要是关于如何格式化页面上的数据,而访问逻辑则是关于如何取出数据,视图帮助通常用来显示数据的JSP标记(tag)或是读取数据的JavaBean。

   这种设计模式的出现主要是由于目前的应用系统通常需要实时地开发显示内容,并且能处理动态的程序数据。如果这些程序数据的访问逻辑和显示逻辑的关系过于紧密,则系统的表示层就会经常需要改动,从而系统的灵活性、重用性会大大地受到破坏;同时在相同的模块中实现访问逻辑和显示逻辑将会影响系统的模块化,也会使得开发团队的任务划分不清。

 一个视图通常包含格式化信息,并将其处理任务分发给自己的帮助类,后者通常是用JavaBeans或标记(tag)来实现的,帮助类同时可以存储视图的中间数据模型并实现数据适配器的功能,即适当地转化数据格式;开发人员可以采用多种方法实现视图组件,通常,开发人员可以使用JSP来实现,并且这也是一种值得推荐的方法。当然,相应地开发人员也可以使用Servlet来实现它,将视图中一定的程序逻辑植入到帮助类中,会有利于应用系统的模块化和可重用性。系统可以使用同一个帮助类为不同的用户显示不同的数据信息,并在不同的显示格式下显示;通常,如果开发人员发现视图的JSP页面中存在大量的脚本代码时,就可以考虑使用视图帮助这种模式了,因为在这种情况下,基本都是程序逻辑和显示逻辑具有过于紧密的联系;这时开发人员可以将一些适用于所有类型的请求的逻辑处理放置到一定的帮助类中,而根据需要,也可以将另外一些逻辑处理放置在视图层上的其他程序模块中,比如说以前讨论过的截取过滤器。   

   视图帮助这种模式的设计理念主要是分离应用系统的逻辑职责,下面我们提供一些图示,以方便大家更好地理解这种模式。

  图4以类图(class diagram)的形式说明了视图帮助的系统结构。

图4 视图帮助类图

 

  

图5表示了视图帮助模式的序列图,它表明了这种模式中的主要成分及互相之间的运行情况;不过需要说明的是,在很多应用系统中,客户端和视图层之间会存在一个控制器加以适当的调节。

图5视图帮助序列图

 

  在类图表中,大家可以发现,可能存在没有任何相关帮助类的视图,这种情况下,通常代表视图的JSP页面会有一些静态的或小数量的脚本代码。

  这里我们对于序列图中的各个元素加以简单的介绍:

  (1)视图(view)。视图负责向用户展示动态数据信息,而帮助类则负责支持视图的工作,即打包和建立相应的数据模型。

  (2)帮助类(helper)。一个帮助类负责帮助视图或控制器完成相关的处理工作,包括收集数据、存储中间模型等;帮助类也可以在保证数据完整性和准确性的情况下,为不同显示需求修改数据模型,也就是说,根据用户的请求,帮助类可以向视图提供未经处理的原始数据,或是已经格式化后的Web内容;一个视图同时可以和多个帮助类协同工作,而后者通常是由JavaBeans和标记(tag)实现的。

(3)值bean(ValueBean)。值bean实际上是用于存储中间数据模型的帮助类的另一种叫法,例如在序列图5中,business service就根据请求返回了一个值bean。

  (4)业务服务(business service)。业务服务是指用户试图得到的,应用系统可以提供的相关服务;通常来说,业务服务可以通过一个业务代表(business delegate)来访问,而后者主要是提供对于业务服务的控制和保护。

    在应用系统的视图模块中使用帮助类可以将不同的程序逻辑很好地分离开来,并在视图模块之外为开发人员提供设计程序逻辑的空间;基于JavaBean和标记(tag)所开发的帮助类通常都可以被多个视图模块重用,因此也提高了组件的重用性和可维护性;把显示逻辑从数据处理逻辑分离出来,也有利于开发团队中角色及人物的划分;比如说,如果各种程序逻辑过于结合的话,软件开发人员可能需要在HTML,网页中修改代码而Web设计师则需要在处理数据访问的JSP中修改页面布置,这些情况都可能会导致系统设计和开发中由于不同技术人员的介入,而产生相关的问题。


5、会话面

   会话面(session facade)模式在合作的企业对象间调节操作,并将应用函数合成一个单一简单的界面;它减少了类之间合作的复杂性,并使得类的调用者在该类变化的时候无需改动,这种模式通常以一个会话bean实现,以用来隐藏底层ejb的复杂交互。

   这种设计模式出现的背景在于EJB通常既包括程序数据,又包括程序逻辑,而这些代码都会通过一定的界面作用于客户层,在多层次的J2EE平台应用程序中,就会造成一定的困难。

   具体来说,在J2EE平台上的多层次系统中,通常会存在以下的问题:

    (1)层次之间联系过于紧密,客户层和后端的业务对象具有较强的依赖关系;

    (2)在客户和服务器之间有多次方法调用,因而导致了Web性能方面的问题;

    (3)缺乏一定的客户访问机制,使得一些后台对象被随便访问。

    一个多层次的J2EE应用程序通常具有很多由EJB实现的服务器端对象,它们通常负责提供系统服务、数据信息等,也就是说作为业务对象,它们既包括相关的程序数据,也包括其程序逻辑;在J2EE应用系统中,负责程序逻辑的对象通常由会话bean实现,而表示持久性存储,并在多个用户间共享的对象则由实体bean来实现;当然,应用系统的用户需要访问企业对象来满足自己的需求,如果企业对象向用户提供接口,用户可以直接地与相关对象通信,但是这样一来,用户必须负责管理所调用的企业对象之间的关系,并且能够处理其间的业务流程;然而,如果用户和业务对象之间存在过于直接的交互,两者的联系就会过于紧密,同时也使得用户过于依赖企业对象的具体实现,并负责管理与交互过程有关的业务对象查找和创建,以及不同的对象间相互调用的关系,甚至一些时候用户还需要管理多次调用之间的事务管理环节。

    在用户需求不断增加时,这也是应用系统经常发生的情况,用户与不同的企业对象之间的交互也会变得越来越复杂,而企业对象可能需要一定内部的更新才能满足前者的需要,但是这样的话用户又需要根据企业对象实现的变化而做出相应的改变,这种情况将为应用系统带来相当大的麻烦;在访问EJB应用系统时,用户需要与远程对象进行交互。如果用户直接与所有相关的业务对象交互的话,将带来很大的Web负担;因为对于每一个ejb的激活,都将产生一次远程的调用,而如果存在大量的系统用户,用户与对象间的交互就将为Web通信带来很大的压力,使系统性能受到很大破坏;如果用户可以直接访问后端的企业对象,但是系统中又缺少一个统一的用户访问机制,那么这些访问很有可能变得杂乱无章,引起系统性能的下降,甚至导致一些安全问题。

    为了解决以上的问题,开发人员可以采用会话面的设计模式,即使用会话bean来实现一个面(facade)来包含一个工作流中所有相关对象的交互;这个会话面负责管理业务对象,并向用户提供一个统一的服务访问层,会话面可以面向底层对象的交互过程,并提供一个仅仅包含必须提供的接口的服务层,由此它将复杂的对象交互和用户之间隔离开来;    会话面也负责管理企业数据和企业对象之间的交互,并表达其中需要的企业逻辑,因此会话面也可以管理企业对象之间的作用关系;同时,根据工作流的需要,会话面也管理对象的创建、查找、修改和删除。

    在一个复杂的应用系统中,会话面可以将其生命周期的管理下放到一个单独的帮助对象去,比如说,会话面可以将管理会话和实体bean生命周期的工作交给服务定位对象;    同时,在应用系统中,检查业务对象之间的作用关系也是非常重要的,一些关系可能是暂时的,即只使用于一定的交互过程,而另外一些关系则是永久的,暂时的关系适合建模于会话面中的工作流,永久的关系则需要具体情况具体分析。

    图6中的类图简要描述了会话面的设计模式,图7给出了会话面的序列表示,即参与组件及其交互关系。

图6 会话面类图

 

图7会话面序列图

 

这里我们对于图7的各个组件加以简要的介绍:

    (1)客户(Client)。这表示会话面的客户,即需要访问相关企业服务的客户端应用程序,当然也可以是在同一层面或不同层面的另外一个会话bean。

    (2)会话面(Session Facade)。会话面通常是用会话bean来实现的,它管理着多个企业对象的作用关系并提供一个高层次的抽象界面给用户。

    (3)业务对象(Business Object)。业务对象是一个可以使用多个不同设计方案的对象,例如会话bean、实体bean和数据访问对象。在图6中业务对象负责提供数据和服务,而会话面则需要与多个业务对象实例交互而获得相应的服务。

     会话面实际上就是业务层的一个控制对象,它负责控制用户与企业数据和企业服务对象之间的交互;在一个复杂的应用系统中,甚至可能会有多个会话面作为用户和对象模块之间的中介。

    下面介绍两种实现会话面的常见方法。

    (1)无状态的会话面

    在实现会话面的时候,首先应该决定是用状态化还是无状态的会话bean来实现,这主要取决于会话面所建模的业务流程;如果一个业务流程只需要一次方法调用就可以实现其服务,那么就可以使用无状态的会话bean来实现它。

    (2)状态化的会话面

    当一个业务流程需要多次方法调用来实现其服务时,开发人员最好使用状态化的会话bean来实现这一流程,因为每次方法调用的状态信息都必须在会话bean中保存。

    通过在应用系统中采用会话面的设计模式,将在系统中得到以下的收益:

    ①为用户提供一个简单的接口,并隐藏所有与系统组件复杂的交互过程;

    ②减少暴露给用户的企业对象,从而降低它们之间的依赖关系;

    ③向用户隐藏系统组件间的交互过程和依赖关系,从而使得系统更加容易管理,并提供相当的灵活性;提供一套统一的用户访问机制,便于管理用户对于系统服务的请求与访问。

6、 数据访问对象

    数据访问对象(data access object,DAO)模式将数据访问逻辑抽象为特殊的资源,也就是说将系统资源的接口从其底层访问机制中隔离出来;通过将数据访问的调用打包,数据访问对象可以促进对于不同数据库类型和模式的数据访问。

    这种模式出现的背景在于数据访问的逻辑极大程度上取决于数据存储的格式,比如说关系型数据库、面向对象数据库、磁盘文件等。

    目前大部分的J2EE应用程序都需要在一定程度上使用可持久性的数据,而实现持久性数据的方法因应用程序不同而异,并且访问不同存储格式数据的应用程序接口(API)也有着显著的差别;有的时候,应用程序还会访问存储在不同操作平台上的数据,这使得问题更为复杂,通常,应用程序会使用共享的分布式组件,如实体bean来表达持久性数据。应用程序可以使用bean管理的持久性实体bean,而在实体bean中植人数据访问逻辑,或者使用容器管理的持久性实体bean,从而使容器管理所有的事务和持久性细节;而如果应用程序对于数据访问的需求十分简单的话,也可以采用会话bean或Servlet直接访问持久性存储来读取和修改数据。

    一些应用程序可以使用JDBC应用程序接口来访问关系数据库中的数据,JDBC负责一般的持久性数据访问和管理,在J2EE应用程序中,JDBC中可以嵌入SQL语句,用以访问关系型数据库,当然根据数据库类型的不同,SQL语句的词法和语法也会有所不同;需要说明的是,当数据存储格式不同的时候,数据访问逻辑的区别就更加明显了,例如关系型数据库、面向对象数据库和磁盘文件,各自数据的访问逻辑各有千秋,这样一来就造成了程序代码和数据访问代码之间的依赖关系;当程序组件,即实体bean、会话bean或servlet、JSP等需要访问数据源时,它们会使用正确的应用程序接口来得到连接并管理数据源,但这样也会造成这些组件与数据源物理实现之间的依赖关系,从而使得应用程序很难从一个数据存储实体移植到另一个数据存储实体中去;当数据源的物理实现变化的时候,应用程序也必须相应地加以改变。

    基于以上所讨论的问题,开发人员开始采用数据访问对象的方法。数据访问对象实际上就是包含对于所有数据访问逻辑的对象,并管理着对于数据源的连接,根据数据源的不同,数据访问对象实现了不同的访问机制,这里所说的数据源可以是持久性存储介质,如关系型数据库,也可以是外部服务,如B2B的数据交换;不仅是用户,而且包括应用系统中的其他组件,也可以使用数据访问对象所提供的数据访问接口,数据访问对象将数据源的物理实现细节与其用户完全分离开来,并且在底层数据源变化的时候,数据访问对象向用户提供的接口是不会变化的;这种方法使应用系统使用数据访问对象时可以适应多种数据存储介质,总之,数据访问对象就是系统组件和数据源中间的适配器。

图8中的类图表示了数据访问对象设计模式的参与对象和之间的调用关系,图9是这种设计模式的序列图。

图8 数据访问对象类图

 

图9 数据访问对象序列图

 

对于图9序列图中的组件加以解释如下:

    (1)业务对象(Business Object)。表示数据的用户,它需要对于数据的访问,一个业务对象可以用会话bean、实体bean或是其他Java程序来实现。

    (2)数据访问对象(Data Access Object)。数据访问对象是这种模式中的主题,它提供了底层数据访问的对象,并将其提供给业务对象以使得后者能够透明地访问数据源;同时业务对象也将数据的加载和存储操作移交给数据访问对象处理。

    (3)数据源(Data source)。这里指的是数据源的物理实现,这个数据源可以是一个数据库,包括关系型数据库、面向对象数据库或文件系统。

    (4)传输对象(Transfer Object)。这里的传输对象指的是数据载体。数据访问对象可以使用传输对象来向用户返回数据,而数据访问对象同样可以从用户那里得到传输对象来对数据源中的数据进行更新。

    下面给出几种实现数据访问对象设计模式的方法。   

    (1)自动数据访问对象代码的生成

    既然每一个业务对象都对应于一个数据访问对象,那么开发人员就可以建立业务对象、数据访问对象和底层实现的关系;一旦这种关系建立起来,开发人员就可以为所有的数据访问对象编写特殊的代码生成工具。

    生成数据访问对象的信息通常存储在一个开发人员定义的描述文件中,如果对于数据访问对象的要求过于复杂,开发人员可以考虑使用第三方工具来为关系型数据库提供对象对关系的映射。这些工具通常是一些GUI程序,可以用来将业务对象映射为持久性的存储对象,并定义中间运作的数据访问对象,在映射完成的时候,这些工具可以自动地生成代码,并提供一些相应的功能,如缓存结果、缓存查询、与应用服务器整合、与第三方产品整合等。

    (2)数据访问对象代理(Factory for Data Access Objects)

    当底层的数据存储不会轻易改变的时候,开发人员可以采取这种方法来实现相应的,数据访问对象,图10是这种方法的类图。

 

图10 使用DAO代理类图

   当底层的数据存储可能会变化的时候,开发人员可以采用抽象代理的方法来实现数据访问对象;抽象代理的方法会创建一些虚拟的数据访问对象代理和各种类型的实际数据访问对象代理,每种对象对应一种持久性存储介质的实现,一旦组件得到这些代理,就可以利用来创建需要使用的数据访问对象。

    图11给出了这种情况的类图。该类图表示了一个基础的数据访问对象代理,它是一个抽象类,被其他一些实际的数据访问对象代理继承以支持特定的数据访问函数;用户可以得到一个实际的数据访问对象,并利用它来创建需要的数据访问对象而访问相关的数据,每一个实际的数据访问对象都负责建立对于数据源的连接,并得到和管理所支持的业务数据。

    

图11  抽象代理使用DAO

下图12是这种情况下的序列图。

图12抽象代理使用DAO序列图

这种设计模式的优势:

  • 透明性好。业务对象可以在不知道数据源实现细节的情况下访问数据。由于一切数据访问细节被数据访问对象所隐藏,所以这种访问过程是透明的。
  • 可移植性好。在应用系统中添加数据访问对象,可以使得前者能够很方便地移植到另外一种数据库实现上。业务对象与数据实现是隔离的,所以在移植过程中,仅仅对数据访问对象进行一些变化即可。
  • 减少业务对象的代码复杂度。由于数据访问对象可以管理所有的数据访问复杂细节,这也就简化了业务模块和其他数据客户的代码。同时也提高了应用系统的整体可读性和开发率。
  • 集中处理所有数据访问。由于所有的数据访问操作都移交给数据访问对象,这样应用系统其他部分就与数据访问实现隔离开来,而全部相关操作都与数据访问对象集中处理,这样也使得相关操作更加容易被维护和管理。

这种设计模式的缺陷:

  • 对于容器管理的持久性不能利用。如果EJB容器采取容器管理的方式,那么所有对于持久性数据存储的管理都由容器负责。这样的话应用系统就无需实现数据访问对象了,因为应用服务将透明地提供这一功能。
  • 添加了额外的层面。数据访问对象在数据用户和数据源之间添加了一个层面,也就增加了一些额外的设计和实现的负担。当然,我们认为它是物有所值的。

总之,在开发人员选择不同模式的时候,应该注意,一定的模式对应于一定的应用层次。比如说,与视图和显示相关的模式就是在Web层应用的。而一些与业务逻辑控制相关的模式则是与EJB层次相关的。另外一些关于读取数据和分派操作的模式则适用于不同的层次之间。

7、值对象或传输对象

   值对象(value object)模式通过减少分布式通信的消息而促进数据的交换,通常这里所指的通信是在Web层和EJB层之间。在一个远程调用中,一个单一值对象可以被用来取出一系列相关数据并提供给客户。

   这种设计模式的出现是基于客户需要与ejb大量地交换数据的情况。具体来说,在J2EE平台中,应用系统通常将服务器端的程序组件实现为会话bean和实体bean,而这些组件的部分方法则需要将数据返回给客户;这种情况下,通常一个用户会重复调用相关方法多次,直到它得到相关信息,应该注意的是,多数情况这些方法调用的目的都是为了取得单一的信息,例如用户名或者用户地址等。

  显而易见,在J2EE平台上,这种调用基本上都是来自远程的。也就是说,用户多次调用相应的方法会给Web带来极大的负担,即使用户和EJB容器加载相同的JVM、OS和计算机上运行EJB程序,由于方法调用被缺省地认为是远程任务,所以这种问题依然存在。

   由于以上所提到的问题,在远程方法的调用次数增加的时候,相关的应用程序性能将会有很大的下降,因此利用多次方法调用而取得单一的信息是非常低效的;在这种情况,J2EE的研究人员建议使用传输对象来包含所有的程序数据,即每次方法调用可以发送和接收这个传输对象;当用户向EJB发出对于程序数据的请求时,EJB会创建这个传输对象,将它的各个域赋以相关的数值,并将整个对象传送给用户。

    当EJB使用传输对象的时候,用户可以通过仅仅一次方法调用来取得整个对象,而不是使用多次方法调用以得到对象中每个域的数值;由于传输对象是通过值传递而交送给用户的,所以所有对于该传输对象的调用或取值都是本地调用,而不是远程方法调用。不过需要注意的是,这个传输对象必须具有对应于每个属性的访问方法,或者将所有属性都设为公共的。   

    类图13表示了传输对象模式的体系结构。

    

图13 传输对象类图

在图13中,传输对象首先在EJB中创建,然后返回给远程客户;当然,传输对象也可以根据需要融合其他的设计模式。

   图14显示了传输对象模式中的参与模块和它们之间的交互。

图14 传输对象序列图

   下面我们说明一下传输对象模式的各个参与模块:

    (1)客户(Client)。客户代表了EJB所提供服务的使用者,通常是运行于用户终端的应用程序。

    (2)业务对象。业务对象表示在一个模式中由会话bean、实体bean或数据访问对象(Data Access Object)实现的角色。业务对象通常负责创建传输对象,并根据请求将其传送到相关的用户;业务对象也可以从用户中取得一个传输对象格式的数据,并应用这些数据来执行一些更新。

    (3)传输对象。传输对象是一个可序列化的Java对象。在这个对象的类中,通常会有一个包含所有域的构造函数,用来创建这个传输对象。

    这个传输对象中的成员变量基本都被定义为公共,从而无需为它们提供相关的访问方法。当然如果存在一定安全的需要,相关的成员变量也可以设为保护或私有,并且给定各自的访问方法。由此可见,传输对象的设计是随着应用系统的需要不同而改变的,是否将对象中的成员变量设为公共,或提供一定的访问方法,将是一个很重要的设计问题。

    通常在实现这个模式时,最多采取的是可更新的传输对象策略和多传输对象策略。    在可更新的传输对象策略中,传输对象不仅可以从服务于用户的业务对象中取得相关信息和数据,还可以从业务对象中得到用户对于数据所需要进行的改变。

    图15以类图表的形式表明了业务对象和传输对象之间的关系。

     

图15 可更新传输对象类图

   业务对象创建了传输对象。而用户通过访问业务对象,既得到了所需的信息,也对相关数据做出了一定的修改;为了能够使得用户可以修改业务对象各个域的取值,这个对象必须提供一定的变值方法,而出于对Web负担的考虑,业务对象所提供的方法最好以传输对象为参数。相应地,这些方法可以去调用传输对象所提供的方法,来设置传输对象的各个成员变量的取值;同时在传输对象的方法中,我们也可以植入数据验证和完整性检查的逻辑,这样在用户从业务对象的方法得到传输对象时,可以直接调用传输对象的成员方法进行本地数据访问,当然这种本地数据访问不会影响到业务对象。

    当用户调用业务对象的变值方法时,该方法会将用户端的传输对象序列化,再将它发送给业务对象;业务对象接收到更新的传输对象,便将这些更新写回到自己的对象拷贝中去;    这里需要说明的是,上面提到的写回只是涉及到被更新的变量,而不是全部变量的写回,因此我们需要在传输对象中另设置一个变量,来指定哪些成员变量被用户更新过,这也就使得这种模式的设计相对复杂,开发人员需要考虑同步化和版本控制的问题。

 

图16显示了这个更新过程的序列图。

图16 可更新传输对象序列图

   多传输对象的方法是指一个单一的业务对象可以根据用户请求制造多个不同的传输对象。也就是说,业务对象和它所创建的传输对象保持一对多的关系。类图17表示了这种实现方法的各个参与模块以及它们之间的调用关系。

图17 多传输对象类图

 

当一个用户需要A类型的传输对象时,他会激活相关EJB的getDataA()方法来取得传输对象A;当他需要B类型的传输对象时,他会激活getDataB()方法来获取传输对象B;依此类推。序列图18表示了这一过程。

图18  多传输对象序列图

   

使用这种设计模式,应用系统的实体bean及其远程接口会变得十分简单。实体bean中无需再为每一个成员变量都实现一个set()和get()方法,并在远程接口中实现相应的定义。用户无需再进行多次的方法调用来取得信息和数据,所需要的只是一次方法调用以获得整个传输对象。当然这里需要考虑Web负担和大量数据一次传输的权衡。开发人员可以根据不同的需要来选择不同的实现方法。

  如上所述,用户和实体bean之间可以通过在一次方法调用中使用传输对象而交换所有的数据,也就是说传输对象作为数据载体工作,并减少了远程的方法调用,从而大大减轻了Web负担。通过使用传输对象的方法,我们也将有可能减少实体bean和其传输对象间的代码重复。不过在使用可更新的传输对象方法时,用户可以修改其本地的传输对象,之后再将其传送回业务对象中,后者将所需的更新整合到自己一端;但是这样一来,就会存在一个版本控制的问题,不同的客户可能在同时修改相同类型的传输对象,而如果相关的业务对象没有发现这一点的话,可能就会造成一些用户的数据没有得到及时更新,而另外一些用户的数据又被覆盖的情况;在系统设计中必须考虑这个问题。

8、截取过滤器

截取过滤器(intercepting filter)主要用于对于用户请求的之前处理和之后处理,也就是说它对于客户的请求使用了额外的操作。比如说,servlet可以处理一个网站的所有客户请求并提供一个核心的认证机制。

这种模式主要工作于表示层,负责处理不同类型的请求,同时也需要进行多种不同的处理。在这些请求中,有一些请求会直接传送给后端模块处理,而另外一些请求则先会在过滤器里解释或补充内容,之后才能传送给后端模块。这种模式的提出主要是由于一个客户的Web访问和系统响应都需要一定的预处理和后处理,例如用户身份、用户环境信息、用户请求的合法性等。通常这些处理的结果都会决定用户的请求是否能够进行,或是系统的响应应该用什么格式来表示。

对于这种预处理和后处理问题,传统上,开发人员会设计一系列额外的检测程序模块,也就是一整套if/else语句,并且指定如果其中任何一个检测失败,所有的处理工作都会退出。显然,这种方法是存在很大弊端的,即代码的可读性、可维护性都会被大大降低,同时将检测工作融于一般的程序模块,使得整个程序的模块性难以保证。

解决这种问题的关键在于,设计一种简单的技术,以能够添加或移除额外处理的模块,而这些模块通常都能够完成一定的检测和过滤功能。根据以上的讨论,J2EE研究人员提出了设计模式----截取过滤器作为解决方案,这种模式可以在不影响核心处理模块的情况下,实现可插入的过滤器来执行一般的处理功能。   

从理论上来说,这种过滤器可以截取客户请求和系统响应,并进行相应的预处理和后处理;同时开发人员也可以随时根据需要移除这些过滤器,并不用担心会改变任何其他模块。   

我们这里所说的预处理和后处理功能,通常是指一些基本的系统服务,如安全、登录,系统调试等。执行这些功能的过滤器通常是需要与核心模块分开的,并且由于系统职能或规则的变化,这些模块随时可能被添加或删除。

下面提供一些关于截取过滤器的图示,以帮助大家更好地理解这种设计模式,并合理地加以运用。图19表示了截取过滤器模式的整体结构,图19显示了截取过滤器中的参与模块和相互之间的联系。

  

图19  截取过滤器模式

   

图20 截取过滤器序列图

下面我们分别来说明图20中的各个模块:

(1)过滤管理器(Filter Manager)。过滤管理器负责过滤器的主要处理工作,即创建过滤器链对象以及相应的过滤器组建,并初始化整个处理过程。

(2)过滤器链(Filter Chain)。过滤器链是一组互不依赖的过滤器有序集合。

(3)过滤器1,过滤器2,过滤器3(FilterOne,FilterTwo,FilterThree)。这些都是提供不同服务的过滤器,而过滤器链则负责它们的协调工作。

通过采用这种设计模式,应用系统可以取得更方便的中心控制,这是由于过滤器可以提供处理多种请求的中心模块,并能根据后端的处理模块而解释和润色用户的请求,使得该请求能更好地与处理模块所提供的功能匹配。另外,过滤器通常可以将不同种类的服务聚集在一起,并提供相当灵活的服务组合,应用系统可以通过使用截取过滤器提高其重用性,过滤器可以随时根据需要从其他程序模块中插入或移除,并且由于它们通常具有标准的接口,开发人员可以使用一组类似的过滤器,并在不同的情况下进行全组的重用。

  采用这种设计模式也会带来一定的问题,即在过滤器之间共享信息将变得非常困难,这是由于根据其定义和需求,每个过滤器的设计和开发都大相径庭。因而如果在不同的过滤器之间需要共享信息的话,其代价将是非常昂贵的。


作者:务实,多年从事J2EE网站及应用系统项目的开发和应用。

参考资料:

《J2EE设计开发编程指南》     Rod Johnson     电子工业出版社

《J2EE参考大全》             Jim Keogh       电子工业出版社

《实用J2EE设计模式编程指南》 Craig A.Berry   电子工业出版社

 

 

版权所有:UML软件工程组织