目录

目录

1. 引言

2. 几种不好的GUI编程架构的表现形式

2.1 三种类都放到一个篮子里

2.2 监听器类、界面类放到一个篮子里

2.3 模型类与界面组件存在耦合

2.4 设计的监听器类粒度太细

3. 改进的GUI编程架构

3.1 相同类型的组件共享同一个监听器

3.2 监听器类的构造方法仅需传入一个参数

3.3 通过多分支结构实现事件源的区分

3.4 引入ModelView泛型抽象类,转换为MVVC架构

4. 改进的GUI源码架构示例



1. 引言

Java GUI程序通过Java标准库或第三方扩展库提供的可视化组件(如awt,swing,jfx等)来实现和程序的用户进行交互,利用这些组件的事件监听器实现业务处理。从MVC(模型-视图-控制器)角度来看,一个典型的Java GUI程序,在组成上,主要包含下面三种类:

(1)界面类(体现视图功能):一个GUI程序通常包含若干继承自某个可视化基类(如JFrame或JDialog)的类,用户通过界面类对象,与应用程序进行交互,触发监听器进行事件处理,监听器利用模型类完成数据处理,把结果呈现给用户。

(2)监听器类(体现控制器功能):一个GUI程序可包含若干用于监听及处理特定界面类对象上的组件事件的类。这种类作用是:监听界面上由用户或系统触发的鼠标、键盘操作等某个组件(按钮、菜单等)上发生的事件,并调用内部重写的方法进行事件处理。在重写内部方法时,通常会调用业务模型类。

(3)模型类(体现业务处理功能):模型类与具体的业务逻辑密切相关,监听器类在内部对某个方法进行重写时,方法内部是实际调用模型类的地方。

    给定一个Java GUI应用程序开发项目,如何在源码上合理地组织上面三种类的架构,关系到项目的可维护性。好的架构应该确保代码能够最大限度地得到重用,将三种类的耦合程度尽可能降低。本文将对这一问题进行较为深入地讨论,提出一种有效的组织的GUI程序的编程架构。

2. 几种不好的GUI编程架构的表现形式

笔者在多年的Java程序设计教学中发现,学生能够从MVC视角理解GUI程序的架构,但是,在项目实践中,对于代码上如何实施这种编程架构,往往五花八门,没有章法。其源码组织架构常常具有如下糟糕的表现形式:

2.1 三种类都放到一个篮子里

这种方式把三种类别的类都耦合在一起,即:所有的类的定义都放到一个源文件中,正所谓所有鸡蛋都放到一个篮子里,当要使用某个鸡蛋时,势必影响到其他鸡蛋。程序的可维护性之差,难以想象。更有甚者,一遇到监听器,就把它写成一个匿名对象,代码类似如下:

btnExit.addActionListener(new ActionListener(){
    //业务逻辑与界面更新逻辑
    ....
    
});

如此一来,当程序中需要写的监听器很多时,特别是业务逻辑和界面更新逻辑比较复杂时,这个篮子就会乱七八糟的东西一堆,修改某个事件处理的代码时,仅仅定位到需要修改的地方将是件耗时且容易出错的差事。当然,有些人想到了一个改进方案,如把上面省略号的代码写到界面类的一个方法中,只需要调用这个方法即可。但这仍然没有本质的改变,篮子里只是隔出了一些小空间,但鸡蛋还是在一个篮子里。 

注意哦,大名鼎鼎的WindowBuilder插件,生成的事件处理代码就是这种表现形式。如果你希望更好地组织代码,就不要采用WindowsBuilder插件中提供的添加事件处理代码的功能。

2.2 监听器类、界面类放到一个篮子里

这种方式虽然把模型类放到单独的源文件中定义,在一定程度上降低了业务与界面逻辑的耦合,但模型与界面联系还是比较紧密,模型的更改,很可能会导致界面代码的更改。此外,监听器类中的重写方法,不但涉及到业务模型的调用,还涉及到界面组件的更新,有时,界面更新还比较复杂,这就导致界面类的代码过多,程序在可维护性上仍然欠佳。

2.3 模型类与界面组件存在耦合

    例如,在开发一个三角形面积求解的GUI程序时,很多人把三角形类(Triangle)定义为:

