一种有效组织Java GUI 源码的编程架构
目录
目录
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;}@Overridepublic 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);}@Overridepublic void updateModel(){...}@Overridepublic void updateView(){...}
}
通过这种方式,界面和业务逻辑完全被监听器控制器分开了,同时,利用抽象类,尽可能地降低了控制器和业务逻辑、界面的耦合,真正的耦合放到ModelView的实现类中,要改,也只改实现类。
这种方式下,控制器的代码的编写非常方便,因为完全可以充分利用IDE的自动代码完成功能。上述代码很大一部分都可以通过编写一个工具软件来自动生成。
4. 改进的GUI源码架构示例-略
欢迎大家,探讨给出。
一种有效组织Java GUI 源码的编程架构相关推荐
- 确定有限状态机和非确定有限状态机详解 包含Java实现源码(Nondeterministic finite automata)
本文将讲解确定有限自动状态机和非确定有限自动状态机的特点和区别.将结合图片例子重点讲解什么是非确定有限自动状态机.最后讲解如何将非确定状态机转换为确定的状态机.多图预警!! 有限自动状态机可以分为确定 ...
- java B2B2C 源码多租户电子商城系统-Spring Cloud组件详解
我们从整体上来看一下Spring Cloud各个组件如何来配套使用: 需要JAVA Spring Cloud大型企业分布式微服务云构建的B2B2C电子商务平台源码 一零三八七七四六二六 从上图可以看出 ...
- Java集合源码分析(二)ArrayList
ArrayList简介 ArrayList是基于数组实现的,是一个动态数组,其容量能自动增长,类似于C语言中的动态申请内存,动态增长内存. ArrayList不是线程安全的,只能用在单线程环境下,多线 ...
- java B2B2C 源码多租户电子商城系统-Spring Cloud组件详解...
我们从整体上来看一下Spring Cloud各个组件如何来配套使用: 从上图可以看出Spring Cloud各个组件相互配合,合作支持了一套完整的微服务架构. 其中Eureka负责服务的注册与发现, ...
- java B2B2C源码电子商务平台 - Zuul回退机制
1.在一些不稳定因素导致路由后面的微服务宕机或者无响应时,zuul 就会累计大量的请求,久而久之基本上所有的请求都会超时,但是请求链接数却不断的在增加,不断的占用资源池不能结束知道超时消耗殆尽导致zu ...
- aqs java 简书,Java AQS源码解读
1.先聊点别的 说实话,关于AQS的设计理念.实现.使用,我有打算写过一篇技术文章,但是在写完初稿后,发现掌握的还是模模糊糊的,模棱两可. 痛定思痛,脚踏实地重新再来一遍.这次以 Java 8源码为基 ...
- 面试官系统精讲Java源码及大厂真题 - 24 举一反三:队列在 Java 其它源码中的应用
24 举一反三:队列在 Java 其它源码中的应用 世上无难事,只要肯登攀. 引导语 队列除了提供 API 供开发者使用外,自身也和 Java 中其他 API 紧密结合,比如线程池和锁,线程池直接使用 ...
- 线程的3种实现方式并深入源码简单分析实现原理
前言 本文介绍下线程的3种实现方式并深入源码简单的阐述下原理 三种实现方式 Thread Runnable Callable&Future 深入源码简单刨析 Thread Thread类实现了 ...
- java socket 握手_TCP建立连接三次握手过程详解(wireshark截图、java socket源码)
TCP(Transmission Control Protocol传输控制协议)是一种面向连接的.可靠的.基于字节流的传输层通信协议. 三次握手协议建立连接图 握手过程步骤如下(配wireshark分 ...
最新文章
- java 捕获异常并存入数据库_java异常处理,报异常的话怎么处理对象值,并持久化到数据库中...
- python print 的使用方法
- 小皮面板phpstudy的MYSQL服务无法启动_ERROR 2003 (HY000): Can‘t connect to MySQL server on ‘localhost‘ (10061)
- Linux 字符设备驱动开发基础(四)—— ioctl() 函数解析
- 利用Python进行数据分析(1) 简单介绍
- 域控制器的强制卸载,Active Directory系列之十四
- python实现链表的删除_B站上的免费Python课程
- bat打包成exe_拜托!看完这篇文章别再问我怎么Python打包成exe了!
- f-stack nginx 多进程模式启动 main_loop 流程分析
- MATLAB分集接收技术仿真,分集接收技术.doc
- JDBC+MySQL入门增删改查案例
- CSDN文章如何迁移至微信公众号
- 如何在 Linux 中更新 Flatpak 软件包
- idead文件折叠问题终极解决办法
- 数字IC设计的一些英语术语总结
- 诸葛java_java - 诸葛_子房的个人空间 - OSCHINA - 中文开源技术交流社区
- C - 一只小蜜蜂...
- 贝叶斯网专题12:参数学习之贝叶斯估计
- Rxjava:基础入门
- fcpx使用教程:final cut pro 导出视频的图文方法