Publishing Software Libraries With An Optional External Class Loader
This morning, I was going to tinker with creating a ColdFusion component wrapper for the parts of the Google Closure compiler that I looked at over the weekend. But, when I sat down to code, I realized that I had an interesting dilemma on my hands. The code from my previous blog post was written using ColdFusion 10; here at the office, however, I'm still on ColdFusion 9. So, how do I instantiate the Java classes provided in the compiler.jar archive?
At home, it was easy - I just added the JAR file as a per-application setting (a feature of ColdFusion 10) and used createObject(); at the office, however, I would have to either use the JavaLoader or add the JAR file to my ColdFusion class paths. But, this thinking lead me to another question - once I chose an approach, how should I build that decision into my software library?
As I was considering my options, I remembered something that I had read in Practical Object-Oriented Design in Ruby, by Sandi Metz. In her chapter on "Managing Dependencies," Metz suggested isolating the creation of new instances that could not be injected into your object:
If you are so constrained that you cannot change the code to inject a Wheel into a Gear, you should isolate the creation of a new Wheel inside the Gear class. The intent is to explicitly expose the dependency while reducing its reach into your class. Practical Object-Oriented Design in Ruby (p. 42).
This sounded like the perfect plan! I could isolate the creation of the Closure compiler classes within private methods; then, I could create a sub-class of my ColdFusion component wrapper that overrides the instance-creation methods, using a Class Loader instead of the native createObjet() function.
To experiment with this approach, I created a partial ColdFusion wrapper for the Java class, HashSet. In the following code, notice that the actual instantiation of the HashSet class is isolated within its own method:
<cfscript>
component
output = false
hint = "I provide an interface to some Java library (ex, HashSet)."
{
// Return an initialized wrapper for the Java HashSet collection.
public any function init( array initialItems = [] ) {
collection = createCollection();
addAll( initialItems );
return( this );
}
// ---
// PUBLIC METHODS.
// ---
// I add all of the given items to the collection. Is chainable.
public any function addAll( required array items ) {
collection.addAll( items );
return( this );
}
// I return the hashSet as a ColdFusion array.
public array function toArray() {
var result = [];
result.addAll( collection );
return( result );
}
// ... and many more methods ...
// ---
// PRIVATE METHODS.
// ---
// I instantiate and return the Java collection. This create
// action is inside a private method so that it can be overridden
// in situations that may require a class-loader.
private any function createCollection() {
return(
createObject( "java", "java.util.HashSet" ).init()
);
}
}
</cfscript>
With the creation of the HashSet encapsulated within its own method, I can now create a sub-class component to override the way in which the Java object is created:
<cfscript>
component
extends = "HashSet"
output = false
hint = "I extend the core library, but provide alternate CREATE methods."
{
// Return an initialized wrapper for the Java HashSet collection.
public any function init(
required any classLoader,
array initialItems = []
) {
loader = classLoader;
return(
super.init( initialItems )
);
}
// ---
// PRIVATE METHODS.
// ---
// I instantiate and return the Java collection using the given
// class loader.
// --
// NOTE: I am using a VECTOR here instead of a HASHSET so that I
// can see a difference in the output - Vector produces ordered
// output; hashset does not.
private any function createCollection() {
return(
loader.createInstance( "java.util.Vector" ).init()
);
}
}
</cfscript>
Here, the ColdFusion component constructor takes an additional argument - the external class loader. Then, the sub-class overrides the createCollection() method, using the external class loader to create the HashSet instance.
You may have noticed that my sub-class isn't actually creating a HashSet; instead, it's creating a Vector object. I did this so that I could see a difference in my testing output. Both of these Java classes implement the Collection interface; but, the Vector class is ordered whereas the HashSet class is not ordered (or, at least you cannot depend on its ordering).
Once I had these two classes, I then created two HashSet wrappers, one that relied on the native createObject() function and one that relied on an external class loader:
<cfscript>
// Let the HashSet use the native class loader.
hashSet = new lib.HashSet( [ 1, 2, 3 ] );
writeOutput( "Native Class Loader <br />" );
writeOutput( arrayToList( hashSet.toArray(), ", " ) );
// ------------------------------------------------------ //
// ------------------------------------------------------ //
writeOutput( "<br />" );
// ------------------------------------------------------ //
// ------------------------------------------------------ //
// This time, create a HashSet that will defer to this custom
// loader when it needs to create Java objects that rely on
// external JAR files.
loader = new lib.ClassLoader();
altHashSet = new lib.HashSetWithLoader( loader, [ 4, 5, 6 ] );
writeOutput( "Custom Class Loader <br />" );
writeOutput( arrayToList( altHashSet.toArray(), ", " ) );
</cfscript>
When I run the above code, I get the following output:
Native Class Loader
3, 2, 1
Custom Class Loader
4, 5, 6
Notice that the sub-class output is in order, "4, 5, 6," whereas the primary class output is in reverse order, "3, 2, 1." This indicates that the sub-class was, indeed, overriding the createCollection() method, deferring to the external class loader.
I really like this approach! It allows me to build software libraries that depend on some assumptions; but, that are flexible enough to allow those assumptions to be overridden by sub-classes in special use-cases.
Want to use code from this post? Check out the license.
Reader Comments
@All,
I just put this practice into affect in my ClosureCompiler.cfc facade to the Google Closure compiler:
www.bennadel.com/blog/2518-ClosureCompiler-cfc-A-ColdFusion-Facade-For-Google-s-Closure-Compiler.htm
Since the library depends on the compiler.jar file, it can either use the core ColdFusion class paths; or, you can sub-class the ClosureCompiler.cfc and override the methods that create and initialize the underlying Java objects.