I've been playing more with CFC and method injection, and started writing up a CFC to handle it intelligently for me.
After some playing around, I thought it might be nice to document the things I have found about doing method injection, including some very interesting behaviour.
First things first - a UDF is actually it's own class in it's own right, completely seperate from the CFC defintion. It's more like the CFC is simply linked to the UDFs that it is told to, rather than it actually being a solid container. This explains what happens when you type:
<cfoutput>#obj.myMethod#</cfoutput>
rather than
<cfoutput>#obj.myMethod()#</cfoutput>
and you get all that weird stuff like 'cfindex2ecfm1512334719$funcMYMETHOD@1b1dd12'. This is just the output of the toString() function on the UDF itself. It seems to be the generated class name.
So if you do some reflection on the UDF class (udfName.getClass() works just fine), you find that there is a getMetaData() function - so depending on your preference:
myUDF.getMetaData() or getMetaData(myUDF) works - and returns a struct with everything you could ever ask for:
- name
- hint
- access
- returntype
- output
- An array of parameters
This is rather useful if you want to maintain the name of aUDF from one place to another.
Now... method injection in and of itself is very simple:
1) myCFC["methodName"] = myUDF;
Presto, you have created a new public UDF on a CFC.
You can also do the same thing inside a CFC by either doing (but each will make it work differently):
2) this["methodName"] = myUDF;
3) variables["methodName"] = myUDF;
But some things to note:
If you add an access="public" or access="private" UDF directly to a CFC (method 1 or 2), the UDF will be PUBLIC.
BUT, if you add a UDF to the variables struct inside a UDF (method 3), regardless of whether or not it's an access="public" or "private" or "package", it will be a PRIVATE UDF. (How to get it to the variables scope, I will leave up to you ;o) )
The real clincher here however, is that if you add a access="package" UDF directly to a CFC (method 1 or 2), then it will only be accessible from the folder the CFC is in. So obviously there is some runtime checking there that determines if it is being called from inside its own package.
That is pretty much the basics of method injection - hope you find that helpful.
A while ago, I posted an example of creating CFC definitions on the fly using a function I wrote called StructToBean().
It essentially used the concepts of Prototype Based programming to implement the getter and setter methods on a blank CFC, so that the data of the Struct was encapsulated in an object without having to actually write the object itself.
I put that post on here while I was writing a library that was an attempt to cut down on the amount of time I spent writing simple Beans for use as Business Objects.
The basics of the library was such that I had a XML config file that defined each of the Beans that I wanted to be able to use in the system, like so (cut down version):
<compoundObjects>
<package name="Address">
<object name="Street">
<property name="number" type="numeric" />
<property name="name" type="string" />
</object>
</package>
</compoundObjects>
The library would then create a .cfm file on the fly, which would contain all the UDFs required to add the required getters and setters to the blank CFC.
When the specified Bean was called for, the library would create a blank CFC, and insert each of the methods onto the blank CFC at runtime, so it was ready to use.
As personal projects tend to do, this idea fell by the wayside for a while, and now I've been rebuilding compoundtheory, it's come back to me, and I've started to question whether or not it's really something I want to get behind in terms of how I build my CF applications. I'm still pretty much sitting on the fence as to whether or not I'm actually going to rewrite the code (it needs a good refactor), and utilise it in the new version of CT.
While CFCs aren't exactly prototype based, the basic flexibility of being able to utilise slots in the CFC to add and remove methods remains the same, but the question that I am wondering at the moment, is should it be used.
On one side there are alot of interesting things you can do with method injection and removal:
- Adding Object functionality on the fly
- Create Interfaces on the fly
- Faking Base class functionality
- Changing protection on methods due on the fly (i.e. remove a init() constructor once it has been called once)
- Fix bugs on the fly
But obviously there are cons as well:
- Adds a whole level of complexity to an application that is harder to document
- Breaks some core OO concepts (which may not be a bad thing, but you have to be happy about it)
- Can lead to some awefully spagettied code if not managed correctly
Now, obviously, whether or not you leverage this capability is very dependent on applicaiton requirements, your own personal views on OO Analysis and Design etc, but the usage of this functionality does exist, yet seems to be a topic that is not talked about much in the CF community.
So I ask you guys, as an information gathering excercise:
- Have you considered taking a Prototype based approach before?
- If you had, did you do it?
- If you did, what was it, and why did you do it? (I would love to hear of some places it was used!)
- If you didn't, why not?
- Would/wouldn't you consider using this approach for something in the future?
That being said, in the next few weeks (as I continue to develop the next version of CT), I'll do a follow up post on whether or not I decided to utilise the prototype based approach to developing the Business Objects for this site, and why I decided to do it, and hopefully this will get some interesting discussion going!
Some more reading on Prototype Based Programming
Hit this one the other day, and thought it was rather cute, because I hadn't even thought of it - even though it makes sense.
If you do a cfmodule call inside a CFC, the caller scope refers to the CFC itself.
This kinda took me aback, because I'm so used to calling a cfmodule from another cfm page, that it was an immediate assumption the caller would be a cfm page.
But if you think about it - the caller scope, just refers to the place that calls the cfmodule - so really it makes alot of sense.
(That and if you look at the underlying code structure, you can see that a cfm page and cfc both extend the same base class, so it's not totally weird).
Just thought it was cute, so I would share.