Truly mixing your components

I've recently been looking at different ways to extend and reuse code within various frameworks with a couple of the guys at work. We looked a all aspects of the systems that go to make up a web application and the content management that is involved.

One of the area's we looked at was data definition, how it could be made more intuitive, more convention based, more extensible and more reusable. We talked around a lot of concepts. One of those concepts was something called data streams from the Fedora Content Model.

I can't pretend that I actually understand what the Fedora model is doing, but what I took away from it is that it would be neat if we could have content objects eg. "Product" or "News" that could be built using pre-built "data streams". The data streams would provide properties and functionality that the content object would inherit and be able to use. A content object would have one or more datastreams and more could be added at a later date to extend the content object further without having to write any additional functionality.

Let me give you a quick example : News - has a title, some body content and a publish date. At some point down the line the client decides that they need to have historical versions of the news. Previously we built a data stream for products that allows us to manage version data for products. Regardless of the fact that products have completely different data properties the functionality for version control is basically the same. The functions relating to version control would mostly be identical. I should be able to take the version control data stream from products, apply it to the news content object and immediately be able to have version control in the news object.

Now you may be thinking inherit/extend, but that really only works for one object in one direction. It should be possible to use more than one data stream in a content object. You might also be thinking "Dependency Injection", but that doesn't really work in this instance either. You have to know that you are injecting a bean/service/helper into your component. You have to "get" the injected component and write functions inside your parent component that map to the injected functions, so that they become available outside of your parent component.

So how the hell can you simply "inherit" the complete contents of another CFC into a parent CFC? I decided to make a prototype to see if this concept would even be possible in ColdFusion. Turns out it is, using the onMissingMethod functions and the metadata functions. At this point it is definitely worth reading John Whish's article on onMissingMethod. Some of the techniques he uses I've blatently stolen and extended upon. Under standing those techniques will help with understanding some of the stuff I do that I'm not going to explain.

The code I'm going to share with you is a prototype, so its really only meant to prove the point that it is possible. Its probably not very practical and probably not very efficient, but it is pretty cool.

So, the goal I want to achieve; a cfc with a bunch of cfproperties, some of which are just data and some of which that are streams, and some functions. The functions in the streams should function exactly the same way as functions in the cfc itself.

The first thing I did quite unconciously is decide that I would have a naming convention for the properties, so that it was easy to tell the difference between a property and stream. Stream property names end in the word "Stream" - inventive!! Here's a simple News content object :

view plain print about
1<cfcomponent displayname="newsArticle" output="false" extends="ContentObject">
2    <cfproperty name="articleStream" type="CO.streams.ArticleStream" >
3
4    <cffunction name="dumpThis">
5        <Cfdump var="#this#">
6    </cffunction>
7</cfcomponent>

Couple of things to notice : extends="ContentObject" - we're telling this component that it a "ContentObject" type="CO.stream.ArticleStream" - there are no properties in this component to do with data there is only an "article" data stream in the component "CO.streams.ArticleStream" ArticleStream looks like this :

view plain print about
1<cfcomponent output="false" displayname="Article Data Stream" extends="CO.base">
2    <cfproperty name="Title" type="string">
3    <cfproperty name="description" type="string">
4    <cfproperty name="ImageName" type="string">
5
6    <cffunction name="uploadImage" access="public">
7        <cfreturn true>
8    </cffunction>
9
10</cfcomponent>

The ContentObject and ArticleStream CFCs both extend off of a base CFC. There's a bunch of functionality in there that both streams and content objects will need. At this time in this prototype I don't have any additional functionality in streams that would require an intermediate "StreamObject" CFC, so I'm just heading straight to the "base" CFC.

In the Article Stream you will see I have a bunch of simple properties and a stub function called "uploadImage" - make believe that this function actually handles all the upload etc relating to the ImageName property. [Normally stuff like file uploads would be handled in a service not a business object. I just came up with a random function name when I was prototyping this. Now shush Mr Whish & let me get on with this. ;)]