public class Triangle{
    JTextField txtA;
    JTextField txtB;
    JTextField txtC;
    public Triangle(JTextField txtA,JTextField txtB, JTextField txtC){
        ...
    }
    public String getArea(){
        ...
    }
}

       这种模型类的设计,不是一个好的设计,好的设计应该把模型完全从应用场景中抽象出来,它只关心所处理的数据和业务逻辑本身,不应用关心这些数据是通过什么界面组件体现处理。这样设计出来的类才具有最大的可重用性(即便界面输入三角形的组件使用JTextArea,也不会影响到这个Triangle类)因此,上面的这个类应该修改为: 

public class Triangle{
    double a,b,c;
    
    public Triangle(double a,double b,double c){
        ...
    }
    public double getArea(){
        ...
    }
}

2.4 设计的监听器类粒度太细

     一些开发人员能够将上面三种类在源码上分开处理,即界面类、监听器类、模型类在源文件级别分别定义,但在实现监听器类时,过于强调降低耦合(一些教材中推荐这样),把监听器分得太细、太多,对每一个需要进行事件处理的组件都设计了单独的类。

     例如,要设计一个能求解三角形面积的GUI程序,假设主窗口上有4个文本框(txtA,txtB,txtC,txtResult),1个按钮(btnCompute),希望在单击btnCompute时,能计算三角形面积并显示在txtResult文本框中,当在txtC文件框中输入完成后,回车后也能完成三角形的面积计算与显示。这里有需要对两个组件进行监视,他们会设计类似如下两个监听器类:    

public class MyKeyListener implements KeyListener{
     public MyKeyListener(JTextField txtA,JTextField txtB, JTextField txtC,JTextField txtResult){
         this.txtA = txtA;
         this.txtB = txtB;
         this.txtC = txtC;
         this.txtResult = txtResult;
     }
     //实现KeyListener中的各个抽象方法
     ...
     ...

  }

      

public class MyButtonListener implements ActionListener{
     public MyKeyListener(JTextField txtA,JTextField txtB, JTextField txtC,JTextField txtResult){
         this.txtA = txtA;
         this.txtB = txtB;
         this.txtC = txtC;
         this.txtResult = txtResult;
     }
     //实现ActionListener中的各个抽象方法
     ...
     ...

  }

仔细观察这两个监听器类的设计,你会发现存在如下问题:

(1)当事件处理时涉及到的组件太多是,构造方法传入的参数过多,调用时将非常繁琐,这里需要传入4个参数,编写代码时容易出错。此外,如果模型有所改变,会导致构造方法参数的变化,这令人非常烦恼。

(2) 监听器类的粒度过小,每一个组件的事件响应都要对应一个监听器类,如果要触发事件的组件非常多,管理这么多的监听器类,本身就是一个麻烦。

3. 改进的GUI编程架构

    以上讨论的几种Java GUI编程架构表现形式,只有第4种(2.4节)能很好地体现MVC的设计原则,但是这种方式又存在上述不足。

    如何既能体现MVC设计原则,又能克服监听器粒度过细带来的不足?这就是本文要提出的改进GUI编程架构,该架构设计原则主要有如下几点。

3.1 相同类型的组件共享同一个监听器

   例如,主窗口上的所有JButton组件使用同一个监听器ButtonListener,所有JTextField组件使用同一个监听器TextFieldListener,等等。以下是一段界面类构造方法中的代码,它通常放到所有界面上的组件创建之后,这条代码的作用在于给所有的按钮添加ActionEvent事件监听器:

new ButtonListener(this);

上述代码为按钮添加了Action事件监听器对象,即:this指代的是当前界面对象,通常就是主窗体(JFrame或JDialog的派生类对象)。

   为什么要这样组织?原因在于同一种类型的组件,能够处理的事件类型都是相同的,并且在实现各种监听器接口、重新接口中的方法时,对于组件属性的访问,具有相似性,有利于程序代码中提炼出相同的操作方法,提高程序代码的可重用性。

  

3.2 监听器类的构造方法仅需传入一个参数

 所有监听器类的构造方法只需包含一个参数,这个参数的数据类型为监听器所要监听的组件所在的界面类类型,假设界面类类型为MainWindow,则上述的ButtonListener类的构造方法如下:

public class ButtonListener implements ActionListener {

	private MainWindow w;

	public ButtonListener(MainWindow w) {
		this.w = w;
        //给w上的各个按钮添加监听器
        w.btnA.addActionListener(this);
        w.btnB.addActionListener(this);
        ...
	}

	...
}

  这样,可以在同一个包中的监听器类中直接访问w的各个非private成员,从而实现对界面类中的组件的访问。注意:应该把界面类中的一些需要动态访问的组件对应的成员变量设为protected或不加任何权限修饰字,且成员变量命名遵循约定,即:

       相同类型的组件名具有相同的前缀,例如,JButton组件的前缀都为Btn,JTextField组件的前缀都为Txt等等。

       这样做的好处在于:可以充分借助IDE的自动代码完成功能,非常方便地给输入上面的类似于
w.btnX.add...这样的代码。甚至可以通过一段程序给窗体上所有的按钮添加这个事件监听器。这种代码完全可以通过编写一个自动代码生成器完成。

3.3 通过多分支结构实现事件源的区分

由于同类型的组件共享的是同一个监听器对象,因此,在监听器类中的重写方法里面,有必要区分到底是哪一个组件上发生了这种事件,即事件源。

一种惯例就是,通过if...else if结构进行区分。以下代码实现了对窗口MainWindow的所有JButton组件(btnA、btnB)的ActionEvent事件的处理。

public class ButtonListener implements ActionListener{
    MainWindow w;
    public ButtonListener(MainWindow w){
        this.w = w;
    }
    @Override
    public void actionPerformed(ActionEvent e){
        Object obj = e.getSource();
        if(obj == w.btnA)
             btnAHandler(e);
        else if(obj == w.btnB)
             btnBHandler(e);
        
    }
    void btnAHandler(ActionEvent e){
         ...
    }
    void btnBHandler(ActionEvent e){
        ...
    }

}

3.4 引入ModelView泛型抽象类,转换为MVVC架构

通过以上分支结构,可以非常方便地实现对窗体上按钮组件事件的处理。程序的维护非常简便。

如此一来,界面类的作用仅在于界面布局和界面显示。监听器类的作用如下:

  (1)把界面对象上各个组件的状态数据转换为业务模型所需要的格式;

  (2)调用业务模型类实现对数据的处理;

  (3)处理后的结果转换为适合在界面组件上显示的格式显示出来。

    可见,监听器充当了业务模型和界面视图之间的桥梁。

第1个作用,可以通过在监听器类中定义如下所示的一序列的重载方法来实现: 

private updateModel(ModelA m){

      ModelView mv = new ModelViewA(m,this.w);

      mv.updateModel()

}

private updateModel(ModelB m){

     ModelView mv = new ModelViewB(m,this.w);

     mv.updateView();

}

第3个作用可以通过在监听器类中定义如下所示的一序列的重载方法来实现 :

private updateView(ModelA m){

     ModelView mv = new ModelViewA(m,this.w);
     mv.updateView();

}

private updateView(MainWindow w, ModelB m){
    ModelView mv = new ModelViewA(m,this.w);
    mv.updateView();

}

...

上面两个代码块中的ModelView是另外定义的一个泛型抽象类,其定义如下:

public abstract class ModelView <M, V> {
   private M m;
   private V v;
   public ModelView(M m, V v){
      this.m = m;
      this.v = v;
   }
   public abstract void updateModel();
   public abstract void updateView();
}

ModelViewA、ModelViewB都是这个抽象类的实现类。由此,程序的主要工作是根据系统所需要的各种模型,编写实现ModelView这个抽象类各个类,这些类的构造方法都具有如下的形式:

public ModelViewA extends ModelView<ModelA,MainWindow>{
    public ModelViewA(ModelA m, MainWindow v){
        super(m,v);
    }
    @Override
    public void updateModel(){
     ...
    }
    @Override
    public void updateView(){
      ...
    }
}

通过这种方式,界面和业务逻辑完全被监听器控制器分开了,同时,利用抽象类,尽可能地降低了控制器和业务逻辑、界面的耦合,真正的耦合放到ModelView的实现类中,要改,也只改实现类。

这种方式下,控制器的代码的编写非常方便,因为完全可以充分利用IDE的自动代码完成功能。上述代码很大一部分都可以通过编写一个工具软件来自动生成。

4. 改进的GUI源码架构示例-略

    欢迎大家,探讨给出。