程序師世界是廣大編程愛好者互助、分享、學習的平台,程序師世界有你更精彩!
首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫知識
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
 程式師世界 >> 編程語言 >> JAVA編程 >> 關於JAVA >> 動態擴展Java應用

動態擴展Java應用

編輯:關於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。在這種情況下,你要讓你的客戶知道,如果是由於他們代碼中的問題而花費了你們的調試時間,他們應該為此而付費。

  1. 上一頁:
  2. 下一頁:
Copyright © 程式師世界 All Rights Reserved