Based on my updated plan for domain data validation in my Exercise List object oriented programming project, I have designed the ColdFusion component that will collect errors and facilitate messages delivery. It is the ErrorCollection.cfc. Right now, I am not 100% confident as to where all the validation will be going; I have had some good suggestions as well as some constructive criticism, but I feel that that kind of a decision is separate enough from the ErrorCollections.cfc that this object can be designed without a full understanding of the data validation plan.
The ErrorCollections.cfc ColdFusion component has two basic roles: collecting errors and collecting error messages. These two things are relevant to two different layers of the application, and therefore are done in two different places. The service layer handles the business logic and therefore it handles the data validation. It's job is to collect the errors. The service layer can provide some error "hinting", but it cannot really be responsible for defining the related error message as this is more of a view-related decision.
The view / display layer is what communicates with the end user, and therefore, it is the view's responsibility to translate the errors into useful user messages. The thing that ties these two layers together is the "error flag". This error flag is a unique key such as "NameLength" or "EmailValid" that both the service layer and the display layer must agree on in order for the error handling to be effective. The trick, as Rich Kroll pointed out, is that by using this handshake, the view and the service layer are not so tightly coupled together. One can change without the other one bombing out.
Now, let's take a look at the methods within the ErrorCollections.cfc. Instead of looking at all of them at once, let's look at which methods will be used by the service layer and then which will be used by the display layer. First, the Service layer:
AddError( Flag, Hint ) :: Void
The primary job of the service layer it to validate the data and add errors to the error collection object which will be passed back to the display layer. The first argument, Flag, is the unique key (unique to the object being validated) that defines what type of error this is. The second argument, Hint, is merely some additional information that the Service layer can provide as to why the validation error occurred. The hint is not really meant to be used anywhere unless necessary.
HasErrors() :: Boolean
This just returns true if the ErrorCollection.cfc instance has any errors added to it. This will be used prior to the database interaction - we don't want to insert or update to the database if the passed in data is not valid. This will also be used by the display layer to determine the page flow.
So, that's really all that the service layer needs to do in terms of validation. Now, let's take a look at the display layer related functions:
AddMessageIfError( [Flag ,] Message ) :: Void
This is basically where the hand shaking between the display layer and the service layer takes place. This method has two ways to be invoked. The first way, in which both arguments are defined, checks to see if the given error flag is in the errors collection, and IF SO, the given message is added to the message collection (also stored within the ErrorCollection.cfc).
If this method is invoked with only a single argument, Message, it will add the given message if the number of errors is not equal to the number of messages. Ideally, this method signature would only be utilized once to see if there are any errors that we don't know about. So, for example, let's say we had a form that had name and email, we might have something like this:
<cfset objErrors = ServiceObject.Save( bean ) /> <!--- Set error messages if necessary. ---> <cfset objErrors.AddMessageIfError( "NameLength", "Please enter the name" ) /> <cfset objErrors.AddMessageIfError( "EmailValid", "Please enter a valid email address" ) /> <!--- Add a message to see if there are any errors that we didn't intend to handle (ie. SQL exception errors that we don't agree on). ---> <cfset objErrors.AddMessageIfError( "There was an internal error - no data was committed" ) />
Here, we are adding error messages for the two expected (possible) errors, NameLength and EmailValid, and then we are adding an additional message, not tied to any flag, that will indicate to the user that some error took place.
GetErrors() :: Struct
This returns the struct of error flag / hints.
GetMessages() :: Array
This returns the array of messages set by calls to AddMessageIfError().
HasError( Flag ) :: Boolean
This checks to see if the error collection contains the given error flag.
So that's all there is to it. I am sure that as I begin to implement this, I will find things that need to be added; but, I am making the method set for this object purposefully small so as to confine myself to the expected usage. For example, the ErrorCollection.cfc is not intended to have random messages added to it, and therefore, it does not have a generic AddMessage() method.
Like I said before, I am really just feeling this out. Domain data validation who-what-where-when is in no way fully codified in my head. But, again, this is an iterative process, so I am sure it will need to be tweaked in future passes. The single argument method of invoking AddMessageIfError() needs to be thought out better; as German Buela previously pointed out to me, there ARE SQL exceptions that are not meant to be caught as part of validation - they are meant to be caught as application errors. I will have to be very careful about how I catch Database errors with my CFCatch tags. More to come.
Want to use code from this post? Check out the license.
It doesn't feel right for me that a save method in service returns an error object. So almost every service return error object? What if the method in the service already needs to return something else?
I would much prefer the validation takes place in a seperated method, and method like save() in the service layer only throws save-related exceptions...
Maybe validate before saving in save(), and throw InvalidBean exception? view caught the exception and call validate() on bean to get the error struct for more details? Maybe error struct can be somehow cached so the view doesn't call validate() but somehow through a method to get the error struct?
I have no problem with validate() being called twice. Althought it violates DRY, can we justify it as "defensive programming"? Sacrafices performance for robustness?
I thought about some sort of caching mechanism, but we can't really go that way since there is no way for the Service layer to know it is working with consecutive calls... what if we had 5 domain objects we were validating and then we were gonna call Save() on those 5 as well. This has to be considered an asynchronous process and we cannot depend on the user to do anything useful.
I guess, it's not the end of the world to call the Validate() method twice. It feels wrong, but as I have said elsewhere, I am new to this stuff, so it all feels a little wrong.
Just one more thought. If you look at a lot of functions, they do return some sort of success / failure flag. For example, in ColdFusion, the StructAppend() method returns a boolean indicating success. Is this not some sort of validation on the action?
Although, I guess this doesn't return a given error, it just returns the existence of any errors. Just thinking.