queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
阅读:1876回复:5

动态扩展Java应用

楼主#
更多 发布于:2004-02-22 17:55
摘要:你想写出无需改变源代码就可以进行扩展的程序吗?这篇文章介绍了如何使用interface和动态class载入来创建高扩展性的系统。从中你也可以学习到如何令其他的编程者和用户不需你的源代码,就可以对程序进行扩展。首先我们看一个没有使用interface和动态载入的简单例子,然后再讲述一个动态载入类的例子,这些类是由一个文件或者数据库的表格中读取的。

  你曾经开发过一个要经常添加新功能的应用吗?在下面的例子中,市场部将会为每个顾客提供各种各样的价格处理。你的程序需要处理这些新的需求,你也必须让用户可以定制你的软件而无需改变源代码。

  你可以做到避免修改现有的代码并且测试加入的新功能吗?你可以做到无需重新编译全部的东西来加入新的类吗?答案是可以的,你可能已经猜到了,就是使用interface和动态类载入。

  要说明一下的是,为了说明方便,这里介绍的类和体系都是经过简化的。

  什么是interface(接口)?

  interface只是描述一个对象是如何被调用的。当你定义了一个接口,你就定义了其它的对象如何使用它。

  对于大部分使用Java的人来说,你们可能已经知道接口是什么东西。但对于那些仍然不清楚的人,我将介绍一些基本的知识,然后创建一些复杂的例子。如果你已经很清楚接口的知识,你可以直接跳到“使用字符串来指定类名字”的部分。

  接口的威力

  以下的例子说明了接口的威力。假定你的客户是搞经纪的,他们想让你建立一个交易的系统。他们的交易是各种各样的:包括有股票、债券和日用品等等。不同客户的交易数量也是不一样的,该数量由客户称为pricing plans的东东来定义。

  你首先考虑类的设计。主要的类和它们的属性由客户来定义,可以是:

  Customer(顾客):Name(名字),Address(地址),Phone(电话)和PricingPlan

  Trade(交易):TradeType(股票、债券或者日用品),ItemTraded(股票的记号)、NumberOfItemsTraded, ItemPrice, CommissionAmount

  PricingPlan:通过一个过程的调用来计算该交易的CommissionAmount

喜欢0 评分0
[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
1楼#
发布于:2004-02-22 17:55
不使用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;
}
[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
举报 回复(0) 喜欢(0)     评分
queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
2楼#
发布于:2004-02-22 17:55
使用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实际上并不关心实现该接口的实际对象,它只是关心接口。
[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
举报 回复(0) 喜欢(0)     评分
queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
3楼#
发布于:2004-02-22 17:56
使用字符串来指定类名

  假定老板告诉你该公司又有两个新的价格计划,接着还有更多。这些价格计划是每交易$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类的源代码。
[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
举报 回复(0) 喜欢(0)     评分
queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
4楼#
发布于:2004-02-22 17:56
数据库/基于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。

  你还要注意本文末提到的一个技巧,就是为你的接口加上版本号,以避免当你的程序修改时,令动态扩展无效。
[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
举报 回复(0) 喜欢(0)     评分
queensf
总版主
总版主
  • 注册日期2003-12-04
  • 发帖数735
  • QQ
  • 铜币3枚
  • 威望0点
  • 贡献值0点
  • 银元0个
5楼#
发布于:2004-02-22 17:56
让你的应用变灵活

  现在你已经有足够的知识来使用接口和动态类载入,以令你的程序更加 灵活。在例子中,我向你展示了如何使用一个文本文件来载入新的功能。你可以体验一下这些代码,并且思考如何扩展它。现在你可以创建出灵活的程序,无需你的源代码,别人就可以加入新的功能。

  为接口加入的版本信息

  如果你创建了一套接口来让你的客户/用户来扩展你的应用,要确保加入版本的信息。这样可让你在未来修改或者加入接口时,不会影响到客户已经编写的代码。其中的一个方法是为你的包名指定一个版本信息。

  假定你的应用中的基本package名为brokerage.。你决定客户通过接口来扩展你的应用时,使用的是brokerage.customer。在上面的例子中,IPricingPlan接口可以放到这个包中。你需要在包名中加入版本的信息以和将来修改的接口隔离开来。 在第一次发布你的接口时,包名可以是brokerage.version1.customer。如果将来你要修改IPricingPlan接口,你可以将它放到brokerage.version2.customer中。你必须在你的代码中支持

  这两个接口。如果不支持第一次发布的接口的话将需要客户修改他们现有的程序,这样将令用户不快,第一次加入的版本号也没有意义了。

  其它要记住的方面是:在声明你的方法或者变量的时候,你应该经常包含版本的名字。这可以让你以后免受版本方面的烦恼。你也应该要求你的客户这样做。我并不是说要在你的变量名字中加入version1,而是在声明变量的时候使用版本的信息:

public brokerage.version1.customer getCurrentCustomer() { ... }

  当然,允许更大的用户定制意味着客户可能会给你的应用带来bug。在这种情况下,你要让你的客户知道,如果是由于他们代码中的问题而花费了你们的调试时间,他们应该为此而付费。

[color=blue][size=4][i][b][u] 【 解决不了的事情,就不要想。世界不会因为我而改变。 】 [/size][/u][/b][/i][/color]
举报 回复(0) 喜欢(0)     评分
游客

返回顶部