一种寄生型设计模式在 Swing 应用开发中的实践

本文描述一种在大型的、基于 Swing 开发的系统中,简化代码和对象生命周期管理的设计方法,可以有效地降低代码量和实现界面的重用,提高程序处理效率。

宋 荆汉, 软件工程师, 中兴通讯

宋荆汉从事多年电信网管软件和嵌入式软件开发,目前从事研发质量管理工作,对软件工程和研发过程改进有广泛的研究和兴趣,在 IBM 网站曾发表相关技术文章。



2012 年 12 月 20 日

问题背景

在基于 Java 开发的电信级系统中,会有大量的 GUI 界面设计工作,但众所周知 Java 的目前的 IDE 解决方案对 Swing 界面开发支持的友好性不尽如人意,要做出友好的界面还是要耗费大量的时间,对有些模块可能比业务逻辑的工作量还要大。所以,现在对于 GUI 界面比较多的系统中,很多公司都会用到界面引擎和 XML 方式来自动生成界面,优点在于:

1、使用 XML 文档描述界面,通过界面生成引擎来解释 XML 文档并最终产生显示的界面。这使得开发界面更加容易,界面风格更加一致,维护更加方便。

2、实现了功能代码和界面代码的分离,使它们之间的耦合性减小,这也降低了故障发生的概率,提高了软件的重用率,减少了代码 Java 代码数量。

其基本实现原理见下图 1:

图 1.XML 文件自动生成界面的原理
图 1.XML 文件自动生成界面的原理

具体的界面引擎代码看 GUIEngine.java 文件。

我们给一个简单的界面描述文件的范例见如下清单 1:

清单 1. XML 界面描述文件实例
 <?xml version="1.0" encoding="GB2312"?> 
 <gui_desc> 
    <init> 
        <window_width>260</window_width> 
        <window_height>230</window_height>        
    </init> 
    <component type="javax.swing.JLabel"> 
            <height>45</height> 
            <label>UPS Type</label> 
            <name>labeltest</name> 
            <positionY>12</positionY> 
            <width>230</width> 
            <positionX>12</positionX> 
        </component> 
        <component type="javax.swing.JTextField"> 
            <height>45</height> 
            <default_value>0</default_value> 
            <name>txttest</name> 
            <positionY>67</positionY> 
            <width>230</width> 
            <positionX>12</positionX> 
        </component> 
        <component type="javax.swing.JButton"> 
            <name>btnOK</name> 
            <width>91</width> 
            <action>OutdoorUPS_OkAction</action> 
            <disable /> 
            <positionY>132</positionY> 
            <positionX>12</positionX> 
            <icon>ok.gif</icon> 
            <label> 确定 </label> 
            <height>23</height> 
        </component> 
        <component type="javax.swing.JButton"> 
            <name>btnCancel</name> 
            <width>91</width> 
            <action>CancelAction</action> 
            <disable /> 
            <positionY>132</positionY> 
            <positionX>112</positionX> 
            <icon>cancel.gif</icon> 
            <label> 取消 </label> 
            <height>23</height> 
        </component>       
 </gui_desc>

通过程序创建显示出来的 Swing 界面如下图 2:

图 2 .XML 描述文件生成的界面
图 2 .XML 描述文件生成的界面

程序调用逻辑如下:

请单 2. 根据描述文件创建界面程序清单
 /* 创建一个主程序框架 */ 
 JFrame jf=new JFrame("test"); 
 /* 传递界面描述文件,初始化界面引擎实例 */ 
 GUIEngine ge=new GUIEngine("Outdoor_UPS.xml"); 
 ge.createJDialog(jf, "hello world!!").setVisible(false);

界面辅助类

但在实际使用过程中,基于我们已有的开发习惯,我们会发现如果您要访问里面的具体控件就不是那么方便了。您需要通过界面引擎提供的接口来实现 getComponentByName(String name),其实现逻辑就是在创建界面时将所有的界面控件以控件名称为关键字存放在一个 HashMap 中,获取控件就是从存放所有控件的 HashMap 读取出来。

以上面程序为例,我们在 XML 界面文件中定义了一个控件名称为“txttest”,此时我是不能直接用 ge.txtest 的方式来访问的,我只能通过调 GUIEngine 实例提供的接口来完成,ge.getComponentByName(“txttest”),而且获取后的对象其类型信息已经丢失了,必须要用强制类型转换为 javax.swing.JTextField 才能使用。按此种方法访问的缺点是显而易见的:

1、从 GUIEngine 实例中获取的控件对象丢失了类型;

2、另一个问题是需要使用字符串作为关键字从 HashMap 中出错时,由于没有 Java 的编译类型的检查过程,不容易发现错误。

为解决上述问题,在实践过程中出于使用习惯的,通常会针对每一个界面都会构建一个辅助类。在辅助类中直接定义各种界面定义的各种类型的组件的 public 成员,并在辅助类中提供一个静态方法用于根据 GUIEngine 生成一个辅助类。在需要对界面控件访问时,直接使用辅助类的成员进行访问。在辅助类中调用 GUIEngine 的方法获取组件并赋给辅助类中的成员对象,在其他地方就可以避免调用 getComponentByName 方法来访问界面控件了。

以上面界面为例:

清单 3. 界面辅助类的实例
 /** 
 * 增加、修改 UPS TYPE 数据的操作方法。
 */ 
 public class UPSTypeHelper { 

 public JButton btnOk;// 确定按钮
 public JButton btnCancel;// 取消按钮
 public JTextField txttest;// 文本框
 public JLabel  labeltest ;// 文本标签   
	 /** 
 * 本类的实例对象池。 key:界面描述文件名。 value:类实例。
	 */ 
 private static HashMap instancePool = new HashMap(); 

	 /** 
 * 当前界面引擎
	 */ 
	 private GUIEngine guiEngine = null; 

 private UPSTypeHelper (GUIEngine guiEngine) { 
		 this.guiEngine = guiEngine; 
 this.getAllComponents(); 

	 } 

	 /** 
 * 通过 GUIEngine 获取界面上的所有组件对象。
	 */ 
 private void get All Components () { 
 txttest = (JTextField) this.guiEngine.jcmSet.get("txttest"); 
 btnOk=(JButton) this.guiEngine.jcmSet.get("btnOk"); 
 btnCancel=(JButton) this.guiEngine.jcmSet.get("btnCancel"); 
                 labeltest=(JLabel) this.guiEngine.jcmSet.get("labeltest"); 
	 } 

	 /** 
 *   采用单例模式,保证只创建一次同样的界面对应的辅助类
	 */ 
 public static UPSTypeHelper getInstance(GUIEngine guiEngine) { 
 /* 获取该界面引擎对应的 xml 界面描述文件 */ 
 String guifile = guiEngine.getXmlfile(); 
 UPSTypeHelper instance = (UPSTypeHelper) instancePool.get(guifile ); 
		 if (instance == null) { 
 instance = new UPSTypeHelper (guiEngine); 
 instancePool .put(guifile , instance); 
		 } 
 return instance; 
	 } 

	 /** 
 * 清除使用过的实例。
	 */ 
 public static void removeInstance(String guifile ) { 
 if (instancePool .get(guifile ) != null) { 
 instancePool .remove(guifile ); 
		 } 
	 } 
 }

这种辅助类与每一个界面 XML 文件 ( 实际上是根据 XML 文件生成的 GUIEngine 对象 ) 一一对应。即每画一个界面 XML 文件都需要创建一个界面助手类。


界面辅助类的缺陷

界面辅助类使用中也存在可以优化的地方,比如辅助类的构建,完全可以在界面创建时自动构建并完成组件的映射设置工作,不用在 getAllComponents 方法中再手工设置实现映射功能。

另外也存在一个问题,当主窗体 ( 或其上的弹出对话框 ) 关闭时,为了清除内存,需要调用 removeInstance 来将辅助类的实例移除,否则会导致内存泄露。在一个由 100 多人组成的开发团队,同时新老搭配,产品生产周期很长的情况下,我们会发现很多 BUG 的出现就是忘记调用该方法。很多员工特别对新员工,对其原理不是太理解,可能还认为,Java 内存还需要我去管理的么?而且,错误定位非常复杂麻烦。


界面助手类的对象特性分析

图 3. 界面助手类特性分析示意图
图 3. 界面助手类特性分析示意图

让我们回想一下我们创建界面助手类的目的是什么?很简单,简化访问。界面助手类所访问的真正对象是 GUIEngine 中的界面组件。那么是否应有这样的特性,GUIEngine 对象不存在了,助手类的对象也应随之消失。或者说,界面助手类对象就象是 GUIEngine 对象的寄生体。当宿主人不存在了,寄生体肯定也消失了。

