Simplifying Validations Using the Strategy(Enum) Pattern
Validations can be tricky, but that can be gotten around. See how the Strategy Design Pattern, implemented via Enum, can make your life easier.
Join the DZone community and get the full member experience.
Join For FreeLet's say we're dealing with a number of input fields that require validation. The validation logic might be simple or complex, involving many different factors or parameters to validate a single field. Normally, we use a number of if...else blocks to validate each and every input field. This approach will lead to concerns regarding maintainability, readability, and extensibility. Hence, I would like to discuss the best approach using the Strategy pattern, but using Enum for the implementation. This approach was already used in one of my previous projects and proved to be useful. Let's start our discussion.
Problem Statement
Let's assume that we ask the user to provide some of his personal and bank details when he applies for a loan. Once we receive the details, we might want to validate those. In real life, it might be many more fields and more complex validations. But, for our simple example, we will take around 10 fields and will perform simple validations — like whether the fields are empty.
The number of fields for validation may change over time. We may want to add more fields or may want to temporarily avoid validating certain fields. Hence, the solution should be flexible enough to adapt to these requirements.
Also, we need two different types of results. The first one is whenever a field is not valid. We need to return that field without continuing the validations for other remaining fields. The second one is to validate all the fields and return a list of invalid fields.
Solution Approach
As per the requirement, the validation strategy for each field will be different, and we may need to extend the validations for more fields in future. Hence, the best approach will be to use the Strategy pattern. But, with the usual 'Class' way of implementation, we need more classes to be defined. Hence, we will use Enum for the strategy implementation.
The structure is explained in the picture below.
As given in the above picture, the interface, ValidationStrategy, has methods to perform the validation and to display the invalid field. The fields are defined in the Enum ValidationType. This strategy interface is implemented by the Enum UserValidationStrategy, which has the validation fields as its members and each member has the overridden validate() method. One thing to note here is that the members need to be defined in the order in which they need to be validated. For example, in our case, the DATE_OF_BIRTH field will be validated first and then the field CITY and so on.
The class UserValidationContext accepts set of validation strategies and uses those strategies for the execution. It has the method execute() which returns the field name whenever it finds the invalid one for the given set of fields for the validation. The second method — executeAndGetList() — will return the list of all the invalid fields after performing validation for all the given fields.
The Implementation Details
Below is the validation strategy interface.
package com.validation.enumuse;
import constants.ValidationType;
public interface ValidationStrategy {
<T extends UserInput> boolean validate(T input);
ValidationType getValidationType();
}
This is the class which implements the above interface. Each member of this Enum is a separate strategy implementation. For simplicity, we have taken validation logic like isUserValid() and isEmpty(). It is fine to have very complex logic inside here, as long as we have a separate implementation.
package com.validation.enumuse;
import com.calculator.enumuse.CalculatorInput;
import com.calculator.enumuse.InvalidOperationException;
import constants.ValidationType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public enum UserValidationStrategy implements ValidationStrategy {
DATE_OF_BIRTH (ValidationType.DATE_OF_BIRTH) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.getDateOfBirth().isEmpty() ) {
return true;
}
return false;
}
},
CITY (ValidationType.CITY) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.getCity().isEmpty() ) {
return true;
}
return false;
}
},
ZIP (ValidationType.ZIP) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.getZip().isEmpty() ) {
return true;
}
return false;
}
},
COUNTRY (ValidationType.COUNTRY) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.getCountry().isEmpty() ) {
return true;
}
return false;
}
},
SSN (ValidationType.SSN) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.getSsn().isEmpty() ) {
return true;
}
return false;
}
},
CREDIT_CARD_NUMBER (ValidationType.CREDIT_CARD_NUMBER) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isCreditCardAdded() && input.getCreditCardNumber().isEmpty() ) {
return true;
}
return false;
}
},
BANK_ROUTING_NUMBER (ValidationType.BANK_ROUTING_NUMBER) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankRoutingNumber().isEmpty() ) {
return true;
}
return false;
}
},
BANK_ACCOUNT_NUMBER (ValidationType.BANK_ACCOUNT_NUMBER) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankAccountNumber().isEmpty() ) {
return true;
}
return false;
}
},
BANK_NAME (ValidationType.BANK_NAME) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankName().isEmpty() ) {
return true;
}
return false;
}
},
BANK_CITY (ValidationType.BANK_CITY) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankCity().isEmpty() ) {
return true;
}
return false;
}
},
BANK_ZIP (ValidationType.BANK_ZIP) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankZip().isEmpty() ) {
return true;
}
return false;
}
},
BANK_COUNTRY (ValidationType.BANK_COUNTRY) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankCountry().isEmpty() ) {
return true;
}
return false;
}
},
UNSUPPORTED (ValidationType.UNSUPPORTED) {
public <T extends UserInput> boolean validate(T input) {
return false;
}
};
private ValidationType validationType;
private UserValidationStrategy(ValidationType validationType) {
this.validationType = validationType;
}
public ValidationType getValidationType() {
return validationType;
}
}
The class below is the context class, which accepts the set of strategies for performing the validation. It uses the received strategies for its execution.
package com.validation.enumuse;
import com.calculator.enumuse.InvalidOperationException;
import constants.ValidationType;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Set;
public class UserValidationContext {
private Set<ValidationStrategy> userValidationStrategies;
public UserValidationContext(Set<ValidationStrategy> userValidationStrategies) {
this.userValidationStrategies = userValidationStrategies;
}
/*
* This method performs validation for fields one by one and return the invalid one if found.
* Otherwise, it will continue validating remaining fields. If all the fields are valid then will return null.
*/
public ValidationType execute(UserInput userInput) {
ValidationStrategy userValidation = UserValidationStrategy.UNSUPPORTED;
for (Iterator<ValidationStrategy> iterator = userValidationStrategies.iterator(); iterator.hasNext();) {
userValidation = iterator.next();
if (userValidation.validate(userInput)) {
return userValidation.getValidationType();
}
}
return null;
}
/*
* This method performs validation for fields one by one and
* add the invalid one into a list if found and returns that list.
*/
public List<ValidationType> executeAndGetList(UserInput userInput) {
ValidationStrategy userValidation = UserValidationStrategy.UNSUPPORTED;
List<ValidationType> validatedTypes = new ArrayList<ValidationType>();
for (Iterator<ValidationStrategy> iterator = userValidationStrategies.iterator(); iterator.hasNext();) {
userValidation = iterator.next();
if (userValidation.validate(userInput)) {
validatedTypes.add(userValidation.getValidationType());
}
}
return validatedTypes;
}
}
The below Enum defines the validation types. This helps avoid using undefined fields in our program.
package constants;
public enum ValidationType {
DATE_OF_BIRTH,
CITY,
ZIP,
COUNTRY,
SSN,
CREDIT_CARD_NUMBER,
BANK_ROUTING_NUMBER,
BANK_ACCOUNT_NUMBER,
BANK_NAME,
BANK_CITY,
BANK_ZIP,
BANK_COUNTRY,
UNSUPPORTED
}
The below class is the input class which has all the fields collected from the user.
package com.validation.enumuse;
public class UserInput {
private String dateOfBirth;
private String city;
private String zip;
private String country;
private String ssn;
private String creditCardNumber;
private String bankRoutingNumber;
private String bankAccountNumber;
private String bankName;
private String bankCity;
private String bankZip;
private String bankCountry;
private boolean isUserValid;
private boolean isFullFunding;
private boolean isBankAdded;
private boolean isCreditCardAdded;
public String getDateOfBirth() {
return dateOfBirth;
}
public void setDateOfBirth(String dateOfBirth) {
this.dateOfBirth = dateOfBirth;
}
public String getCity() {
return city;
}
public void setCity(String city) {
this.city = city;
}
public String getZip() {
return zip;
}
public void setZip(String zip) {
this.zip = zip;
}
public String getCountry() {
return country;
}
public void setCountry(String country) {
this.country = country;
}
public String getSsn() {
return ssn;
}
public void setSsn(String ssn) {
this.ssn = ssn;
}
public String getCreditCardNumber() {
return creditCardNumber;
}
public void setCreditCardNumber(String creditCardNumber) {
this.creditCardNumber = creditCardNumber;
}
public String getBankRoutingNumber() {
return bankRoutingNumber;
}
public void setBankRoutingNumber(String bankRoutingNumber) {
this.bankRoutingNumber = bankRoutingNumber;
}
public String getBankAccountNumber() {
return bankAccountNumber;
}
public void setBankAccountNumber(String bankAccountNumber) {
this.bankAccountNumber = bankAccountNumber;
}
public String getBankName() {
return bankName;
}
public void setBankName(String bankName) {
this.bankName = bankName;
}
public String getBankCity() {
return bankCity;
}
public void setBankCity(String bankCity) {
this.bankCity = bankCity;
}
public String getBankZip() {
return bankZip;
}
public void setBankZip(String bankZip) {
this.bankZip = bankZip;
}
public String getBankCountry() {
return bankCountry;
}
public void setBankCountry(String bankCountry) {
this.bankCountry = bankCountry;
}
public boolean isUserValid() {
return isUserValid;
}
public void setUserValid(boolean isUserValid) {
this.isUserValid = isUserValid;
}
public boolean isFullFunding() {
return isFullFunding;
}
public void setFullFunding(boolean isFullFunding) {
this.isFullFunding = isFullFunding;
}
public boolean isBankAdded() {
return isBankAdded;
}
public void setBankAdded(boolean isBankAdded) {
this.isBankAdded = isBankAdded;
}
public boolean isCreditCardAdded() {
return isCreditCardAdded;
}
public void setCreditCardAdded(boolean isCreditCardAdded) {
this.isCreditCardAdded = isCreditCardAdded;
}
}
Below is the unit test class for testing a few of the scenarios. The method name is self-explanatory to understand its functionality.
package com.validator.enumuse;
import com.validation.enumuse.UserInput;
import com.validation.enumuse.UserValidationContext;
import com.validation.enumuse.UserValidationStrategy;
import com.validation.enumuse.ValidationStrategy;
import constants.ValidationType;
import org.junit.Assert;
import org.junit.Test;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.Set;
public class ValidatorTest {
@Test
public void testValidatorForDOB() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
strategies.add(UserValidationStrategy.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
strategies.add(UserValidationStrategy.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
strategies.add(UserValidationStrategy.BANK_NAME);
strategies.add(UserValidationStrategy.BANK_CITY);
strategies.add(UserValidationStrategy.BANK_ZIP);
strategies.add(UserValidationStrategy.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Assert.assertEquals(ValidationType.DATE_OF_BIRTH, ctxUser.execute(input));
}
@Test
public void testValidatorForCITY() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
strategies.add(UserValidationStrategy.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
strategies.add(UserValidationStrategy.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
strategies.add(UserValidationStrategy.BANK_NAME);
strategies.add(UserValidationStrategy.BANK_CITY);
strategies.add(UserValidationStrategy.BANK_ZIP);
strategies.add(UserValidationStrategy.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("01/01/1990");
input.setCity("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Assert.assertEquals(ValidationType.CITY, ctxUser.execute(input));
}
@Test
public void testValidatorForUNSUPPORTED_DOB() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
//strategies.add(UserValidation.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
UserInput input = new UserInput();
input.setUserValid(true);
// this will be ignored for validation as we didn't include in the Set
input.setDateOfBirth("");
input.setCity("");
input.setZip("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Assert.assertEquals(ValidationType.CITY, ctxUser.execute(input));
}
@Test
public void testValidatorForGetList() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
strategies.add(UserValidationStrategy.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
strategies.add(UserValidationStrategy.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
strategies.add(UserValidationStrategy.BANK_NAME);
strategies.add(UserValidationStrategy.BANK_CITY);
strategies.add(UserValidationStrategy.BANK_ZIP);
strategies.add(UserValidationStrategy.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("01/01/1990");
input.setCity("SanJose");
input.setZip("");
input.setCountry("USA");
input.setSsn("");
input.setBankAdded(true);
input.setCreditCardAdded(true);
input.setCreditCardNumber("");
input.setBankRoutingNumber("");
input.setBankAccountNumber("");
input.setBankName("");
input.setBankCity("");
input.setBankZip("");
input.setBankCountry("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Object[] arrResultActual = ctxUser.executeAndGetList(input).toArray();
Object[] arrResultExpected = {ValidationType.ZIP, ValidationType.SSN, ValidationType.CREDIT_CARD_NUMBER, ValidationType.BANK_ROUTING_NUMBER, ValidationType.BANK_ACCOUNT_NUMBER, ValidationType.BANK_NAME, ValidationType.BANK_CITY, ValidationType.BANK_ZIP, ValidationType.BANK_COUNTRY};
Assert.assertArrayEquals(arrResultExpected, arrResultActual);
}
@Test
public void testValidatorForAllValidAndGetList() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
strategies.add(UserValidationStrategy.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
strategies.add(UserValidationStrategy.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
strategies.add(UserValidationStrategy.BANK_NAME);
strategies.add(UserValidationStrategy.BANK_CITY);
strategies.add(UserValidationStrategy.BANK_ZIP);
strategies.add(UserValidationStrategy.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("01/01/1990");
input.setCity("SanJose");
input.setZip("95131");
input.setCountry("USA");
input.setSsn("123-45-6789");
input.setBankAdded(true);
input.setCreditCardAdded(true);
input.setCreditCardNumber("123456789");
input.setBankRoutingNumber("123456789");
input.setBankAccountNumber("12345678898");
input.setBankName("Abc");
input.setBankCity("SanJose");
input.setBankZip("95131");
input.setBankCountry("USA");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Object[] arrResultActual = ctxUser.executeAndGetList(input).toArray();
Object[] arrResultExpected = {};
Assert.assertArrayEquals(arrResultExpected, arrResultActual);
}
@Test
public void testValidatorForAllValid() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
strategies.add(UserValidationStrategy.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
strategies.add(UserValidationStrategy.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
strategies.add(UserValidationStrategy.BANK_NAME);
strategies.add(UserValidationStrategy.BANK_CITY);
strategies.add(UserValidationStrategy.BANK_ZIP);
strategies.add(UserValidationStrategy.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("01/01/1990");
input.setCity("SanJose");
input.setZip("95131");
input.setCountry("USA");
input.setSsn("123-45-6789");
input.setBankAdded(true);
input.setCreditCardAdded(true);
input.setCreditCardNumber("123456789");
input.setBankRoutingNumber("123456789");
input.setBankAccountNumber("12345678898");
input.setBankName("Abc");
input.setBankCity("SanJose");
input.setBankZip("95131");
input.setBankCountry("USA");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Assert.assertNull(ctxUser.execute(input));
}
@Test
public void testValidatorForUNSUPPORTED() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
//strategies.add(UserValidationStrategy.CITY);
strategies.add(UserValidationStrategy.ZIP);
//strategies.add(UserValidation.COUNTRY);
strategies.add(UserValidationStrategy.SSN);
//strategies.add(UserValidation.CREDIT_CARD_NUMBER);
strategies.add(UserValidationStrategy.BANK_ROUTING_NUMBER);
strategies.add(UserValidationStrategy.BANK_ACCOUNT_NUMBER);
//strategies.add(UserValidation.BANK_NAME);
//strategies.add(UserValidation.BANK_CITY);
//strategies.add(UserValidation.BANK_ZIP);
//strategies.add(UserValidation.BANK_COUNTRY);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("");
input.setCity("SanJose");
input.setZip("95131");
input.setCountry("USA");
input.setSsn("");
input.setBankAdded(true);
input.setCreditCardAdded(true);
input.setCreditCardNumber("");
input.setBankRoutingNumber("123456789");
input.setBankAccountNumber("");
input.setBankName("");
input.setBankCity("");
input.setBankZip("");
input.setBankCountry("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Object[] arrResultActual = ctxUser.executeAndGetList(input).toArray();
Object[] arrResultExpected = {ValidationType.DATE_OF_BIRTH, ValidationType.SSN, ValidationType.BANK_ACCOUNT_NUMBER};
Assert.assertArrayEquals(arrResultExpected, arrResultActual);
}
}
Extensibility
I know you might be wondering how can we extend the Enum implementation without violating the Open/Closed principle, which is Open for extension and Closed for modification. For extension, you can define another Enum implementing the same interface, and when you prepare your set of validation strategies, you can include this new Enum.
This is explained in the below code.
package com.validation.enumuse;
import constants.ValidationType;
public enum ExtendedValidationStrategy implements ValidationStrategy {
BANK_ACCOUNT_TYPE (ValidationType.BANK_ACCOUNT_TYPE) {
public <T extends UserInput> boolean validate(T input) {
if(input.isUserValid() && input.isBankAdded() && input.getBankAccountType().isEmpty() ) {
return true;
}
return false;
}
};
private ValidationType validationType;
private ExtendedValidationStrategy(ValidationType validationType) {
this.validationType = validationType;
}
public ValidationType getValidationType() {
return validationType;
}
}
And, the way to include and execute is given below.
@Test
public void testValidatorForExtension() {
Set<ValidationStrategy> strategies = new LinkedHashSet<ValidationStrategy>();
strategies.add(UserValidationStrategy.DATE_OF_BIRTH);
strategies.add(ExtendedValidationStrategy.BANK_ACCOUNT_TYPE);
UserInput input = new UserInput();
input.setUserValid(true);
input.setDateOfBirth("01/01/1994");
input.setBankAdded(true);
input.setBankAccountType("");
UserValidationContext ctxUser = new UserValidationContext(strategies);
Assert.assertEquals(ValidationType.BANK_ACCOUNT_TYPE, ctxUser.execute(input));
}
Conclusion
I hope the discussed solution will help you resolve similar problems. You can download the entire code from the linked git repo. You can find another example for the Calculator application using this approach in the same repo.
Opinions expressed by DZone contributors are their own.
Comments