Monday, December 22, 2008

Creating a Model-Glue-esque Bean Factory

I've been playing around with Model-Glue 3 lately and found that there are some really nice features that eliminate a lot of repetitive code, such as automatically injecting helpers and beans into a Controller without having to create getters and setters.

One problem I had with these new features is that they're only available to Model-Glue controllers (obviously) and I want to use them in my model and service layer. And so it began...

My first implementation required each object to extend a base object, which worked, but didn't feel quite right; there was still too much repetitive code. Plus I don't really like extending objects directly since it binds each object to a single implementation of the extended object. I'll get into that a little later.

After playing around with things a little more, I found a way that still works, is a lot cleaner, feels a lot better, and has an added bonus.

The first thing I did was create a custom bean factory that extends Brian Kotek’s DynamicXmlBeanFactory extension for ColdSpring (http://coldspringutils.riaforge.org/) and overrides the getBean method.

My bean factory is created onApplicationStart() like this:


<cfset application.beanFactory = createObject("component", "coldspring.beanutils.DynamicBeanFactory").init("config/coldspring/beans.xml", application.config) />


My bean factory looks like this:


<cfcomponent output="false" extends="coldspring.beanutils.DynamicXMLBeanFactory">

<!------>

<cffunction name="init" access="public" output="false" returntype="any">
<cfargument name="pathToColdspringXML" required="true" type="string" />
<cfargument name="params" required="false" default="" />

<cfif not isStruct(arguments.params)>
<cfset arguments.params = StructNew() />
</cfif>

<cfset super.init() />
<cfset loadBeansFromDynamicXmlFile(arguments.pathToColdspringXML,arguments.params) />

<cfreturn this />

</cffunction>

<!------>

<cffunction name="getBean" access="public" output="false" returntype="any">
<cfargument name="beanName" required="true" />

<cfset var local = StructNew() />

<!--- get the bean from coldspring --->
<cfset local.bean = super.getBean(arguments.beanName) />

<!--- inject the bean with helpers, beans, includes, etc... --->
<cfinclude template="DynamicBeanFactoryMixin.cfm" />

<cfreturn local.bean />

</cffunction>

<!------>

<cffunction name="injectVariable" access="public" output="false" returntype="void" hint="I get copied into beans in order to set variables">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

<cfset variables[arguments.key] = arguments.value />

<cfreturn />

</cffunction>

<!------>

</cfcomponent>


You'll notice my getBean method includes DynamicBeanFactoryMixin.cfm, which is responsible for injecting additional logic into my bean. I'm not sold on using a .cfm include, but I wanted to re-use my code in another component that I've been working on and I didn't want to duplicate my efforts. I figure I can always refactor later, but wanted to get the base functionality down.

The DynamicBeanFactoryMixin looks like this:


<!--- inject a function to set variables into the "variables" scope --->
<cfif StructKeyExists(variables,"injectVariable")>
<cfset local.bean.injectVariable = variables.injectVariable />
<cfelseif StructKeyExists(this,"injectVariable")>
<cfset local.bean.injectVariable = this.injectVariable />
</cfif>

<cfif StructKeyExists(variables,"helpers")>

<!--- if the helpers are already defined in the current bean, set them into the target --->
<cfset local.bean.injectVariable("helpers",variables.helpers) />

<cfelse>

<!--- inject the helpers and create the "helpers" scope --->
<cfset local.bean.injectVariable("helpers",super.getBean("helpers").getHelpers()) />

</cfif>

<cfset local.metaData = getMetaData(local.bean) />

<cfif StructKeyExists(local.metaData,"includes")>

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.includes#" index="local.includedBeanName">

<cfset local.includedBeanName = trim(local.includedBeanName) />

<cfif StructKeyExists(variables,"getBean")>
<cfset local.includedBean = getBean(local.includedBeanName) />
<cfelse>
<cfset local.includedBean = application.beanFactory.getBean(local.includedBeanName) />
</cfif>

<!--- get all the public functions in the included bean and copy them to the main bean --->
<!--- even though private functions aren't copied, references to them from within copied functions will still work --->
<cfloop collection="#local.includedBean#" item="local.includedBeanVariableName">

<!--- only copy the function if it isn't defined yet --->
<cfif not StructKeyExists(local.bean,local.includedBeanVariableName)>

<!--- put the function into the object (the "this" scope) --->
<cfset local.bean[local.includedBeanVariableName] = local.includedBean[local.includedBeanVariableName] />

<!--- set the function into the variables scope, otherwise would need to reference the function as this.fn() --->
<cfset local.bean.injectVariable(local.includedBeanVariableName,local.includedBean[local.includedBeanVariableName]) />

</cfif>

</cfloop>

</cfloop>

</cfif>

<!--- create a container for all the beans that will be injected --->
<cfset local.beans = StructNew() />

<!--- check to see if the bean needs to inject any beans --->
<cfif StructKeyExists(local.metaData,"beans")>

<!--- include each bean in the order specified --->
<cfloop list="#local.metaData.beans#" index="local.injectedBeanName">

<cfset local.injectedBeanName = trim(local.injectedBeanName) />

<!--- if getBean is defined, use it --->
<cfif StructKeyExists(variables,"getBean")>
<cfset local.injectedBean = getBean(local.injectedBeanName) />
<cfelse>
<cfset local.injectedBean = application.beanFactory.getBean(local.injectedBeanName) />
</cfif>

<!--- insert each bean into the local beans struct, which will be injected after the loop --->
<cfset local.beans[local.injectedBeanName] = local.injectedBean />

</cfloop>

</cfif>

<!--- inject the beans --->
<cfset local.bean.injectVariable("beans",local.beans) />


I'll try to describe what's going on here.

First, I copy a reference to an "injectVariable" function into the bean that allows me to inject variables (helpers, beans, etc...) into the variables scope of the target bean.

Next, I create the "helpers" scope inside the bean by using a HelperInjector extension for ColdSpring that I wrote. Here's how I define my helpers in ColdSpring:


<bean id="helpers" class="coldspring.beanutils.HelperInjector">
<property name="directories">
<list>
<value>/clipboard/app/helpers/</value>
</list>
</property>
<property name="files">
<map>
<entry key="date"><value>/clipboard/app/helpers/date.cfc</value></entry>
</map>
</property>
</bean>


Helpers can be passed in as a list (array) of directories or a map (struct) of individual CFCs. This is a little different than the viewMappings node in Model-Glue, but I think it's a little more flexible since you can include, exclude, and override specific components.

In case you're curious, the HelperInjector extension looks like this:


<cfcomponent name="HelperInjector" output="false">

<!------>

<cffunction name="init" access="public" output="false" returntype="any">

<cfset variables.instance = StructNew() />

<cfset setDirectories(ArrayNew(1)) />
<cfset setFiles(StructNew()) />

<cfreturn this />

</cffunction>

<!------>

<cffunction name="setDirectories" access="public" output="false" returntype="void">
<cfargument name="directories" required="true" />

<cfif isArray(arguments.directories)>
<cfset variables.instance.directories = arguments.directories />
</cfif>

</cffunction>

<!------>

<cffunction name="getDirectories" access="public" output="false" returntype="any">
<cfreturn variables.instance.directories />
</cffunction>

<!------>

<cffunction name="setFiles" access="public" output="false" returntype="void">
<cfargument name="files" required="true" />

<cfif isStruct(arguments.files)>
<cfset variables.instance.files = arguments.files />
</cfif>

</cffunction>

<!------>

<cffunction name="getFiles" access="public" output="false" returntype="any">
<cfreturn variables.instance.files />
</cffunction>

<!------>

<cffunction name="getHelpers" access="public" output="false" returntype="any">

<cfif not StructKeyExists(variables.instance,"helpers")>
<cfset setHelpers() />
</cfif>

<cfreturn variables.instance.helpers />

</cffunction>

<!------>

<cffunction name="setHelpers" access="public" output="false" returntype="any">

<cfset var directories = getDirectories() />
<cfset var directory = "" />
<cfset var files = "" />
<cfset var file = "" />
<cfset var i = "" />

<cfset variables.instance.helpers = StructNew() />

<cfif ArrayLen(directories) neq 0>

<cfloop from="1" to="#ArrayLen(directories)#" index="i">

<cfset directory = directories[i] />

<cfdirectory action="list" directory="#expandPath(directory)#" name="files">

<cfloop query="files">
<cfif listLast(files.name,".") eq "cfc">
<cfset setHelper(listFirst(files.name,"."),injectComponent(directory & "/" & files.name)) />
</cfif>
</cfloop>

</cfloop>

</cfif>

<cfset files = getFiles() />

<cfloop collection="#files#" item="i">

<cfif listLast(files[i],".") eq "cfc">
<cfset setHelper(i,injectComponent(files[i])) />
</cfif>

</cfloop>

</cffunction>

<!------>

<cffunction name="setHelper" access="private" output="false" returntype="any">
<cfargument name="key" required="true" />
<cfargument name="value" required="true" />

<cfset variables.instance.helpers[arguments.key] = arguments.value />

<cfreturn />

</cffunction>

<!------>

<cffunction name="injectComponent" access="private" output="false" returntype="any">
<cfargument name="path" required="true" />

<cfset var helperPath = expandPath(arguments.path) />
<cfset var helperFileName = listFirst(getFileFromPath(helperPath), ".") />
<cfset var componentName = replaceNoCase(arguments.path, "/", ".", "all") />
<cfset var instance = "" />

<cfif left(componentName, 1) eq ".">
<cfset componentName = right(componentName, len(componentName) - 1) />
</cfif>

<cfset componentName = listDeleteAt(componentName, listLen(componentName, "."), ".") />

<cfset instance = createObject("component", componentName) />

<cfreturn instance />

</cffunction>

<!------>

</cfcomponent>


Next, I check the bean's metadata for an "includes" attribute, which is similar to the "extends" attribute but with a twist.

When you extend a component, you must specify the full path to the CFC, such as extends="coldspring.beanutils.DynamicXMLBeanFactory", which directly binds your component to the implementation of another component, which doesn't lend itself towards loose coupling. Plus, you can only extend a single component.

With the "includes" attribute, you can specify a comma-separated list of ColdSpring beans that you want to extend. That way, the dependencies are still managed in ColdSpring. If you decide to switch implementations of a component, you only have to update the class path in your bean definition to point to a different CFC; the rest of your code remains unchanged. It's almost like extending an interface. Also, it allows you to extend multiple beans, which could come in handy and help keep your class files smaller.

I've been using the "includes" attribute in my DAOs and beans in order to include generic functionality, such as creating a new instance of a bean or creating implicit getters and setters by using onMissingMethod(). Doing this allows my beans to be a simple list of properties like this:


<cfcomponent name="Product" table="products" includes="bean" output="false">

<cfproperty name="id" type="string" default="" />
<cfproperty name="name" type="string" default="" />
<cfproperty name="abbrev" type="string" default="" />

</cfcomponent>


Hopefully that makes sense. I'm still working on a DynamicTransientFactory extension and DynamicBean extension, so I'll go into more details in a future post. I just thought it might explain how the "includes" attribute works better if I provided an example.

Finally, I create the "beans" scope inside the target bean, which allows me to specify which beans I need in the component's metadata without having to create getters and setters for each injected bean. This also keeps my ColdSpring XML file smaller since I don't need to specify all the dependencies between beans. I tried keeping the functionality as similar as possible to the Model-Glue 3 implementation, since I really liked how that worked.

All in all, I'm quite happy with the way things have turned out so far. I had a little trouble with the .cfm mixin and getting it to work with the "includes" attribute, so I had to hack a couple things like referencing application.beanFactory directly and requiring a "helpers" bean to be defined. But when it came down to it, I decided to go with convention over configuration.

I tried commenting the code where I could, so hopefully it makes sense. I haven't fully tested everything yet, but it seems to work so far. Also, I should note the code requires ColdFusion 8.

UPDATE: I re-worked the bean factory.

2 comments:

  1. I don't want to go any further with my softball app without implementing this. I made use of the helpers scope in Model-Glue yesterday, only to remember it is not accessible everywhere I need it to be. My plan is to get this in place tonight :).

    ReplyDelete

Note: Only a member of this blog may post a comment.