About OOError, OOResult, and OOVoidResult
Context
Relieve GUI panel code
On the question of where should we catch exceptions in relation to GUI code it was suggested (Jonatan Asketorp in this thread, “most of them (all?) should be handled latest in the ViewModel.”) that catching them early could help simplifying the higher levels.
Same messages in different contexts
Some types of exceptions are caught in different GUI actions, often resulting in basically the same error dialog, possibly only differing in the indicated context (which GUI action).
Problems found during precondition checking (for example: do we have a connection to a document) and error conditions (for example: lost connection to a document during an action) can overlap.
OOBibBase as a precondition and exception handling layer
Since most of the code originally in OOBibBase was moved to logic and almost all GUI actions go through OOBibBase, it seemed a good location to collect precondition checking and exception handling code.
Note: some of the precondition checking still needs to stay in OpenOfficePanel: for example to provide a list of selected BibEntry instances, it needs to go through some steps from frame.getCurrentLibraryTab() to (!entries.isEmpty() && checkThatEntriesHaveKeys(entries))
To avoid OOBibBase depending on the higher level OpenOfficePanel message texts needed in OOBibBase were moved from OpenOfficePanel to OOError. (Others stayed, but could be moved if that seems worthwile)
OOError
OOErroris a collection of data used in error dialogs.- It is a
JabRefExceptionwith an added field:localizedTitle - It can store: a dialog title, a localized message (optionally a non-localized message as well) and a
Throwable - I used it in
OOBibBaseas a unified format for errors to be shown in an error dialog. - Static constructors in
OOErrorprovide uniform translation from some exception types toOOErrorwith the corresponding localized messages:
public static OOError from(SomeException ex)
There is alsopublic static OOError fromMisc(Exception ex)for exception types not handled individually. (It has a different name, to avoid ambiguity) - Another set of constructors provide messages for some preconditions.
For examplepublic static OOError noDataBaseIsOpenForCiting()
- It is a
Some questions:
- Should we use static data instead of static methods for the precondition-related messages?
- pro: why create a new instance for each error?
- con:
OOError.setTitle()currently just setsthis.localizedTitleand returnsthis. For static instances this would modify a shared resource unless we create a new copy insetTitle. HoweversetTitlecan be called repeatedly on the same object: as we bubble up, we can be more specific about the context.
- Should we remove title from
OOError?- pro: we almost always override its original value
- con: may need to duplicate the title in different files (preconditions for an action in OpenOfficePanel and in OOBibBase)
- Should we include
OOError.showErrorDialog?- pro: since it was intended for error dialogs, it is nice to provide this.
- con: the reference to
DialogServiceforces it togui, thus it cannot be used inlogicormodel
- Should we use
JabRefExceptionas base?- pro:
JabRefExceptionis mentioned as the standard form of errors in the developers guide.
All Exceptions we throw should be or extend JabRefException - against:
JabRefExceptionis inlogiccannot be used in model.
(Could this be resolved by movingJabRefExceptiontomodel?)
- pro:
OOResult
During precondition checking
- some tests return no data, only report problems
- we may need to get some resources that might not be available (for example: connection to a document, a functional textview cursor)
- some test depend on these resources
While concentrating on these and on “do not throw exceptions here” … using a Result type as a return value from precondition checking code seemed a good fit:
- Instead of throwing an exception, we can return some data describing the problem.
- Conceptually it is a data structure that either holds the result (of a computation) or and error value.
- It can be considered as an extended
Optional, that can provide details on “why empty”? - It can be considered as an alternative to throwing an exception: we return an
errorinstead. - Methods throwing checked exceptions cannot be used with for example
List.map.
Methods returning a Result could. Resultshares the problem (with any other solutions) that in a function several types of errors may occur, but we can only return a single error type. Java solves this using checked exceptions being all descendants of Exception. (Also adds try/catch/catch to select cases based on the exceptions type, and some checking against forgotten cases of checked exception types)
In OOBibBase I used OOError as the unified error type: it can store error messages and wrap exceptions. It contains everything we need for an error dialog. On the other hand it does not support programmatic dissection.
Implementation
Unlike Optional and List, Result (in the sense used here) did not get into java standard libraries. There are some implementations of this idea for java on the net:
Generics allow an implementation built around
class OOResult<R, E> {
private final Optional<R> result;
private final Optional<E> error;
}
with an assumption that at any time exactly one of result and error is present.
class X<R,E> { boolean isOK; Object data; }expresses this assumption more directly, (but omits the relation between the type parameters<R,E>and the type indata)
- Since
OOResultencodes the stateisOKinresult.isPresent()(and equivalently inerrror.isEmpty()), we cannot allow construction of instances where both values areisEmpty.
In particular,OOResult.ok(null)andOOResult.error(null)are not allowed: it would make the stateisOKambiguous.
It would also break the similarity toOptionalto allow bothisEmptyandisOKto be true. -
Not allowing null, has a consequence on
OOResult<Void,E>
According to baeldung.com/java-void-type, the only possible value forVoidisnullwhich we excluded.OOResult<Void,E>.ok(null)would look strange: in this case we needok()without arguments.
To solve this problem, I introduced
class OOVoidResult<E> {
private final Optional<E> error;
...
}
with methods on the error side similar to those in OOError<R,E>, and OOVoidResult.ok() to construct the success case with no data.
The relation between Optional<E> and OOVoidResult<E>
- Both
OptionalandOOVoidResultcan store 0 or 1 values, in this respect they are equivalent- Actually,
OOVoidResultis just a wrapper around anOptional
- Actually,
- In terms of communication to human readers when used, their connotation in respect to success and failure is the opposite:
Optional.empty()normally suggests failure,OOVoidResult.ok()mean success.Optional.of(something)probably means success,OOVoidResult.error(something)indicates failure.OOVoidResultis “the other half” (the failure branch) ofOOResult- its content is accessed through
getError,mapError,ifError, notget,map,ifPresent
- its content is accessed through
OOVoidResult allows
- a clear distinction between success and failure when calls to “get” something that might not be available (
Optional) and calls to precondition checking where we can only get reasons for failure (OOVoidResult) appear together.
UsingOptionalfor both is possible, but is more error-prone. - it also allows using uniform verbs (
isError,getError,ifError, returnOO{Void}Result.error) for “we have a problem” when- checking preconditions (
OOVoidResult) is mixed with - “I need an X” orelse “we have a problem” (
OOResult)
- checking preconditions (
- at a functions head:
OOVoidResult<String> function()says: no result, but may get an error messageOptional<String> function()says: aStringresult or nothing.
Summary: technically could use Optional for both situation, but it would be less precise, leaving more room for confusion and bugs. OOVoidResult forces use of getError instead of get, and isError or isOk instead of isPresentor isEmpty.
What does OOResult buy us?
The promise of Result is that we can avoid throwing exceptions and return errors instead. This allows the caller to handle these latter as data, for example may summarize / collect them for example into a single message dialog.
Handling the result needs some code in the caller. If we only needed checks that return only errors (not results), the code could look like this (with possibly more tests listed):
OOResult<XTextDocument, OOError> odoc = getXTextDocument();
if (testDialog(title,
odoc,
styleIsRequired(style),
selectedBibEntryIsRequired(entries, OOError::noEntriesSelectedForCitation))) {
return;
}
with a reasonably small footstep.
Dependencies of tests on earlier results complicates this: now we repeat the
if (testDialog(title,
...)) {
return;
}
part several times.