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.