動態擴展Java應用
摘要:你想寫出無需改變源代碼就可以進行擴展的程序嗎?這篇文章介紹了如何使用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。在這種情況下,你要讓你的客戶知道,如果是由于他們代碼中的問題而花費了你們的調試時間,他們應該為此而付費。