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.

Your initial implementation of getField
 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:

  1. What if one of the method parameters is null?

  2. The method returns null if an exception is thrown, potentially propagating null around the codebase

  3. 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:

  1. The failure and success paths are now explicit

  2. The different ways that getField can fail have been captured in code

  3. No null value has been returned from gettField

  4. The code to handle the two path is standard and easy to follow

  5. Darien tools generate the code above so that you can focus on what you need to do

  6. Considering the failure cases helps you write better tests.