In my index.cfm file I have a bit of code that looks like this :

view plain print about
1<cfscript>
2oNews = new news();
3
4news.setTitle('Ben Nadel and Ray Camden caught in comprising photograph');
5news.setDescription("Today allegation were made that Ben Nadel has been using a fake arm to secretly pinch the behinds of people he is photographed with. In this photograph of Ben and Ray, you can clearly see the poor stitch work in the shoulder of the fake arm and Ben's real hand reaching to grasp Ray's left buttock");
6uploadResult = news.uploadImage('benandray.jpg');
7
8writeOutput('Image upload complete : '&YesNoFormat(uploadResult));
9
10writeDump(news.getMemento());
11
</cfscript>

So I create a new News object, I add a title and an inflamtory and entirely untrue news description, I "upload" an image and get a result which I output. I'm also outputting a memento using writedump(). The memento idea I stole from Mark Mandel's Transfer ORM. Its a nice way to easily dump the data stored in an object and helped me with debugging the prototype, so I left it in there. Something to note here: I'm using CF9 notation, but equally you could call oNews = createObject('component','news').init() and the outcome would be the same.

How the hell did that all work then with just a cfproperty in my news ContentObject? Well hopefully the init() on the createObject will have given a hint.

The ContentObject CFC has 3 functions, initMetaProperties, initStreamProperties and initStreamFunctions. InitMetaProperties is called when the content object is initialised. It pulls the properties out of the content object and then either save those property attributes and sets the default values or runs the initStream methods.

The initStreamProperties method perform the same process of saving the properties as happens for the content object, but rather than saving the properties into the variables scope of the stream CFC it pulls them forward into the content object variable scope. The initStreamFunctions does the same thing, for the functions that are in the stream cfc.

Now that I have the properties and functions pulled into a nice struct in my content object I need to be able to call them. The properties and functions in the content object itself are fine, we can call them as we would normally. The properties and functions from the stream still don't exist in the component in the normal way, nor can we call functions that are assigned to variables in the normal way. This is where the onMissingMethod function in the base cfc comes into its own.

The onMissingMethod has 3 "modes"; get and set, both of which you will have seen in John's onMissingMethod blog post, and thirdly there is a callMethod() mode. This last mode will pass the name of a "missing" function and its arguments to a callMethod(). callMethod pulls the function out of the variable scope and executes the function by passing in an arguments collection. NOTE : this is a prototype; the code doesn't handle a truely missing function, but it wouldn't be hard to add it in.

So why is this pretty cool? Take a look at the Property.cfc :

view plain print about
1<cfcomponent output="false" extends="ContentObject">
2    <cfproperty name="ProductName" type="string" default="Product with no name">
3    <cfproperty name="articleStream" type="CO.streams.ArticleStream" >
4    <cfproperty name="versionStream" type="CO.streams.VersioningStream" >
5
6    <cffunction name="dumpVariables">
7        <Cfdump var="#variables#">
8    </cffunction>
9
10</cfcomponent>

I've now got two streams in the component; I'm reusing the articleStream and adding in a versioning stream. If my application were capable of "reading" the component my Product content object can go from being a product with article stream properties injected to a product with a version number, change date and author in one cfproperty.

Obviously you need a system that is capable of managing changes to objects like this, as well able to understand and use specific streams, such as the concept of the version stream, but it was certainly fun to build this prototype and to actually truly mix components into one amorphous blob.

NOTE : Just in case you hadn't noticed, you can download the code for this prototype using the download button below.

TweetBacks
Comments
You found me out!! :P
# Posted By Ben Nadel | 8/27/10 3:52 PM
heh heh. Hope you aren't offended.

I happened to be reading one of your blog posts at the same time as writing this. At midnight-ish it was one of those silly thoughts that come to you at those times and I need some text for my "news article" ;)
# Posted By Stephen Moretti | 8/27/10 5:10 PM
You know me - I'm all for keeping it fun :)
# Posted By Ben Nadel | 8/27/10 5:17 PM