Java Tip 74: Build dynamically extensible frameworks

Make your frameworks data-independent with property objects and dynamic class loading

Extending well-designed, object-oriented frameworks is a simple task. This tip will teach you one way to allow for such extensions. Frameworks written in Java typically have a number of interfaces that are natural extension points. This Java tip shows you how to implement a framework using property files and dynamic class loading that allows extensions. The resulting framework enables both objects and classes to be added dynamically without touching the framework source code. A simple example is provided that illustrates the benefits and drawbacks of this solution.

Example problem

Booking resources is a common problem in different domains. Offices have systems to book conference rooms. Car rental companies have systems for reserving cars. And the system for borrowing books from the library is very similar to the system for booking a conference room or reserving a car.

I will present a simple booking framework that can be used in all such situations.

The framework

The framework has two interfaces, Bookable and BookableStorage, and one coordinating class, Booker. It is the responsibility of the Bookable object, shown below, to keep track of its own bookings and decide whether booking requests can be met.

package bookingframework; 
import java.util.Calendar; 
/** 
 * Implementors of this interface represent a bookable item in a 
 * booking system. Bookable instances are loaded and stored with a 
 * BookableStorage object. I was thinking about extending java.io.Serializable 
 * but decided not to since there may be implementations that don't use 
 * serialization for persistence. 
 */ 
public interface Bookable 
{ 
  /** 
   * Book this bookable for the specified date. (In a real-world 
   * booking system, we would have a start Calendar and an end Calendar.) 
   * 
   * @return boolean True if the booking succeeded, false otherwise. 
   */ 
  public boolean book( Calendar aDate ); 
  /** 
   * Check if this bookable is free the specified date. 
   * 
   * @return boolean True if this Bookable object is free, false otherwise. 
   */ 
  public boolean isFree( Calendar aDate ); 
  /** 
   * A name that identifies this bookable can be useful for identification 
   * and/or presentation. 
   */ 
  public String getName(); 
} 

A BookableStorage object is responsible for loading and storing Bookable instances of a specific type as given in a Properties object. Here is the code for BookableStorage:

package bookingframework; import java.io.IOException; /** * This is the interface that deals with persistence of bookables. * Implementors of this interface must be associated with a Bookable * implementation to make sense. The implementation must have a constructor with * one Properties argument where the Properties object has values for one * corresponding Bookable class and perhaps also setup values. *

* The responsibility of this interface is to provide means of loading and * storing Bookable objects. */ public interface BookableStorage { /** * Load one Bookable with the specified name. The implementation is * responsible for ensuring that there is an easy way to identify stored objects. * * @param name The name corresponds to the name retrieved from a Bookable * with the getName method and is the way of identifying a unique * Bookable object in this BookableStorage. * @return Bookable If there is a Bookable with the given name, it will * be returned. * * @exception IOException If there is some kind of I/O error while trying * to find the Bookable. * @exception BookableNotFoundException If there is no Bookable with the given * name in this BookableStorage. */ public Bookable load( String name ) throws IOException, BookableNotFoundException; /** * Load all Bookables in this storage. In a more advanced interface * there will surely be intermediate versions in between loading all stored * Bookable objects and loading one. Different types of queries may be * introduced. * * @return Bookable[] Returns an array with all Bookables stored in this * BookableStorage. */ public Bookable[] loadAll(); /** * Since BookableStorage is responsible for loading Bookables, it is * natural to give it the responsibility of storing them. * * @param aBookable The Bookable to store. The getName method of Bookable * is used as id for the object. * @exception IOException If there is an I/O error while storing aBookable. */ public void store( Bookable aBookable ) throws IOException; }

The Booker object is responsible for coordinating the booking. The Booker finds a suitable BookableStorage instance from the information in the Properties object and uses it to load and store Bookable instances.

A single BookableStorage implementation, CarStorage, for example, can be related to one Bookable implementation, let's say Car, or many Bookable implementations, such as Car and Truck. (Car and CarStorage are implementations provided in the distribution.)

package bookingframework; 
import java.util.Properties; 
import java.util.Calendar; 
import java.io.PrintWriter; 
import java.io.IOException; 
import java.lang.reflect.Constructor; 
import java.lang.reflect.InvocationTargetException; 
/** 
 * The coordinating class in this framework. It has the responsibility of 
 * executing booking requests for different types of Bookable objects. In 
 * order to accomplish its task, it has to find the BookableStorage
 * implementation that supports the given type, dynamically load it,
 * and search for a free Bookable in that storage. 
 */ 
