In typical project development, create and update operations are inevitable. These two operations often share identical parameter validation logic, especially for custom checks that cannot be handled by annotation-based validation frameworks. For example, when organizing an event, if the event type is offline, the event address must be provided; if it is online, the address is not required. Such conditional validations must be coded manually because standard bean validation cannot express the dependency between fields.
2. Requirement Analysis
Initially, the same custom validation was written twice—once inside the create method and once inside the update method. This approach worked, but whenever the validation rules changed during early development stages, two places had to be updated, making maintenance tedious. A better way was to write the validation logic once and reuse it across both operations.
3. Solution
The natural first step is to extract the common validation checks into a private method. The challenge is to convey the validation outcome back to the caller. The method needs to indicate whether the validation passed, and if not, provide a reason. This led to designing a simple result holder:
@Data
public class ValidationOutcome {
public ValidationOutcome() {}
public ValidationOutcome(boolean defaultValid) {
this.valid = defaultValid;
}
private boolean valid = false;
private String message = "";
public ValidationOutcome setValid(boolean valid) {
this.valid = valid;
return this;
}
public ValidationOutcome setMessage(String message) {
this.message = message;
return this;
}
}
Using this helper, the extracted private validation method creates an instance at the start (the default valid is false, so only a successful path needs to set it to true). When a rule fails, it sets an error message and returns the object immediately. The calling create or update method then inspects the outcome: if its invalid, it stops processing and returns the error information. This eliminates duplicate validation code.
The following pseudo‑code demonstrates the pattern inside the validation method:
ValidationOutcome outcome = new ValidationOutcome();
// first custom check
if (someConditionFails) {
return outcome.setMessage("Reason for failure");
}
// second custom check
...
// all checks passed
return outcome.setValid(true);
If a developer prefers the default to be true, the parameterized constructor can be used instead. The fluent setter methods keep the code concise and readable.