Skip to main content
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Tyler Devries
Ben Nadel at CF Summit West 2024 (Las Vegas) with: Tyler Devries

An Experiment In Non-Data-Type ColdFusion Components

By
Published in Comments (20)

Over the last two weeks or so, I've done a lot of thinking about the use of ColdFusion Components as data types. While I like the idea in principle, I've found that it adds very little practical value. As such, last week I decided to stop thinking about ColdFusion components in such a way, and instead, concentrate on how I can better leverage the dynamic nature of the ColdFusion language.

To get rid of all fear, I decided to try writing some code that actually makes me feel uncomfortable. I know that is a laughable statement; but, I was hoping that by writing things I normally wouldn't, I might free my mind up to see alterate and potentially better ways of architecting my domain. And so, I started to think about how I build Domain Objects and what kinds of behaviors I could factor out into super classes.

I decided that every ColdFusion component would extend a base object. But, what would that extension mean? What did it mean to be an object? In my experiment, I decided that to be an object meant only that the sub-class would have instance variables that could be Get() and Set(). It didn't give any insight into the objects use or its behaviors - only that it had accessible and mutatable properties. As such, the Object base class only concerns itself with defining an instance variable container as well as a Get() and Set() method for access and mutating the instance properties:

Object.cfc

<cfcomponent
	output="false"
	hint="I provide base component functionality.">


	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="I return an intialized object.">

		<!--- Define the default instance variables structure. --->
		<cfset VARIABLES.Instance = {} />

		<!---
			Do not return this object - this class is not meant
			to be instantiated on its own.
		--->
	</cffunction>


	<cffunction
		name="CFC"
		access="public"
		returntype="any"
		output="false"
		hint="I provide a short hand for creating a CFC.">

		<!--- Create the CFC and return it, uninitialized. --->
		<cfreturn CreateObject( "component", ARGUMENTS[ 1 ] ) />
	</cffunction>


	<cffunction
		name="Get"
		access="public"
		returntype="any"
		output="false"
		hint="I get the given property (if it exists), or if no property is supplied, returns all of them.">

		<!--- Define arguments. --->
		<cfargument
			name="Property"
			type="string"
			required="false"
			hint="I am the property being returned."
			/>

		<!---
			Check to see if we are accessing given property or
			if we are accessing entire instance variable set.
		--->
		<cfif StructKeyExists( ARGUMENTS, "Property" )>

			<!--- Return property. --->
			<cfreturn VARIABLES.Instance[ ARGUMENTS.Property ] />

		<cfelse>

			<!--- Return all properties. --->
			<cfreturn StructCopy( VARIABLES.Instance ) />

		</cfif>
	</cffunction>


	<cffunction
		name="Set"
		access="public"
		returntype="any"
		output="false"
		hint="I set the given properties if they exist. Need to pass name-value pairs.">

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Loop over collection and set each property. --->
		<cfloop
			item="LOCAL.Property"
			collection="#ARGUMENTS#">

			<!---
				Check to see if argument exists. If someone
				passed an ARGUMENTS collection in with non-
				required values, we will have undefined
				arguments values.
			--->
			<cfif (
				StructKeyExists( ARGUMENTS, LOCAL.Property ) AND
				StructKeyExists( VARIABLES.Instance, LOCAL.Property )
				)>

				<!--- Set the property. --->
				<cfset VARIABLES.Instance[ LOCAL.Property ] = ARGUMENTS[ LOCAL.Property ] />

			</cfif>

		</cfloop>

		<!--- Return this object for method chaining. --->
		<cfreturn THIS />
	</cffunction>

</cfcomponent>

There are two important things to notice about the Get() and Set() methods. First, they only get and set properties that are already defined in the VARIABLES.Instance container. Because of this, you cannot use the Set() method to change the core structure of the instance variables, only the content of pre-defined properties. Second, both the Get() and Set() methods work as bulk operators - the Set() method always loops over its name-value argument pairs and the Get() method will return a copy of the instance variables if no property name is provided.

The base object only creates an empty instance variable container; it is up to the sub-classes to further define the properties of VARIABLES.Instance. Also notice that the Init() method of the base Object.cfc does not return an object; this is my attempt to enforce sub-classing.

Once I had my base object defined, I thought about the next level of "behaviors." In my Domain Model, the next level of objects would be "Beans." So, again, I asked myself the same question as before - what does it mean for an object to be a Bean? To me, it means that this object has CRUD-like behavior; that is, that is has a way to be Created, Read, Updated, and Deleted. As such, my Bean.cfc would concern itself with only these behaviors:

Bean.cfc

<cfcomponent
	extends="Object"
	output="false"
	hint="I provide base functionality for beans.">


	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="I return an intialized object.">

		<!--- Call super init method. --->
		<cfset SUPER.Init() />

		<!--- Define the default instance variables structure. --->
		<cfset VARIABLES.Instance.LoadCommand = "" />
		<cfset VARIABLES.Instance.SaveCommand = "" />
		<cfset VARIABLES.Instance.DeleteCommand = "" />
		<cfset VARIABLES.Instance.ValidateCommand = "" />

		<!---
			Do not return this object - this class is not meant
			to be instantiated on its own.
		--->
	</cffunction>


	<cffunction
		name="Delete"
		access="public"
		returntype="any"
		output="false"
		hint="I delete this object and return the deleted object.">

		<!--- Delete and return result. --->
		<cfreturn THIS.Get( "DeleteCommand" ).Execute( THIS ) />
	</cffunction>


	<cffunction
		name="Load"
		access="public"
		returntype="any"
		output="false"
		hint="I load this object and return the loaded object.">

		<!--- Load and return result. --->
		<cfreturn THIS.Get( "LoadCommand" ).Execute( THIS ) />
	</cffunction>


	<cffunction
		name="Save"
		access="public"
		returntype="any"
		output="false"
		hint="I save this object and return the saved object.">

		<!--- Save and return result. --->
		<cfreturn THIS.Get( "SaveCommand" ).Execute( THIS ) />
	</cffunction>


	<cffunction
		name="Validate"
		access="public"
		returntype="any"
		output="false"
		hint="I valiate this object and return a bean validation object.">

		<!--- Validate and return result. --->
		<cfreturn THIS.Get( "ValidateCommand" ).Execute( THIS ) />
	</cffunction>

</cfcomponent>

A while back, I played around with the idea of creating Command Proxies by binding Components with Method calls behind a single interface. I actually really liked that concept and decided to bake it into all my Beans (no pun intended). If you look at this code, a bean is nothing more than four defined command proxies and the CRUD methods used to invoke those behaviors (command proxies). I personally like the idea of having my CRUD methods in the service layer, so you'll see down farther that my Command Proxies point to the appropriate service layer object.

Notice that this Bean.cfc extends Object.cfc. Since Object.cfc is responsible for defining the VARIABLES.Instance structure, the Bean.cfc must call the super constructor (SUPER.Init()). You'll see that going forward, all ColdFusion components that extend another component must invoke the super component's constructor to make sure that all core instance variables are created and accounted for. And, just as with the Object.cfc, Bean.cfc's Init() method does not return an object to enforce sub-classing.

Now that my Object and Bean components were defined, I decided to try and an actual, concrete Bean sub-class. Because my last few posts have revolved around creating and maintaining a list of romantic dates, I figured I would follow suit. As such, my first actual bean was Date.cfc:

Date.cfc

<cfcomponent
	extends="Bean"
	output="false"
	hint="I represent a romantic date.">


	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="I return an initialized object.">

		<!--- Call super init method. --->
		<cfset SUPER.Init() />

		<!--- Define the default instace variables. --->
		<cfset VARIABLES.Instance.ID = 0 />
		<cfset VARIABLES.Instance.Girl = "" />
		<cfset VARIABLES.Instance.Activity = "" />
		<cfset VARIABLES.Instance.DateOccurred = "" />

		<!--- Set all properties. --->
		<cfset THIS.Set( ArgumentCollection = ARGUMENTS ) />

		<!--- Return this object. --->
		<cfreturn THIS />
	</cffunction>

</cfcomponent>

As you can see, Date.cfc extends the Bean.cfc. As such, it must call the Bean super constructor to make sure that all base instance variables (including Bean command behaviors) are created. Once it does that, it goes on to define default values for its instance properties. But, once that is done, it does something that we haven't seen before - it calls Set() and passes in its own ARGUMENTS collection. As you saw in the Object.cfc, this Set() method will loop over all Name-Value pairs and set instance variables if possible. So, by calling Set() from within the Date.cfc Init() method, we are allowing the calling context to instantiate the Date.cfc instance with given property values.

But where are the arguments? There are no CFArgument tags defined anywhere? This is the part that really made me uncomfortable, but I just went with it! Not only are the arguments not explicitly defined, but, because Date.cfc extends Bean.cfc, it requires arguments not only for its primary instance variables but also for the instance variables required by its Bean-related behavior (ie. the CRUD command proxies).

At first, this really made my stomach turn; I'm a huge proponent of over-defining code - I always use every relevant CFFunction and CFArgument tag attribute, even if the default value is what I need. As such, the idea of not defining any arguments was a huge departure from my standard code ideals. But, after playing around with it for a few hours, I really started to like it. In a really weird way, I think there's something quite elegant about it.

Now that I had a concrete Bean in place, I needed to take a look at the Service layer that would marshall my Bean instances. So, the next component I built was DateService.cfc. Since the DateService.cfc doesn't have any Bean-like behaviors, it merely extends the base Object:

DateService.cfc

<cfcomponent
	extends="Object"
	output="false"
	hint="I provide service methods for Dates.">


	<cffunction
		name="Init"
		access="public"
		returntype="any"
		output="false"
		hint="I return an initialized object.">

		<!--- Call super init method. --->
		<cfset SUPER.Init() />

		<!--- Define the default instace variables. --->
		<cfset VARIABLES.Instance.DSN = "" />

		<!--- Set all properties. --->
		<cfset THIS.Set( ArgumentCollection = ARGUMENTS ) />

		<!--- Return this object. --->
		<cfreturn THIS />
	</cffunction>


	<cffunction
		name="DeleteDate"
		access="public"
		returntype="any"
		output="false"
		hint="I delete the given date from persistence.">

		<!--- Define arguments. --->
		<cfargument
			name="Date"
			type="any"
			required="true"
			hint="I am the date being deleted."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Delete the given record. --->
		<cfquery name="LOCAL.Delete" datasource="#THIS.Get( 'DSN' )#">
			DELETE FROM
				romantic_date
			WHERE
				id = <cfqueryparam value="#ARGUMENTS.Date.Get( 'ID' )#" cfsqltype="cf_sql_integer" />
		</cfquery>

		<!--- Return the deleted date. --->
		<cfreturn ARGUMENTS.Date />
	</cffunction>


	<cffunction
		name="GetDateByID"
		access="public"
		returntype="any"
		output="false"
		hint="I return the date at the given unique ID.">

		<!--- Define arguments. --->
		<cfargument
			name="ID"
			type="numeric"
			required="true"
			hint="I am the unique ID of the date."
			/>

		<!--- Return a new, loaded date. --->
		<cfreturn THIS.GetNewDate()
			.Set( ID = ARGUMENTS.ID )
			.Load()
			/>
	</cffunction>


	<cffunction
		name="GetDates"
		access="public"
		returntype="query"
		output="false"
		hint="I return a query of all the dates.">

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Query for dates in proper order. --->
		<cfquery name="LOCAL.DateRecords" datasource="#THIS.Get( 'DSN' )#">
			SELECT
				id,
				girl,
				activity,
				date_occurred
			FROM
				romantic_date
			ORDER BY
				date_occurred DESC
		</cfquery>

		<!--- Return ordered dates. --->
		<cfreturn LOCAL.DateRecords />
	</cffunction>


	<cffunction
		name="GetNewDate"
		access="public"
		returntype="any"
		output="false"
		hint="I return a new, empty date object.">

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Create command proxies. --->
		<cfset LOCAL.Commands = {
			LoadCommand = THIS.CFC( "CommandProxy" ).Init(
				Component = THIS,
				Method = "LoadDate"
				),
			SaveCommand = THIS.CFC( "CommandProxy" ).Init(
				Component = THIS,
				Method = "SaveDate"
				),
			DeleteCommand = THIS.CFC( "CommandProxy" ).Init(
				Component = THIS,
				Method = "DeleteDate"
				),
			ValidateCommand = THIS.CFC( "CommandProxy" ).Init(
				Component = THIS,
				Method = "ValidateDate"
				)
			} />

		<!--- Return the new date. --->
		<cfreturn THIS.CFC( "Date" ).Init(
			ArgumentCollection = LOCAL.Commands
			) />
	</cffunction>


	<cffunction
		name="LoadDate"
		access="public"
		returntype="any"
		output="false"
		hint="I load the given date (with the given internal ID).">

		<!--- Define arguments. --->
		<cfargument
			name="Date"
			type="any"
			required="true"
			hint="I am the date being loaded."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Query for date record. --->
		<cfquery name="LOCAL.DateRecord" datasource="#THIS.Get( 'DSN' )#">
			SELECT
				id,
				girl,
				activity,
				date_occurred
			FROM
				romantic_date
			WHERE
				id = <cfqueryparam value="#ARGUMENTS.Date.Get( 'ID' )#" cfsqltype="cf_sql_integer" />
		</cfquery>

		<!--- Check to see if we found the record. --->
		<cfif (LOCAL.DateRecord.RecordCount)>

			<!--- Load all values. --->
			<cfset ARGUMENTS.Date.Set(
				ID = LOCAL.DateRecord.id,
				Girl = LOCAL.DateRecord.girl,
				Activity = LOCAL.DateRecord.activity,
				DateOccurred = LOCAL.DateRecord.date_occurred
				) />

		<cfelse>

			<!--- The record was not found - remove ID. --->
			<cfset ARGUMENTS.Date.Set( ID = 0 ) />

		</cfif>

		<!--- Return the loaded date. --->
		<cfreturn ARGUMENTS.Date />
	</cffunction>


	<cffunction
		name="SaveDate"
		access="public"
		returntype="any"
		output="false"
		hint="I persist the given date.">

		<!--- Define arguments. --->
		<cfargument
			name="Date"
			type="any"
			required="true"
			hint="I am the date being persisted."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Validate date object. --->
		<cfset LOCAL.Errors = ARGUMENTS.Date.Validate() />

		<!--- Check to see if we have any errors. --->
		<cfif LOCAL.Errors.HasErrors()>

			<cfthrow
				type="InvalidAction"
				message="The given date cannot be saved."
				detail="The given date cannot be saved due to errors in the following properites: #StructKeyList( LOCAL.Errors )#."
				/>

		</cfif>

		<!--- Get the date properties. --->
		<cfset LOCAL.Properties = ARGUMENTS.Date.Get() />

		<!---
			Check to see if we are inserting or updating (based on
			the ID existence).
		--->
		<cfif LOCAL.Properties.ID>

			<!--- Updating. --->

			<cfquery name="LOCAL.Update" datasource="#THIS.Get( 'DSN' )#">
				UPDATE
					romantic_date
				SET
					girl = <cfqueryparam value="#LOCAL.Properties.Girl#" cfsqltype="cf_sql_varchar" />,
					activity = <cfqueryparam value="#LOCAL.Properties.Activity#" cfsqltype="cf_sql_varchar" />,
					date_occurred = <cfqueryparam value="#LOCAL.Properties.DateOccurred#" cfsqltype="cf_sql_timestamp" />
				WHERE
					id = <cfqueryparam value="#LOCAL.Properties.ID#" cfsqltype="cf_sql_integer" />
			</cfquery>

		<cfelse>

			<!--- Inserting. --->

			<cfquery name="LOCAL.Insert" datasource="#THIS.Get( 'DSN' )#">
				INSERT INTO romantic_date
				(
					girl,
					activity,
					date_occurred
				) VALUES (
					<cfqueryparam value="#LOCAL.Properties.Girl#" cfsqltype="cf_sql_varchar" />,
					<cfqueryparam value="#LOCAL.Properties.Activity#" cfsqltype="cf_sql_varchar" />,
					<cfqueryparam value="#LOCAL.Properties.DateOccurred#" cfsqltype="cf_sql_timestamp" />
				);

				<!--- Return the new ID. --->
				SELECT
					( @@Identity ) AS id
				;
			</cfquery>

			<!--- Store the new ID. --->
			<cfset ARGUMENTS.Date.Set( ID = LOCAL.Insert.id ) />

		</cfif>

		<!--- Return the persisted date. --->
		<cfreturn ARGUMENTS.Date />
	</cffunction>


	<cffunction
		name="ValidateDate"
		access="public"
		returntype="any"
		output="false"
		hint="I validate the given date object for persistence.">

		<!--- Define arguments. --->
		<cfargument
			name="Date"
			type="any"
			required="true"
			hint="I am the date being validated."
			/>

		<!--- Define the local scope. --->
		<cfset var LOCAL = {} />

		<!--- Get the date properties. --->
		<cfset LOCAL.Properties = ARGUMENTS.Date.Get() />

		<!--- Create a validation collection. --->
		<cfset LOCAL.Errors = THIS.CFC( "BeanValidationCollection" ).Init() />

		<!--- Validate ID. --->
		<cfif (
			(NOT IsNumeric( LOCAL.Properties.ID )) OR
			(Fix( LOCAL.Properties.ID ) NEQ LOCAL.Properties.ID) OR
			(LOCAL.Properties.ID LT 0)
			)>

			<cfset LOCAL.Errors.AddError(
				Property = "ID",
				Type = "InvalidProperty",
				Error = "The ID property which is, #LOCAL.Properties.ID#, is not valid. ID must be positive a integer (including zero).",
				Value = LOCAL.Properties.ID
				) />

		</cfif>

		<!--- Validate Girl. --->
		<cfif NOT Len( LOCAL.Properties.Girl )>

			<cfset LOCAL.Errors.AddError(
				Property = "Girl",
				Type = "InvalidProperty",
				Error = "The Girl property which is, #LOCAL.Properties.Girl#, is not valid. Girl must be a string of length greater than zero.",
				Value = LOCAL.Properties.Girl
				) />

		</cfif>

		<!--- Validate Activity. --->
		<cfif NOT Len( LOCAL.Properties.Activity )>

			<cfset LOCAL.Errors.AddError(
				Property = "Activity",
				Type = "InvalidProperty",
				Error = "The Activity property which is, #LOCAL.Properties.Activity#, is not valid. Activity must be a string of length greater than zero.",
				Value = LOCAL.Properties.Activity
				) />

		</cfif>

		<!--- Validate Date. --->
		<cfif (
			(NOT IsDate( LOCAL.Properties.DateOccurred )) OR
			(LOCAL.Properties.DateOccurred GT Now())
			)>

			<cfset LOCAL.Errors.AddError(
				Property = "DateOccurred",
				Type = "InvalidProperty",
				Error = "The DateOccurred property which is, #LOCAL.Properties.DateOccurred#, is not valid. Date must be past date/time stamp.",
				Value = LOCAL.Properties.DateOccurred
				) />

		</cfif>

		<!--- Return errors. --->
		<cfreturn LOCAL.Errors />
	</cffunction>

</cfcomponent>

Just as with any of the other concrete objects, DateService.cfc calls its super Init() method and then defines it's core instance properties.

Now, because the DateService.cfc is responsible for the bean CRUD methods (as defined by the command proxies described above), it defines the CRUD-related handlers: DeleteDate(), LoadDate(), SaveDate(), and ValidateDate(). You'll notice that, to conform to the Command Proxy interface, the CRUD-related handlers only accept a single argument - the target Bean. To see how the command proxies are created, take a look at the GetNewDate() method - it defines the four Bean behaviors and then passes them in as the argument collection.

To see how this all ties together, I wrote a snippet of test code that creates, saves, and outputs a new Date:

<!--- Create a date service.cfc. --->
<cfset objService = CreateObject( "component", "DateService" ).Init(
	DSN = "ben"
	) />

<!--- Get a new date, set properties, and save it. --->
<cfset objDate = objService
	.GetNewDate()
	.Set(
		Girl = "Christina",
		Activity = "Weight Lifting",
		DateOccurred = "04/12/2009"
		)
	.Save()
	/>

<!--- Ouput new date properties. --->
<cfoutput>

	ID: #objDate.Get( "ID" )#<br />
	Girl: #objDate.Get( "Girl" )#<br />
	Activity: #objDate.Get( "Activity" )#<br />
	DateOccurred: #objDate.Get( "DateOccurred" )#<br />

</cfoutput>

When I run this code, I get the following output:

ID: 12
Girl: Christina
Activity: Weight Lifting
DateOccurred: 04/12/2009

As you can see, the Date information is properly saved and new ID value is stored into the Bean.

So, this has been a huge departure from code that I have written in the past. It makes excellent use of the dynamic nature of ColdFusion, including the ability to overload method signatures. I started off this experiment as a way for me to free my mind through expected failure; but, I finish off the experiment actually liking what I came up with. Sure, there's no way to auto-generated great documentation from the code, but I find the code to be quite readable (I look at the VARIABLES.Instance struct rather than the CFArgument tags). And, because the "behaviors" of an object are delegated to any particular super-class, it's easy for me to keep track of which arguments are actually need by the constructor.

Want to use code from this post? Check out the license.

Reader Comments

67 Comments

Hi Ben
You seem to use the words "class" and "object" interchangeably, which I think can cause confusion when thinking about what you're doing (or articulating it in a blog post!).

Also, CF doesn't have classes anyhow. It has components. Best to stick to the actual nomenclature, I reckon.

In CF - as you obviously know but are perhaps not articulating so well - a component is a file which has some source code in it: a CFC file. This is roughly analogous to a class in Java (etc), but not exactly, and different enough for it to not be valid to use the word "class" to describe a component. An object is a variable which is an instance of that component; same as an object is an instance of a class in Java.

I think if you're going to discuss this sort of thing, you should use the terms correctly.

--
Adam

15,848 Comments

@Adam,

Thanks for the feedback. I am sorry that you found the blog post confusing. I definitely do use things like "component", "class", and "object definition" interchangeably. I will try to be more consistent going forward.

So, help me out here - instead of terms like "sub-class" and "super-class", should I use "sub-component" and "super-component"?

I am not sure what the difference between a "class" and a "component" is? Can expand on this concept?

29 Comments

I think a large part of the confusion comes from

1) ColdFusion's confusing implementation of OO.

2) Our industry's addiction to using Java concepts and nomenclature whenever talking about OO.

And since CF is built on Java, and now has a Java-like "interface" construct one could be forgiven for finding OO in CF to be a bit confusing.

You see the same thing in JavaScript. JS also lacks classes as such, instead borrowing prototype-based inheritance from languages like Self. And yet we all (myself included) continue to talk about JS classes and instances as if it were Java.

I feel that the CF community could learn a lot from Ruby and Python. Both are dynamically-typed, object-oriented languages with enormous developer communities. They've been solving these problems without "thinking in Java" for over 10 years.

116 Comments

It really doesn't have anything to do with "thinking in Java", but rather "thinking in objects". Ruby and Python may be dynamic, but they still have typing. I think that Ben has over-analyzed his problem and ended up with a solution of dubious value. Here are some of the reasons why I say this:

First, what you've got here, Ben, is a glorified structure. There's no behavior here, it's all data-centric. There's no API, everything is amorphous and nebulous.

What logic there is is all in the service, so you've got the "fat service layer / anemic domain model" antipatterns which many folks seem to grapple with. SQL, validation, it's all here...the service has overlapping responsibilities and low cohesion: it's doing too much.

The indirection introduced by the "commands" also seems to introduce additional complexity without any tangible benefit. Why specify commands on the domain object that do nothing but turn around and invoke the service again? Even if you were to move those methods into a dedicated data access object, the question remains: are you gaining anything by doing that?

The end result is essentially a structure that allows only a limited set of keys, something like a typeless enum. Dynamic code has its advantages, but totally dynamic code leads quickly something resembling chaos.

Just food for additional thought: consider Bolt, the upcoming CF IDE. I wouldn't base my coding practices on what Bolt may or may not do, but Bolt is widely expected to have some kind of hinting and introspection capabilities. With a solution like this, there's no way the IDE will be able to give you any help, because there's no API here. From the outside looking in, these objects will take anything and return anything.

I also wonder what happens when the model gets more complex, and you have multiple domain objects with relationships. I think this approach would get untenable pretty quickly.

Now let me hasten to say that I applaud Ben for his inquisitiveness, curiosity, open mind, and thirst for knowledge. Experimenting is great. It's necessary. And I'm just one guy, I could be missing something here that makes this extremely useful. But I'd urge you to think a bit further about what you've got here and whether it's actually going to hold up as the number of objects go up and as changes occur over time.

15,848 Comments

@Brian,

Here's where I get hung up on the "no behavior" thing. From the calling context perspective, my business object, Date.cfc, does appear to have behavior - Save(), Validate(), Load(), and Delete(). It just happens that inside of the domain object, the internal implementation is that these behaviors are handed off to another object.

Perhaps from an internal point of view, the domain object doesn't have much behavior, but one of the primary goals of OO - encapsulation / implementation hiding - dictates that the internal architecture is not the concern of the outside world?

Also, this raises another question in my mind regarding the "Strategy" design pattern. From what I think you are saying, can / should I conclude that any behavior that is executed using a given "Strategy" is not truly a behavior?

I think, of course, about things like a betting strategy in a card game or a tax calculation strategy in a checkout system. Does the use of strategies mean that the consuming object does not exhibit those behaviors?

Is this just a glorified struct that has limited keys? Well, Yes. But, it seemingly doesn't need much of anything else. In my example app, a "Date" doesn't exactly have any crucial behaviors. The application itself is extremely data-centric and so, should it be a concern that the domain model is also extremely data-centric?

As far as an API, not defining arguments was definitely something that made me more uncomfortable than anything else I did in the demo code. But, what I discovered was that I simply looked at the VARIABLES.Instance definition within the Init() method rather than the CFArgument tags (which would have mirrored them closely). As such, I did feel that the objects had as good an API as they would have had with CFArguments. The only real benefit that CFArguments would have provided would be to define both the current sub-class needs and the needs of the super-class. I won't sell that short - it is a good benefit; but, if you live in a world where many of your objects are "beans", I just assume that it would be obvious that all beans have the given CRUD behavior.... plus, if the Bean's were always created via a factory or service layer, the Init() api was almost irrelevant to the greater application.

Regarding the "Fat service" layer, this is a problem that I have grappled with before and I simply don't know how to solve this. I would love some specific feedback on how you might architect this same solution - where the validation and other CRUD methods go?

This is an experiment and it is an experiment because, as you can all see, I struggle (very publicly) to wrap my head around both "practical" and "pure" object oriented solutions.

That said, I would love some advice specific to this example. How would you solve this problem? Help me help myself :)

16 Comments

Hey Ben - I actually employ a somewhat similar solution on a system with ~350K objects, and ~45 component types. It really has benefited me in a domain where everything needs to be dynamic. So while I can't comment specifically on how to "fix" your solution to be a more academic OO solution, I can give you some feedback about the above based on my experience modeling components as objects.

My first thought is if the goal is employing strict OO precepts in ColdFusion we should really change that. ColdFusion as everyone knows is not OO, so we have to be willing to bend the rules, or be willing to endure a lot of frustration and lost time.

I've always found component instantiation to be very expensive in ColdFusion. CreateObject and getMetaData are performance killers. This directly influences my development in that I tend to not create a component to handle everything. Many of my components are multi-purposed. So my reasons are more specific to ColdFusion than academic.

So immediately the CommandProxy jumps out at me. My page requests can consist of up to 50 component objects (and each may have an extends chain 1-3 components), so having a 1-N relationship for every command will really hurt as it scales. If these were to be left in, I would recommend storing the ComponentName reference and instantiate when the command is called.

I do move these commands out into a DAO though, as Brian notes. My DAO is written though to inspect the object and dynamically generate the interface with the datasource, so I guess it would be more ORM than as DAO.

So in the end there little value left other than a structure, _but_ when we add in the ability to dynamically add properties it becomes much more powerful. With properties being defined dynamically it opens the door to dynamic views and validation. To do this the instance properties for the date object need have additional metadata associated with them.

The other thing that stuck out to me right way is I wasn't sure why you were doing StructKeyExists( ARGUMENTS, LOCAL.Property ) inside of a loop of the Argument collection (object.cfc), because that would imply the property already exists in the argument scope.

116 Comments

Hi Ben. Just to be clear up front, it can be difficult to talk about OO design when the example being used is such a small application. As always, it's the double-edged sword: OO is difficult to understand for newcomers, so examples must be simple, but the benefits of OO are most apparent in larger and more complex projects.

With that said, if the goal here was only to find a solution to this limited problem space (saving a Date), then your solution does that. in fact I'd argue that if that were all it needs to do, your solution is actually overkill.

But since I assume that the goal here is NOT actually to find a solution to this simple problem, but rather to use it as a test bed for more complex situations, then the solution has issues.

I don't consider saving, loading, validating, or deleting an object from the database to be behavior. At least not behavior for a domain object. It might be behavior for a DAO, but that's not what we're talking about here. Persisting objects is something that really should be considered in a completely different layer of the software from the domain model; it's the province of a data layer.

Behavior in a domain object should involve business logic. So something like date.shouldAskOutAgain() or date.end( kiss ). The point is that something should usually be happening, the object should be DOING something, other than just existing as a bucket of properties. Sometimes objects do exist primarily to move data around, like a Memento or DTO, but that shouldn't be the norm.

Regarding the subject of delegated method calls, no, there's nothing wrong with that. If it's appropriate to the object, and it can respond to a message by doing something that involves invoking another object, that's fine. BUT, if the object does NOTHING but delegate method calls to other objects, its reason for existing becomes highly suspect. (This is called the Middle Man antipattern btw.)

I think some of your questions about OO in general, as well as the "what goes where" issues, would be helped if you went through an exercise like the Object Calisthenics challenge that I talked about a few weeks ago on by blog (http://www.briankotek.com/blog/index.cfm/2009/2/11/Taking-the-Object-Calisthenics-Challenge). While doing this in CFML is going to be harder than doing it in AS3 or Groovy, I still think it would end up being a useful exercise. Or, of course, it might be an excuse to kick the tires on a language other than CFML, just to see how the differences play out.

To come back to the beginning, it's difficult to offer advice "specific to this example" because the example is so deliberately limited in scope. To be honest, if this specific example were all I needed to do, I'd just use a structure and a simple CFC with some queries in it to handle the database access. I know that doesn't really help you, but I also hope you can see the difficulty in trying to offer guidance since the scope in question is so constrained.

15,848 Comments

@Brett,

I think the CommandProxy could easily be cached as singleton instances in order to get around the instantiation issue. After all, all "Date Beans" would use the same commands anyway... with the option to switch them at run-time if necessary. Or, if you don't like the command proxy idea, certainly, a Bean could abstractly use a DAO singleton to manage its CRUD behaviors.

Also, you could easily move the commands into the DAO without the Bean actually knowing anything about it. The Command Proxy could simply bind to the DAO rather than the Service object... that's what I really liked about the Command Proxy concept - the Bean is open to extension, closed to change.

As far as why I use StructKeyExists() from within the Set() method, it's because I didn't want people to add arbitrary data to the component - they can only get/set data that is defined within the default Instance variables. And, yes, they are already set within the Init() methods (even if just default values).

I did this to keep some sense of cleanliness to the objects. But, maybe that "need" is purely emotional and not functional.

15,848 Comments

@Brian,

I certainly understand the dilemma of the simple example vs. the usefulness of OO. You have it correct - my primary goal was not to solve the problem at hand, but rather build up a foundation of understanding. Heck, if I just needed to solve the problem, I could have coded it with 3 files in 15 minutes (rather than 4 hours of coding and refactoring :)).

As far as the MiddleMan antipattern, it's good to know that has a name. However, I am not sure that it is an actual anti-pattern in my mind as the *point* of it is really a convenience. I could call the SaveDate(), DeleteDate(), etc. methods on the Service layer (or DAO) and pass in the Date instance directly... but, I like the fact that I can easily call these methods directly on the Bean to allow single, chained, readable code (IMO).

Perhaps it would make things more comfortable if my "Bean" class was rather called something like "Persistable". In that, it might more clearly outline the "persistence-specific" behaviors, rather than implying that they are behaviors of the domain object.

Persistence methods aside, however, I don't think my current code excludes behavior potential. I can certainly add any Functions to my Date.cfc that I needed to, such as shouldAskOutAgain(). It's just that my specific sample app didn't call for such a thing.

I'll see if I can come up with a better app that is still small, but still with some behavior. Thanks for all of your feedback! I'll take a look at the Object Calisthenics you mentioned.

3 Comments

Hey Ben,

I love the thought exercise posts out there. I love thought exercises, personal challenges and "what happens if I do..." games. I've spent the better part of a week just playing with a concept (not recently, but still) figuring out what worked, what didn't, why (ultimately) the idea wasn't useful... and people scoffed. But I learned. I learned technical details about ColdFusion, I learned programming concepts, I learned about all sorts of different things. It's totally a worthwhile endeavor and it's cool to see you sharing your thoughts publicly.

Keep playing with ideas... what works and what doesn't will shake itself out in the long run anyway. If you run into trouble, chances are you'll already know why and be thinking about workarounds before you even hit the wall.

Still, tho... patterns and antipatterns have been defined by more experienced and wiser men (and women) than I and I often find that if I accept some of what they have to say on faith, I find it true... eventually. Things like the Man in the Middle antipattern, or Anemic Domain... both very relevant and, eventually, very true. With Man in the Middle, the sheer weight of the codebase eventually starts to work against itself because there are so many more characters than there needs to be.

But, to be totally honest and fair, I have a fascination with both the strategy pattern and the command pattern myself and with using ColdFusion's typeless runtimeyness to maximum advantage. Hrm... this brings up a couple blog posts I should write! :)

12 Comments

@Ben,

I really appreciate that you're trying different approaches, butting your head up against the wall as it were. And I find it very cool that you are sharing your efforts with the rest of us.

As Brian indicated, simple examples won't get you far. On the contrary, I'm afraid they have the potential to mislead. This is one of the toughest things, to me, about learning OO, and why I don't think that the rest of the community can pitch in and suggest changes to this design in any way that will help you.

To do it the "hard" way, by discovering everything for and by yourself, which is usually a good way to learn, you need to work with many complex examples, most likely making a mess of them, but only discovering (for yourself) why it's a mess after you are weeks, or months, (or years) into the project.

I've noticed over the years since the OO wave hit ColdFusion that Brian Kotek seems to have made a significant amount of progress in this area. He might be a good person to ask the question "How do you (how did _you_) learn this stuff, efficiently? If you are going to master OO, a good plan for how to tackle it might be helpful.

15,848 Comments

@Jared,

I definitely accept that smarter people have come up with good patterns. Perhaps its time for me to go back and re-read a few books on OOP that I have read before but was, perhaps, not ready to fully understand what they were saying.

@Nando,

I don't think that a system *has* to be big to demonstrate the benefits of OO. I think it only has to have behavior. After all, when you think about most applications, the things that make them so big is not necessarily the depth any *one* feature, but rather the number of total, potentially unrelated, features.

As such, I think it's just a matter of picking out the right sample application to experiment on. I think, the only leap of faith you need to take on a small application is that even though you might be able to write it faster with procedural code, it's still worth the learning effort of OO.

So, this begs the question - what is the good sample application that demonstrates behavior?

116 Comments

As you probably saw in my Object Calisthenics post, I chose to build a Bowling score engine. I found this to be pretty good, since it's not super complex but it's definitely not trivial either, and there is plenty of room to explore: How do you model the final frame vs. the standard frames? What IS a "score"? What is a frame? How do you handle the fact that a frame's score may not be known until additional balls are rolled? etc. It was pretty interesting to think about.

But regardless of what you pick, pick something that sounds fun that is complex enough for you to really dig your teeth into it.

15,848 Comments

@Brian,

I started to think about it last night, but I got a bit confused on the concept of wrapping up primitives and Strings in their own classes and then using polymorphism. I was not sure how far to take that.

Just as an example, imagine I had a "hairColor" class. My first inclination would simply be to have hairColor with a "color" property:

haircolor ( strColor ) {
string color = strColor;
}

.. but are they saying that rather than that, I would get rid of the color property and actually have something like:

hairColor {}

blondeHair extends hairColor
brownHair extends hairColor

?? Or am I totally misunderstanding what they are saying?

116 Comments

Yes, that's what they're saying. However, I wouldn't get totally hung up on that one. The goal here is to eliminate "primitive obsession" and ensure that the purpose of each object is clearly identified by its type. But I would say the other rules like small classes, one level of indention per method, two properties per class, no else statements, etc. were the things that triggered the more interesting thinking.

2 Comments

I like thought exercises as well. I get stumped a lot in trying to figure out good ways to do things in CF.

constants drive me nuts in CF. Trying to find a good pattern for those in a large application is tricky, but that's another story.

Nice post. I agree with a lot of what Brian said. But, all in all, I like your way of hanging it all out there for the world to look at ;0)

I believe in love. I believe in compassion. I believe in human rights. I believe that we can afford to give more of these gifts to the world around us because it costs us nothing to be decent and kind and understanding. And, I want you to know that when you land on this site, you are accepted for who you are, no matter how you identify, what truths you live, or whatever kind of goofy shit makes you feel alive! Rock on with your bad self!
Ben Nadel