Java Q&A: 抽象类和接口- 实战练习
在Java中,什么时候该用抽象类,什么时候该用接口?下面的文章将通过实例,而不是从理论的角度给你一个清晰的回答,让你有可能获得醍醐灌顶般的彻悟。以下是原文翻译。
----------------------------------------------------
针对2001年4月的 "抽象类和接口" 一文,读者给予了积极的反馈。
其中经常提到的一个要求是,希望能够提供一个完整的例子,来说明到底该如何使用接口(interface)和抽象类(abstract class)。看来,我上次的回答过于理论化了。所以,在本次Java Q&A专题中,我将通过一个使用了接口和抽象类的程序实例,将这一讨论继续深化。
进行网络通信编程时,大家会发现,通信往往是通过 "成对的键和值" (key/value pair,以下称为 "键/值对")的传输来完成的。HTTP POST和HTTP GET都采用了 "键/值对" 通信。和WebMethod这样的EAI服务器通信,"键/值对" 通信也是一种选择的机制。甚至在使用Java特性时也可以看到 "键/值对"。"键/值对" 无处不在。
"键/值对" 通信之所以常见,在于它可以通过简单的机制赋予数据以含义。虽然简单,但对于每种协议来说,将 "键/值对" 数据放到线路上的方式却各不相同。如果想和Web服务器通信,你会使用URLConnection来和服务器进行HTTP连接。其它类型的通信则需要你使用其它某种方式。本专题中,我将演示如何使用接口和抽象类来生成一个程序框架,使得这个程序可以和任何支持 "键/值对" 消息的服务器通信。
对"键/值对" 通信可以提取两种抽象。第一,传送给接收者的 "键/值对" 数据构成一条Message。第二,消息是通过某种协议传送给服务器的。可以将这种协议抽象为MessageBus。所以,假如要和Web服务器通信,可以通过HTTP MessageBus传送消息。
被传送的消息以及传送消息的机制会经常变化,至少不同程序之间是这样。当你确信某个东西会经常发生变化时,它就是接口的最好选择。
下面一一分析我们的消息发送程序所需要的各个接口。
MessageBus
从下面的代码可以看到,MessageBus知道它能够将一个二维数组类型的 "键/值对" 传送给某个接收者。但要注意,这个接口并没有说如何传送或者传送给谁。相反,这些细节留给了实现这个接口的类:
public interface MessageBus {
public String sendMessage( String [][] values ) throws BusException;
}
这个接口使用起来功能十分强大。你可以用URLConnections来实现它,以进行HTTP通信;也可以通过socket,用自定义协议来实现它;你甚至可以只是将数据记录到普通文件或数据库中。
总之,这个接口允许你建立多种不同的实现。更进一步来说,采用接口而不是某个特定的实现来进行编程,你就能够尽享多态所带来的好处,可以在你的程序中随意更换各种不同的实现。
举例来说,当想调试程序时,你可以将一个 "HTTP MessageBus实现" 替换成一个 "通信记录实现"。这种方法使得你可以轻松地改变程序的工作方式,而无需对原始程序做大量的修改。任何时候需要支持一种新的通信方式,只需简单地建立一个新的MessgaeBus实现就可以了。
Message
下面的示例代码中,Message只知道可以通过MessageBus将自己传送出去。它的具体实现才会去考虑如何得到Message的值。
注意:你不可能找到一个用于获得Message值的函数。相反,我们让Message完全包装它的数据,并且相信它可以将自己正确地放到MessageBus上:
public interface Message {
public String send( MessageBus mb ) throws BusException;
}
MessageBus使得你可以在支持MessageBus的程序中增加新的通信机制;与此类似,Message使得你任何时候可以在程序中增加新的Message类型。
HttpMessageBus
看下面一个具体的MessageBus实现,它通过HTTP来POST消息:
import java.io.BufferedReader;
import java.io.DataOutputStream;
import java.io.InputStreamReader;
import java.io.IOException;
import java.net.MalformedURLException;
import java.net.URL;
import java.net.URLConnection;
import java.net.URLDecoder;
import java.net.URLEncoder;
/**
* HttpMessageBus是一个MessageBus实现,
* 它采用HTTP POST来发送消息。
* @author Tony Sintes JavaWorld Q&A
*/
public class HttpMessageBus implements MessageBus {
private String _url;
private final static String _KEY_VALUE_DELIM = "=";
private final static String _NEW_KEY_VALUE = "&";
private final static String _ENCODING = "application/x-www-form-urlencoded";
private final static String _TYPE = "Content-Type";
private final static int _KEY = 0;
private final static int _VALUE = 1;
public HttpMessageBus( String url )
{
_url = url;
}
public String sendMessage( String[][] values ) throws BusException
{
String post = _formulatePOST( values );
try
{
return _sendPOST( post );
}
catch( MalformedURLException exception )
{
throw new HttpBusException( exception.getMessage() );
}
catch( IOException exception )
{
throw new HttpBusException( exception.getMessage() );
}
}
private String _formulatePOST( String [][] values ) {
if( ( values == null ) || ( values.length == 0 ) )
{
return "";
}
StringBuffer sb = new StringBuffer();
String new_pair = "";
for( int i = 0; i < values.length; i ++ )
{
sb.append( new_pair );
sb.append( URLEncoder.encode( values[i][_KEY] ) );
sb.append( _KEY_VALUE_DELIM );
sb.append( URLEncoder.encode( values[i][_VALUE] ) );
new_pair = _NEW_KEY_VALUE;
}
String post = sb.toString();
return post;
}
private String _sendPOST( String post ) throws MalformedURLException, IOException
{
URLConnection conn = _setUpConnection();
_write( post, conn );
return _read( conn );
}
private URLConnection _setUpConnection() throws MalformedURLException, IOException
{
URL url = new URL( _url );
URLConnection conn = url.openConnection();
conn.setDoInput ( true );
conn.setDoOutput ( true );
conn.setUseCaches ( false );
conn.setRequestProperty( _TYPE, _ENCODING );
return conn;
}
private void _write( String post, URLConnection conn ) throws IOException
{
DataOutputStream output = new DataOutputStream ( conn.getOutputStream() );
output.writeBytes( post );
output.flush ();
output.close ();
}
private String _read( URLConnection conn ) throws IOException
{
BufferedReader input =
new BufferedReader( new InputStreamReader( conn.getInputStream() ) );
String temp_string;
StringBuffer sb = new StringBuffer();
while ( null != ( ( temp_string = input.readLine() ) ) )
{
sb.append( temp_string );
}
input.close ();
return sb.toString();
}
}
大家可以自己研究一下这段代码。关于POST,可以在Java World网站的Java Tip栏目中找到很多介绍。在此我要强调的是,在一个简简单单的MessageBus接口背后,隐藏了大量的细节。HttpMessageBus在构造时只是简单地取了一个URL;传给sendMessage()的所有值都发送到那个URL。除此之外的任何细节都隐藏在接口之后。
AbstractMessage
写过几个Message实现之后就会发现,send()可以分解为两个基本步骤:
1. 将Message的内部数据转换成二维数组。
2. 在MessageBus上发送数组值。这是通过调用MessageBus的sendMessage()并传给它二维数组参数来实现的。
每次写一个新的Message实现,都要写类似的send()。
若能合理地使用抽象类,你将能够很轻松地写出新的Message实现并消除冗余代码。看看下面这个AbstractMessage的定义:
public abstract class AbstractMessage implements Message
{
public String send(MessageBus mb) throws BusException
{
String [][] values = values();
String response = mb.sendMessage( values );
return response;
}
protected abstract String [][] values();
}
从上面可以看到,AbstractMessage为send()提供了一个缺省实现。这一缺省实现带来三大好处:
? 你无需一遍又一遍地写相同的代码。相反,现在有了一个缺省实现,它可以完成前面所定义的那两步基本操作。
? 现在,你只用提供一个values()的实现就可以写出一个新的Message。这种方式下,子类只需要知道如何将自身表示为 "键/值对" 数组就可以了。子类不必关心如何在线路上传送自己。传送的动作对任何Message来说都是相同的。
继承(inheritance)的一个重要用途在于,它可以用于 "根据差异来编程" (programming by difference)的场合。根据差异去编程,也就确定了子类与它的父类如何不同。例如Message,它和父类的差异仅仅在于所包含的数据不同,在线路上传送的方式则是一样的。采用抽象类,使得我们可以干净清晰地使用继承。
? AbstractMessage实际定义了一个规范,通过子类来定义新的Message时都要遵守这一规范。这样一来,程序员在建立子类时就可以清楚地知道需要重新实现哪些函数。上面的例子虽然很简单,但采用这种编程方法使得在派生复杂的子类时,事情会变得更简单。
随着一个类越变越大,以上三点也会更趋明显。
Message示例
假设有一个网站提供股票行情服务。若想查找一支股票信息,就要对某一URL发送POST命令。除了URL之外,还要附上股票代号的名称。
股票代号的 "键/值对" 看起来象这样:
ticker=bvsn
此处的ticker是 "键",bvsn是 "值"。键告诉接收者,值bvsn是一个股票代号。
如果在Yahoo查看股票行情,你要发送这样的URL:
http://finance.yahoo.com/q
以及两对键/值:
? s - 股票代号
? d - 查看级别(基本信息,详细信息等)
所以,想要查看bvsn,需要POST下面的消息:
http://finance.yahoo.com/q?s=bvsn&d=v1
类似地,如果是Quicken的股票行情系统,你得发送这样的URL:
http://www.quicken.com/investments/quotes/
以及一对键/值:
symbol - 股票代号
所以,在Quicken查看bvsn,需要POST下面的消息:
http://www.quicken.com/investments/quotes/?symbol=bvsn
下面的代码是针对这两种Message的实现:
public class YahooStockQuote extends AbstractMessage {
private String _ticker;
private final static String _TICKER_KEY = "s";
private final static String _D_VALUE = "v1";
private final static String _D_KEY = "d";
public YahooStockQuote( String ticker )
{
_ticker = ticker;
}
public void setTicker( String ticker )
{
_ticker = ticker;
}
protected String [][] values()
{
String [][] values = {
{ _TICKER_KEY, _ticker },
{ _D_KEY, _D_VALUE }
};
return values;
}
}
以及:
public class QuickenStockQuote extends AbstractMessage
{
private String _ticker;
private final static String _TICKER_KEY = "symbol";
public QuickenStockQuote( String ticker )
{
_ticker = ticker;
}
public void setTicker( String ticker )
{
_ticker = ticker;
}
protected String [][] values()
{
String [][] values = {
{ _TICKER_KEY, _ticker }
};
return values;
}
}
这些Message只知道如何处理它们的数据以及构造二维 "键/值对" 数组。
程序示例
看看下面的股票信息示例程序:
public class QuoteGetter
{
public final static void main( String [] args )
{
if( args.length != 2 )
{
System.out.println( "Incorrect Useage:" );
System.out.println( "java QuoteGetter <quicken or yahoo> <TICKER>" );
return;
}
String service = args[0];
String ticker = args[1];
Message message = null;
MessageBus bus = null;
if( service.equalsIgnoreCase( "quicken" ) )
{
bus =
new HttpMessageBus( "http://www.quicken.com/investments/quotes/" );
message = new YahooStockQuote( ticker );
}
else // default to yahoo
{
bus =
new HttpMessageBus( "http://finance.yahoo.com/q" );
message = new QuickenStockQuote( ticker );
}
try
{
String response = sendMessage( message, bus );
System.out.println( response );
}
catch( Exception ignore )
{
System.out.println( "Lookup Failed for: " + ticker );
ignore.printStackTrace();
}
}
private static String sendMessage( Message message, MessageBus bus )
throws BusException
{
return message.send( bus );
}
}
main首先创建合适的Message和MessageBus,然后调用sendMessage()发送Message。sendMessage()是一个针对接口来进行编程的例子,而不是针对实现。如果sendMessage()只是专门支持YahooStockQuote或HttpMessageBus,你会说它是针对实现来编程的。
然而,上面的sendMessage()是针对接口来编程的。所以你可以将这个接口的任何实现作为参数传递给这个函数,使得函数更具一般性。
总结
我在日常工作中已经成功地使用了这一程序框架。通过接口,我可以将通信机制从HTTP POST无缝地转换为一个自定义的协议。我还可以使用Message接口在程序中轻松地增加新的消息。另外,只要写一次Message抽象基类,我可以更容易地写出新的函数并在其它程序中复用它们。
希望这个详尽的例子有助于深化上一次关于抽象类和接口的探讨。另外,读者可以去下载本专题所附带的源代码,它包括整个消息发送程序的所有代码以及本专题未曾提到的其它一些类的代码。
----------------------------------------------------
相关资源
? 下载本篇文章的源代码:
http://www.javaworld.com/javaworld/javaqa/2001-08/interface/03-qa-0831-interface.zip
? 和本文相关的另一篇文章 "Abstract Classes Vs. Interfaces" Tony Sintes (JavaWorld, April 2001):
http://www.javaworld.com/javaworld/javaqa/2001-04/03-qa-0420-abstract.html