瘦客户机框架(Thin Client Framework,TCF)是一种用于 java 客户机应用程序的轻量级、灵活和功能强大的编程框架。在这一由两部分组成的系列文章中,您将从它的两位作者那里了解 TCF。请跟随 Barry Feigenbaum 博士和 PeterBahrs 博士,他们使用详细的讨论、工作示例以及生动的代码样本向您介绍了 TCF 体系结构、设计和实现。
瘦客户机框架(TCF)是一种设计、开发和部署方法,它使用 java 语言容易和快速地开发高性能的、可扩展的以及响应迅速的电子商务客户机。TCF 构建在模型-视图-控制器(Model-View-Controller)设计模式之上,并将该模式提升到了应用程序体系结构级别。通过基于事件的通信模型的统一实现,TCF 向客户机端应用程序开发提供了高度可插入的、基于组件的结构。
TCF 定义了编写 java 客户机应用程序的最佳实践,这些最佳实践强调编码问题与职责分离。其高度模块化提供了最大限度的重用。因为 TCF 与多种数据、服务器和网络模型相兼容,所以它极其灵活,可以使 java 客户机按需充当瘦客户机或胖客户机。最后,TCF 支持基于公式的方法来评估开发费用,并且在支持客户机和服务器的并发开发时,它还支持客户机上的并行组件开发。
在这个由两部分组成的系列文章中,我们将向初学者介绍 TCF。本系列文章的第 1 部分将使您熟悉该体系结构的基本元素。我们将解释 TCF 背后的概念,并描述该体系结构的组件。我们还将提供一个工作示例应用程序,所以您自己可以看到如何对这些组件编码。在第 2 部分中,我们将提供对 TCF 的设计和实现的更详细讨论。
![]() |
|
开发分布式应用程序客户机涉及许多相关的设计考虑事项。图 1 演示了这些考虑事项中的一些。一个成功的瘦客户机开发框架必须以简单明了的方式满足这些考虑事项。
图 1. 客户机端设计考虑事项

在端对端系统开发的范围内,中间层和后端服务器通常是比较困难和耗时的部分。它们(不是客户机)要耗费大部分的设计工作。图 2 是端对端系统设计的一个示例:
图 2. 复杂的端对端系统

通过为客户机端开发提供已测试的模式,TCF 确保解决了客户机最重要的几个方面,同时极大地减少了在客户机端设计考虑事项上所花费的时间。
一个重要的设计考虑事项是客户机应该是“瘦”还是“胖”。基于客户机拥有功能(或业务逻辑)的多少,可以将它定义为胖客户机或瘦客户机。可以从包括纯瘦型到纯胖型在内的广大范围上实现应用程序客户机,混合型也并不罕见。
TCF 应用程序的特点是将应用程序业务逻辑在客户机和服务器之间进行分离。一般情况下,在 TCF 应用程序中,消息的大小很小且很有效,不需要高速链接就可以产生很好的互操作性。TCF 应用程序另外为人称道的一点是客户机和服务器之间是松散耦合的,这样可以独立开发客户机端和服务器端。TCF 设计的分隔功能对于高度分布式应用程序最实用。图 3 显示了典型的混合型 TCF 配置:
图 3. 混合型瘦客户机配置

