While I know that it is not perfect, and I know that OOP form validation does not quite sit right with me just yet, I have gone and implemented my first shot at it. First, I upgraded my ErrorCollection.cfc so that it could accept random messages, not just messages that were based on errors found by the service objects. This has to do with the fact that form validation doesn't take place solely inside the service objects but also in the controller that is creating a certain user experience.
Once the ErrorCollection.cfc was updated, I added two methods to my ExerciseService.cfc:
Both these methods do contextual validation based on the action implied by the function; ValidateDelete() validates for exercise deletion and ValidateSave() validates for exercise persistence. Both function return an instance of the ErrorCollection.cfc. Both functions also take an optional instance of the ErrorCollection.cfc. If one is passed in, this is the one that is populated and returned. If one is not passed in, then each method will create its own instance of it, populate it, and return it. This allows one error object to be passed around to multiple service objects as well as the controller layer.
Once the validation methods were in place, I then added internal calls to them from the Save() and Delete() functions within the ExerciseService.cfc. Each of these database-interactions methods check for validation prior to the CFQuery tag. If any errors are found, the function is returned out of, returning a boolean false to indicate that the method was not successful:
Before we save the exercise information, let's double check
to make sure that it is valid.
<cfif THIS.ValidateSave( ARGUMENTS.Exercise ).HasErrors()>
There were errors to exit this method and return false
to indicate that the save was not successful.
<cfreturn false />
The programmer might invoke the ValidateSave() and ValidateDelete() methods from the controller layer as well - it is a public function. At first, I was concerned that this would lead to a redundant system that violated the DRY principle (don't repeat yourself), but then German Buela calmed my nerves. German pointed out that DRY was more about code rather than execution; meaning, while I might have the Validate methods called in a redundant way, both calls were still to the same encapsulated code chunk, which cut down hugely on repetition. Business logic was not duplicated, only calls to it were. This way of thinking made me more comfortable with the concept.
Based on advice from others and due to my inexperience with gracefully handling SQL exceptions, I did not move ahead with the idea of catching SQL errors in the ErrorCollection.cfc. Instead, I now just catch the SQL error, rollback the current transaction (I added ColdFusion CFTransaction tags), and then reThrow the error. This way, the application itself will have to handle any SQL exceptions.
Once the entire service layer was updated, I moved onto the controller layer of my ColdFusion application. I turned the REQUEST.Errors object from a ColdFusion array into an instance of the ErrorCollection.cfc ColdFusion component (which was returned by the ServiceFactory.cfc). Once the data was set into the Exercise.cfc and the validation was performed, I then set the error messages based on the error flags set by the service layer. Right now, there are only two possible error flags that need to be agreed upon bu both the service layer (creator of) and the controller layer (consumer of):
- NameExistence - Flag for no value being set.
- NameLength - Flag for set value being too long.
I like the idea of this hand-shaking on error flags, but at the same time, I don't like that it ties the display to the service layer - at least, a bit more than I like. I think, though, that this is unavoidable. This kind of coupling is always going to happen. Think about the MaxLength attribute of a text field. This is coupled to the property of the data allowed in the database. If the DB and the service layer suddenly decreased the value of a given field, I would have to go back and change all the input references as well as the error message handling. I think this is something that is unavoidable, so as much as I am not happy about the coupling, I will accept that there is some neccessity to it.
Uncomfortable or not, I think the key take away here is the fact that the data will never be corrupted. Even if the controller layer doesn't handle the errors well, the service layer will never allow corrupt data to be entered.
I think the weirdest part of this conversion for me to handle was that fact that the error messages where moved to after the whole Exercise.cfc bean interaction. I am used to having all of my form validation done before the data in the form is used for anything; now, the form data is injected into the Exercise.cfc before any form data value validation is performed. This makes me slightly uneasy because it means that all data TYPE validation has to be performed properly before there is any Exercise.cfc interaction otherwise we run the risk of setting invalid data types into the ColdFusion component.
Ok, so now that my object oriented form data validation is in effect, I am feeling like this Object Oriented ColdFusion Application is in a pretty good place. I am not sure what steps to take next. I know that some optimization could be done, but mostly this would include reducing calls to the ServiceFactory.cfc's GetService() method.
Want to use code from this post? Check out the license.