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.