![]() ![]() |
![]()
|
在决定了客户机上需要一些业务逻辑之后,就必须决定如何实现它。两种最常见的方法是:
- 使用某种由浏览器激活的脚本语言形式,例如 JavaScript/动态 HTML(Dynamic HTML,DHTML)
- 用诸如 java 语言之类的健壮的编程语言将客户机编写成 applet 或应用程序
在本文中,我们将进行基于 java 的客户机实现。在以下情况中,Java 语言是首选的(或要求的)实现:
- 应用程序必须考虑到未连接到网络的用户的需要
- 应用程序需要比 DHTML 所能提供的更丰富或更高响应能力的 GUI
- 需要在客户机端进行数据高速缓存,或者客户机必须能够操作大量数据
- 在客户机端编写脚本无法满足需要
- 服务器的会话状态如此巨大,以致必须将它分布到客户机端
- 应用程序需求声明必须用 java 语言编写应用程序
清单 1 显示了典型的 java 应用程序编码风格。看一下您是否能看出代码何处有错。
清单 1. 典型的 Java 应用程序代码
import java.util.*; import com.xyz.*; Customer customer = createCustomer(); : Socket socket = createSocket(); customer = (Customer) socket.readObject(); String name = customer.getName(); TextField tf = new TextField(); Frame frame = new Frame(); tf.setText(name); frame.add(tf); LogFile log = createLogFile(); log.writeStatus("updated"); |
此时我们拥有的是一个难以管理的胖客户机的开始部分。
- 为什么?因为整个应用程序混合了多种代码职责。
- 如何进行修正?将代码按照职责进行分离。
![]() ![]() |
![]()
|
模型-视图-控制器(MVC)模式是一种经典的已被充分理解的设计模式。正如图 4 所示,MVC 模式在整个应用程序中可以在多个级别上实现。在其最细致级别,MVC 模式确定 GUI 如何控制工作。在中间级别,它结构化应用程序组件。在最广泛级别,MVC 模式将应用程序划分成多个层,来表示流行的三层应用程序模型。TCF 就是中间级别的 MVC 使用示例。
图 4. TCF 与 MVC 模式的关系

此时,您应该基本了解了瘦客户机框架在分布式应用程序开发过程中的作用;典型的 TCF 客户机看上去如何;如何 避免用 java 代码编码分布式应用程序客户机以及 MVC 模式在 TCF 中所担当的底层角色。从这里开始,我们准备研究 TCF 体系结构。
![]() ![]() |
![]()
|
图 5 显示了完整的 TCF 体系结构。正如您看到的,它相当小,且易于理解。椭圆型框中的名称代表主要的 TCF 接口或类,而矩形或菱形框中的名称代表 TCF 支持类。组件间的通信是通过事件进行处理的,如图 5 所示。基于事件的通信是 TCF 体系结构模式的基本特征。
图 5. TCF 体系结构

主要 TCF 组件的角色是:
- ViewController 接口定义了可重用的用户界面组件,该组件是整个客户机应用程序 GUI 的一部分。
ViewController
通常是由诸如Panel
那样的 AWT(或 Swing)组件实现的。 - PlacementListener 接口管理
ViewController
在屏幕上的布局。PlacementListener
通常是由应用程序实现的。 - ApplicationMediator接口定义了应用程序的控制逻辑。
- Transporter 接口选择处理
RequestEvent
的适当目标位置。 - Destination是 TCF 客户机需要的任何服务的抽象。
- TopListener 接口用于将特定于业务或环境的职责与其它 TCF 组件部分分隔开。
TopListener
通常是由应用程序实现的。 - ViewEvent 是 GUI 事件的抽象。
ViewEvent
是ViewController
和ViewListener
(通常是ApplicationMediator
)之间的通信机制。 - RequestEvent 是轻量级事务请求。
RequestEvent
是ApplicationMediator
和RequestListener
(通常是Transporter
)之间的通信机制。 - PlacementEvent 是
ApplicationMediator
和PlacementListener
(通常是应用程序)之间的通信机制。 - TopEvent 是
ApplicationMediator
和TopListener
(通常是应用程序)之间的通信机制。
在图 6 中,您可以看到主要的 TCF 接口。回忆一下 TCF 将开发过程划分成多个编码问题和职责。每个 TCF 接口代表不同方面的职责。开发人员可以根据他们在开发团队中的角色精通一个或多个接口。
图 6. TCF 的主要接口/类

TCF 提供了每个接口的缺省或抽象实现。这样,就提供了您可以构建在其上的部分实现。许多要求使用 TCF 的常见任务都构建到了缺省接口实现中。由源接口实现所触发的事件处理接口间的所有交互。
因为 TCF 体系结构基于组件并由事件驱动,所以对 TCF 系统中的每个组件独立进行开发和单元测试都很容易。一旦定义了事件及其 major、 minor和 command值,就可以通过简单的支架(scaffolding)代码来测试组件。这样,发送方和接收方之间的事件定义就是组件间唯一强大的纽带。
因为 TCF 系统中的组件都是松散耦合的,所以在运行时可以插入和删除它们。另外,这样还提供对流跟踪和数据过滤/转换的方便支持。
因为 TCF 使用松散耦合、高度可伸缩的开发范例,所以它使开发规模估计非常简单。如图 7 所示,标准化之后,简单的基于倍增的规模估计将满足大多数 TCF 项目。
图 7. 示例组件估算

