OOPhoto - Almost An Object Oriented ColdFusion Application
The latest OOPhoto application can be experienced here.
The OOPhoto code for this post can be seen here.
It's been over a month since I have started OOPhoto, my latest attempt at learning object oriented programming in ColdFusion. It's been a difficult journey so far and one that, no doubt, will continue to challenge me as I move forward. At this point, however, I believe that I've created what might actually be considered a ColdFusion object oriented application.
After some internal debate about whether to go fully object intensive, or ColdFusion optimized, I decided to go with all out objects. This means that no queries are ever returned to the Views; the views have nothing but arrays of objects to work with. But, of course, I couldn't abandon the query object all together. I actually use queries to build my objects, whether it be one record or many. Each of the three service objects has a private method named LoadObjectsFromQuery(). This takes a ColdFusion query and, for each record, creates an object instance, populates it, and adds it to a return array.
While this sounds like something that could be generic and therefore factored out into the Base Service, my application has no configuration that relates database column names to object properties (nor do I think that such relationships should be assumed). Therefore, each service object has to have one method customized to its own needs. So, for example, the LoadObjectsFromQuery() in CommentService.cfc looks like this:
<cffunction
name="LoadObjectsFromQuery"
access="private"
returntype="array"
output="false"
hint="I take a query of comments and return an array of comment objects.">
<!--- Define arguments. --->
<cfargument
name="Query"
type="query"
required="true"
hint="I am a query of comments."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Return a return array. --->
<cfset LOCAL.Return = [] />
<!---
Loop over the query and load a comment object
for each record.
--->
<cfloop query="ARGUMENTS.Query">
<!--- Load a new comment object. --->
<cfset LOCAL.Comment = THIS.New() />
<!--- Set the values from the query. --->
<cfset LOCAL.Comment
.SetID( ARGUMENTS.Query.id )
.SetComment( ARGUMENTS.Query.comment )
.SetDateCreated( ARGUMENTS.Query.date_created )
.SetPhoto( VARIABLES.PhotoService.Load( ARGUMENTS.Query.photo_id ) )
/>
<!--- Add this comment to the return array. --->
<cfset ArrayAppend(
LOCAL.Return,
LOCAL.Comment
) />
</cfloop>
<!--- Return the array of comments. --->
<cfreturn LOCAL.Return />
</cffunction>
All service functions that load objects actually use this method internally. The only difference is that some methods return the whole populated array while others return only the first object in the return array (which should be the only object in the array) as in this Load() method example:
<cffunction
name="Load"
access="public"
returntype="any"
output="false"
hint="I load and return a comment object based on the given ID. If no comment is found, throws an exception.">
<!--- Define arguments. --->
<cfargument
name="ID"
type="numeric"
required="true"
hint="I am the ID of the given comment."
/>
<!--- Define the local scope. --->
<cfset var LOCAL = {} />
<!--- Query for the comment. --->
<cfquery name="LOCAL.CommentData" datasource="#VARIABLES.DSN.Source#">
SELECT
c.id,
c.comment,
c.date_created,
c.photo_id
FROM
comment c
INNER JOIN
photo p
ON
(
c.photo_id = p.id
AND
p.id = <cfqueryparam
value="#ARGUMENTS.ID#"
cfsqltype="cf_sql_integer"
/>
)
ORDER BY
c.date_created ASC
</cfquery>
<!---
Check to see if a record was found. If so, return the
loaded object, else throw an exception.
--->
<cfif LOCAL.CommentData.RecordCount>
<!--- Load the object from the given query. --->
<cfset LOCAL.Comments =
VARIABLES.LoadObjectsFromQuery(
LOCAL.CommentData
) />
<!--- Return the first object. --->
<cfreturn LOCAL.Comments[ 1 ] />
<cfelse>
<!--- Throw exception. --->
<cfthrow
type="OOPhoto.InvalidComment"
message="The selected comment could not be found."
detail="The comment with ID #ARGUMENTS.ID# could not be found."
/>
</cfif>
</cffunction>
From the View standpoint, the changes were very minor. Since I had already moved all of my queries into the Service objects, I merely went from looping over queries to looping over arrays. Then, once inside the loops, I went from referring to query columns to referring to object property Get methods.
LOCAL.Photo.id
... became:
LOCAL.Photo.GetID()
Updating the entire set of views only took about three or four hours which is pretty good considering that I also moved all photo upload functionality into the service layer as well.
The bulk of the time went into the actual fleshing out of the domain model. This was a very strange transition to make. Not so much because the domain model was overly complicated; more so because I had no idea how to build it in a piece-wise fashion. What I ended up doing, as crazy as this sounds, was literally converting the entire application over to object orientation before I tested any of it. Then, once I had all the calls in place, I ran the application and started debugging whatever broke. Clearly it's not a "brains" kind of operation that I'm running over here.
While I managed to pull it off, this plan of attack would fail on anything larger than an application of OOPhoto magnitude. For future projects, I need to figure out how to build these domain objects in such a way that I can build and test them one at a time. Clearly, I will never be able to fully test each object prior to domain completion as each object relies so heavily on others. But is this a bad thing? Does this mean that they are too highly coupled? I don't think so. What I do think is that developing the application in a Test Driven manner would have helped me think it through better.
Ok, so here's the domain model that I finally came up with. While some of these objects technically use generic Get() and Set() methods for property access and mutation (respectively), I am going to list out the user-friendly versions that have been exposed via OnMissingMethod().
BaseModel
- InjectDependency()
BaseService
- ExecuteWithTransaction()
- InjectDependency()
Comment
- Init()
- GetID()
- GetComment()
- GetDateCreated()
- GetPhoto()
- SetID()
- SetComment()
- SetPhoto()
CommentService
- Init()
- Delete()
- GetCommentsForPhoto()
- Load()
- LoadObjectsFromQuery()
- New()
- Save()
ObjectFactory
- Init()
- CreateCFC()
- Get()
Photo
- Init()
- GetID()
- GetFileName()
- GetFileExt()
- GetSize()
- GetSort()
- GetViewCount()
- GetDateCreated()
- GetComments()
- GetPhotoGallery()
- SetID()
- SetFileName()
- SetFileExt()
- SetSize()
- SetSort()
- SetViewCount()
- SetComments()
- SetPhotoGallery()
PhotoGallery
- Init()
- GetID()
- GetName()
- GetDescription()
- GetJumpCode()
- GetDateUpdated()
- GetDateCreated()
- GetPhoto()
- GetPhotos()
- SetID()
- SetName()
- SetDescription()
- SetJumpCode()
- SetDateUpdated()
- SetDateCreated()
- SetPhoto()
- SetPhotos()
PhotoGalleryService
- Init()
- Delete()
- GetGalleriesByKeyword()
- GetGalleryByJumpCode()
- Load()
- LoadObjectsFromQuery()
- New()
- Save()
PhotoService
- Init()
- Delete()
- GetPhotosByIDList()
- GetPhotosForGallery()
- GetPhotosWithNoGallery()
- GetRecentlyUploadedPhotos()
- Load()
- LoadObjectsFromQuery()
- New()
- Save()
- UploadPhoto()
What you will notice is that I don't have any Data Access Objects (DAO) in my domain model. I simply don't see the value [yet] in separating them out into yet another set of objects. Sure, if I ever change databases, I will need to go back and change my service objects; but guess what, if I ever change databases, I'd need to go and write the new DAOs anyway, so it's not like I am saving any time. I suppose if I was going to maintain this project with multiple data access layers in parallel, such as one might do with an open source project, then yeah, I could see the value. But for my purposes, for this application, I just wrapped it all up into the service layer and it feels good to me.
While ColdFusion is getting better at CFC creation speeds, many people believe that it's simply not there yet when it comes to fully object oriented applications (such as this one). As such, I have tried to build some lazy loading into the domain model to ease the burden of instantiation. In the way I have it set up now, all arrays of composed objects are not loaded until they are requested. What this means, for example, is that when you load a PhotoGallery object, its array of Photos is not loaded until it is requested via GetPhotos(). Once it is loaded, it is cached inside the PhotoGallery instance:
<cffunction
name="GetPhotos"
access="private"
returntype="array"
output="false"
hint="I return the array of photos. If the array has not been loaded yet, then I load it first.">
<!---
Check to see if the photos array has been populated
yet. If not, then, we need to load it. This property
will always be an array, so if it is a simple object
then we know that it has not been loaded.
--->
<cfif IsSimpleValue( VARIABLES.Instance.Photos )>
<!---
Use the photo service (injected) to load photos
array and cached it in our instance variables.
--->
<cfset VARIABLES.Instance.Photos =
VARIABLES.PhotoService.GetPhotosForGallery(
THIS.Get( "ID" )
) />
</cfif>
<!--- Return populated array. --->
<cfreturn VARIABLES.Instance.Photos />
</cffunction>
Talk and theory is one thing, but have I actually noticed a performance issue with a fully object oriented approach in this application? Actually, yes. The home page on my development server takes about 1.5 seconds to load. That doesn't sound like a lot of time, but considering that I'm on the local server, that is definitely a long time. When I turn on the DebugOutput, it's pretty crazy. Between all the object creation and Set() method calls, there might be over to 300 "tracked" items in the Execution Time grid. Granted, my local home page has 37 images which means over 70 CFC instantiations (when you consider that each Photo has a PhotoGallery reference), so the trace is not unwarranted.
I can really see where caching objects with something like Transfer would start to pay off in a huge way. If you think about what the homepage is doing, it's displaying new photos. And, since most people build a gallery in one go (I assume), all the photos for a new gallery will be shown on the homepage at a given time. This means that N photos for a gallery will need to create N instances of a PhotoGallery object for internal reference. If, however, we were caching objects intelligently, then even N photos would only need one instance of a PhotoGallery to share between them.
Object caching feels like a huge topic unto itself; to be tackled another time.
One of the aspects sorely lacking in my domain model is object validation. Right now, I have left all the validation in my Controller. This is a horrible choice for several reasons. The biggest problem is that it opens up the door for corrupt data since the service layer is not doing any validation before it persists the objects. The other problem, also very big, is that with the validation in the Controller, anytime I create another touch-point to the application (ex. a RemoteFacade), I might have to duplicate my validation logic, destroying the very benefit provided by object oriented programming.
Validation, like object caching, is beast of a problem to solve. As I have explored before, validation occurs at all levels within an application:
- Controller layer (ex. confirmation fields)
- Service layer (ex. username availability)
- Model layer (ex. valid numeric data)
As such, it is hard to nail down exactly how the validation should work. I think this will be the next item of functionality that I introduce into the application. Once I do that, I believe that I will have moved all of my procedural code into my objects. Pretty exciting. Time to go back and review the many blog posts I have on data validation.
Object Oriented Reality Check
This was the biggest step that I've made in the OOPhoto application so far. Now, let's take a step back and review the situation.
Was This Step Worth Implementing?
I think this step was definitely worth implementing. With this step, I learned much more about object oriented programming than I ever knew before. Even if not all of my logic and implementations are good, I still feel that I am way beyond where I was a week ago. I also think that it was good to do this in a completely object oriented fashion rather than trying to optimize for ColdFusion. This methodology taught me a good deal about the principles of object relations. But of equal importance, it also exposed the pain points of using a fully object oriented approach. With this experience in the bag, it is clear now why something like an Iterating Business Object or an object caching framework is such a good idea.
Is My Application Object Oriented?
I think, for the first time, I would actually consider this application to be object oriented. It is chock full of encapsulation. All property storage and data persistence is encapsulated in my service and model layer. All object creation is also performed by the service layer. Really, all that the Controller needs to know is the service layer and model API - all business logic is performed behind the scenes. And, when it comes to data persistence, all I have to do is persist a top level object (ex. PhotoGallery) and the composed objects within it are all persisted automatically. When it comes to data retrieval, I can ask my domain model instance for its composed objects which it knows how to retrieve and return internally. This feels very object oriented to me. I think once I move my validation logic out of the Controller and into the domain model, it will really close the deal. Maybe in my next post!
Want to use code from this post? Check out the license.
Reader Comments
Congratulations - looks like you're starting to get a handle on the process and the trade offs and the like. Definitely easier just giving it a go and seeing what works or doesn't - like deciding in your case that DAOs/gateways aren't yet required until you have multiple data sources or your service classes start to get a little bloated.
So, would you also recommend to others that they "just build something" - refactoring as required rather than trying to "get" all of the patterns upfront and write something "correcty" the first time?
@Peter,
It was definitely overwhelming trying to come up with the "right" solution right off the bad. Had I really tried to do that, I might still be in the design and domain modeling phase. I think there is something useful to just building something and then refactoring. When you are in that mindset, you let go of the fear - you are not concerned about the best way, you are more concerned about just getting it done. It is then, through that experience, that the best way is perhaps clarified.
Of course, I am still just learning, so, who knows :)
Thank you for all your feedback and help on this journey. And thank you for knowing that it is not over ;) After the object validation, I want to move in the direction of the IBO. I see now, first hand, how noticeably slower the OO creation is. But, with an IBO, not only will the looping have to change, but the underlying structures will have to change. After all, no reference IDs are being stored currently, which might need to change to reduce OO requirements.
I wonder if it will get harder or easier from here?
I think you've broken the back of it. There will continue to be more refinements, but once you have something working with service classes and objects being passed around and the idea of composed objects, you have a structure to fit everything else within. The other biggies that may seem like a bit of a leap will be DI and an ORM, but they're both well worth learning about and getting comfortable with.
@Peter,
I do have some mini-DI in my ObjectFactory.cfc, but it is manually configured rather than running off of any XML file.
As far as ORM, I have never looked into it. That would be a good topic to learn. Soon enough.... :)