UML软件工程组织

Java设计模式之复合模式篇
作者:冯睿    本文选自:赛迪网  2003年01月21日

 

自从J2EE出现以来,就大大简化了在Java下的企业级开发。但是随着J2EE越来越普遍地被应用到各个领域中,开发者们渐渐意识到需要一种方法来标准化应用程序的开发过程,他们采用的方法是标准化应用程序的结构层。在结构层通常封装了一些独立于业务逻辑的复杂技术,以便在业务逻辑和底层的架构之间建立起弱连接。无可否认,J2EE是一个很成功的技术,它为一些基本的任务提供了一致的标准,例如数据库连接、分布式应用程序等。但是使用J2EE并不能保证开发人员开发出成功的应用程序。有些人认为J2EE本身就是一种框架技术,但是这种认识是不正确的,我们应该意识到J2EE并没有提供一个能够帮助开发人员开发出高质量应用程序的框架,因此很多有经验的开发人员通过利用设计模式来弥补这一缺陷。

在开发人员的圈子中,大家通过相互交流在开发过程中所遇到的问题以及解决方法来丰富整个圈子的经验。而设计模式就是在这样的情况下产生的。一个设计模式必然是针对某个特定的问题的,这个问题的解决方案以及这样解决问题产生的后果。在本文中我将讨论复合模式的特点和它的应用。

复合模式(Composite Patten)介绍


在介绍复合模式前,我们需要定义一下什么是复合对象(Composite Object)。复合对象是包含了其它对象的对象。例如,一幅图由一些基本的对象组成,例如线、圆、矩形和文本等,因此图就是复合对象。因为在Java中,开发人员操作基本对象的方式和操作复合对象的方式常常相同,因此需要利用到复合模式。例如,线或文本等基本图形对象都需要支持绘制、移动或缩放等功能;而图这种复合对象也需要支持相同的功能。在理想的情况下,我们希望对复合对象和基本对象以完全相同的方式完成这些操作,否则实现的代码将会产生不必要的复杂性,并且不易于维护和扩展。

那么什么是复合模式呢?将对象组织到树结构中以表达部分整体的层次关系就实现了复合模式,它使程序能够以相同的方式对待基本对象和复合对象。

在程序中实现复合模式并不难。复合类继承一个代表基本类型的基类就可以了。图1显示了一个表现复合模式思想的类图。



图1 复合模式的实现


在图1中,Component是代表基本类型的基类(也可以是一个接口);Composite是复合类。例如Component代表的是基本图形元素的基类,而Composite代表的是图;Operation1()和Operation2()方法分别是移动和缩放操作。图1中的Leaf类代表的是点、线或者圆等基本图形元素。

针对Component类中的每个方法,Composite类都有相同名称的方法与之对应。Composite类保存了一个基本对象的集合。通常Composite类中的方法在实现时都会将集合中的对象遍历一次,然后调用每个对象中相应的方法。例如图对象Drawing的draw()方法可能是这样实现的:


// 代码1一个复合方法
public void draw() {
   // I遍历所有的对象
   for(int i=0; i < getComponentCount(); ++i) {
      // 获得对对象的应用,调用它的draw方法
      Component component = getComponent(i);
      component.draw();   
   }
}


由于Composite类继承了Component类,因此你可以将一个Composite对象传递给需要Component对象作为参数的方法。例如:


// 代码2  repaint方法
public void repaint(Component component) {
   // 事实上component可能是一个复合对象,因此该方法没有区分基本对象和复合对象
   component.draw();
}


上面的repaint()方法中,Component对象被作为参数传递到了函数体,这个对象可以是Component,也可以是Composite。然后函数体中调用draw()方法。由于Composte类继承了Component,程序就不用区分传入的参数到底是哪种类的实例,只需要调用该对象的draw()方法就可以了。

图1中的类图展示了复合模式的一个方面:开发人员必须在引用一个Component对象时必须区分它到底了一个Component对象还是一个Composite对象。通常开发人员可以在Component类中加入一个方法,例如isComposite(),来辨别Composite类。如果该方法返回的是True,开发人员就需要将Component对象强制转换为Compoiste对象。


// 代码3,区分Component和Composite
...
if(component.isComposite()) {
   Composite composite = (Composite)component;
   composite.addComponent(AComponent);
}
...


图2显示了另一种实现复合模式的方法:



