At this point in my journey of learning object oriented programming in ColdFusion, I have a lot of the domain logic encapsulated into our Domain and Service objects, but our validation logic is still hanging out in the controller code. Because form data validation is essential for the application to work properly, I am 100% sure that this is something that should be encapsulated to a large degree. Think about it this way - if you needed to create an exercise record from two different places in the application, you would only have one set of objects, but you would have to duplicate the data validation code. One of the largest benefits of object oriented programming is ensure that processes have maintained integrity no matter where they are called from within the application. If validation logic has to be duplicated in each area that uses the model, then clearly, we cannot ensure anything except large maintenance costs.
As such, I think it is essential that validation logic be encapsulated somehow. But how?!? And where?!? And, even more importantly, what? There are two types of data validation that need to occur and I am not sure that both types should be in the same place. When it comes to a piece of data, we need to think both about the type of data that it is and then about its value. Take for instance, the Read() method. The Read() method takes an integer. On one hand, we need to ensure that we pass in an Integer value otherwise the ColdFusion CFArgument tag will throw a validation error. Then, on the other hand, we need to make sure that the ID is a proper value.
If I wanted to move all the data validation logic into the Domain Models then I would have to basically get rid of all the type checking that I have in my CFArguments tags within my setter / mutator methods. Otherwise, if I tried to pass an empty string into a method that expected an ID, ColdFusion would throw an argument validation error. Somehow, I feel like taking away all parameter validation is NOT the answer. As such, I must assume that DATA TYPE validation must remain in the business logic but outside of the domain model.
Assuming that the data type validation will remain external to the domain model, that means that it is the responsibility of the calling code to pass in data of the correct type. This makes sense. The domain model API creates a contract between the programmer and the domain model object. If a method says that it requires a numeric ID, then it is up to the programmer to pass in a numeric ID. Think about an Automated Teller Machine (ATM). The "api" states that I am supposed to put a card of some type into the machine. That is the TYPE of data that is requires. If, however, I try to pour my tasty seltzer into the machine's card slot, then, only bad things can happen. Is that the fault of the ATM? I say, No. It's my fault because I violated the agreement that I made with the ATM to put only items of type CARD into the slot. This would be the equivalent of passing string data into a method that expected numeric data.
Now, had I put my Vitamin Shoppe card into the ATM, it is the ATM's job to reject it because, while it is the right TYPE of data (a card of some sort), its value is not correct. This would be the equivalent of passing a negative number into a method that required positive integer values.
I am very comfortable with this concept of separating the two different data validation responsibilities. But, who's responsibility is it to validate those data values? Right now, I'm really feeling like this is a "Service" to be provided to the programmer. As such, it should probably be part of the Service objects for the related domain model objects. So, for example, if I was dealing with an Exercise.cfc ColdFusion component, I would expect that the ExerciseService.cfc ColdFusion component would have a method called Validate() that would take one argument which was an Exercise.cfc instance. Validate() would then return some sort of array or "error collection" object which could then be used in the XHTML page view.
This raises another question then; if the domain model validation is a service that is called by the programmer, there is nothing to ensure that only valid data is passed to the Save() methods which persist the domain data in the database. Just because the error collection is returned to the user, there is nothing to stop the user from ignoring the errors and still passing the domain object instance to the Save() method. At this point, the Validate() method doesn't really guarantee anything. So, how do I make sure that no invalid data will be stored in a way that doesn't depend on the end user to pass in only valid data.
My first thought was to have the Save() method also call the Validate() method to make sure there were no data value errors before the data was committed to the database. But, then, I would be calling the Validate() method twice for every page request (potentially), once by the user and once by the Service object itself. Again, one of the main benefits of object oriented programming is that we don't have to duplicate effort. Remember the whole DRY principle - Don't Repeat Yourself. By calling Validate() twice, we are going against everything that we are aiming for.
So, how can I make sure that Validate() is only called once? Right now, the only thing that I can think of is that Validate() is a private method of the Service object and is called only from within the Save() method. The Save() method would then return the collection of data validation errors if there were any. The calling code in the controller would then look something like this:
<cfse objErrors = objExerciseService.Save( objExercise ) />
Here, the controller code is calling the Save() method and then collection any errors that the Save() method might generate. This approach makes sure that the Validate() method is both called when it is essential and only once such that we are not repeating database calls and validation effort.
This seems pretty good, but then we run into another problem: how do we translate the validation errors into errors that are meaningful to the user. For instance, the Validate() method might tell us that the "Name" property of the Exercise.cfc instance is not valid, but how do we translate that into the message "Please enter an exercise name" which would be displayed to the end user. This translation cannot be done in the ExerciseService.cfc or even in the Exercise.cfc because neither of them can have any real sense of the view. In fact, the names might not even line up; the Exercise.cfc might have a "Name" property, but on the view, we are calling it an "Exercise Label". As such, readable messages cannot be generated by the domain model or the service objects.
Before we even figure that problem out, it raises yet another point - the "error collection" returned from the Save() method cannot return messages as the message are view-related not domain related. As such, the "error collection" can really only be some collection of invalid property names. These are the only things that can really translate from the domain model to the view without issue.
But now, this raises even another issue (oh dear)! If the error collection is just a list of properties, how would we alert something like "That exercise name is already in use". We could return "Name" as being invalid in the Exercise.cfc, but at that point, we cannot differentiate between "Please enter an exercise name" and "That exercise name is already in use". So, it seems that returning a list of invalid properties is not sufficient; we need to return both properties and some sort of reason as to why they are invalid. But, of course, at the same time, we need to be able to translate this into something that is usable in the view!
This is starting to make my head spin. Data validation seems to be harder than the leap it took me to understand the domain model itself. I don't think that this is something that I can just figure out on my own. Time to hit the books and blogs looking for tips.
Please, if anyone can point me in the right direction here, that would be awesome! I would totally make it worth your while ;)
Want to use code from this post? Check out the license.