AppLoader: Loading Application-Scoped Components
Early in my use of Application-scoped CFCs, I realized that I would have to have some mechanism to reload them when I changed the code.
I started with code that should look familiar to most people using CFCs:
<!--- load and init mycomp --->
<cfset Application.MyComp = CreateObject("component","MyComp").init("mydsn")>
<!--- load and init yourcomp --->
<cfset Application.YourComp = CreateObject("component","YourComp").init(mycomp)>
<cfset Application.init = true>
</cfif>
If you aren't already familiar with the topic, you can read an introduction to CFC instantiation.
As my application grew in complexity, so did my instantiation code:
<!--- load and init DataMgr --->
<cfset Application.DataMgr = CreateObject("component","DataMgr").init("mydsn","MSSQL")>
<!--- load and init SessionMgr --->
<cfset Application.SessionMgr = CreateObject("component","SessionMgr").init("Session")>
<!--- load and init users --->
<cfset Application.Users = CreateObject("component","Users").init(DataMgr)>
<!--- load and init permissions --->
<cfset Application.Permissions = CreateObject("component","Permissions").init(DataMgr,Application.Users)>
<!--- load and init security --->
<cfset Application.Security = CreateObject("component","Security").init(DataMgr,SessionMgr,Users)>
<cfset Application.init = true>
</cfif>
In practice, this would actually be a very short example but hopefully demonstrative of basic situation.
Despite the growing length of the code, this actually works well overall.
The major problem that I ran into with the approach however, was the lack of targetted reload. By that I mean that I would use the "reinit" variable in the URL, but I couldn't re-instantiate just one component with this code.
So, I could add a few conditionals to take care of this.
<cfif ListFindNoCase(URL.reinit,"DataMgr")>
<!--- load and init DataMgr --->
<cfset Application.DataMgr = CreateObject("component","DataMgr").init("mydsn","MSSQL")>
</cfif>
<cfif ListFindNoCase(URL.reinit,"SessionMgr")>
<!--- load and init SessionMgr --->
<cfset Application.SessionMgr = CreateObject("component","SessionMgr").init("Session")>
</cfif>
<cfif ListFindNoCase(URL.reinit,"Users")>
<!--- load and init users --->
<cfset Application.Users = CreateObject("component","Users").init(DataMgr)>
</cfif>
<cfif ListFindNoCase(URL.reinit,"Permissions")>
<!--- load and init permissions --->
<cfset Application.Permissions = CreateObject("component","Permissions").init(DataMgr,Application.Users)>
</cfif>
<cfif ListFindNoCase(URL.reinit,"Security")>
<!--- load and init security --->
<cfset Application.Security = CreateObject("component","Security").init(DataMgr,SessionMgr,Users)>
</cfif>
<cfset Application.init = true>
</cfif>
The conditionals add a lot of code but doesn't completely solve the problem. I also need to add code to reload all of the components if needed. More significantly, however, is another problem.
If I refresh the DataMgr component in the example, the Users and Security components will still be using the previous instantiation of the DataMgr component. I need a way to make sure that every time I initialize a component I also reinitialize any component to which that component is passed.
Enter AppLoader
I created a component to handle just this problem. I takes an XML definition of my component structure and manages targetted reloading of the those components.
<site>
<components>
<component name="DataMgr" path="DataMgr">
<argument name="datasource" arg="datasource" />
<argument name="database" arg="dbtype" />
</component>
<component name="SessionMgr" path="SessionMgr">
<argument name="scope" arg="SessionScope" />
</component>
<component name="Users" path="Users">
<argument name="DataMgr" component="DataMgr" />
</component>
<component name="Permissions" path="Permissions">
<argument name="DataMgr" component="DataMgr" />
<argument name="Admins" component="Admins" />
</component>
<component name="Security" path="Security">
<argument name="DataMgr" component="DataMgr" />
<argument name="SessionMgr" component="SessionMgr" />
</component>
</components>
</site>
To instantiate the components with AppLoader, use the following code:
<cfset Loader = CreateObject("component","AppLoader").init(XmlFilePath=ExpandPath("components.xml"))>
<cfinvoke component="#Loader#" method="setArgs">
<cfinvokeargument name="datasource" value="mydsn">
<cfinvokeargument name="dbtype" value="MSSQL">
<cfinvokeargument name="SessionScope" value="Session">
</cfinvoke>
<cfinvoke component="#Loader#" method="load">
<cfinvokeargument name="refresh" value="#url.reinit#">
</cfinvoke>
The setArgs methods allows you to pass arguments into AppLoader to be reference in the arg attribute of the argument tag.
The load method tells AppLoader to load the components. AppLoader will instantiate any component that doesn't already exist or any component that is indicated in the refresh argument (or all of them if the argument is true) as well as any components that depend on it (continuing down any number of levels).
The XML is pretty basic. The component element has two attributes:
- name: The name of the Application-scoped variable that will hold the reference to the component.
- path: The path to the component that you want to instantiate (the second argument of the CreateObject() function.
The component element has an argument element for each argument of the init() method of the component. The argument element has a few attributes:
- name: The name of the argument (AppLoader passes arguments in by name)
- component: If a component is being passed in, use this to indicate the name of the component to be passed in.
- arg: Use this attribute to indicate a value passed in to AppLoader through the setArgs method.
- value: Use this attribute to pass in a static value to the argument.
While the initial code examples require that the components be listed in order of dependency, the XML passed in to AppLoader can be in any order and it will still accurately determine the order of dependencies.
If you are using components as Application-scoped services, then AppLoader can simplify your development and allow for targetted reloading of your components.
If you have more complicated needs, then you might check out ColdSpring or LightWire (neither of which had I heard of when I first started AppLoader a few years ago).
Feel free to download AppLoader and use it as you see fit. It is open-source and free for any use.
Yep. ColdSpring is good. When I talked to Chris and Scott about targeted reloading, I understood that it didn't do that. This (along with plenty of code using this XML format and one other - unmentioned feature of AppLoader) was a major factor in me not switching to it for my own use.
That being said, I certainly agree that ColdSpring and LightWire are both good solutions in the DI/IOC space and (as I said) I would recommend them for most uses.
I was wondering how long it would be until I got the "Use ColdSpring instead" comment.
Incidentally, if ColdSpring does automatically create Application-scoped variables holding CFC references and does targeted reloading of those variables, I would love to link to an example of how to do that with ColdSpring as well (and that would open up more options for me as well).
I think the problem is that with ColdSpring, where you have nested sets of dependencies, the logic to just reload one bean would be quite complex. Basically it would have to recurse through the entire dependency tree to see if any other components depend on the target, and reload all of them as well (since they need a handle on the new instance). At that point, you're better off just reloading everything and knowing everything is fresh and up to date.
It has been effective for me. Yes, I am recursing through the components. Despite that, I have seen a significant time savings in targeted reloading versus reloading every component for each code change that I make.
That may suggest a coding problem, but that is really a separate topic.
The point is that it is a feature that I want that doesn't exist in ColdSpring.
If I recall correctly, I would also have to have an extra line of code for each bean to load it into Application scope from ColdSpring, which is extra work for me.
Again, I believe that ColdSpring is very good for most use-cases, but this fit mine and I didn't think it would hurt to make it available in case it helped someone else as well.
I haven't had much trouble with the cost of reloading ColdSpring, even for large models (usually just a few seconds). It's also quite rare that I would push a single change to production (everything is handled through change management and deployed via ANT and Subversion), but again, if that is the setup you're working with and you find this useful, there's certainly nothing wrong with using what works!
Just an aside, it's no extra code to put things into the application scope. If you put the ColdSpring BeanFactory in the application scope, all of the singletons it manages are also in the application scope as well. You get at them using getBean().
That is correct. It recursively determines what needs to be reloaded and then does so in the correct order. It did take some effort to write that into AppLoader, but after that I just set a URL variable (which is passed to the "refresh" argument of the load method) and AppLoader refreshes all of the components as needed.
For me this isn't a matter of pushing changes to production. This allows me to quickly make and test changes while I am on the development site. That being said, it can be very nice when I do need to push a small change (say, to one component) onto a production server with very short notice (it does happen).
Unless I am missing something, it is extra work to get components into Application scope using ColdSpring. If I have a component named "Users", then I automatically want Application.Users to reference that component. I don't want to have to say <cfset Application.Users = ColdSpring.getBean('Users')>. I admit that may be an unusual use-case, but I expect it isn't unique.
I haven't used ColdSpring, so I would be interested to hear if I have misunderstood how that works.
The great benefit of Dependency Injection / Inversion of Control is that it can wire in all your CFCs so nothing anywhere in your application knows or cares about application scope. That's especially important when you get into unit testing - it's much cleaner to inject mock objects into a CFC for testing rather than having to set everything up in application scope. Yuk!
It seems to me that you've hit a wall with a bad coding practice and then created something to make it easier to continue that bad practice. In time I expect you'll find the broad dependency on application scope everywhere will cause you problems and then you'll end up using ColdSpring anyway...
I think that you have misunderstood what I am doing here. None of my CFCs reference any shared scope variables - ever. The application scope is just a reference to the CFC that can be referenced by my .cfm files.
I pass in components via the init() method of my components, whereupon I copy them from arguments scope to variables scope.
I absolutely agree that CFCs should be well encapsulated. They never know anything about the outside world except what they are explicitly told about via init() or some other method. Application scoping the components just makes for a handy reference for .cfm files. I seem to recall that you have yourself mentioned this approach.
If I mistaken anywhere, I would very much like to know.