图2 另一种复合模式的实现方法


在图2所示的复合模型中,开发人员不必区分Component对象和Composite对象,也不需要将Component对象强制转换Composited对象。这样代码3中的代码就变成了一行:


// 代码4,不区分Component和Composite
...
component.addComponent(AComponent);
...


但是如果代码4中的component不是Composite的实例,addComponent()方法会做些什么呢?这是图2中的复合模式中最重要的一部分。显然一个基本类型不可能包含其他基本类型或复合类型,Component.addComponent()可能什么也不干,或者抛出一个异常。通常我们认为将基本类型加入其他基本类型的操作是一个错误,因此抛出异常是开发人员最好的选择。

那么这两种复合模式的实现方法哪一个更好呢?这是一个常常引起争论的问题。我个人觉得第二种方法更好一些,因为开发人员不必区分Component对象和Composite对象,也不用强制转换对象。

使用复合模式的实际例子:Struct Tiles

下面让我们来看一个复合模式应用用到Apache Struts JSP框架中的例子。在Apache Struts的框架中包含了一个被称为Tiles的JSP标签库,它是你能够将多个JSP组合成一个Web页面。事实上,它实现J2EE复合视图模式。在我们讨论复合模式和Tiles标签库的联系前,让我们先来看一个Tiles的例子。如果你对Struts已经非常熟悉了,你可以跳到"在Struts Tiles中使用复合模式"一节。

通常一张网页是有几个区域构成的。例如图3中的网页包含了边栏、头、内容和角注四个部分。这四个部分在网页上的分布构成了布局。Struts Tiles使你能够重用单独的每个区域和布局。在我们讨论重用的问题之前,让我们来看看怎样用不同的方法实现图3中的网页。



图3 例子网页


1.手工实现布局

下面是相应的HTML代码:


代码5 手工实现布局的HTML代码
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<%@ page contentType='text/html; charset=UTF-8' %>
<html>
   <head>
      <title>Implementing Complex Layouts by Hand</title>
   </head>
   <body background='graphics/blueAndWhiteBackground.gif'>
      <%-- One table lays out all of the content for this page --%>
      <table width='100%' height='100%'>
         <tr>
            <%-- Sidebar--%>
            <td width='150' valign='top' align='left'>
               <table>
                  <tr>
                     <%-- Sidebar top --%>
                     <td width='150' height='65' valign='top' align='left'>
                        <a href=''>
                           <img src='graphics/flags/britain_flag.gif'/></a>
                        <a href=''>
                           <img src='graphics/flags/german_flag.gif'/></a>
                        <a href=''>
                           <img src='graphics/flags/chinese_flag.gif'/></a>
                     </td>
                  </tr>
                  <tr>
                     <%-- Sidebar bottom --%>
                     <td>
                        <font size='5'>Links</font><p>
                        <a href=''>Home</a><br>
                        <a href=''>Products</a><br>
                        <a href=''>Downloads</a><br>
                        <a href=''>White papers</a><br>
                        <a href=''>Contact us</a><br>
                     </td>
                  </tr>
               </table>
            </td>
            <%-- Main content--%>
            <td valign='top' height='100%' width='*'>
               <table width='100%' height='100%'>
                  <tr>
                     <%-- Header--%>
                     <td valign='top' height='15%'>
                        <font size='6'>Welcome to Sabreware, Inc.</font>
                        <hr>
                     </td>
                  <tr>
                 <tr>
                     <%-- Content--%>
                     <td valign='top' height='*'>
                        <font size='4'>Page-specific content goes here</font>
                     </td>
                  </tr>
                  <tr>
                     <%-- Footer--%>
                     <td valign='bottom' height='15%'>
                        <hr>
                        Thanks for stopping by!
                     </td>
                  </tr>
               </table>
            </td>
         </tr>
      </table>
   </body>
</html>


然后你需要实现JSP。根据代码5编写出的JSP代码有两个缺点。第一,页面的内容被嵌入了JSP代码中,因此开发人员无法重用它。而事实上,边栏、头和角注在一个网站的网页中可能会被多次重用。第二,页面的布局也被嵌入到JSP代码中,因此即使有很多网也采用同样的布局,开发人员也无法重用它。通过使用 可以避免第一个问题。

2.使用 <jsp:include> 实现布局

