I asked this questions on StackOverflow but it's definitely a bit too broad. Even for this website, although the question is about software design, it might not be enough "focused".
I am working on a family of java 8 applications where a bit of refactoring is definitely overdue. Right now I'm focusing on the error handling, but the problem can be stated more generally: how would you represent the concept of "alternative result values"?
To give a more concrete explanation, let's say I have a source I can query in order to retrieve some data:
T value = source.get(x)
For a variety of reason outside my control this method might not be able to produce the value of type T I requested. In this case I would like, as the caller, to know what went wrong. Be it some DB connection problem or just the fact that what I was searching for does not exist.
The prolem gets even harder when a single procedure needs to gather data from different sources each possibly requiring the values retrieved earlier from the other sources:
T procedure(p) {
A a = sourceA.get(p);
B b = sourceB.get(p,a);
C c = sourceC.get(p,a,b)
return new T(p,a,b,c);
}
Each of these sources would have its own set of "response codes" which are returned when something prevents them from producing the expected result.
As of now in my applications this issues are handled in a variety of questionable ways: sometimes a null is returned, sometimes (for the applications that are webapps) HTTP codes are used deep inside the "logic" layer of the application and sometimes an empty/uninitialized version of the expected object is returned just for the sake of not returning null.
I would like to find a general-enough way of handling this issue so that, by adopting it in all the applications, no one needs to invent a solution on the spot (further increasing the entropy) when a problem like this arises.
To choose the "strategy" I am focusing on three main (not necessarily indipendent) points:
- how the error is returned to the caller
- how the error is handled (here the ease of use is the main concern)
- how much freedom I have in the representation of the error (sometimes a code might not be enough and more information needs to be stored in the error)
Here are the solutions I thought about divided into two categories depending on how the first point (error returning) is addressed.
Result object. This is essentially a wrapper object (Result<E,T> or Result<T>) which contains a "response code" and, optionally, a value of type T if everything worked fine.
This solution allows for the error to be returned in a "procedural" fashion
Result<T> procedure(p) {
Result<A> resA = sourceA.get(p);
if (!resA.isOK())
return Result.of(resA.getCode())
A a = resA.getValue();
Result<B> resB = sourceB.get(p,a);
if (!resB.isOK())
return Result.of(resB.getCode())
B b = resB.getValue();
Result<C> resC = sourceC.get(p,a,b)
if (!resC.isOK())
return Result.of(resC.getCode())
C c = resC.getValue();
return Result.of(new T(p,a,b,c));
}
or in a "functional" one (it is essentially an Either<L,R>).
Result<T> procedure(p) {
return sourceA.get(p)
.flatMap(a -> sourceB.get(p,a)
.flatMap(b -> sourceC.get(p,a,b)
.flatMap(c -> Result.of(new T(p,a,b,c))));
}
The functional solution is "conceptually" cleaner but a lot harder to read and i think it looks a bit out of place in java. The procedural one looks fine but it's a lot more prone to programming error considering that no one forces you to check the result with isOK before calling getValue/getCode.
Here the handling depends directly on how I choose to represent the error/status. The simplest solution would be to use an enum. In that case the caller would handle everything with a switch-case:
Result<T> procedureResult = precedure(p);
if (procedureResult.isOK()){ ...do stuff... }
else {
switch(procedureResult.getCode()) {
case CODE1 : ...
case CODE2 : ...
...
default : ...
}
}
This is a bit limiting because enums are not very customizable.
To be more flexible I might use a different class for each error. This gives me complete freedom but adds quite a bit of complexity in the "handling phase". In fact, the only way (I can think of) to handle all the different objects in a general way is to use a visitor pattern. This is the exact opposite of "ease of use" mostly because if different errors need to be handled differently depending on the context I might end up defining a Visitor implementation for each possible combination. Furthermore I am not the only one working on this code and introducing the visitor pattern would require convincing everybody that the benefits justify the counter-intuitive setup they will be forced to work with.
As a note I'll point out that a variation on the Result object might be some sort of C-like status:
Status procedure(p, resultContainer)
Or an "inverted" version of the same idea:
Optional<T> procedure(p, statusContainer)
But they present the same issues as Result and more.
Checked Exceptions. Here the errors can have whatever shape they want and are easily returned and handled with the famous goto shenanigans.
T procedure(p) throws SourceAException1,
SourceAException2,
SourceBException,
SourceCException {
A a = sourceA.get(p);
B b = sourceB.get(p,a);
C c = sourceC.get(p,a,b)
return new T(p,a,b,c);
}
try {
T = procedure(p);
}
catch(SourceAException1 sae1){ ... }
catch(SourceAException2 sae2){ ... }
catch(SourceBException sbe){ ... }
catch(SourceBException sbc){ ... }
On paper this look like the better solution (note that I like the "errors annotations" in the method signature even if it's a bit verbose) but there are 2 problems:
- in java using exception as an "alternative return" is considered bad practice (by some an anti-pattern even) because an exception signals an exceptional occurrence and should not be used for "expected behaviour". Moreover, by generalizing the problem to "status codes" and not necessarily errors, I've thrown myself quite a bit outside of the exceptions' scope and intended use.
Even assuming that this might actually be just a matter of preference, what worries me is:
- the exceptions are slow. From what I found it seems this is caused by the construction of the stack trace. In order to alleviate this problem I found out that you can make the stacktrace non-writable but this looks very non-standard and I am not even sure it would make that much of a difference.
At this point it's obvious I need to find a compromise. My most recent idea is to give up a bit of "representation freedom" and (reluctantly) some type safety and use an enum where each value of the enum has a Map<String,Object> (maybe even Map<String,String>). The key-value pairs are general enough to let me represent almost anything I can think of. With a builder method (here called "with") that adds the pair to the enum value, the creation and returning of the code would look like this:
class Source {
Result<T> get(x) {
...
if (everything-is-ok) return Result.of( new T(...) )
else return Result.of(CODE1.with("key1",val1).with("key2",val2)...)
}
}
and the caller could handle everything with a switch-case (and probably some terrible type cast).
Of all the solutions I can think of, despite what most people say, I find the checked exceptions to be the cleanest. Nevertheless (even if I expect no state-altering logic to be executed in the catch clause) it would definitly be an abuse, even in their "lightweight" version without stacktrace.
So, at the end of it all, I will ask you guys:
- should I forget about using exceptions this way?
- do you know some other solution that better addresses some of the concerns I listed here?
Thank you
Chain of responsibility pattern
. The!isOk then return code
is the breaking condition to halt the chain. You only have to get rid of thisResult.of(result.getCode())
I'm sureresult
is very capable of solving this internally.pipeline pattern
. I always struggle to see/understand the differences between these two patterns. The chain has certain advantages I have tested myself, for example, you can fork/join the chain with conditional / merge links.chain of commands
can be used when a request is dispached and you need to find who, in some sort of hierarchy, can handle it. Thepipeline
is closer to what I need since it looks like function composition (am I intepreting them right?). The problem is that, since all the "pipes" take exactly one argument, for each step I need to define a new "container" object which gathers all the results from the previous calls. Anyway, I was focused more on the "failure" side of the computation: how do I communicate to the caller that something failed and WHY it failed(p, result)
would be ok.p
is a common denominator among all the functions andresult
can be an aggregate, or can be recalculated in each "step function". Given the unknown number of "T" types and "sources", I would not use generics.