使用适配器模式设计RMI方式的网络应用程序
为网络应用编写本地Java对象指南
简介:
使用Java的RMI方式编写基于网络的应用程序是非常简单的。然而,使用不是基于网络的类,而且还把它复杂化来为网络服务肯定不是一个好办法。这样做会使程序变得混乱、迟钝、难以阅读并且难以维护 。Dan Becker 论证了一种有效的将本地的Java类应用于网络的方法,这就是使用适配器设计模式( Adapter design pattern)。使用这种设计技术非常容易建立可维护性好,并且工作出色的应用程序。
如果你曾使用过java的远程调用(RMI),你肯定知道那是很容易理解而且容易使用的建立远程对象和服务的方法。事实上,SUN的java教程里提供了一个优秀的例子来帮助你建立你的第一个RMI程序。但是 我也看到了一些建立在这个例子上的设计,这些设计因功能的增加而变得非常笨拙。不太熟练的程序员经常只能借助于一些约定来区分本地行为和远程行为。并且,随着新的远程服务加入,远程的对象在糟糕的网络交通中很容易变成一个大麻烦。
在这篇文章里,我将会论证什么是适配器模式和适配齐莫是的使用技术。使用它区分本地行为和远程行为是非常简单的。并且你也能在非网络的应用程序中继续使用你的本地类。在远程服务增加的情况下, 使用这种设计模式很容易明白那些类必须处理请求,那些对象必须被同步。最后,使用这种技术,你的设计会变得更加容易理解,更加容易让更多的开发着修改和使用。
什么是适配器模式?
适配器模式是结构化的模式之一,它出现在“四人组(Erich Gamma, Richard Helm, Ralph Johnson, and John Vlissides)”编写的参考书《设计模式》中。适配器模式不仅仅适用于远程的C/S编 程,而且也适用于这种情况:你希望重用一个类,但是程序的接口不匹配这个类的借口。在这种情况下,你可以使用适配器或者封装器使应用程序的接口匹配已存在的类的借口。在软件开发中,一个适配器简单 的映射了其它的类的借口,它总是在开发过程中不断地被使用,最终变成了一种设计模式。
图一说明了适配器是如何工作的。在这个图中,client想在接口Target中调用方法request()。由于 Adaptee类没有request()方法,把请求转化成为有效的匹配的方法就变成了适配器的任务。在这里适配器 把对方法request的调用转化成了对方法specificRequest的调用。适配器为每一个需要的方法做这种转变,就像封装器一样。
图一
这种适配类型恰恰正是一个想建立WEB远程服务的人想要的。你对外公布一个服务不是通过加入一个本地类,而是通过远程接口来适配本地类。我将在下面的部分想你解释这一切是如何做的。
设计一个基于网络的应用程序
就像上面的部分所说的,通过把本地方法转化成远程接口所建立的类是最好的“WEB类”。SUN公司的 java教程已经告诉了我们使RMI工作必须遵守的步骤,这些步骤如下:
1 建立一个远程接口,它必须继承自java.rmi.Remote interface
2 提供一个远程接口的实现。
3 在远程的客户端调用远程的方法。
很巧,这些步骤恰好匹配了适配器模式的功能。当远程客户端想使用被公布在远程接口中的方法时,适配 器模式被初始化,远程接口继承自java.rmi.Remote 以便让RMI知道这个接口实现了RMI,而不是把注意力 集中在实现远程方法的本地类上。通过调用在本地类中的简单方法完成了对远程服务的调用,而本地类没 有改变并且也察觉不到它已经可以完成网络请求的服务了。图二表示了应用适配器模式到RMI的类关系。
图二
从概念上说,我们已经建立了一个将适配器模式应用于RMI的程序图。现在,让我们完成一个具体的基于这种设计的实现。这个例子的功能是使许多远程的客户端查询、加入和删除元素。在这个例子中我会 使用the java.util 包中的哈西表类,许多程序员已经多次的使用过这个类了。
设计本地类
建立远程Collection的第一布就是设计一个本地类来实现一些功能。这一步很方便,SUN公司已经为我们建立了Hashtable类,我们将使用这个类来完成一些功能。我忽略了同步这一概念,但是我会在后面 讨论线程安全使重新介绍它。为了方便,我在列表一中列出了哈西表类的一些相关方法。
列表一
int size();
boolean isEmpty();
boolean contains( Object value );
boolean containsKey( Object key );
Object get( Object key );
Object put( Object key, Object value );
Object remove( Object key );
void clear();
Enumeration keys();
Enumeration elements();
设计远程接口
这个练习的第二步是建立一个远程接口,由它来公布网络的远程服务。接口RemoteMap是建立在 java.util.Map 上的collection接口,如下的这些方法框架式远程的客户端能够调用我们的网络哈西表。
public interface RemoteMap extends java.rmi.Remote {
// Constants
public static final String SERVICENAME = "RemoteMap";
public int size() throws RemoteException;
public boolean isEmpty() throws RemoteException;
public boolean containsKey( Object key ) throws RemoteException;
public boolean containsValue( Object value ) throws RemoteException;
public Object get( Object key ) throws RemoteException;
public Object put( Object key, Object value ) throws RemoteException;
public Object remove( Object key ) throws RemoteException;
public void clear() throws RemoteException;
}
列表二 远程接口中的远程方法
有一些细节由必要指出,所有的远程方法都要抛出java.rmi.RemoteException,这是不用担心的,这是 Java的RMI的一个简单的要求;而且,RemoteMap 接口必须继承自java.rmi.Remote ,这是一个标志,它 告诉Java的rmic工具这个接口准备在网络上共享,这是RMI的另一个要求。在这个接口中还有一个常量字符串SERVICENAME,当服务器公布RMI服务和客户端查找服务的名字时这个字符串会很有用。
所以,现在我们有了一个本地对象和一个远程的接口,我们现在准备为远程接口写适配器类。
容易犯的错误
许多社记者在进入下一步时出现了错误,这就是将本地类适应远程接口。通过继承Hashtable 类来生 成一个新的类并且实现远程接口这样的做法是很有诱惑力的,但是这是一个不好的变成选择,因为如下的 原因:
· 使用组件而不是采取继承的方法来设计类是更好的办法。为什么呢?因为继承的设计通常是用来为存在的类加入新的方法和属性。而在这里,我们只是简单的把本地方法适配成为远程方法罢了,所以组件 设计会更加适合一些。
· 继承设计会带来继承单独的父类这一问题。因为JAVA只允许单独继承,这就带来了扩展上的莫棱两可的选择,继承java.rmi.server.UnicastRemoteObject是RMI的要求,java.util.Hashtable在继承的设 计中也是必须继承的。干脆放弃继承的设计来实现RMI的功能吧。
· 一些本地类是final的,它不允许被继承。例如,这个设计练习就不能用向量,因为向量类是final 的。如果一个类是final的,那么你就不能用继承设计。
· 混合本地方法和远程方法会造成类的混乱,特别是当远程接口和本地类有相同的方法名时。我就曾看到一个解决方案中程序员们不得不改变方法的名字来防止这种情况的发生。例如sizeLocal和 sizeRemote,这多么难看啊!单独功能的类变成了一个巨大的复杂功能的类,多么可怕!
为了向你说明生成一个适配器是多么的简单,列表三提供了全部的源代码。你看,适配器是多么简单得类,并且我们没有改变我们的本地实现。
public class RemoteMapAdapter extends UnicastRemoteObject
implements RemoteMap {
// Constructors
// The owner publishes this service in the RMI registry.
public RemoteMapAdapter( Hashtable adaptee ) throws RemoteException {
this.adaptee = adaptee;
}
// RemoteMap role
public int size() throws RemoteException {
return adaptee.size(); }
public boolean isEmpty() throws RemoteException {
return adaptee.isEmpty(); }
public boolean containsKey( Object key ) throws RemoteException {
return adaptee.containsKey( key ); }
public boolean containsValue( Object value ) throws RemoteException {
return adaptee.contains( value ); }
public Object get( Object key ) throws RemoteException {
return adaptee.get( key ); }
public Object put( Object key, Object value ) throws RemoteException {
return adaptee.put( key, value ); }
public Object remove( Object key ) throws RemoteException {
return adaptee.remove( key ); }
public void clear() throws RemoteException {
adaptee.clear(); }
// Fields
protected Hashtable adaptee;
}
列表三 远程适配器类
我们现在已经完成了这个例子练习的最重要的部分---我们已经写完了我们的适配器类。
最后的接触
为了完成这个练习,我们还有点事情要做。第一,我们必须编译这个接口并且把适配器放在前面所说的两部分里。第二,我们必须为这个例子在网络上运行生成存根和骨架。这是由JAVA的rmic工具来完成的 。键入:rmic RemoteMapAdapter。
接着,我们需要两个简单的程序来运行这个例子-----客户端和服务器端。首先,我们实现一个远程的服务器,它可以生成一个本地的哈西表并且公布我们设计的远程适配器接口。最重要的代码片断列在了 下边。它不像一些我见过的其他例子,这个例子可以通过createRegistry 方法来运行命令行注册这一步 。然后,我们生成了本地哈西表和使远程发布工作的适配器。最后,使用一个字符串和rebind 方法绑定 远程服务,服务器就开始运行,等待远程的客户端了。看列表四。
...
try {
System.out.println( "RemoteMapServer creating a local RMI registry on
the default port." );
Registry localRegistry = LocateRegistry.createRegistry(
Registry.REGISTRY_PORT );
System.out.println( "RemoteMapServer creating local object and remote
adapter." );
adaptee = new Hashtable();
adapter = new RemoteMapAdapter( adaptee );
System.out.println( "RemoteMapServer publishing service \"" +
RemoteMap.SERVICENAME + "\" in local registry." );
localRegistry.rebind( RemoteMap.SERVICENAME, adapter );
System.out.println( "Published RemoteMap as service \"" +
RemoteMap.SERVICENAME + "\". Ready." );
} catch (RemoteException e) {
System.out.println( "RemoteMapServer problem with remote object,
exception:\n " + e );
}
...
列表四 服务器端程序片断
最后一步是生成一些客户端,他们可以加入、查询和清除远程的对象。列表五说明了客户端是如何发现在 服务器上的远程注册并且查询这些服务的过程。注意,lookup方法返回了RemoteMap 接口,而不是一个 RemoteMapAdapter 对象,在所有的RMI服务中都是这样的,因为远程的对象实现了远程的接口。
...
try {
System.out.println( "RemoteMapClient locating RMI registry on remote
host \"" + name + "\"." );
Registry remoteRegistry = LocateRegistry.getRegistry( name );
System.out.println( "RemoteMapClient looking up service \"" +
RemoteMap.SERVICENAME + "\"." );
remoteMap = (RemoteMap) remoteRegistry.lookup( RemoteMap.SERVICENAME
);
} catch (Exception e) { // expecting RemoteException, NotBoundException
System.out.println( "RemoteMapClient problem with RemoteMap,
exception:\n " + e );
}
...
列表五 查找远程注册和远程的服务
我们曾看过RemoteMap的参考,可以调用任何在接口中的方法。事实上,远程方法调用和本地方法的调用 看起来恰好是一样的。有一点要注意,远程方法的调用会因为internet的交通问题而发生延迟。理论上这些调用应该放在不同的线程里确保本地java程序的正确。
列表六说明了如何调用远程对象。
...
try {
Object value = remoteMap.get( name );
if (value != null )
System.out.println( "RemoteMapClient found " + name + ", value=" +
value );
else
System.out.println( "RemoteMapClient could not find key " + name +
"." );
} catch (RemoteException e) {
System.out.println( "RemoteMapClient problem with " + name + ",
exception:\n " + e );
} /* endcatch */
...
为了使用这个C/S结构的系统,首先要启动服务器。服务器初始化了一个哈西表和远程的适配器,并把对 象放入哈西表。哈西表被RMI的本地注册机发布。在windows,unix或者OS/2的命令行输入:
java RemoteMapServer Harriet Bailey Max Zuzu
现在启动一个客户端查询和初始化远程的对象。如果你没有网络条件,在本地机上也可以。用你的IP地址 (127.0.0.1),你的机器名或主机名,执行下面的命令:
java RemoteMapClient hostName Zuzu
如果你的java环境已经安装并且运行正确的话,你应该看到下面的远程响应。
RemoteMapClient found Zuzu, value=3
可以喘口气休息一下了!你已经在理解RMI和适配器模式的路上走了很久了。花点时间放松一下,享受一 下你的劳动果实,我建议编译客户端和服务器端的代码,然后运行它,并且在使用中修改它。如果可能的话,在一些不同的机器上或不同的Java虚拟机上运行这个程序,你应该能够发现这个程序能够运行在不同 的平台上。
总结
读完这篇文章后,你应该能够意识到在Java的RMI方式中使用适配器模式的价值,它的优点就是你不用改变你的本地对象,而且不用生成一个特别复杂且效率低的远程对象。区分类的责任,你的设计可以被很容 易地排错,本地对象中出现的问题属于本地类,网络对象中出现的问题属于远程适配器类。除了这些优点之外,一旦你建立了一个适配器,你就能一遍又一遍的重用这种模式。这是一种很有用的设计模式,实现 起来很简单并且容易被那些审查和维护你的代码的人阅读。
关于作者
DAN工作在IBM的网络计算部(the Network Computing Software Division of IBM Corporation),它正 在致力于实现将Java 2应用于AIX、OS/2、system 390和windows平台。DAN的个人主页是 http://www.io.com/~beckerdo。
本文地址:http://com.8s8s.com/it/it18354.htm