Introduction
Most of the computer systems are designed to be accessed initially by a few clients like UI, and some batch processes, when the application interface number grows (for example allowing RMI, WebServices, ResFull, etc), the validations and even the business rules get complex, and sometimes developers need refactor some pieces of software to maintain consistency.
Some systems should adapt to handle different business rules on different environments, and those rules may impact on the validation of the incoming commands, and validation mechanisms gains comeplexity.
While some frameworks guide us to implement data validations in some specific way, most of the time the decision is taken by architects. These examples will show some basic validation principles to be considered.
Background
The first advice is: implement each external access to the system across MVC.
The MVC pattern takes some responsibilities related to validations in every section:
- The View is responsible of data collection, should collect the enough amount of information to execute a comand on a controller, and there is no obligated responsibility to validating any of the input values, think about batch process that can access to our controllers and does not have any input validation.
To improve user interaction on views that perform interactions with human users, it's desired to have some input type data cheek to prevent empty values, to avoid invalid data types or to check correct data format, etc. This kind of human user validations are desired to prevent incorrect command calls. - The Controller has the responsibility to get values of the request, check the correct data type and perform data conversions if are needed. Also the controller could check some architecture limitations on the input data, validations like max value, min value, length, invalid characters, or null values. With this information che controller fills the Command Value Object and execute the command in the model.
- The Model receives the requested command with the correct data type as expected, so its responsibility is to check every business rule and architecture limitations of each data type before apply any system status change.
Do you know the concept of Fat Model, Skinny Controller? Basically says that the controller should be as simple as possible, and limits its behavior to something like :
- Parse requested data types
- Call the model
- Process the model response
- Build the context for the view
- Call the view
I will use a simple concept to focus on validations.
To simplify implementation and to not deal with in any
framework specific issue, i will implement the controllers using
servlets, and everyone can adapt this solution to most used frameworks
and model access techniques.
Simple Solution
Imagine that we have a selling portal that allows our clients buy items, and we will work on the process to buy an item, the entities are represented by: Client, Item and Buy.
The Controller is represented by BuyController.
A simple way, in which a controller interacts with the model, could be:
Controller :
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String> dataTypeValidationErrors = new HashMap<String, String>();
Integer itemCode = NumberUtils.toInt(request.getParameter("itemId"), 0);
if(itemCode == 0) {
dataTypeValidationErrors.put("itemIdError", "Select an item to buy.");
}
Integer itemQuantity = NumberUtils.toInt(request.getParameter("itemQuantity"), 0);
if(itemQuantity == 0) {
dataTypeValidationErrors.put("itemQuantityError", "Select how many items will buy.");
}
Integer paymentMethodCode = NumberUtils.toInt(request.getParameter("paymentMethodId"), 0);
if(paymentMethodCode == 0) {
dataTypeValidationErrors.put("paymentMethodIdError", "Select the payment method.");
}
if(dataTypeValidationErrors.size() > 0) {
for(String key: dataTypeValidationErrors.keySet()) {
request.setAttribute(key, dataTypeValidationErrors.get(key));
}
}
dataTypeValidationErrors = buyModelService.validateBuyItem(itemCode, itemQuantity);
if(dataTypeValidationErrors.size() > 0) {
for(String key: dataTypeValidationErrors.keySet()) {
request.setAttribute("itemQuantityError", dataTypeValidationErrors.get(key));
}
}
dataTypeValidationErrors = buyModelService.validatePaymentMethod(paymentMethodCode);
if(dataTypeValidationErrors.size() > 0) {
for(String key: dataTypeValidationErrors.keySet()) {
request.setAttribute("paymentMethodIdError", dataTypeValidationErrors.get(key));
}
}
if(dataTypeValidationErrors.size() > 0) {
for(String key: dataTypeValidationErrors.keySet()) {
request.setAttribute(key, dataTypeValidationErrors.get(key));
}
}
BuyItemCommand biCommand = new BuyItemCommand();
biCommand.setItemId(itemCode);
biCommand.setItemQuantity(itemQuantity);
biCommand.setPaymentMethodId(paymentMethodCode);
try {
Buy performedBuy = buyModelService.buyItem(biCommand);
request.setAttribute("buy", performedBuy);
} catch (BuyException | ParameterValidationException ex) {
request.setAttribute("error", ex.getMessage());
} catch (Exception ex) {
}
}
When the cotroller set request attributes like 'itemIdError', 'itemQuantityError' and 'paymentMethodIdError' i bound values to fields that shows the error message in the correct UI form location. 'error' is a generic error to be sown in the UI.
Business Service :
public Buy buyItem(BuyItemCommand biCommand) throws ParameterValidationException, BuyException {
Customer currentCustomer = sessionInfo.getCurrentCustomer();
if(currentCustomer == null) {
throw new SecurityException("Client not logged In.");
}
Item item = itemRepository.get(biCommand.getItemId());
if(item == null) {
throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
}
PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
if(paymentMethod == null) {
throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
}
if(validateBuyItem(item, biCommand.getItemQuantity()).size() > 0) {
throw new ParameterValidationException("itemId", "Select a valid Item to Buy.");
}
if(validatePaymentMethod(paymentMethod).size() > 0) {
throw new ParameterValidationException("paymentMethodId", "Select a valid payment method.");
}
if(validatePaymentMethodForItem(paymentMethod, item).size() > 0) {
throw new BuyException("You cannot buy this item with the specified payment method.");
}
BuyBuilder buyBuilder = new BuyBuilder();
buyBuilder.setCustomer(currentCustomer);
buyBuilder.setItem(item);
buyBuilder.setPaymentMetod(paymentMethod);
buyBuilder.setQuantity(biCommand.getItemQuantity());
return buyRepository.add(buyBuilder.build());
}
On the previous example the validations and rules are checked in three blocks:
- The validation on the first block is about incoming data types.
- The validation on second block is performed on the business layer, and related to business rules.
- Finally the buyItem method throws an exception if something fails in an unexpected way.
The first block is inevitable, every controller should validate data input and adapt them to call the correct command. It's a good idea to implement some library to unify the conversions in the architecture layer.
The second block validates business rules, but also defines a validation flow, and this is undesired to do in the controllers, suppose that validation changes depending on context, the validations procedure should have a complex logic and it's unnecessary implemented on each controller.
Also, sometimes the UI team is not the same as the model team, moving
validation flow to the controllers team means an interaction between
model developers and UI developers that can be avoided.
Basically the problem is that the validation flow that the model needs to do on buyItem is transferred to the controllers.
Validation Handler
A better design would be to implement a business validation handler to validate commands, this will follow the validation flow for the buyItem event considering the context that it's executed. Doesn't means that field validations should desapear, when the environment needs validation of each data input performed by the client in real time, then the validation handler could expose methods to validate each UI field data, but when the user sends the command to commit the operation, the server should run the correct validation on demand.
Controller:
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String> basicValidationErr = validateInputDataTypes(request);
if(basicValidationErr.size() > 0) {
for(String key: basicValidationErr.keySet()) {
request.setAttribute(key, basicValidationErr.get(key));
}
}
BuyItemCommand biCommand = new BuyItemCommand();
biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
Map<String, String> bssValidationErr = buyItemValidator.validateBuyItem(biCommand);
if(bssValidationErr.size() > 0) {
for(String key: bssValidationErr.keySet()) {
if(key.equals("itemCode")) {
request.setAttribute("itemCodeError", bssValidationErr.get(key));
} else if (key.equals("itemQuantity")) {
request.setAttribute("itemQuantityError", bssValidationErr.get(key));
} else if (key.equals("paymentMethodCode")) {
request.setAttribute("paymentMethodCodeError", bssValidationErr.get(key));
}
}
}
try {
Buy performedBuy = buyModelService.buyItem(biCommand);
request.setAttribute("buy", performedBuy);
} catch (BuyException | ParameterValidationException ex) {
request.setAttribute("error", ex.getMessage());
} catch (Exception ex) {
}
}
Validator (BuyItemValidator.java) :
public Map<String, String> validateBuyItem(BuyItemCommand biCommand) {
Map<String, String> problems = new HashMap<String, String>();
Item item = itemRepository.get(biCommand.getItemId());
if(item == null) {
problems.put("itemId", "Select a valid Item to Buy.");
}
PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
if(paymentMethod == null) {
problems.put("paymentMethodId", "Select a valid payment method.");
}
if(!problems.isEmpty()) {
return problems;
}
if(validateItemQuantity(item, biCommand.getItemQuantity()).size() > 0) {
problems.put("itemId", "Select a valid Item to Buy.");
}
if(validatePaymentMethod(paymentMethod).size() > 0) {
problems.put("paymentMethodId", "Select a valid payment method.");
}
if(validatePaymentMethodForItem(item, paymentMethod).size() > 0) {
problems.put("paymentMethodId", "You cannot buy this item with the specified payment method.");
}
return problems;
}
And Business Service :
public Buy buyItem(BuyItemRequest bIRequest) {
Customer currentCustomer = sessionInfo.getCurrentCustomer();
if(currentCustomer == null) {
throw new SecurityException("Client not logged In.");
}
Map<String, String> problems = buyItemValidator.validateBuyItem(bIRequest);
if(!problems.isEmpty()) {
throw new BuyException("Your buy cannot be performed.");
}
Item item = itemRepository.get(bIRequest.getItemId());
PaymentMethod paymentMethod = paymentMethodRepository.get(bIRequest.getPaymentMethodId());
BuyBuilder buyBuilder = new BuyBuilder();
buyBuilder.setCustomer(currentCustomer);
buyBuilder.setItem(item);
buyBuilder.setPaymentMetod(paymentMethod);
buyBuilder.setQuantity(bIRequest.getItemQuantity());
return buyRepository.add(buyBuilder.build());
}
In this example I split the data type validations in a new method, also define a business method that receives the same object that the final business method, so any validation can be done in this validator, and most important, we solve the validation flow in the model layer.
This new class BuyItemValidator will validate the command buyItem, receiving the same parameters that the buyItem will receive with the same context. This handler centralizes the validation flow, and also enables different business rules on different contexts, fitting different client needs.
BuyItem method is optimistic, calling validations on handlers, it permits a clean focus on one objetive: save the buy.
Can we ask more? Yes sure...
The Transactional Problem
With this approach our enterprise application will feel powerful, but may fall in unexpected results on concurrent access, the validation could success the initial validation, but fail when the business method is invoked. Think about the item availability, when you call the validator maybe the provider has the enough stock to perform the sell, but when the event is invoked it fails, someone else commit a buy between the validation and the method call. So, would be better if the validations run inside the buyItem method then we could block stock in the same transaction, in the meanwile could return the error or success.
Also another problem is solved with this validation procedure, the validation code runs only once performing a bettwe use of the server resources; with the previous implementations some validations runs twice.
One way to handle this situation is to encapsulate the response into a Response object that could have the response itself and a list of errors, something like this:
Model :
public BuyResponse buyItem(BuyItemCommand biCommand) throws BuyException {
BuyResponse response = new BuyResponse();
Customer currentCustomer = sessionInfo.getCurrentCustomer();
if(currentCustomer == null) {
throw new SecurityException("Client not logged In.");
}
Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
if(!problems.isEmpty()) {
response.setValidationProblems(problems);
return response;
}
Item item = itemRepository.get(biCommand.getItemId());
PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
BuyBuilder buyBuilder = new BuyBuilder();
buyBuilder.setCustomer(currentCustomer);
buyBuilder.setItem(item);
buyBuilder.setPaymentMetod(paymentMethod);
buyBuilder.setQuantity(biCommand.getItemQuantity());
response.setBuy(buyRepository.add(buyBuilder.build()));
return response;
}
The business method should be able to respond the object if everything is ok, and the list of validation errors. This is the functionality of BuyResponse, encapsulate the object for success calls or error list if something is wrong. The controller now gets the errors inside the BuyResponse, or the correct Buy when success.
There is an alternative, sometimes the power of Exceptions is underestimated, the Exception mechanism is a powerful tool that in good hands do magic, suppose that we have an special ParameterValidationException that contains all the details about the validation errors, lets implement the Model like this :
Model:
public Buy buyItem(BuyItemCommand biCommand) {
BuyResponse response = new BuyResponse();
Customer currentCustomer = sessionInfo.getCurrentCustomer();
if(currentCustomer == null) {
throw new SecurityException("Client not logged In.");
}
Map<String, String> problems = buyItemValidator.validateBuyItem(biCommand);
if(!problems.isEmpty()) {
throw new ParameterValidationException(problems);
}
Item item = itemRepository.get(biCommand.getItemId());
PaymentMethod paymentMethod = paymentMethodRepository.get(biCommand.getPaymentMethodId());
BuyBuilder buyBuilder = new BuyBuilder();
buyBuilder.setCustomer(currentCustomer);
buyBuilder.setItem(item);
buyBuilder.setPaymentMetod(paymentMethod);
buyBuilder.setQuantity(biCommand.getItemQuantity());
return buyRepository.add(buyBuilder.build());
}
Controller :
protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
Map<String, String> basicValidationErr = validateInputDataTypes(request);
if(basicValidationErr.size() > 0) {
for(String key: basicValidationErr.keySet()) {
request.setAttribute(key, basicValidationErr.get(key));
}
}
BuyItemCommand biCommand = new BuyItemCommand();
biCommand.setItemId(NumberUtils.toInt(request.getParameter("itemCode")));
biCommand.setItemQuantity(NumberUtils.toInt(request.getParameter("itemQuantity")));
biCommand.setPaymentMethodId(NumberUtils.toInt(request.getParameter("paymentMethodCode")));
try {
Buy currentBuy = buyModel.buyItem(biCommand);
request.setAttribute("buy", currentBuy);
} catch (ParameterValidationException pe) {
for(String key: pe.getValidationErrors().keySet()) {
if(key.equals("itemCode")) {
request.setAttribute("itemCodeError", pe.getValidationErrors().get(key));
} else if (key.equals("itemQuantity")) {
request.setAttribute("itemQuantityError", pe.getValidationErrors().get(key));
} else if (key.equals("paymentMethodCode")) {
request.setAttribute("paymentMethodCodeError", pe.getValidationErrors().get(key));
}
}
} catch (BuyException ex) {
request.setAttribute("error", ex.getMessage());
} catch (Exception ex) {
}
}
These implementation 'breaks' some exception rules defining an special exception for parameter validation errors, that Exception will be thrown on validation problems, and internally has complete information about problems, like a map with each request object property and the error message.
Finally a solution based on Exceptions allows business validations implemented with aspects easy, running business validations before method execution.
Enjoy Validations.