如果将 GUIEngine 对象比喻成人,那么界面助手类对象就象是使用 X 射线机对人体进行透视看到的图像。虽然通过理论知识的学习,我们知道人体有多少块骨骼,但是却被皮肤遮住了,我们无法直观地看到,通过 X 射线机我们就可以清晰地看到了。


界面助手类的管理

既然界面助手类主要麻烦在内存需要手动释放其原理见下面示意图 4,我们需要找到一个更为简单释放内存的方法。

图 4 手动释放内存原理示意图
图 4 手动释放内存原理示意图

当界面关闭时,由于辅助类是调用界面引擎的实例,所以界面类内存并没有完全释放,需要手动销毁辅助类实例,否则,长期运行会导致内存泄露。

此时我们可以首先想到,我们把这个辅助类,直接放在宿主类不就可以了吗?见下图 5

图 5 改善后内存自动释放示意图
图 5 改善后内存自动释放示意图

当界面关闭时,由于辅助类是挂接在界面引擎类中,所以界面关闭时,根据 Java 内存回收原理,辅助类就自动被销毁,内存对象被收回,避免了内存泄露的情况。

这样在能够访问 GUIEngine 的地方我们都可以获得辅助类,当 GUIEngine 销毁时,也无法再获取到界面助手类了。我们可以在 GUIEngine 中,增加一个方法 getGUIHelpMap(), 返还一个 HashMap 来替代辅助类中的

 private static HashMap instancePool = new HashMap();

方法可以改写一下见下面代码:

清单 4. 可以自动释放内存的关键代码清单
 public static UPSTypeHelper getInstance(GUIEngine guiEngine) { 
 String guifile = guiEngine.getXmlfile(); 
 UPSTypeHelper instance = (UPSTypeHelper) guiEngine.getGUIHelpMap().get(guifile ); 
 if (instance == null) { 
 instance = new UPSTypeHelper(guiEngine); 
 guiEngine.getGUIMap().put(guifile , instance); 
		 } 
 return instance; 
	 }

代码行数差不多,但不用再手工调用 removeInstance 方法释放实例了。Java 语言的一个重要特性就是垃圾回收,当 GUIEngine 对象被销毁后,附着在其上的对象也不再可达,辅助类就将被 GC 垃圾回收器回收。我们利用 Java 的垃圾回收特性实现了辅助对象的自动销毁,不用再负责辅助类的生命周期的管理工作了,这不也正是 Java 的便捷特性之一么。

我们还可以再深入想想,解决自动映射的问题,是否可以自动地完成 GUIEngine 中的界面控件到辅助类中定义的成员的映射呢?我们知道,完成这个映射工作的主要是辅助类中的 getAllComponents 方法 , 既然要自动完成映射,这个方法首先挪开。


辅助类的自动映射

我们知道在 Java 的反射功能,为自动映射提供了可能性。从实际应用中,可以判断,在界面辅助类中,需要映射的都是控件类型,都派生自 JComponent 类,我们可以遍历所有定义的 Java 成员,如果其类型为 Swing 界面控件类型,则从 GUIEngine 中提取相应的界面组件并进行成员赋值。当然为能够正确地完成界面组件自动映射的条件是:界面组件的名称要与界面辅助类中定义的 public 成员名称保持一致。实现方法很简单,如下面代码所示:

清单 5. 辅助类自动映射实现关键代码清单
 private void reflectComponents(GUIEngine ge){ 
      /* 遍历类所有的属性 */ 
       Field[] fields = getClass().getFields(); 
       try { 
       for (int i = 0; i < fields.length; i++) { 
			   Type type=fields[i].getType(); 
			   String tmpstr=type.toString(); 
               if (tmpstr.indexOf("javax.swing")!=-1){ 
                  fields[i].set(this ,ge.getComponentByName(fields[i].getName())); 
		       } 
		    } 
       } catch (Exception iae) { 
             iae.printStackTrace(); 
		   } 

	 }

由于这个方法对所有的辅助类都是公用的方法,所以可以抽象一个父类 UIHelp,在构造函数中,完成控件属性的自动映射功能。在每个界面辅助类中的 getAllComponents() 方法可以取消了,代码更为的简洁。给出 UIHelp 的代码:

清单 6. 优化后的 UIHelp 类实现代码清单
   public class UIHelp { 
          public static <T extends UIHelp>T getInstance(
          GUIEngine ge,Class<T> cls){ 
              T help=(T)ge.getGUIHelpMap().get(cls.getName()); 
              try { 
                 if (help==null ){ 
				 help=cls.newInstance(); 
				 help.reflectComponents(ge,cls); 
                 String clsname= help.getClass().getName(); 
				 ge.getGUIHelpMap().put(clsname, help); 
			    } 
               }catch (InstantiationException il){ 

       }catch (IllegalAccessException ie){} 
       return help; 
	 } 
 private void reflectComponents(GUIEngine ge,Class<? extends UIHelp> cls){ 
 // 该代码前面已经给出,这里不重复了。
 } 
 }

上面代码分下面几步:

  1. 在 GUIEngine 实例中,判断是否创建有该界面对应的辅助类;
  2. 因为所有的辅助类都是 UIHelp 的子类,所以通过泛型的方法返还的也是 UIHelp 的子类,传递的 Class 也是该子类,不用再单独进行类型转换的判断;
  3. 自动完成对传递进来的 辅助类的界面控件的自动映射;
  4. 将创建的辅助类,附着在 GUIEngine;

上述辅助类的代码就非常简单了:

清单 7. 清单 1 描述的界面的辅助类的实现
 public class UPSTypeHelper extends UIHelp { 
     public JButton btnOk;// 确定按钮
     public JButton btnCancel;// 取消按钮
     public JTextField txttest;// 文本框
     public JLabel  labeltest ;// 文本标签

 public UPSTypeHelper getInstance(GUIEngine ge){ 
     return getInstance (ge,UPSTypeHelper.class ); 
	 } 

 }

只需要在辅助类中,定义好控件类为 public 类型,调用 getInstance 就可以自动在父类中完成映射功能了。


功能的扩展

根据下面的示意图和上面的代码我们可以看出,一个 GUIEngine 其实不只可以附着一个辅助类,因为定义了一个 HashMap 来存放辅助类的实例,我们只要保证实例的名称不一样就可以了,这样我们可以用几个辅助类来映射界面类。在配置数据的界面中,当字段非常多的时候,而且字段有明显的分类比如通过 Tab 页的形式,防止辅助类过大,可以定义多个辅助类来完成。

图 6 界面引擎与辅助类的映射关系示意图
图 6 界面引擎与辅助类的映射关系示意图

另外一种情况是,辅助类其实也可以复用,当界面大部分相同时,我们可以对一些经常使用到的控件,创建公用的界面助手类。每个界面助手类只映射 GUIEngine 对象的一部分界面控件,则助手类可以被更广泛地重用


寄生模式的导出

根据上面的对辅助类内存泄露方案的处理,可以推广到所有存在寄生特征的设计场景中进行通用化,进而定义寄生模式。

图 7. 寄生模式结构示意图
图 7. 寄生模式结构示意图
  1. 宿主对象:为其他对象提供所需服务的。调用寄生对象提供服务并为它提供一个钩子,以前面章节的实例对应可以理解为,GUIEngine 是宿主提供 Outdoor_UPS.xml 文件描述的界面创建服务,实际是调用对应的辅助类来实现界面组件的访问服务的,辅助类会调用 GUIEngine 类,同样 GUIEngine 也会挂接辅助类。
  2. 寄生对象:为宿主对象提供服务,如:为真实对象提供服务的模拟等;此处可以理解为,辅助类为界面对象提供模拟服务,使得操作起来更方面和符合习惯。
  3. 开关变量:确定寄生对象如何提供服务的变量,我们可以认为其实宿主类本身是可以提供服务的,但有寄生类来提供更为方便,但在某些情况下,以上文的案例,如果仅仅只需要临时访问某个界面的很少数量的控件,而界面本身控件数量很多,如果还创建寄生类就不太合算了,此时可以由宿主类直接提供服务;
  4. 创建者:创建宿主对象的对象,它将开关变量传递给寄生变量以动态决定谁提供服务;
  5. 客户对象:使用宿主对象服务的对象,当开关变量被设置,则由寄生对象提供服务;