上面的估算基于低级设计和代码估算,并假设您正和有 java 经验的程序员一起工作。所显示的估算是示例;您的值可能会不同。
![]() ![]() |
![]()
|
在本文其余部分,我们将使用一个示例应用程序来演示 TCF 背后的概念。Example04 是一个非常简单的客户信息系统,它维护一个客户列表。应用程序是一个面板。可以将它封装到框架或 applet 中。下面的几张图中显示了 applet 表单。为本文起见,我们将只显示应用程序的“选择”部分。您可以在 alphaWorks 站点查看完整示例以及多个其它示例(请参阅 参考资料)。
请再次记住本文主要是一篇概述;我们将在本系列的第二部分进行更详细的描述。
Example04 由以下组件组成:
- 四个
ViewController
:VC1
、VC2
、VC3
和StatusVC
- 一个
ApplicationMediator
:AM4
- 一个
Transporter
和一个Destination
- 一个基于 键值对的数据模型
- 一个主应用程序(Example04)
- 一个
Codes
接口,定义符号常量
在接下来的各节中,我们将讨论除 Codes
接口之外的每个组件。
![]() ![]() |
![]()
|
简单的键值对(即 Map
)结构用作应用程序数据模型。在应用程序中传递它,并根据用户的输入进行适当更新。清单 2 显示了两个简便的方法,这两个方法使通常从登录屏幕和客户信息屏幕上获得的信息复位:
清单 2. 数据模型类定义
public class Data extends Hashtable { public static final String FAMILY = "Example04"; public void initCustomer() { put(Codes.TITLE, Codes.TITLE); put(Codes.FIRSTNAME, Codes.FIRSTNAME); put(Codes.LASTNAME, Codes.LASTNAME); put(Codes.FULLNAME, Codes.FIRSTNAME + " " + Codes.LASTNAME); put(Codes.WWW, Codes.WWW); put(Codes.OFFICE, Codes.OFFICE); put(Codes.PHONENUMBER, Codes.PHONENUMBER); put(Codes.EMAIL, Codes.EMAIL); } public void initLogin() { put(Codes.USERID, Codes.USERID); put(Codes.PASSWORD, Codes.PASSWORD); } public Data() { initLogin(); initCustomer(); } } |
![]() ![]() |
![]()
|
Example04 包含四个视图控制器:
VC1
是登录屏幕VC2
是客户选择屏幕VC3
是客户信息屏幕StatusVC
显示前三个ViewController
中哪个在当前是活动的
示例应用程序总是同时显示两个 ViewController
。 StatusVC
总是显示在屏幕的顶部,而其余三个中的一个显示在屏幕的中央。图 8 让您查看登录屏幕:
图 8. VC1:登录屏幕

![]() ![]() |
![]()
|
常规的 java 事件处理用于处理 GUI 事件。每个 ViewController
将一个或多个 AWT 事件(例如,按钮按下)转换成 ViewEvent
。事件数据参数作为数据对象被传递。
在以下代码样本中,我们将查看每个 ViewController
的 ViewEvent
。每一节代码都封装在 xxxxButton_actionPerformed
方法中,其中的 xxxx
是按钮名称。为保持简洁(和简单),没有显示封装器。
StatusVC
显示了当前 ViewController
的 java 类名称。所有屏幕上都显示 StatusVC
,但只有在显示 VC2
时它的按钮才是活动的。
Save 触发 SAVE ViewEvent
:
// save customer records to a file fireViewEvent( new ViewEvent(this, ViewEvent.SAVE)); |
Load 触发 LOAD ViewEvent
:
// load customer records fireViewEvent( new ViewEvent(this, ViewEvent.LOAD)); |
VC1
输入登录值,通常是用户标识和密码。下面显示它的 ViewEvent
。
登录将输入域添加到数据模型中并触发 LOGIN ViewEvent
:
// grab textfields, update data model Data d = this.data; d.put(Codes.USERID, getNameField().getText()); d.put(Codes.PASSWORD, getPasswordField().getText()); setEnabled(false); fireViewEvent( new ViewEvent(this, ViewEvent.LOGIN, d)); |
注:我们禁用了 ViewController
。这样可以防止在完全处理完 ViewEvent
之前接受新按钮的按下。一旦处理完 ViewEvent
,它将被 refresh()
方法重新启用。
Done 触发 CANCEL ViewEvent
:
setEnabled(false); fireViewEvent( new ViewEvent(this, ViewEvent.CANCEL)); |
当完成登录时,显示 VC2
,如图 9 所示:
图 9. 客户选择屏幕

正如您可看到的, VC2
显示了现有用户列表,以及管理和更新该列表的几个按钮。由于本屏幕上的 ViewEvent
处理非常类似于 VC1,所以我们只显示 Edit
事件。
Edit创建或更新用户信息:
Data d = this.data; d.put( Codes.FULLNAME, getAccountList().getSelectedValue()); fireViewEvent( new ViewEvent(this, ViewEvent.DETAILS, d)); |
被选中的用户名被放置在 FULLNAME
数据模型输入项中。
在单击 Edit或 New 按钮时,显示 VC3
,如图 10 所示:
图 10. 客户信息屏幕

