Using the Darien Library
It is a truth seldom acknowledged that developers mostly focus on the Happy Path. Developers are busy with lots to do. And today your challenge is to
retrieve a field from a Java object, even when that field is marked private, so you develop getField
below.
You write a couple of unit tests to be sure that what you have written retrieves a named field. For each test, you pass in the required name of the field, its classname, and the object the field is to be retrieved from.
Your tests work, and you deploy your code.
1public static Object getField(String classname, String fieldname, Object inst) {
2 try {
3 Class<?> cls = Class.forName(classname);
4 Field fld = cls.getDeclaredField(fieldname);
5 fld.setAccessible(true);
6 return fld.get(inst);
7 } catch (ExceptionInInitializerError eiie) {
8 log(eiie);
9 } catch (ClassNotFoundException cnfe) {
10 log(cnfe);
11 } catch (NoSuchFieldException nsfe) {
12 log(nsfe);
13 } catch (SecurityException se) {
14 log(se);
15 } catch (IllegalArgumentException ile) {
16 log(ile);
17 } catch (NullPointerException npe) {
18 log(npe);
19 } catch (IllegalAccessException iae) {
20 log(iae);
21 }
22
23 return null;
24}
However, there are a number of points to note:
What if one of the method parameters is null?
The method returns null if an exception is thrown, potentially propagating null around the codebase
Your two unit tests only test the happy path
A null method parameter will result in a ClassNotFoundException, NoSuchFieldException or NullPointerException being thrown, logged and the
method returning null. Passing back null requires the code that calls getField
to distinguish these two cases or else another
NullPointerException will be thrown, which might remain a silent bug in the code.
As each of the three method parameters could be null and any of the seven catch
statements might occur, there are ten different ways that this
method might fail. The happy path represents the code block in the try
statement (highlighted) running to completion with no issues.
You decide to rewrite the above using the Darien Library.
The Darien Library
The rewrite wraps your results in Darian Libray objects and tool support generates the code to unwrap them.
The code above becomes:
1public static S getField(String cn, String fn, Object inst) {
2 if (FailureUtils.oneIsNull(cn, fn, inst)) {
3 return FailureUtils.theNull(cn, fn, inst);
4 }
5
6 try {
7 Class<?> cls = Class.forName(cn);
8 Field fld = cls.getDeclaredField(fn);
9 fld.setAccessible(true);
10 return new Success(fld.get(inst));
11 } catch (ExceptionInInitializerError eiie) {
12 return new FErr(eiie);
13 } catch (ClassNotFoundException cnfe) {
14 return new FExp(cnfe);
15 } catch (NoSuchFieldException nsfe) {
16 return new FExp(nsfe);
17 } catch (SecurityException se) {
18 return new FExp(se);
19 } catch (IllegalArgumentException ile) {
20 return new FExp(ile);
21 } catch (NullPointerException npe) {
22 return new FExp(npe);
23 } catch (IllegalAccessException iae) {
24 return new FExp(iae);
25 }
26}
getField
returns an instance of the type S
. All of the method parameters are passed to FailureUtils.oneIsNull
, which returns true
if one of them is null. FailureUtils.theNull
returns
an instance of the type FailureArgIsNull
that lists the arguments that are null along with the filename and line where the call to theNull
takes place. This is useful when tracing issues in
deployed, live systems.
Line 10 returns the retrieved field, wrapped in a Success
class that implements the S
type.
The ExceptionInInitializerError
and all of the exceptions are caught and returned wrapped in an appropriate Failure
type, Ferr
or FExp
.
Calling getField
The invocation of the rewritten getField
is this which is taken from a unit test.
1FailureArgIsFalse faif = FailureUtils.theFalse(new Boolean[] {false, false});
2S obj = TestUtils.getField("org.darien.types.impl.ArgsList", "idxs", faif);
3
4if(obj.eval()) {
5 List<Number> idxs = (List<Number>) obj.unwrap();
6
7 assertTrue(idxs.size() == 2);
8 assertTrue((int)idxs.get(0) == 0);
9 assertTrue((int)idxs.get(1) == 1);
10 } else {
11 switch (obj) {
12 case FailureError err -> assertTrue(err.getLocation(), false);
13 case FailureException exp -> assertTrue(exp.getLocation(), false);
14 case FailureArgIsNull fain -> assertTrue(fain.getLocation(), false);
15 default -> assertTrue(false);
16 }
17 }
Darien tool support writes the if
, obj.unwrap
, else
and pattern matchine switch
. The code writing tool will output code compatible with pre 17 Java for those that do not use the pattern matching preview feature.
getField
(line 2) is called with a classname, fieldname and instance.
An object (obj
) of type S
is returned. If eval
returns true, obj
represents the success case and unwrap
is called. Otherwise, the call has failed and the switch
on line 11
is executed.
In the success case, unwrap
returns the result from line 10 of the implementation of getField
above (fld.get(inst)
).
If the failure path is execued, the switch
on obj
executes and obj
is cast into one of the three failure types generated from the eight ways the method can fail (FailureError
,
FailureException
, and FailureArgIsNull
). In each case, an assertion fails (on the righthand side of the ->), passing in a string message from getLocation
that describes where in the
code the failure type was created.
As written, the default case cannot execute as obj
will only be one of the three failure types. If getField
returned an additional type, the switch would have to be updated with an explicit
case or else the default would exceute. This is the reason for the assertion failure on the default line.
Advantages of this Approach
The advantages of this approach are:
The failure and success paths are now explicit
The different ways that
getField
can fail have been captured in codeNo
null
value has been returned fromgettField
The code to handle the two path is standard and easy to follow
Darien tools generate the code above so that you can focus on what you need to do
Considering the failure cases helps you write better tests.