以前面的案例为例,我们想访问并设置 UPS TYPE 界面中文本输入控件的值为 100,首先创建一个 GUIEngine 对象,通过开关变量,设置是否创建辅助界面类,如果创建,可以通过 UPSTypeHelper 来完成对界面值的设置了。

图 8 寄生模式对象调用顺序
图 8 寄生模式对象调用顺序

我们来看代码的实现过程

清单 8. 寄生模式的实现实例
 public class Client { 
       private Create ct; 
       public Client(){ 
              ct=new Create("Outdoor_UPS.xml",true );/* 打开开关辅助对象提供服务 */ 
	          } 
        public boolean SetUPSTypeValue(int typeValue) 
	         { 
                try { 
                 ct.getServiceObj().setText(String.valueOf (typeValue)); 
                 return true ; 
                }catch (Exception ex){ 
                     ex.printStackTrace(); 
                     return false ; 
		 } 
	 } 
 /** 
 * @param args 
	 */ 
 public static void main(String[] args) { 
        Client clt=new Client(); 
        System.out .println(clt.SetUPSTypeValue(100)); 
	 } 
 }

使用者,主要是通过创建者,创建然后完成设置值的操作;

清单 9. 寄生模式的创建者的实现实例
public class Create { 
       private String guixml=null ;/* 界面描述文件 */ 
       public boolean Switch;/* 开关量是否创建寄生对象 */ 
       private GUIEngine ge; 
       public Create(String guifile,boolean open){ 
               this .guixml=guifile; 
               this .Switch=open; 
               ge=new GUIEngine(this .guixml); 
               ge.createJDialog(null , "Hello Test").setModal(false ); 
               ge.getCurrJD().setVisible(true ); 
	 } 
 /* 获取要设置值的控件对象 */ 
 public JTextField getServiceObj(){ 
        if (Switch){ 
               UPSTypeHelper upshelp=
                 UPSTypeHelper.getInstance (ge, UPSTypeHelper.class ); 
               return upshelp.txttest; 
               }else { 
        return (JTextField)ge.getComponentByName("txttest"); 
		 } 
		
	 } 
 }

创建者,根据 open 这个开关量,决定是否创建寄生对象,如果不创建则调用宿主对象的自身方法使用,该代码的执行结果如下图:

图 9 代码执行结果界面
图 9 代码执行结果界面

显示设置成功。

结束语

本文所描述的编码方式,简化了程序开发,而且对于大型的基于 Swing 的 GUI 应用开发可以大幅度的降低代码量,代码更清晰易懂。本文重点在于面向对象编程中对象生命期的一种管理方式,是对现有的代码中不当的对象生命期管理方式的改进。一旦了解,其实非常简单。

相比较与代理模式和工厂模式,对象的创建可以更灵活动态确定。对寄生对象做了改变,这些改变只会传递到与其相关的系统对象而不会影响其余的系统对象。而且提供一种界面辅助操作可重用的方法。


下载

描述名字大小
示例代码samplecode.zip7KB

参考资料

学习

讨论

  • 加入 developerWorks 中文社区。查看开发人员推动的博客、论坛、组和维基,并与其他 developerWorks 用户交流。

条评论

developerWorks: 登录

标有星(*)号的字段是必填字段。


需要一个 IBM ID?
忘记 IBM ID?


忘记密码?
更改您的密码

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件

 


在您首次登录 developerWorks 时,会为您创建一份个人概要。您的个人概要中的信息(您的姓名、国家/地区,以及公司名称)是公开显示的,而且会随着您发布的任何内容一起显示,除非您选择隐藏您的公司名称。您可以随时更新您的 IBM 帐户。

所有提交的信息确保安全。

选择您的昵称



当您初次登录到 developerWorks 时,将会为您创建一份概要信息,您需要指定一个昵称。您的昵称将和您在 developerWorks 发布的内容显示在一起。

昵称长度在 3 至 31 个字符之间。 您的昵称在 developerWorks 社区中必须是唯一的,并且出于隐私保护的原因,不能是您的电子邮件地址。

标有星(*)号的字段是必填字段。

(昵称长度在 3 至 31 个字符之间)

单击提交则表示您同意developerWorks 的条款和条件。 查看条款和条件.

 


所有提交的信息确保安全。


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=10
Zone=Java technology
ArticleID=852861
ArticleTitle=一种寄生型设计模式在 Swing 应用开发中的实践
publish-date=12202012