public class Booker 
{ 
  protected Properties myProperties; 
  protected PrintWriter myMessageWriter; 
  /** 
   * A booker is created with a Properties object and a writer for 
   * outputing messages. 
   * 
   * @param someProperties 
   * The properties object may originate from a file 
   * either as text or serialized. In our example, though, we go for 
   * the easy option of using a text properties file with only 
   * string values. 
   * 
   * @param messageWriter 
   * The writer can be System.out, a file, or directed to a graphics 
   * component. By using a writer, user communication is left to the caller 
   * and not assumed here. 
   */ 
  public Booker( Properties someProperties, PrintWriter messageWriter ) 
  { 
    myProperties = someProperties; 
    myMessageWriter = messageWriter; 
  } 
  /** 
   * Try to book an item of type for the given date. This method is 
   * straightforward: Get a BookableStorage, load all bookables in the 
   * storage, and loop through them in order to find a free bookable for the date. 
   * 
   * @return boolean True if the booking succeeded, false otherwise. 
   * @exception NoStorageFoundException if the storage associated with the 
   *                                    type doesn't exist. This can be for 
   *                                    two reasons. 
   *                                    <ul> 
   *                                    <li>The Properties object held by 
   *                                    this Booker may not have a property 
   *                                    with the name dynamic.<type>.storage . 
   *                                    <li>The class pointed to by the property 
   *                                    is either missing or does not 
   *                                    implement BookableStorage correctly. 
   *                                    </ul> 
   * @param type 
   * @param date 
   */ 
  public boolean book( String type, Calendar date ) throws NoStorageFoundException 
  { 
    BookableStorage aStorage = getBookableStorage( type ); 
    if( aStorage == null ) 
      throw new NoStorageFoundException(); 
    Bookable[] someBookables = aStorage.loadAll(); 
    for( int i=0; i < someBookables.length; i++ ) 
    { 
      if( someBookables[i].isFree( date ) ) 
      { 
        someBookables[i].book( date ); 
        try 
        { 
          aStorage.store( someBookables[i] ); 
        } 
        catch( IOException e ) 
        { 
          myMessageWriter.println( "I could not store your request." ); 
        } 
        return true; 
      } 
    } 
    return false; 
  } 
  /** 
   * Dynamically load a class for the type given and return an instance of that 
   * class. The method uses the Properties object to decide the class name. 
   * The property dynamic.<type>.storage should hold the class name. 
   * It furthermore assumes that the dynamically loaded class has a 
   * constructor with one argument of type Properties to which the properties 
   * object is sent. The Properties object may hold setup information 
   * for instance creation. 
   * 
   * @return BookableStorage A storage that can be used to load and store 
   *                         Bookables of the given type. Null if no storage is 
   *                         found. 
   * @param type 
   */ 
  protected BookableStorage getBookableStorage( String type ) 
  { 
     /* 
      * Get the class name from the properties object. Use a name standard 
      * saying that the class supporting the type "car," for example, can be 
      * found with the key dynamic.car.storage. 
      */ 
     String className 
       = myProperties.getProperty( "dynamic." + type + ".storage" ); 
     /* 
      * getProperty returns null if there is no value for the key. (Or if 
      * the value is null...) This check is the closest we come to type 
      * checking in this framework. 
      */ 
     if( className == null ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       return null; 
     } 
     try 
     { 
       /* 
        * Load the class using the static forName method in Class and then 
        * create a new instance of the loaded class. We assume that the loaded 
        * class implements BookableStorage and has a constructor with one 
        * Properties argument. See the error texts in the many catch blocks 
        * below to get a feeling for the various exceptions that may rise. If 
        * the constructor of the loaded class throws an exception it is 
        * wrapped by newInstance in an InvocationTargetException. 
        */ 
       Class storageClass = Class.forName( className ); 
       Constructor storageConstructor 
         = storageClass.getConstructor( new Class[]{ Properties.class } ); 
       Object aBookableStorage 
         = storageConstructor.newInstance( new Object[]{ myProperties }); 
       return (BookableStorage)aBookableStorage; 
     } 
     catch( ClassNotFoundException e ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       myMessageWriter.println( "Couldn't find the class " + className ); 
     } 
     catch( NoSuchMethodException e ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       myMessageWriter.println( "Couldn't find an appropriate constructor in " 
                                + className ); 
     } 
     catch( InvocationTargetException e ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       myMessageWriter.println( "The constructor of " + className + 
                                " did throw an exception." ); 
     } 
     catch( IllegalAccessException e ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       myMessageWriter.println( "Couldn't access " + className ); 
     } 
     catch( InstantiationException e ) 
     { 
       myMessageWriter.println( "It seems that " + type + " isn't supported." ); 
       myMessageWriter.println( className + 
                                " is an interface or an abstract class." ); 
     } 
     return null; 
  } 
} 

A BookableStorage implementation is identified by an entry in the Properties object (see the code below), and that implementation either uses further properties to set up relations with Bookable implementations or that information is hardcoded into it. The different Bookable implementations can be thought of as different types of resources that may be booked.

# dynamic.car.properties 
# 
# This file is read by the booking framework in order to find classes 
# implementing bookingframework.BookableStorage. Any initial parameters 
# needed to construct an instance of such a class may be given here as 
# well. 
# 
# The naming standard for the class name is dynamic.<type>.storage where 
# type is a name describing a type of booking resources, for example a car, 
# a book, or a helicopter. The init properties may have any name except 
# names starting with "dynamic." and ending with ".storage". It is advisable 
# that a naming standard is adopted for the init properties as well. 
# The class used to store a car. 
dynamic.car.storage=bookingframework.CarStorage 
# The storage directory for a car storage. 
dynamic.car.storagedir=c:\\temp\\cars 
Related:
1 2 Page 1
Page 1 of 2