Most read:
Popular archives:
Java Q&A Forums - Let the great migration begin
We're pleased to announce the first phase of the integration of the Java Q&A Forums with our community platform, JavaWorld's
Daily Brew. Whether you're one of our longtime forum users or a brand newbie, we hope you'll visit the Java Q&A Forums in their new home alongside JW Blogs.
| Enterprise AJAX - Transcend the Hype |
| Oracle Compatibility Developer's Guide |
Can you avoid changing the code you have already written and tested to add new functionality? Can you add new classes to your program without having to recompile the whole thing? The answer to both questions is yes, and as you might have guessed, is based on interfaces and dynamic class loading.
As an aside, most of the classes and architecture used in this article are simplified from their use in professional programming. The overall code demonstrates how to use interfaces to dynamically extend programs; it is not intended for use in real systems.
An interface simply describes the way an object is called. When you define an interface, you relate how other objects will use a specific object.
Most of you working with Java should already understand interfaces, as it is difficult to use Java without that knowledge. But for those of you unclear on the subject, I'll start at the beginning and then create more complicated examples. If you already understand interfaces, you can probably skim until we get to the Using Strings to Specify Class Name section.
The first example illustrates the power of interfaces. Assume your client is a brokerage house, and they want you to set up their trading system. They trade in all sorts of financial instruments: stocks, bonds, commodities, and so on. Different customers are charged different amounts for their trades; the amounts are defined by what the client calls pricing plans.
First, you think about the design of your classes. The main classes and their properties, defined by the client, may be as follows:
Customer: Name, Address, Phone, PricingPlanTrade: TradeType (stock, bond, commodity), ItemTraded (stock symbol), NumberOfItemsTraded, ItemPrice, CommissionAmountPricingPlan: Provides a call procedure to calculate the CommissionAmount for a trade
You can code the pricing plans without using an interface and then enhance the code from there. Right now, the client has two pricing plans defined as follows:
The Trade object uses a PricingPlan object to calculate how much commission to charge the customer. You create a PricingPlan class for each pricing plan. The class for Plan 1 is called PricingPlan20 and Plan 2's class is called PricingPlan1510. Both classes calculate the commission to charge using a procedure called CalcCommission(). The code looks like the following:
Class Name: PricingPlan20
public double calculateCommission( Trade trade )
{
return 20.0;
}
Class Name: PricingPlan1510
public double calculateCommission( Trade trade )
{
double commission = 0.0;
if( trade.getCustomer().getNumberOfTradesThisMonth() <= 10 )
commission = 15.0;
else
commission = 10.0;
return commission;
}
Here's the code to get the commission in Trade:
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;
}
How can an interface make your life easier in the previous example? You can create the PricingPlan interface that the PricingPlan classes implement:
Interface Name: IPricingPlan
public interface IPricingPlan {
public double calculateCommission( Trade trade );
}
Since you are defining an interface, you don't define a body for the calculateCommission() method. The actual PricingPlan classes will fill in the code. The first modification you make to the PricingPlan classes is to declare that you will implement the interface you just defined. You do this by adding the following code to
the top of the PricingPlan class definitions:
public class PricingPlan20 extends Object implements IPricingPlan {
When you declare that you will implement an interface in Java, you must provide bodies for all the methods that the interface
defines (unless you are creating abstract classes, which is unrelated to this topic). So any class that implements the IPricingPlan interface must define a method called calculateCommission(). The method signature must be defined exactly the way the interface defines it, so it must accept a Trade object. Since we already had calculateCommission() methods in both PricingPlan classes, we won't modify them any further. If you create new PricingPlan classes, they must implement the IPricingPlan interface and the calculateCommission() method.
Next you can change the Trade class's getCommissionPrice() method to use the interfaces as follows:
Class Name: 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;
}
Notice how you define the PricingPlan variable as an IPricingPlan interface. The instance you actually create depends on the customer's pricing plan. Since both PricingPlan classes implement the IPricingPlan interface, you can set the variable equal to a new instance of either one. Java, in general, doesn't care about the actual
object that implements the interface, only about the interface itself.
Say your boss comes to you and says that the company just approved two new pricing plans, with more coming. The pricing plans
are a flat rate of and 0 per trade. You decide to create two new PricingPlan classes: PricingPlan8 and PricingPlan10.
In that case, you must change the Trade class to include these new PricingPlans. You could simply add more if/then/else clauses, but that would get unwieldy as the number of pricing plans grows. Another
option is to use the Class.forName() method instead of new when creating PricingPlan instances. Class.forName() lets you create instances using a string to name the class instead of hardcoding the name in the program. Here is an example
of how you could use this in the Trade class's earlier code:
Class Name: 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;
}
This code doesn't offer much of an advantage over the previous code. It's actually longer considering you must now include
the exception-catching code. Look at what happens if you create an array of PricingPlan class names in the Trade class:
Class Name: Trade
public class Tradeextends Object {
private Customer customer;
private static final String[]
pricingPlans = { "string_interfaces.PricingPlan20",
"string_interfaces.PricingPlan1510",
"string_interfaces.PricingPlan8",
"string_interfaces.PricingPlan10"
};
Now you can change the getCommissionPrice() method to the following:
Class Name: 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;
}
If you don't count the exception handling, this code is by far the simplest we've seen. It is also relatively easy to add
new pricing plans. You simply create and add the plans to the Trade class's array.
I hope you begin to see the power available with dynamic class loading. Now I'll switch gears and refactor my design for cleaner code. (See the Resources section for more information on refactoring.)
You can improve this design to make adding new pricing plans even easier. You still have to recompile the source file that
contains the Trade class whenever a new pricing plan is added.
Imagine what would happen if you stored the class names in a database table, XML file, or even a plain text file. You could
then add pricing plans by simply creating a new class, placing it where the program can find it, and adding a record to the
database table or file. Then you wouldn't need to change the Trade class every time a new pricing plan was introduced. I will use a plain text file, since that is the easiest to code and makes
for a simpler example. In a real system, I would certainly recommend either a database or XML file, since they allow more
flexibility. The text file looks like the following: