Ben Nadel
On User Experience (UX) Design, JavaScript, ColdFusion, Node.js, Life, and Love.
I am the chief technical officer at InVision App, Inc - a prototyping and collaboration platform for designers, built by designers. I also rock out in JavaScript and ColdFusion 24x7.
Meanwhile on Twitter
Loading latest tweet...
Ben Nadel at the jQuery Conference 2010 (Boston, MA) with:

Binding Components And Methods To Create Validation Behaviors

By Ben Nadel on
Tags: ColdFusion

Earlier this week, my understanding of what it meant for an object to be in a "valid state" was shaken to the core. I thought of "valid state" in terms of business context; however, business context does not really play into the, "what does it mean to be an Object," existentialist question. That question is answered in terms of data types and mission critical value criteria. Validation at the application-specific level cannot be known at the domain object level.

This brought me back to the thought that the Domain object could handle some low level validation and then turn around and call validation on its service object. By this, I mean that calling validation on the domain object would turn around and do something like this:

  • <cfreturn THIS.Service.Validate( THIS ) />

I like this idea and have used it before, but Hal Helms recommended rolling up this kind of business logic into its own object. In the previous conversation about user accounts, he suggested creating something like a UserCreation component. By doing so, he explained that they could be swapped out at run time. He gave the example of WeekdayUserCreator vs WeekendUserCreator.

I like that idea as well, but I think that that is an outlier in terms of use cases. I don't mean his specific example, I mean generally swapping out creation or validation services at run time - it's something that my applications never need to do (yet). As such, I felt the overhead of defining new components for each creation event (or validation event or persistence event or what have you) would not be worth the actual usage.

But, I do like the idea of being able to encapsulate the relationship between the domain object and its service class. So, I thought about how I could create the swappable behavior with no additional overhead. What I came up with was a sort of proxy behavior component that binds a given component to a given method name and hides it behind a static interface. For example, rather than making the Domain object know that it has to call a Validate method on its service class, I create a Behavior that internally links the "Validate" method to the "Service" instance and invokes that method behind an "Execute" interface.

By doing this, I can create any number of behaviors without having to define any new classes. And, by encapsulating the relationship of the behavior to the service method, I can have different services handle different behaviors - something that would have been much more complicated had the Domain object been calling a Service object directly.

Here is the Behavior.cfc that I came up with:

  • <cfcomponent
  • output="false"
  • hint="I provide a way to create a behavior by binding a static method to a target component and method name.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized behavior.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Service"
  • type="any"
  • required="true"
  • hint="I am the service component providing the behavior."
  • />
  •  
  • <cfargument
  • name="Method"
  • type="string"
  • required="true"
  • hint="I am the target method on the service component that will actually execute the behavior."
  • />
  •  
  • <!--- Define the component properties. --->
  • <cfset VARIABLES.Instance = {
  • Service = ARGUMENTS.Service,
  • Method = ARGUMENTS.Method
  • } />
  •  
  • <!--- Return THIS reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Execute"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I execute the target behavior.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="BusinessObject"
  • type="any"
  • required="true"
  • hint="I am the business object that is requesting the execution of this behavior."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!---
  • Execute the taget method on the service and pass in
  • the business object as the first argument.
  • --->
  • <cfinvoke
  • component="#VARIABLES.Instance.Service#"
  • method="#VARIABLES.Instance.Method#"
  • returnvariable="LOCAL.Return">
  •  
  • <!---
  • Pass in the business object as first and only
  • arguments.
  • --->
  • <cfinvokeargument
  • name="1"
  • value="#ARGUMENTS.BusinessObject#"
  • />
  • </cfinvoke>
  •  
  • <!---
  • Check to see if we have a return value. If not, it
  • means the above method returned void; as such, we
  • will trun void.
  • --->
  • <cfif StructKeyExists( LOCAL, "Return" )>
  • <cfreturn LOCAL.Return />
  • <cfelse>
  • <cfreturn />
  • </cfif>
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, it knows nothing about the object or the method being invoked. Well, that's not entirely true - it assumes that the method being invoked takes only one argument - the target Domain object. But other than that, this is quite generic and easily reusable. It's interface consists of an Execute() method that invokes the behavior. And, thinking about it now, if the arguments did need to change, this could easily be sub-classed.

To demonstrate how this is used, I'm going to create a Pet object that has a Name and Type property. Now, when it comes to the, "what does it mean to be a Pet," question, there are some fundamental criteria. That is, that the Pet has to have a Name and Type property and that neither of those properties can be the empty string. That is what it means for the Pet component to be in a "valid state." Because this is critical to the meaning of "Pet," you'll see that the following ColdFusion component checks this in the Init() method:

  • <cfcomponent
  • output="false"
  • hint="I represent a pet.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized pet.">
  •  
  • <!--- Define the arguments. --->
  • <cfargument
  • name="Name"
  • type="string"
  • required="true"
  • hint="I am the name of the pet."
  • />
  •  
  • <cfargument
  • name="Type"
  • type="string"
  • required="false"
  • default="Unknown"
  • hint="I am the generic type of pet (ie Dog, cat, horse)." />
  •  
  • <cfargument
  • name="ValidationBehavior"
  • type="any"
  • required="true"
  • hint="I am the validation behavior."
  • />
  •  
  •  
  • <!--- Validate arguments. --->
  • <cfif NOT Len( ARGUMENTS.Name )>
  •  
  • <!--- Name required. --->
  • <cfthrow
  • type="Pet.Init.InvalidArgument"
  • message="Name cannot be an empty string."
  • detail="The Name argument passed in cannot be an empty string."
  • />
  •  
  • </cfif>
  •  
  • <cfif NOT Len( ARGUMENTS.Type )>
  •  
  • <!--- Name required. --->
  • <cfthrow
  • type="Pet.Init.InvalidArgument"
  • message="Type cannot be an empty string."
  • detail="The Typoe argument passed in cannot be an empty string. If you do not know the type of pet, omit argument or pass in Unkown."
  • />
  •  
  • </cfif>
  •  
  •  
  • <!---
  • ASSERT: At this point, we have validated that our
  • arguments align with what it means to be a Pet.
  • --->
  •  
  •  
  • <!--- Store the behaviors. --->
  • <cfset VARIABLES.ValidationBehavior = ARGUMENTS.ValidationBehavior />
  •  
  • <!--- Store the instance variables. --->
  • <cfset VARIABLES.Instance = {
  • Name = ARGUMENTS.Name,
  • Type = ARGUMENTS.Type
  • } />
  •  
  • <!--- Return THIS reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetName"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I return the name property.">
  •  
  • <cfreturn VARIABLES.Instance.Name />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="GetType"
  • access="public"
  • returntype="string"
  • output="false"
  • hint="I return the type property.">
  •  
  • <cfreturn VARIABLES.Instance.Type />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="Validate"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I validate this business object.">
  •  
  • <!--- Return validation. --->
  • <cfreturn VARIABLES.ValidationBehavior.Execute( THIS ) />
  • </cffunction>
  •  
  • </cfcomponent>

Notice that if the passed-in arguments are not valid at a core level an exception is thrown; this is what prevents the Pet component from ever being in an "invalid state." Notice also that the Validate() method of the Pet.cfc turns around and executes the Execute() method on its ValidationBehavior object. It does this because in addition to core properties checked in the Init() method, there might be other constraints at the business level (in our demonstration there are max length constraints on those properties). These potential constraints are going to be validated by the Service object. But, since we might want to be able to swap out this Service object at some point, we are going to link the Service object to its Validate() method using the passed-in Behavior ColdFusion component above.

Here is the Service.cfc component:

  • <cfcomponent
  • output="false"
  • hint="I provide service methods for a given set of classes.">
  •  
  •  
  • <cffunction
  • name="Init"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I return an intialized service.">
  •  
  • <!--- Return THIS reference. --->
  • <cfreturn THIS />
  • </cffunction>
  •  
  •  
  • <cffunction
  • name="ValidatePet"
  • access="public"
  • returntype="array"
  • output="false"
  • hint="I validate the given pet object.">
  •  
  • <!--- Define arguments. --->
  • <cfargument
  • name="Pet"
  • type="any"
  • required="true"
  • hint="I am the pet object being validated."
  • />
  •  
  • <!--- Define the local scope. --->
  • <cfset var LOCAL = {} />
  •  
  • <!--- Set up the array of errors to be returned. --->
  • <cfset LOCAL.Errors = [] />
  •  
  •  
  • <!--- Check name. --->
  • <cfif (Len( ARGUMENTS.Pet.GetName() ) GT 50)>
  •  
  • <!--- Create error object. --->
  • <cfset LOCAL.Error = {
  • Property = "Name",
  • Type = "InvalidLength",
  • Message = "Name must be less than or equal to 50 characters."
  • } />
  •  
  • <!--- Append error. --->
  • <cfset ArrayAppend( LOCAL.Errors, LOCAL.Error ) />
  •  
  • </cfif>
  •  
  • <!--- Check type. --->
  • <cfif (Len( ARGUMENTS.Pet.GetType() ) GT 15)>
  •  
  • <!--- Create error object. --->
  • <cfset LOCAL.Error = {
  • Property = "Type",
  • Type = "InvalidLength",
  • Message = "Type must be less than or equal to 15 characters."
  • } />
  •  
  • <!--- Append error. --->
  • <cfset ArrayAppend( LOCAL.Errors, LOCAL.Error ) />
  •  
  • </cfif>
  •  
  •  
  • <!--- Return errors. --->
  • <cfreturn LOCAL.Errors />
  • </cffunction>
  •  
  • </cfcomponent>

As you can see, its ValidatePet() method only checks the application-specific constraints, not the core-meaning constraints of the Pet data type.

Now, let's take a look at a test that brings this all together:

  • <cffunction
  • name="New"
  • access="public"
  • returntype="any"
  • output="false"
  • hint="I create a new component.">
  •  
  • <!--- Return new component based on given name. --->
  • <cfreturn CreateObject( "component", ARGUMENTS[ 1 ] ) />
  • </cffunction>
  •  
  •  
  • <!-- -------- --->
  •  
  •  
  • <!--- Create a service object. --->
  • <cfset objService = New( "Service" ).Init() />
  •  
  • <!--- Create a validation behavior. --->
  • <cfset objPetValidation = New( "Behavior" ).Init(
  • Service = objService,
  • Method = "ValidatePet"
  • ) />
  •  
  • <!---
  • Create a pet and pass it the validation behavior
  • that proxies the service object.
  •  
  • NOTE: We are passing in a crazy long Type value because
  • we want to see the validation fail.
  • --->
  • <cfset objPet = New( "Pet" ).Init(
  • Name = "Molly",
  • Type = "SuperAwesomCuteFluffyKitty",
  • ValidationBehavior = objPetValidation
  • ) />
  •  
  •  
  • <!--- Get the validation errors on the pet instance. --->
  • <cfset arrErrors = objPet.Validate() />
  •  
  • <!--- Output validation errors. --->
  • <cfdump
  • var="#arrErrors#"
  • label="Business Validation Pet Errors"
  • />

As you can see, we create a Behavior instance for the validation behavior of the Pet before we create the Pet instance. Then, during Pet initialization, we pass in the Behavior as the ValidationBehavior. We then Validate() the Pet object which returns business-contextual validation errors:

 
 
 
 
 
 
Pet Validation Of Business-Level Constraints Using A Behavior Proxy Object. 
 
 
 

I like where this is going because it allows me to create and encapsulate behaviors without actually having to define any additional classes. And, at the same time, it allows me the flexibility to swap several behaviors of an object independently even if they all refer to the same, internal Service object.




Reader Comments

@Ben,

I feel like I'm missing something, so maybe you can confirm or deny. It appears that you're just "injecting" your business object with a Behavior object instead of a Service object, and calling objBehavior.execute() instead of objService.validate(). I can't see any difference in the two abstractions -- am I completely missing something?