在下面的例子中,我们使用 <jsp:include> 来实现图3中的网页。


代码6 用jsp:include实现布局
<%@ page contentType='text/html; charset=UTF-8' %>
<html>
   <head>
      <title>Implementing Complex Layouts by Hand</title>
   </head>
   <body background='graphics/blueAndWhiteBackground.gif'>
      <%-- One table lays out all of the content for this page --%>
      <table width='100%' height='100%'>
         <tr>
            <%-- Sidebar section --%>
            <td width='150' valign='top' align='left'>
               <jsp:include page='sidebar.jsp'/>
            </td>
            <%-- Main content section --%>
            <td height='100%' width='*'>
               <table width='100%' height='100%'>
                  <tr>
                     <%-- Header section --%>
                     <td valign='top' height='15%'>
                        <jsp:include page='header.jsp'/>
                     </td>
                  <tr>
                  <tr>
                     <%-- Content section --%>
                     <td valign='top' height='*'>
                        <jsp:include page='content.jsp'/>
                     </td>
                  </tr>
                  <tr>
                     <%-- Footer section --%>
                     <td valign='bottom' height='15%'>
                        <jsp:include page='footer.jsp'/>
                     </td>
                  </tr>
               </table>
            </td>
         </tr>
      </table>
   </body>
</html>


在上面的代码中,通过使用 <jsp:include> 来调用其它JSP。由于在sidebar.jsp、header.jsp、content.jsp、和footer.jsp中封装了边栏、头、内容和角注,因此开发人员可以重用这些元素。但是这种解决方案仍然无法重用网页的布局。下面是sidebar.jsp、header.jsp、content.jsp、和footer.jsp的代码:


代码7 sidebar.jsp
<%@ page contentType='text/html; charset=UTF-8' %>
<table width='100%'>
   <tr>
      <%-- Sidebar top component --%>
      <td width='150' height='65' valign='top' align='left'>
        <a href=''><img src='graphics/flags/britain_flag.gif'/></a>
        <a href=''><img src='graphics/flags/german_flag.gif'/></a>
        <a href=''><img src='graphics/flags/chinese_flag.gif'/></a>
      </td>
   </tr>
   <tr>
      <%-- Sidebar bottom component --%>
      <td>
         <table>
            <tr>
               <td>
                  <font size='5'>Links</font><p>
                  <a href=''>Home</a><br>
                  <a href=''>Products</a><br>
                  <a href=''>Downloads</a><br>
                  <a href=''>White papers</a><br>
                  <a href=''>Contact us</a><br>
               </td>
            </tr>
         </table>
      </td>
   </tr>
</table>



代码8 header.jsp
<font size='6'>Welcome to Sabreware, Inc.</font>
<hr>
代码9 content.jsp
<font size='4'>Page-specific content goes here</font>
代码10 footer.jsp
<hr>
Thanks for stopping by!


3.利用Structs Tiles来实现布局

代码10中展示了如何用Struts Tiles来实现前面提到的网页。这段代码利用了 <titles:insert> 标签来创建图3中对应的JSP网页。该JSP文件被定义在名称为sidebar-header-footer-definition的Tiles定义中,定义信息保存在Tiles的配置文件中,在这个例子中,配置文件是WEB-INF/tlds/struts-tiles.tld。代码11列出了该文件。


代码10 使用Struts Tiles来封装布局信息
<%@ page contentType='text/html; charset=UTF-8' %>
<%@ taglib uri='WEB-INF/tlds/struts-tiles.tld' prefix='tiles' %>
<tiles:insert definition='sidebar-header-footer-definition'/>
代码11 struts-tiles.tld
<!DOCTYPE tiles-definitions PUBLIC
  "-//Apache Software Foundation//DTD Tiles Configuration//EN"
  "http://jakarta.apache.org/struts/dtds/tiles-config.dtd">
<tiles-definitions>
   <definition  name='sidebar-header-footer-definition' 
                path='header-footer-sidebar-layout.jsp'>
      <put name='sidebar' value='sidebar.jsp'/>
      <put name='header'  value='header.jsp'/>   
      <put name='content' value='content.jsp'/>   
      <put name='footer'  value='footer.jsp'/>   
   </definition>
</tiles-definitions>


