I have a class that transforms a complex model, for example an abstract syntax tree or intermediate model. The model can be either valid, invalid or partially invalid, i.e. it contains errors but some parts of it are valid and can be processed further.
The most simple error reporting is to use exceptions:
public class Transformer {
public TargetModel transform(SourceModel model)
throws InvalidModelException {
// ...
}
}
Obviously this doesn't allow to report multiple errors (at least if you don't attach further information to the exception) and exceptions should be for exceptional situations.
Martin Fowler addressed this problem in his article Replacing Throwing Exceptions with Notification in Validations . If you apply his method to the problem you have something like this:
public abstract class Problem {
// ...
}
public final class ProblemA {
// ...
}
public final class ProblemB {
// ...
}
public class Transformer {
public static final class TransformationResult {
private Collection<Problem> problems;
private Optional<TargetModel> targetModel;
// ...
}
public TransformationResult transform(SourceModel model) {
// ...
}
}
You then either use the visitor pattern or instanceof
checks to distinguish the errors. It's a trade-off between type-safety and verbosity.
Another possible solution would be to use the observer pattern:
public class Transformer {
public interface ProblemListener {
public void onProblemA(...);
public void onProblemB(...);
// ...
}
public void addProblemListener(ProblemListener listener) {
// ...
}
public void removeProblemListener(ProblemListener listener) {
// ...
}
public TargetModel transform(SourceModel model) {
// ...
}
}
Using the observer pattern has the advantage that you don't have to accumulate the errors in memory and that excessive instanceof
checks or visitors are not needed. On the other hand it obscures the control flow even more than the visitor pattern.
I think all solutions do not lead to readable code. In a functional language and with enough memory I would use the second solution but in Java structural pattern matching either entails instanceof
checks or use of visitor pattern which are both verbose. Is there anything that I have overlooked? Or implications that I did not consider? What is your experience with any of the solutions?
3 Answers 3
You could describe Validation as a function, that takes data as input and gives you a tuple back
inputdata -> (outpudata, Errors)
or maybe inputdata -> (outpudata, errors, warnings)
Where it will be obvious, that inputdata
is the data, you want to fed in.
Depending on your usecase
outputdata
is the same asinputdata
outputdata
is some kind ofValidationResult
where there isdata
which is yourinputdata
and maybevalidatedData
which represents the subset which is valid (in case, you want to accept partial results).
The point of Fowlers article is, that the naive approach of throwing exceptions has several downsides. An exception is some kind of early exit. Depending on your usecase, it doesn't reveal all, but only the first error occured. Say you are writing a complex forms app, you want the user to inform of all things where action is needed - unless you want to drive your users nuts.
Second: invalid data is not really exceptionally
, data is expected to be sometimes not quite right. Exceptions are a bit overkill in this scenario.
Third: Exceptions could lead to some kind of goto
-like programming structure, which could complicate the understanding of programming flow.
Exceptions should not be used for this.
In Java you would make errors
and warnings
members of ValidationResult
and would write hasErrors
and hasWarnings
and getErrors
and getWarnings
resp. addErrors
and addWarnungs
accordingly.
Your problem comes down to:
general validation function: inputdata -> (outpudata, errors, warnings)
calls a bunch of validation functions
single valiator: data -> (errors, warnings)
or using ValidationResult
as the return type of a single validation
single valiator: inputdata -> (outpudata, errors, warnings)
after each call, you add all errors
and warnings
to the according collections of ValidationResult
or implement a simple merge()
-Method on ValidationResult
.
In the end return the final ValidationResult
Done.
InvalidModelException
should be abstract and subclassed by specialized implementation, which will hold specific details. Each problem can be handled in separate block:
TargetModel target;
try {
target = transform(source);
} catch (ProblemA ex) {
ex.getDetailsA();
} catch (ProblemB ex) {
ex.getDetailsB();
} catch (InvalidModelException ex) {
ex.getGenericDetails();
}
Alternatively return compound object:
public Result<TargetModel, Diagnostic> transform(SourceModel model);
if (result.isInvalid()) {
if (result.getDiagnostic() instanceof ...)
...
// or
switch (result.getProblemType()) {
case Problem.A:
break;
case Problem.B:
break;
default:
break;
}
throw new Exception();
}
target = result.getTarget();
The most simple error reporting is to use exceptions [...] this doesn't allow to report multiple errors (at least if you don't attach further information to the exception)
Then... why not do that?
public class InvalidModelException extends Exception {
private Collection<Problem> problems;
}
Explore related questions
See similar questions with these tags.