Somewhere in your code you need to inject a Behavior or Service (and if it's the Behavior, you have to define/give it the Service), so I'm just not seeing a benefit. I would think a Factory object would allow you to create your domain object with the proper Service and/or other classes -- this would also allow you to encapsulate different types of instantiation of the same domain object. Better yet, you could leverage ColdSpring here as well.

Is it just a preference? Is there a benefit I'm missing (quite possible!)?

As always, thanks for sharing!

Best,
Jamie

Reply to this Comment

@Jamie,

Like I said, the actual need to swap out things like Service layers is not something that I actually need to employ often (or at all). But, with practically no additional overhead, I am building in the flexibility to swap things without really defining additional classes.

So, yes, AS IS, there is not real additional value. But, because the domain object is no longer bound directly to the service object, we lower the coupling between the two which makes future changes easier.

Now consider more behaviors. Let's say I have a Pet object that needs to do the following:

.Save()
.Delete()
.Validate()

If all of those calls turned around and called the same service object:

THIS.Service.Save()
THIS.Service.Delete()
THIS.Service.Validate()

... then it would be very hard to swap those out. Imagine that down the road, I actually want the Save() and Delete() methods to be part of a persistence layer and the Validate() to remain in the service layer. To make that happen, I would have to back and actually change the arguments of the Pet Init() method to take a service object for the Save/Delete and a service object for the Validate.

If, however, I started off passing in a SaveBehavior, DeleteBehavior, and ValidateBehavior from the beginning, then upgrading the application architecture to use two different services for this one Domain object would be quite easy and would not require any updates to the Pet object itself.

Now, I am not saying that I will ever have to do that; but, considered that binding a method to a component using a Behavior.cfc command proxy adds almost no overhead, I'm just saying it makes some sense to build the flexibility in from the start.

Reply to this Comment

@Ben,

Okay, that makes a lot of sense. Thanks for the additional clarification. However, I still feel like I could accomplish the same with ColdSpring: I could let a ColdSpring-managed factory inject a SaveService, DeleteService and ValidateService at domain object creation. Initially, those three services could all be the same service class (CS bean), but you could swap one or all out by making a quick change in your ColdSpring XML config.

I *think* this accomplishes just about the same thing. In both cases you encapsulate/abstract some dependencies -- either in a Behavior proxy or a ColdSpring managed factory.

If these are indeed similar solutions, I certainly wouldn't argue one or the other as better, just different ways of attacking the issue. Your solution is extremely elegant and purposeful, I'm just thinking more in the ways I already know, so just trying to make sense of it.

If I am still missing some other advantages to the Behavior proxy, I'd love to hear. Thanks again!

Reply to this Comment

@Jamie,

I am not too familiar with ColdSpring; I understand the concept but have not looked at the implementation, but it sounds like yes, this is getting at something similar.

Reply to this Comment

@Ben,

Cool, I'm not crazy :) By the way, I made mentions of ColdSpring, but now that I think about it some more, I'm really talking about using the Factory pattern to have a factory object encapsulate the creation of the business object wired up with the proper services (or behaviors, using your example). ColdSpring does simplify this a bit, but it's not a requirement.

The bottom line is that I love the idea you've outlined here -- rather than having your business object rely on a single service being wired/inject in, you can inject a dependency for each service/behavior. A factory pattern would just allow you to encapsulate the definition of those dependencies that are injected into created business objects. I think...

I'll try to blog my basic idea sometime soon. I can only envision so much code in my head at once without actually just writing it :)

Reply to this Comment

@Jamie,

Exactly. I use / love Factories to create object; but, generally, my Factories still assume that the given domain object is still going to use a single service for all three behaviors. However, if we use the behavior proxy, then we can change that in the Factory without touching the domain.

Reply to this Comment

Post A Comment

You — Get Out Of My Dreams, Get Into My Comments
Live in the Now
Oops!
Comment Etiquette: Please do not post spam. Please keep the comments on-topic. Please do not post unrelated questions or large chunks of code. And, above all, please be nice to each other - we're trying to have a good conversation here.