动态扩展Java应用
副标题#e#
摘要:你想写出无需改变源代码就可以举办扩展的措施吗?这篇文章先容了如何利用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;
}
#p#副标题#e#
以下是在生意业务中获得佣金的代码:
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
#p#分页标题#e#
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
#p#分页标题#e#
此刻你就可以建设一个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。在这种环境下,你要让你的客户知道,假如是由于他们代码中的问题而耗费了你们的调试时间,他们应该为此而付费。