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 problemBooking 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 frameworkThe 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:tempcars Pluggable classesTo demonstrate how to use the framework, I have written one implementation of the framework interfaces. The Car class, which implements Bookable, is really nothing more than a data holder. The CarStorage class is a little more interesting. Its basic responsibility is to manage the Car object’s persistence. This implementation uses serialization to implement persistence; if you want to look closer at the technique, it is provided in the example referenced at the end of the article. To add a new extension to the framework requires two steps:Add new classes that implement Bookable and BookableStorageEdit a Properties fileWith my limited framework it will not be possible to update an extension while running the program. Updating extensions while a program is running is achieved by using class loaders, so this is not a general limitation.Get the framework rollingIn order to make use of a framework of this kind we need a connection with the application that is to use it. I have written a simple command-line tool for booking various things. It takes a type (“Car”), a date (YYMMDD), and a properties file name as arguments. It parses the input, creates a Booker object accordingly and calls the Book method once, reports the result, and exits. The class is included in this zip file. Adding another extension to the framework, for example Truck, is a good exercise to understand the framework.Real-world applicationAt Industri-Matematik, we have designed a framework to enable reuse of translations (natural languages) of our main product. In short, the problem is to extract language-dependent strings from components of different types, and package those strings in a format suitable for an external translator. We identified two extension points: one point representing components from the product and one point representing packages for translation. We figured that the main processes should be very much the same regardless of component type and packaging method, so interfaces were defined for those parts.Extending the frameworkNote the data storage transparency in the framework. Implementations of the interfaces may access files, as in the example, access a database, or read data from the Web. Let’s say, for example, that you want to combine the booking systems of a number of car rental companies into one system. You could give the companies the interface specifications, and they could provide you with classes that you could add to the system without even taking it down. One data storage may be serialized objects like the ones described here, another may be a mainframe system, and yet another may be a relational database. Needless to say, the framework must be further extended to provide all the things you want to do with a booking system — adding new items, managing customers, and pricing, for example.Another interesting extension involves a GUI. Consider having a method JPanel getPanel() in the Bookable interface. You could provide a type-specific booking panel without breaking the framework’s implementation independence. For different types, you would display different information to the user: for conference rooms, you need to know the number of people the rooms can hold; for books, you may want to point to an online review; and so on.ConclusionExtending frameworks dynamically using properties files can be useful in many situations. But there are situations in which it’s definitely the wrong way to go. To sum up the findings of this tip, I have listed some pros and cons. ProsThe framework is independent of its extensions. This independence is useful when extensions are made by third parties or if the framework needs to be updated frequently.Customer extensions need no modifications to the framework. In fact, customers don’t even need access to the framework source code. The customer just writes a couple of classes, edits the properties file, and is ready to use it.ConsThe framework provider has no influence over the quality of the extensions.Some programming errors are not detected at compile-time, effectively eliminating one good quality-control mechanism.The interfaces that define the extension points of the framework must be well-defined. The first implementation of a framework is likely to be adapted to its first application and need a number of applications to stabilize and be truly reusable. For a further discussion of framework evolution, I recommend “Evolving Frameworks — A Pattern Language for Developing Object-Oriented Frameworks,” by Don Roberts and Ralph Johnson (see Resources).Fredrik Rubensson works at Industri-Matematik International (www.im.se) as a Software Developer. He is currently finishing an effort to create translation reuse tools for System ESS. He has been using Java for 1 1/2 years and still loves the language. Industri-Matematik International (IMI) provides supply chain and global logistics management solutions to manufacturers, distributors, and third-party logistics providers. Through the flagship product suite System ESS, Industri-Matematik synchronizes collaboration among global business partners with fast-moving, high-volume products, allowing those partners to anticipate and respond to customer delivery expectations worldwide.