摘要:你想寫出無需改變源代碼就可以進行擴展的程序嗎?這篇文章介紹了如何使用interface和動態class載入來創建高擴展性的系統。從中你也可以學習到如何令其他的編程者和用戶不需你的源代碼,就可以對程序進行擴展。首先我們看一個沒有使用interface和動態載入的簡單例子,然後再講述一個動態載入類的例子,這些類是由一個文件或者數據庫的表格中讀取的。
你曾經開發過一個要經常添加新功能的應用嗎?在下面的例子中,市場部將會為每個顧客提供各種各樣的價格處理。你的程序需要處理這些新的需求,你也必須讓用戶可以定制你的軟件而無需改變源代碼。
你可以做到避免修改現有的代碼並且測試加入的新功能嗎?你可以做到無需重新編譯全部的東西來加入新的類嗎?答案是可以的,你可能已經猜到了,就是使用interface和動態類載入。
要說明一下的是,為了說明方便,這裡介紹的類和體系都是經過簡化的。
什麼是interface(接口)?
interface只是描述一個對象是如何被調用的。當你定義了一個接口,你就定義了其它的對象如何使用它。
對於大部分使用Java的人來說,你們可能已經知道接口是什麼東西。但對於那些仍然不清楚的人,我將介紹一些基本的知識,然後創建一些復雜的例子。如果你已經很清楚接口的知識,你可以直接跳到“使用字符串來指定類名字”的部分。
接口的威力
以下的例子說明了接口的威力。假定你的客戶是搞經紀的,他們想讓你建立一個交易的系統。他們的交易是各種各樣的:包括有股票、債券和日用品等等。不同客戶的交易數量也是不一樣的,該數量由客戶稱為pricing plans的東東來定義。
你首先考慮類的設計。主要的類和它們的屬性由客戶來定義,可以是:
Customer(顧客):Name(名字),Address(地址),Phone(電話)和PricingPlan
Trade(交易):TradeType(股票、債券或者日用品),ItemTraded(股票的記號)、NumberOfItemsTraded, ItemPrice, CommissionAmount
PricingPlan:通過一個過程的調用來計算該交易的CommissionAmount
不使用interface的編碼
開始編碼時你可以不使用接口,然後再由該代碼增強其功能。現在,該客戶有兩個標價計劃定義如下:
計劃1:對於常規的顧客,$20/交易
計劃2:一個月中的前10個交易,$15/交易,以後的 $10/交易
Trade對象使用一個PricingPlan對象來計算要收顧客多少傭金。你為每個標價計劃都創建了一個PricingPlan類。對於計劃1,該類稱為PricingPlan20,而計劃2的類則稱為PricingPlan1510。兩個類都通過一個稱為CalcCommission()的過程來計算傭金。代碼如下所示:
類名: PricingPlan20
public double calculateCommission( Trade trade )
{
return 20.0;
}
類名: PricingPlan1510
public double calculateCommission( Trade trade )
{
double commission = 0.0;
if( trade.getCustomer().getNumberOfTradesThisMonth() <= 10 )
commission = 15.0;
else
commission = 10.0;
return commission;
}
以下是在交易中得到傭金的代碼:
public double getCommissionPrice()
{
double commissionPrice = 0.0;
if( getCustomer().getPlanId() == 1 )
{
PricingPlan20 plan1 = new PricingPlan20();
commissionPrice = plan1.calculateCommission( this.getCustomer() );
plan1 = null;
}
else
{
PricingPlan1510 plan2 = new PricingPlan1510();
commissionPrice = plan2.calculateCommission( this.getCustomer() );
plan2 = null;
}
return commissionPrice;
}
使用interface
使用接口的話,將會令上面的例子變得更加簡單。你可以創建PricingPlan的接口,然後定義實現該接口的PricngPlan類:
接口名:IPricingPlan
public interface IPricingPlan {
public double calculateCommission( Trade trade );
}
由於你定義的是一個接口,所以你無需為calculateCommission()定義一個方法體。真正的PricingPlan類將會實現該部分的代碼。接著你就要修改PricingPlan類,第一步是聲明它將會實現你剛剛定義的接口。你只要在PricingPlan類的定義中加入以下代碼就可以:
public class PricingPlan20 extends Object implements IPricingPlan {
在Java中,當你聲明將實現一個接口的時候,你必須實現該接口中的全部方法(除非你要創建一個抽象類,這裡不討論)。因此所有實現IPricingPlan的類都必須定義一個calculateCommission()的方法。該方法的所有標記必須和接口定義的完全一樣,所以它必須接受一個Trade對象,由於我們的兩個PricingPlan類中都已經定義了calculateCommission()方法,因為我們沒有必要作進一步的修改。如果你要創建新的PricingPlan類,你就必須實現IPricingPlan和相應的calculateCommission()方法。
接著你可以修改Trade類的getCommissionPrice()方法來使用該接口:
類名: Trade
public double getCommissionPrice()
{
double commissionPrice = 0.0;
IPricingPlan plan;
if( getCustomer().getPlanId() == 1 )
{
plan = new PricingPlan20();
}
else
{
plan = new PricingPlan1510();
}
commissionPrice = plan.calculateCommission( this );
return commissionPrice;
}
要注意的是,你將PricingPlan變量定義為IPricingPlan接口。你實際創建的對象根據客戶的標價計劃而定。由於兩個PricingPlan類都實現了IPricingPlan接口,所以你可以將兩個新的實例賦給同一個變量。Java實際上並不關心實現該接口的實際對象,它只是關心接口。
使用字符串來指定類名
假定老板告訴你該公司又有兩個新的價格計劃,接著還有更多。這些價格計劃是每交易$8或者$10。你決定要創建兩個新的PricingPlan類: PricingPlan8 和 PricingPlan10。
在這種情況下,你必須修改Trade類來包含這些新的價格計劃。你可以加入更多的if/then/else句子,但這不是一個好方法,如果價格計劃變得越來越多時,代碼將會顯得十分笨重。另一個選擇是通過Class.forName() 方法來創建PricingPlan實例,而不是通過new。Class.forName()方法可讓你通過一個字符串名字來創建實例,以下就是在Trade類中應用該方法的例子:
類名: Trade
public double getCommissionPrice()
{
double commissionPrice = 0.0;
IPricingPlan plan;
Class commissionClass;
try
{
if( getCustomer().getPlanId() == 1 )
{
commissionClass = Class.forName( "string_interfaces.PricingPlan20" );
}
else
{
commissionClass = Class.forName( "string_interfaces.PricingPlan1510" );
}
plan = (IPricingPlan) commissionClass.newInstance();
commissionPrice = plan.calculateCommission( this );
}
// ClassNotFoundException, InstantiationException, IllegalAccessException
catch( Exception e )
{
System.out.println( "Exception occurred: " + e.getMessage() );
e.printStackTrace();
}
return commissionPrice;
}
這部分代碼看起來的改進並不大。由於你必須加入例外處理的代碼,它實際上變長了。不過,如果你要在Trade類中創建一個PricingPlan類的數組時,情況又如何呢?
類名: Trade
public class Trade extends Object {
private Customer customer;
private static final String[]
pricingPlans = { "string_interfaces.PricingPlan20",
"string_interfaces.PricingPlan1510",
"string_interfaces.PricingPlan8",
"string_interfaces.PricingPlan10"
};
現在你可以將getCommissionPrice()方法修改為:
類名: Trade
public double getCommissionPrice()
{
double commissionPrice = 0.0;
IPricingPlan plan;
Class commissionClass;
try
{
commissionClass =
Class.forName( pricingPlans[ getCustomer().getPlanId() - 1 ] );
plan = (IPricingPlan) commissionClass.newInstance();
commissionPrice = plan.calculateCommission( this );
}
// ClassNotFoundException, InstantiationException, IllegalAccessException
catch( Exception e )
{
System.out.println( "Exception occurred: " + e.getMessage() );
e.printStackTrace();
}
return commissionPrice;
}
如果不將例外處理的部分計算在內,這裡的代碼是我們見過最簡單的。在需要加入新的標價計劃時,也相對地簡單。你只要在Trade類中的數組中創建就可以了。
我想你已經開始看到動態類載入的強大了吧。
你還可以改進這個設計,以便在加入新的價格計劃時更加簡單,上面方法的缺點是,在加入一個新的價格計劃後,你仍然必須重新編譯包含有Trade類的源代碼。
數據庫/基於XML的類名、
想象一下,如果你將類的名字存放在一個數據庫表、XML文件或者是一個純文本文件時,會出現什麼情況?在加入新的價格計劃時,你只需要創建一個新的類,並且將它放到一個程序可以找到的地方,然後在數據庫表或者文件中加入一個記錄就可以了。這樣在一個新的標價計劃推出時,你就不必每次修改Trade類。這裡我將使用純文本文件來說明,因為這是最簡單的方法。在一個真正的系統中,我將建議使用數據庫或者是一個XML文件,因為它們更加靈活。該文本文件如下所示:
文件名: PricingPlans.txt
1,string_interfaces.PricingPlan20
2,string_interfaces.PricingPlan1510
3,string_interfaces.PricingPlan8
4,string_interfaces.PricingPlan10
現在你就可以創建一個PricingPlanFactory類,它將可以根據傳入的PlanId來返回一個IPricingPlan實例。這個類讀取和分析該文本文件至一個Map中,這樣它就可以很方便地根據PlanId進行查找。要注意的是,你也可以修改PricingPlanFactory類以使用一個數據庫或者XML文件。
你可以重新設計Customer類,以便返回IPricingPlan實例而不是PlanId。這樣的設計要比返回一個PlanId好,因為其它的類將不需知道它們必須傳送PlanId到PricingPlanFactory()方法。這些類不需知道PricingPlanFactory的任何東西;它們只使用所需的IPricingPlan實例就可以了(前面我使用這個設計的原因是這樣更便於表達我的觀點)。
這些修改都可以在這篇文章的源代碼包中的pricing_plan_factory package找到。
要注意的方面
在這篇文件附帶的源代碼包中(DynamicJavaSource.zip),每個pachage都包含有一個Test類。以下的表描述了這些包中包含有那些東西:
Package 描述
no_interfaces 沒有使用interfaces的例子
hard_coded_interfaces 使用interfaces,但是類名寫入到源代碼中的例子
string_interfaces 使用interfaces,類名以字符串的形式寫到源代碼中的例子
pricing_plan_factory 使用一個文本文件來得到一個類名的例子
對於類載入的方面,有個問題要注意:類載入的工作有時會出現意外。例如,如果調用forName()方法的類是一個擴展,將不會在CLASSPATH的目錄中搜索這個被動態載入的類。如果你想了解關於這個問題的深入討論或者ClassNotFoundExceptions的一些意外,你可以參考http://java.sun.com/products/jdk/1.3/docs/guide/extensions/index.html。
你還要注意本文末提到的一個技巧,就是為你的接口加上版本號,以避免當你的程序修改時,令動態擴展無效。
讓你的應用變靈活
現在你已經有足夠的知識來使用接口和動態類載入,以令你的程序更加 靈活。在例子中,我向你展示了如何使用一個文本文件來載入新的功能。你可以體驗一下這些代碼,並且思考如何擴展它。現在你可以創建出靈活的程序,無需你的源代碼,別人就可以加入新的功能。
為接口加入的版本信息
如果你創建了一套接口來讓你的客戶/用戶來擴展你的應用,要確保加入版本的信息。這樣可讓你在未來修改或者加入接口時,不會影響到客戶已經編寫的代碼。其中的一個方法是為你的包名指定一個版本信息。
假定你的應用中的基本package名為brokerage.。你決定客戶通過接口來擴展你的應用時,使用的是brokerage.customer。在上面的例子中,IPricingPlan接口可以放到這個包中。你需要在包名中加入版本的信息以和將來修改的接口隔離開來。 在第一次發布你的接口時,包名可以是brokerage.version1.customer。如果將來你要修改IPricingPlan接口,你可以將它放到brokerage.version2.customer中。你必須在你的代碼中支持
這兩個接口。如果不支持第一次發布的接口的話將需要客戶修改他們現有的程序,這樣將令用戶不快,第一次加入的版本號也沒有意義了。
其它要記住的方面是:在聲明你的方法或者變量的時候,你應該經常包含版本的名字。這可以讓你以後免受版本方面的煩惱。你也應該要求你的客戶這樣做。我並不是說要在你的變量名字中加入version1,而是在聲明變量的時候使用版本的信息:
public brokerage.version1.customer getCurrentCustomer() { ... }
當然,允許更大的用戶定制意味著客戶可能會給你的應用帶來bug。在這種情況下,你要讓你的客戶知道,如果是由於他們代碼中的問題而花費了你們的調試時間,他們應該為此而付費。