VC3
用于查看或编辑客户输入项。下面显示它的视图事件。
OK 获取 GUI 上的数据、更新模型并触发 UPDATE ViewEvent
:
Data d = this.data(); d.initCustomer(); String fn, ln; d.put(Codes.FIRSTNAME, fn = getFirstNameField().getText()); d.put(Codes.LASTNAME, ln = getLastNameField().getText()); d.put(Codes.FULLNAME, fn + " " + ln); d.put(Codes.TITLE, getTitleField().getText()); d.put(Codes.PHONENUMBER, getPhoneField().getText()); d.put(Codes.WWW, getWwwField().getText()); d.put(Codes.EMAIL, geteMailField().getText()); d.put(Codes.OFFICE, getOfficeField().getText()); fireViewEvent( new ViewEvent(this, ViewEvent.UPDATE, d) ); |
Cancel 触发 DONE ViewEvent
:
fireViewEvent( new ViewEvent(this, ViewEvent.DONE) ); |
![]() ![]() |
![]()
|
ApplicationMediator
处理由各种 ViewController
生成的 ViewEvent
。它对 ViewController
排顺序并生成 RequestEvent
以执行由各种 ViewController
请求的操作。我们将在下面查看 ApplicationMediator
如何处理两个典型事件请求。
下面显示了初始化 ApplicationMediator
。这个初始化序列创建了四个 ViewController
并发出一个布局请求,使 VC1
成为活动的 ViewController
。清单 3 显示了 ApplicationMediator
如何处理这些请求:
清单 3. ApplicationMediator 请求处理
public void init() { super.init(); String[] classes = { "com.ibm.jtc.examples.ex04.VC1", "com.ibm.jtc.examples.ex04.VC2", "com.ibm.jtc.examples.ex04.VC3", "com.ibm.jtc.examples.ex04.StatuVC"}; try { initViewControllers(classes); } catch (Exception e) { return; } firePlacementEvent( new PlacementEvent(this, getVC(0), PlacementEvent.ADD, 0) ); } |
ApplicationMediator
可以用几种不同方法处理请求。下面显示了两种比较流行的解决方案:按事件源进行处理或按事件代码进行处理(注:这两个示例都使用同步事件分派)。
选项 1:按事件源处理 ViewEvent
在清单 4 中,方法 getVC()
返回如上面 init
方法的 classes 变量中列出的 ViewController
。
清单 4. 按事件源进行处理
public void processViewEvent(ViewEvent ve) { RequestEvent re = new RequestEvent(this, MY_FAMILY); ViewController vc = (ViewController)ve.getSource(); if (vc == getVC(0)) doVC1(re, ve); else if (vc == getVC(1)) doVC2(re, ve); else if (vc == getVC(2)) doVC3(re, ve); else if (vc == getVC(3)) doStatusVC(re, ve); } |
选项 2. 按代码值处理 ViewEvent
如清单 5 所示,代码值是 major和 minor。清单 5 中显示的详细行为通常包含在上面的 doxxx
方法中。
清单 5. 按代码值进行处理
public void processViewEvent(ViewEvent ve) { RequestEvent re = new RequestEvent(this, MY_FAMILY); int major = ve.getMajor(), minor = ve.getMinor(); try { switch ( major ) { // process major code case ViewEvent.LOGIN : re.setCommand(Codes.LOGIN); // do LOGIN re.setData(ve.getData()); fireRequestEvent(re); re.setCommand(Codes.GETNAMES); // do GETNAMES re.setData(ve.getData()); fireRequestEvent(re); firePlacementEvent( // remove VC1 new PlacementEvent(this, getVC(0), PlacementEvent.REMOVE) ); firePlacementEvent( // add VC2 new PlacementEvent(this, getVC(1), PlacementEvent.ADD) ); getVC(3).refresh(getVC(1).toString()); // update status getVC(1).refresh(re.getData()); // update VC2 fields getVC(3).setEnabled(true); // enable user actions break; case ViewEvent.DETAILS : re.setCommand(Codes.GETDETAILS); // do DETAILS re.setData(ve.getData()); fireRequestEvent(re); firePlacementEvent( // remove VC2 new PlacementEvent(this, getVC(1), PlacementEvent.REMOVE) ); firePlacementEvent( // add VC3 new PlacementEvent(this, getVC(2), PlacementEvent.ADD) ); getVC(3).refresh(getVC(2).toString()); // update status getVC(2).refresh(re.getData()); break; case ViewEvent.CANCEL : fireTopEvent( // end execution new TopEvent(this, TopEvent.EXIT) ); break; case ViewEvent.REFRESH : re.setCommand(Codes.GETNAMES); // do GETNAMES fireRequestEvent(re); getVC(1).refresh(re.getData()); // update VC2 fields break; case ViewEvent.DELETE : re.setCommand(Codes.DELETE); // do DELETE re.setData(ve.getData()); fireRequestEvent(re); re.setCommand(Codes.GETNAMES); // do GETNAMES re.setData(ve.getData()); fireRequestEvent(re); getVC(1).refresh(re.getData()); // update VC2 fields break; case ViewEvent.UPDATE : re.setCommand(Codes.UPDATE); // do UPDATE re.setData(ve.getData()); fireRequestEvent(re); refresh(re.getData()); // update all VC fields firePlacementEvent( // remove VC3 new PlacementEvent(this, getVC(2), PlacementEvent.REMOVE) ); firePlacementEvent( // add VC2 new PlacementEvent(this, getVC(1), PlacementEvent.ADD) ); getVC(3).refresh(getVC(1).toString()); // update status break; case ViewEvent.DONE : firePlacementEvent( // remove VC3 new PlacementEvent(this, getVC(2), PlacementEvent.REMOVE) ); firePlacementEvent( // add VC2 new PlacementEvent(this, getVC(1), PlacementEvent.ADD) ); getVC(3).refresh(getVC(1).toString()); // update status break; } } catch(Exception e) { e.printStackTrace(); return; } } |
清单 5 演示了如何对典型的 ApplicationMediator
编码:首先它对 major和 minor值解码,随后它处理请求。请求处理通常由以下几部分组成;创建和触发一个或多个 RequestEvent
以处理输入操作、用事件结果刷新组件以及更改至新的屏幕。要在这几个屏幕之间进行切换, ApplicationMediator
指示 PlacementListener
除去当前的屏幕并添加新的屏幕。
通过 Transporter
进行间接作用, Destination
实现了从 ApplicationMediator
触发的 RequestEvent
。注: RequestEvent
从 ApplicationMediator
开始,通过 Transporter
,随后到达 Destination
。 Transporter
充当路由器来选择一个或多个 Destination
以处理 RequestEvent
。
Example04 提供的简单 Destination
的多个实现是可能的。为简单起见,本示例使用 Destination
,它将命令请求值解码到对封装数据提供访问的例程。清单 6 显示了解码逻辑:
清单 6. 示例 Destination 的解码逻辑
String cmd = request.getCommand(); if (cmd.equals(Codes.GETNAMES)) doGetNames(request); else if (cmd.equals(Codes.GETDETAILS)) doGetDetails(request); else if (cmd.equals(Codes.UPDATE)) doUpdate(request); else if (cmd.equals(Codes.NEWRECORD)) doNewRecord(request); else if (cmd.equals(Codes.DELETE)) doDelete(request); else if (cmd.equals(Codes.LOGIN)) doLogin(request); else if (cmd.equals(Codes.NEWLOGIN)) doNewLogin(request); else if (cmd.equals(Codes.SAVE)) doSaveData(); else if (cmd.equals(Codes.LOAD)) doLoadData(); |
![]() ![]() |
![]()
|
我们以查看示例应用程序作为结束。从下面的简单代码样本中,您应该能够看到我们所描述的 TCF 体系结构的各种组件如何在一个分布式应用程序中协同工作。
清单 7 显示了 Example04
应用程序类:
清单 7. 公用类 Example04
public class Example04 extends JPanel implements JTC, PlacementListener, TopListener { LocalDestination dest = null; AM1 am = null; Transporter trans = null; Data data = null; List jtcs = new Vector(); // all JTC implementers String filename = null"; JPanel cp = null; public Example04 app = null; // convenient ref to me : } |
清单 8 显示了应用程序的 main()
方法:
清单 8. Example04 的 main() 方法
public static void main(java.lang.String[] args) { new Example04("Example04", args.length > 0 ? args[0] : "example04.db"); } |
清单 9 显示了应用程序的 constructor
方法:
清单 9. Example04 的 constructor 方法
public Example04(String title, String filename) { // setup overall GUI this.filename = filename; JFrame frame = new JFrame(title); frame.addWindowListener(this); // not shown cp = (JPanel)frame.getContentPane(); cp.setLayout(new BorderLayout()); cp.add(this, BorderLayout.CENTER); frame.pack(); frame.setSize(525, 450); frame.setVisible(true); app = this; // remember for JTC lists // setup Transporter and Destination mapping trans = new DefaultTransporter(); jtcs.add(trans); dest = new LocalDestination(); jtcs.add(dest); trans.addRequestListener(Data.FAMILY, dest); // setup ApplicationMediator am = new AM4(); jtcs.add(am); am.addTopListener(this); am.addPlacementListener(this); am.addRequestListener(trans); //indirectly cause a PlacementEvent am.init(); am.refresh(data = new Data()); // reset the data model } |
而处理 PlacementEvent
的应用程序 PlacementListener
,它显示在清单 10 中:
清单 10. 对 Example04 的布局支持
// the cp variable is the JFrame's ContentPane public void placementEventPerformed(PlacementEvent pe) { ViewController vc = (ViewController) pe.getViewController(); final Component c = pe.getComponent(); if (vc instanceof StatusVC) { cp.add(c, BorderLayout.NORTH); } else { switch (pe.getMajor()) { case PlacementEvent.ADD : cp.add(c, BorderLayout.CENTER); validate(); repaint(); break; case PlacementEvent.REMOVE : cp.remove(c); break; } } } |
尽管 Example04 极其简单,但它还是说明了基于 TCF 的应用程序的每个主要组件和它们之间的相互作用。可能存在复杂得多的应用程序。例如,可以使用多个 ApplicationMediator
和目标位置。 TopListener
和 TopDestination
可以提供对系统服务的访问。可以部署单一的 PlacementListener
以控制应用程序的屏幕位置。通过组合几个简单的 TCF 应用程序,我们可以轻松创建更复杂的应用程序,如图 11 所示:
图 11. 更复杂的应用程序
