Introduction
Recently, in a mailing list I’m on, the question of code reuse came up. Someone wanted to decide at runtime which of two classes to extend in order to share the common code for the job (which was most of the code) across two slightly different implementations. The problem with that, however, is that you have to use a string literal for the extends=”” attribute of the cfcomponent tag. There is, however, a fairly simple solution that accomplishes the same thing.
Let me give a brief overview of the solution and then get into details.
When you’re using the extends=”…” attribute, what you’re really doing is saying “all my common code is in the class I am extending”. We can also call the class we’re building on the “base class”, which is actually a term I’ll be using a lot in this post. So, the process is this: write your base class that contains all the common code you have, then write two other CFCs that specify the base class in the extends attribute. These two CFCs should also take into consideration whatever differences there are that require you to have 2 classes in the first place. That takes care of sharing code between two slightly different implementations. The question then becomes “how do I determine which class to use when I have to do the work that my 3 new CFCs allow me to do?”
The easiest way is to use a simple factory class. This means a CFC that has only 2 methods: init() and getInstance() (or something similar). You could also simply specify a cfif block somewhere in your code that would allow you to create an instance of the right CFC, but that’s a “code smell” red flag that screams “I’m gonna have these cfif blocks all over my application, or have to re-run them on every request!”
That’s not good. So… let’s get on to some sort of real example. What to use, what to use?
We could use creating a data source under CF 6.1 with one class and CF7 or 8 using the AdminAPI with the other, but that’s entails a great deal of code and I think the idea would get lost in the process. How about a simple set of CFCs for reading a text file into a query?
That’ll work.
So what do we need? We’ll need four components:
- BaseQueryParser.cfc
- WddxQueryParser.cfc
- CvsQueryParser.cfc
- QueryParserFactory.cfc
BaseQueryParser.cfc
BaseQueryParser.cfc is the base class for our query parsers and contains all the common code that the implementation classes (or child classes, or extension classes, or any of several other names) will use to do their jobs. It looks like this:
<cfcomponent name="BaseQueryParser" displayname="BaseQueryParser"> <cffunction name="init" access="public" returntype="factories.model.data.BaseQueryParser" output="false"> <cfargument name="qryData" type="string" default="" /> <cfif len(arguments.qryData) GT 0> <cfset setQueryData(arguments.qryData)> <cfelse> <cfthrow message="Argument qryData cannot be an empty string!" detail="#qryData#" /> </cfif> <cfreturn this /> </cffunction> <cffunction name="getQueryTextAsQuery" access="public" returntype="query" output="false"> <cfthrow message="This is a base class." detail="You must extend BaseQueryReader and implement a working version of getQueryDataAsQuery()." /> </cffunction> <cffunction name="setQueryData" access="private" returntype="void" output="false"> <cfargument name="qryData" type="string" required="true"> <cfset variables.qryData = arguments.qryData > </cffunction> <cffunction name="getQueryData" access="private" returntype="string" output="false"> <cfreturn variables.qryData /> </cffunction> </cfcomponent>
Pretty basic stuff. Init() method for initialization, a default method called getQueryTextAsQuery() that throws an error if called (because we don’t want people actually using this one, just the other two), and get/set methods for the string that will be turned into a query.
Take a note though: any code in this class will be used by the other two classes. And, just to make things more interesting, methods that the other classes implement (like init() and getQueryDataAsQuery()) don’t just overwrite these in memory… they exist in a special scope called “super” and can be used by calling super.init(), for example.
Which, in fact, we do in our very next section…
WddxQueryParser.cfc
And now, for you to behold, is the first of BaseQueryParser.cfc’s two children:
<cfcomponent name="WddxQueryParser" displayname="WddxQueryParser" extends="BaseQueryParser"> <cffunction name="init" access="public" returntype="factories.model.data.WddxQueryParser" output="false"> <cfargument name="qryData" type="string" default="" /> <cfset super.init(argumentCollection=arguments)> <cfreturn this /> </cffunction> <cffunction name="getQueryTextAsQuery" access="public" returntype="query" output="false"> <cfset var qryData = getQueryData()> <cfset var qry = 0> <cfwddx action="wddx2cfml" input="#qryData#" output="qry" /> <cfreturn qry /> </cffunction> </cfcomponent>
Take note of how much less code this CFC has than the other one… just two methods. It implements getQueryTextAsQuery()… but look at the init() method. It actually calls super.init()… so we’re not just sharing whole methods here, we’re actually sharing particular bits of functionality, like loading the “instance data” (in this case the WDDX string that will become a query) into the variables scope. It was only written once… but now it’s being used here.
Next!
CsvQueryParser.cfc
<cfcomponent name="CsvQueryParser" displayname="CsvQueryParser" extends="BaseQueryParser"> <cffunction name="init" access="public" returntype="factories.model.data.CsvQueryParser" output="false"> <cfargument name="qryData" type="string" default="" /> <cfset super.init(argumentCollection=arguments)> <cfreturn this /> </cffunction> <cffunction name="getQueryTextAsQuery" access="public" returntype="query" output="false"> <cfset var qryData = listToArray(getQueryData(),chr(10))> <cfset var i = 0> <cfset var r = 0> <cfset var row = ""> <cfset var columns = qryData[1]> <!--- assumes first row is the column list ---> <cfset var qry = queryNew(columns)> <cfset columns = listToArray(columns,",")> <cfloop from="2" to="#arrayLen(qryData)#" index="i"> <cfset row = listToArray(qryData[i],",")> <cfset queryAddRow(qry)> <cfloop from="1" to="#arrayLen(row)#" index="r"> <cfset querySetCell(qry,columns[r],row[r])> </cfloop> </cfloop> <cfreturn qry /> </cffunction> </cfcomponent>
Note, again, the minimalistic approach taken to code here… only enough to do the specific things that changed between the base class and this one… so only getQueryDataAsQuery() and init() are here, and even init()’s the same as init() in theWDDX version.
QueryParserFactory.cfc
But now the real fun begins! Now we get to create the class that will allow our application to use these three CFCs seamlessly… enter the QueryParserFactory component. Check out the code:
<cfcomponent name="QueryParserFactory" displayname="QueryParserFactory"> <cffunction name="init" access="public" returntype="factories.model.data.QueryParserFactory" output="false"> <cfreturn this /> </cffunction> <cffunction name="getQueryReader" access="public" returntype="factories.model.data.BaseQueryParser" output="false"> <cfargument name="qryData" type="string" required="true" /> <cfset var reader = 0> <cfif find("wddx",arguments.qryData)> <cfset reader = createObject("component","factories.model.data.WddxQueryParser").init(arguments.qryData)> <cfelse> <cfset reader = createObject("component","factories.model.data.CsvQueryParser").init(arguments.qryData)> </cfif> <cfreturn reader /> </cffunction> </cfcomponent>
Check out the middle… when you call this component’s getQueryReader() method, you pass in whatever string you’re needing query-ized. It figures out which CFC to return to you, instantiates and configures it, and returns it… BOOM. It couldn’t really get much easier for your application.
You can use something like this to check system settings, environment variables, any number of different things, and return the right CFCs for the job, configured and ready to go, just like that. It all comes down to using inheritance and a factory to deliver the correct child class.
Summary
I’ve written a little sample application to go with this post, and you can download it below. It has an index.cfm file that reads in some text from a WDDX and a CSV file and uses the factory to get a query from each of the child classes… so you can see this in action.
I hope this was helpful!!
Comments on: "Use Simple Factories and Inheritance to Reuse Code" (2)
Looks like you just copied and pasted the last three code fragments – they’re all the same.
Do your setters and getters really need to be public? Since you require a non-empty string for init() but do not validate the string on the setter, I’m assuming you don’t actually intend the setter to be called externally in which case you should make them private so they can only be called within that CFC or its descendants.
LikeLike
EGADS! Thanks.
Fixed… and yeah, the setter is now private.
Much apprecaited.
LikeLike