OOPhoto - Simple Service Objects In Place
The latest OOPhoto application can be experienced here.
The OOPhoto code for this post can be seen here.
Last week, I quickly coded the procedural version of OOPhoto, my latest attempt at learning object oriented programming in ColdFusion. After getting some good feedback from Peter Bell, I decided that the easiest next step would be for me to create some very simple service objects. This wouldn't entail much more than taking my SELECT-style queries and moving them into ColdFusion components. But, as easy as this would be, I decided that it would have some good benefits, summarized here:
I would get more comfortable retrieving my data from objects and not having to worry about optimizing every query.
I would get more comfortable with the dependency injection (DI) needed to move the data source name (DSN) into each service object.
Let's start out with the Dependency Injection as this is an important concept and one that makes complex object creation easy. Dependency Injection, or DI, sounds tricky, but in really it is just the moving of required objects into other objects. ColdFusion has a huge advantage in this area over other languages because in ColdFusion, object instantiation and object initialization are two separate steps. In fact, object initialization is not really something that is inherent to ColdFusion objects at all; sure, there is the pseudo-constructor, but things like the Init() method have emerged as an industry "best practice" and is in no way a required part of creating objects in ColdFusion.
Because object "preparation" uses these two different steps, it is quite easy to inject dependency objects after step one but before step two in the following manner:
Step 1: Create object using something like CreateObject( "component" ).
Step 2: Inject dependency objects.
Step 3: Initialize object, calling something like Init() on the object.
The simple fact that this behavior is available in ColdFusion makes creating circular references and other complex objects much less of a headache than it could be.
To enable our service objects to have their dependency objects injected into them during object creation, we have all the service objects extend a BaseService.cfc which has the utility method InjectDependency():
<cfcomponent
output="false"
hint="I provide the based functionality for all Service objects.">
<cffunction
name="InjectDependency"
access="package"
returntype="any"
output="false"
hint="I inject dependency objecst into the VARIABLES scope of the extending Service object.">
<!--- Define arguments. --->
<cfargument
name="Property"
type="string"
required="true"
hint="The key at which the dependency will be stored."
/>
<cfargument
name="Dependency"
type="any"
required="true"
hint="The dependency object that is being injected into the extending service object."
/>
<!--- Inject the dependency object. --->
<cfset VARIABLES[ ARGUMENTS.Property ] = ARGUMENTS.Dependency />
<!--- Return this object for method chaining. --->
<cfreturn THIS />
</cffunction>
</cfcomponent>
The InjectDependency() method does nothing more than take another object and store it in the VARIABLES scope of the target object. While this method could be dynamically added at runtime to all service objects, I decided to keep things as simple as I could for as long as I could. Having the method as part of the object that has package-only access seemed like the easiest realization of this utility.
Now, the InjectDependency() method is only a weapon in the battle; we still need someone to wield that weapon effectively. In our application, that warrior is the ObjectFactory.cfc. The Object Factory does nothing more than encapsulate the method by which other objects are created. So, for example, instead of calling something like:
<cfset CreateObject( "component", "MyObject" ).Init() />
... you would "ask" the object factory to create you an object of that type:
<cfset Factory.Get( "MyObject" ) />
This prevents the calling page from having to know about any aspect of object creation other than the unique name of the object itself. This includes things like object paths, knowledge about Init() arguments, and, most importantly, any kind of dependency injection that needs to be done.
The OOPhoto object factory is created as a singleton (only one instance ever exists) and is cached in the APPLICATION scope:
<cfcomponent
output="false"
hint="Creates, intializes, and wires together all objects that are needed in the system.">
<!--- Setup data structures and default values. --->
<cfset VARIABLES.Instance = {} />
<cffunction
name="Init"
access="public"
returntype="any"
output="false"
hint="Returns an initialized component.">
<!--- Define arguemnts. --->
<cfargument
name="Config"
type="any"
required="true"
hint="The application configuration object."
/>
<!--- Store the configuration object. --->
<cfset VARIABLES.Instance.Config = ARGUMENTS.Config />
<!--- Create and store the UDF library. --->
<cfset VARIABLES.Instance.UDF = THIS.CreateCFC( "UDF" ).Init() />
<!--- Create and store the photo gallery service. --->
<cfset VARIABLES.Instance.PhotoGalleryService = THIS.CreateCFC( "PhotoGalleryService" ) />
<!--- Create and store the photo service. --->
<cfset VARIABLES.Instance.PhotoService = THIS.CreateCFC( "PhotoService" ) />
<!--- Create and store the comment service. --->
<cfset VARIABLES.Instance.CommentService = THIS.CreateCFC( "CommentService" ) />
<!--- Inject dependencies and then initialize the object. --->
<cfset VARIABLES.Instance.PhotoGalleryService.InjectDependency(
"DSN",
THIS.Get( "Config" ).DSN
).Init() />
<!--- Inject dependencies and then initialize the object. --->
<cfset VARIABLES.Instance.PhotoService.InjectDependency(
"DSN",
THIS.Get( "Config" ).DSN
).Init() />
<!--- Inject dependencies and then initialize the object. --->
<cfset VARIABLES.Instance.CommentService.InjectDependency(
"DSN",
THIS.Get( "Config" ).DSN
).Init() />
<!--- Return This reference. --->
<cfreturn THIS />
</cffunction>
<cffunction
name="CreateCFC"
access="public"
returntype="any"
output="false"
hint="Returns the given CFC.">
<!--- Define arguments. --->
<cfargument
name="CFC"
type="string"
required="true"
hint="The relative path to the CFC to create."
/>
<!--- Return non-initialized CFC. --->
<cfreturn CreateObject( "component", ARGUMENTS.CFC ) />
</cffunction>
<cffunction
name="Get"
access="public"
returntype="any"
output="false"
hint="Returns the given object.">
<!--- Define arguments. --->
<cfargument
name="Type"
type="string"
required="true"
hint="The type of object that we need to return."
/>
<!--- Check to see if the given object exists in our singleton cache. --->
<cfif StructKeyExists( VARIABLES.Instance, ARGUMENTS.Type )>
<!--- Return the cached singleton. --->
<cfreturn VARIABLES.Instance[ ARGUMENTS.Type ] />
<cfelseif (ARGUMENTS.Type EQ "ErrorCollection")>
<!--- Return new instance. --->
<cfreturn THIS.CreateCFC( ARGUMENTS.Type ).Init() />
<cfelse>
<!---
If we have gotten this far, then an unknown object has
been requested. Throw an exception.
--->
<cfthrow
type="OOPhoto.UnknownObjectType"
message="You have requested an unknown object type."
detail="The object type you have requested #UCase( ARGUMENTS.Type )# is not valid for this object factory."
/>
</cfif>
</cffunction>
</cfcomponent>
Notice that in the Init() method of the object factory, I instantiate my three service objects. Then, I inject the dependency object (DSN). Then, as a final step, I call the Init() method on the object so that it can take care of any initialization that needs to be done internally. There are whole frameworks out there that take care of this kind of wiring for you, but for applications as simple as this, so far you can see there is no overhead to wiring this together yourself.
The service objects themselves are not that interesting, so I won't bother showing them all to you. I merely took the queries from the procedural pages and moved them into organized ColdFusion components. The real change that I made was to make the queries a bit more generic. So, for instance, on a page where I really only needed the ID and name of a record, I end up returning the entire record since I am not sure what the calling page will need.
If you are curious as to how they work, please take a look:
Now, rather than having the queries inline to the procedural page, I have code like this:
<!--- Param URL variables. --->
<cfparam name="REQUEST.Attributes.id" type="numeric" default="0" />
<!--- Get the selected gallery. --->
<cfset REQUEST.Gallery = APPLICATION.Factory.Get( "PhotoGalleryService" ).GetGalleryByID( REQUEST.Attributes.id ) />
<!--- Get the photos for this gallery. --->
<cfset REQUEST.Photo = APPLICATION.Factory.Get( "PhotoService" ).GetPhotosForGallery( REQUEST.Gallery.id ) />
This was a small step in my application, but I think, a good step in the right direction. It was nice to see that Dependency Injection was much easier than I ever expected it to be. This just goes to show you that things you don't know or don't understand are often times much grander in your head than they are in reality. This is a good lesson to keep in mind as we continue on our journey to understanding object oriented programming.
So what's the next step? As I think Peter Bell's advice worked well for this last step, I might as well not switch horses in mid-stream; I'll go with Peter's next suggestion which was to create CFC-based controllers. Right now, my front-controller consists of several nested CFSwitch statements. CFSwitch statements are extremely easy to build and I have to say that I am a bit skeptical as to the benefits of a CFC-based controller; but, I will proceed regardless as come of Peter's arguments did make sense.
Object Oriented Reality Check
In my last post, Andy Matthews asked me if I would be honest with myself at the end of this process. To make sure that I stay honest, not only with myself, but with all of you, my dear readers, I have decided that at the end of each of these posts, I will start putting a brief "Reality Check" section with the following questions:
Was This Step Worth Implementing?
I think this step was definitely worth it. While some of the queries that I moved into the service objects were one-off style queries, several of them were repeated throughout the application. By moving them into service objects, I keep my application DRY (do-not repeat yourself) and I help to ensure that any changes to the queries need to be done in a minimal number of places.
Is My Application Object Oriented?
Not even close. All we have done is refactored some code to remove duplication and implemented some patterns that make object creation easier. In reality, we have done nothing more than moved our procedural code into user defined functions (UDFs) that happen to be cached in object instances. All we have done is made our procedural code more organized; there are no aspects of object oriented programming present at this point.
Want to use code from this post? Check out the license.
Reader Comments
Hmmm, I just realized that the upload-photo functionality is getting some weird error that says "Length Required". Not sure where I messed that up. I'll try to figure that out shortly.
Fixed the "length required" issues. Some of my PhotoService.cfc methods required that a photo be joined to a gallery in an INNER JOIN fashion. Of course, not every photo can do that (photos uploaded for new galleries). So, no data was being returned. The code has been updated.
I like the direction this is moving.
The reality check piece is a nice touch also and will provide some good points for conversation. Keep up the thorough reporting on your progress.
DW
Exciting stuff. Looking forward to seeing how it progresses. As Dan says, I think the "reality check" is also a great idea.
@Ben, have you tried LightWire for the DI? I think rather than writing methods to handle that for you, you should take a look at it. I've been using it for quite some time and can't see building any app wtihout it. It leaves me to only think about what I need from my objects vs. thinking of what i need to pass to what to make it work. I've been reading your posts on the learning OO and it's one of those things that will help you along faster.
@Hatem,
I want to try to avoid all third-party frameworks until I feel that I have a good handle on all of this stuff. Until I know what problem they solve, I am not sure that I will be able to leverage them effectively.
But to answer your question, yes, I have seen LightWire. I've even been lucky enough to have Peter Bell (author) go over it with me personally.
@Ben,
I sort of understand dependency injection [learding OOCF myself atm].
Reading through your examples I can see the benefit of injecting some variable into an object.
But why make DSN an object, instead of a string? I took a look at the kinky file explorer and I can't find a DSN object anywhere. I guess I'm missing it.
Thanks always
@Grant,
I think I pass DNS through typically as a "source," "username," and "password". Although, with ColdFusion 9 having an app-wide DSN, I might stop using that approach. Also, I think my start to just put my username/password information in the DSN setup in the admin.