De doorsnee backend-applicatie ontkomt er niet aan om validatie toe te passen op business-objecten voordat deze mogen worden gepersisteerd in de database of verder mogen worden verwerkt in de geldende business-logica.
Vaak begint het dan ook dat de applicatie een EJB krijgt die verantwoordelijk is om het object te controleren op eigenschappen en logica. Gevaar hierbij is dat tijdens de ontwikkeling en doorontwikkeling al snel een groeiend woud aan voorwaarden ontstaat naarmate de functionele requirements van de applicatie worden uitgebouwd. Ook in ons huidige project liepen we tegen dit probleem aan, het aantal validatiestappen groeide en groeide, en ook de verschillende typen objecten waarvoor validatie nodig is nam gestaag toe.
Nu zou je kunnen kiezen voor een rules-framework als bijvoorbeeld Drools, of Easy Rules, maar soms is dat overkill en wil je alleen maar je business-rules op een duidelijke manier onderhoudbaar in je applicatie beschikbaar hebben. Daarom de vraag: “Wat wil je dat er gebeurt bij een validatieregel?”
Het eenvoudige antwoord hierop is: “Een simpele controle dat een bepaalde waarde/eigenschap van mijn onderhanden object voldoet aan de gestelde business-eisen.”
En als vervolgvraag: “Hoe kan ik eenvoudig bijhouden welke validatieregels er gelden voor een bepaald business-entiteit?”. Dit kan door alle geldende regels te bundelen en dan uit te voeren. En als niet aan een validatieregel wordt voldaan, dan wil ik dat kunnen onderkennen middels een exceptie zodat de overige validatieregels niet meer worden uitgevoerd. Een generieke validatieregel is dus eigenlijk:
public interface ValidationRule<O extends Object, E extends Exception> { public void validate(O o) throws E; }
Middels de validate-methode wordt het business-object ‘o’ gevalideerd. Als deze niet valide is dan wordt exceptie ‘E’ gegooid.
En een generieke rule-engine kan worden gedefinieerd als verantwoordelijke voor het bijhouden van de geldende regels voor een business-object en voor het uitvoeren van de regels:
public interface RuleEngine<O, E extends Exception> { /** * Registers a new {@link ValidationRule} in the RuleEngine to apply for the entity defined in the Rule * * @param rule */ void register(ValidationRule<O, E> rule); /** * Execute all {@link ValidationRule}s that are registered in the RuleEngine * * @param object * @throws E When a checked exception occurs while executing the business rules */ void fire(O object) throws E; }
Met de RuleEngine wordt het mogelijk om Validation-rules van een bepaald object type te registeren. Ook kun je een eigen type Exception definiëren waarvan je verwacht dat die zal worden gegooid indien de validatie-regel niet slaagt. Het uitvoeren van de geregistreerde validatieregels wordt getriggered door de fire-methode, met als parameter het business-object dat aan de validatieregels onderworpen moet worden.
Deze rule-engine is ook uit te breiden door het gebruik van een generiek Rule-type in plaats van de ValdationRule-type. De ValidationRuleEngine implementatie zou er vervolgens zo uit kunnen zien:
public class ValidationRuleEngine<O, E extends Exception> implements RuleEngine<O, E> { private List<ValidationRule<O, E>> listOfBusinessRules = new ArrayList<>(); @Override public void register(final ValidationRule<O, E> rule) { listOfBusinessRules.add(rule); } @Override public void fire(final O object) throws E { for (ValidationRule<O, E> rule : listOfBusinessRules) { rule.validate(object); } } }
Dat is niet veel code voor iets dat bestempeld is als ‘engine’. En dat hoeft ook niet. De echte uitdaging bij het uitvoeren van de validatie zit hem in de validatieregels zelf. Deze engine zorgt ervoor dat alle validatiecode netjes in kleine rule-classes kan worden geschreven.
We kunnen dus bijvoorbeeld de eerste implementatie van een ValidationRule als volgt definiëren, als we willen controleren of een auto die volgens de specificaties vier wielen hoort te hebben die ook daadwerkelijk heeft:
public class CarHasFourWheelsRule implements ValidationRule<Car, InvalidCarException> { @Override public void validate(final Car car) throws InvalidCarException { if (car.getWheels().stream().count() != 4) { throw new InvalidCarException("Your Car should have 4 wheels according its specifications"); } } }
De RuleEngine die we nodig hebben om een Car-object te valideren is van het type Car en zal een InvalidCarException gooien als de gedefinieerde ValidationRule een fout constateert in ons Car-object.
public void validateMyCar(final Car myCar) throws InvalidCarException { RuleEngine<Car, InvalidCarException> ruleEngine = new ValidationRuleEngine<>(); ruleEngine.register(new CarHasFourWheelsRule()); ruleEngine.register(new CarHasEngineRule()); ruleEngine.register(new CarHasHeadLightsRule()); ruleEngine.register(new CarHasBreakLightsRule()); ruleEngine.register(...); ruleEngine.fire(myCar); }
Als je meerdere validaties toevoegt aan je RuleEngine dan blijft het geheel nog erg goed leesbaar. Als er wijzigingen in een bepaalde ValidationRule-implementatie nodig zijn, omdat bijvoorbeeld ook twin-engine Car-objecten als valide gelden dan is dat een eenvoudige exercitie.
Het framework dat hier beschreven wordt, is bewust geen implementatie van JSR 303, omdat in de implementatie van de Rules ook gebruik moet kunnen worden gemaakt van andere stateless beans die stukken business-logic uitvoeren. Hierbij worden bijvoorbeeld stamdata opgehaald uit de database en bepaald welke stamdata geldig zijn, waarop deze informatie in de Rule wordt toegepast.