在struts-tiles.tld中可以看到,网页的布局被封装在header-footer-sidebar-layout.jsp中,而网页的内容被封装在sidebar.jsp, header.jsp, content.jsp和footer.jsp中(参见代码7到代码10)。代码12列出了封装了网页布局的JSP。


代码12 header-footer-sidebar-layout.jsp
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.0 Transitional//EN">
<%@ page contentType='text/html; charset=UTF-8' %>
<html>
   <head>
      <title>Struts Tiles implements the Composite pattern</title>
   </head>
   <body background='graphics/blueAndWhiteBackground.gif'>
      <%@ taglib uri='/WEB-INF/tlds/struts-tiles.tld' 
              prefix='tiles'%>
      <%-- One table lays out all of the content --%>
      <table width='100%' height='100%'>
         <%-- Sidebar section --%>
         <tr>
            <td width='150' valign='top' align='left'>
               <tiles:insert attribute='sidebar'/>
            </td>
            <%-- Main content section --%>
            <td valign='top' height='100%' width='*'>
               <table width='100%' height='100%'>
                  <tr>
                     <%-- Header section --%>
                     <td height='15%'>
                        <tiles:insert attribute='header'/>
                     </td>
                  <tr>
                  <tr>
                     <%-- Content section --%>
                     <td valign='top' height='*'>
                        <tiles:insert attribute='content'/>
                     </td>
                  </tr>
                  <tr>
                     <%-- Footer section --%>
                     <td valign='bottom' height='15%'>
                        <tiles:insert attribute='footer'/>
                     </td>
                  </tr>
               </table>
            </td>
         </tr>
      </table>
   </body>
</html>


如果你希望改变网页的内容,你可以定义另外一个Tile(如代码13、14),更改其中与内容相关的部分,但是保留网页原有的布局,这样重用布局和重用内容的问题都解决了。


代码13 另一个Tile的定义
<tiles-definitions>
   <definition  name='a-different-sidebar-header-footer-definition' 
                path='header-footer-sidebar-layout.jsp'>
      <put name='sidebar' value='sidebar.jsp'/>
      <put name='header'  value='header.jsp'/>   
      <put name='content' value='someOtherContent.jsp'/>   
      <put name='footer'  value='footer.jsp'/>   
   </definition>
</tiles-definitions>


然后在 <tiles:insert> 标签中使用新定义的Tile,如代码14所示


代码14 使用新定义的Tiles
<%@ page contentType='text/html; charset=UTF-8' %>
<%@ taglib uri='WEB-INF/tlds/struts-tiles.tld' prefix='tiles' %>
<tiles:insert definition='a-different-sidebar-header-footer-definition'/>


在Struts Tiles中使用复合模式


Struct Tiles实现了复合模式,在Struct Tiles中,JSP就是图1和图2中提到的Component类,而Tiles的定义代表了Composite类。这使开发人员能够指定一个JSP文件或一个Tiles定义作为JSP页面上某个区域中的内容。代码15展示了这个功能:


代码15在Struts Tiles中使用复合模式
<!DOCTYPE tiles-definitions PUBLIC
  "-//Apache Software Foundation//DTD Tiles Configuration//EN"
  "http://jakarta.apache.org/struts/dtds/tiles-config.dtd">
<tiles-definitions>
   <definition  name='sidebar-definition' 
                path='sidebar-layout.jsp'>
      <put name='top'    value='flags.jsp'/>
      <put name='bottom' value='sidebar-links.jsp'/>
   </definition>
   <definition  name='sidebar-header-footer-definition' 
                path='header-footer-sidebar-layout.jsp'>
      <put name='sidebar' value='sidebar-definition'
           type='definition'/>
      <put name='header'  value='header.jsp'/>
      <put name='content' value='content.jsp'/>
      <put name='footer'  value='footer.jsp'/>
   </definition>
</tiles-definitions>


在上面的代码中定义了两个Tile:sidebar-definition和sidebar-header-footer-definition。sidebar-definition被指定为sidebar-header-footer-definition中Value属性的值。这是一个很典型的复合模式的应用。在前面的一些例子中,Value属性的值通常是一个JSP文件,而在这里,Value属性的值是另一个Tile的定义。

小结


复合模式在与界面相关的设计中被经常使用到,最明显的例子就是Swing和Struts。由于它能够使你以同样的方式对待部件和包含部件的容器,因此在某些情况下可以大大提高代码的可重用度,提高开发的效率




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