Skip to content

Instantly share code, notes, and snippets.

@bdkosher
Last active October 4, 2018 17:14
Show Gist options
  • Save bdkosher/58a7c2865ef934a371a0b68e674ff32d to your computer and use it in GitHub Desktop.
Save bdkosher/58a7c2865ef934a371a0b68e674ff32d to your computer and use it in GitHub Desktop.

Tap that Assignment with Java

When working with mutable objects, I often instantiate some object and subsequently call mutator methods to populate its state. I find this both boring and smelly.

Consider the following (admittedly contrived) unit test:

@Test
public void primary_org_is_most_precise() {
    Set<Org> orgs = new HashSet<>();
    Org org1 = new Org();
    org1.setCode("1200");
    orgs.add(org1);
    Org org2 = new Org();
    org2.setCode("1234");
    orgs.add(org2);
    Org org3 = new Org();
    org2.setCode("1230");
    orgs.add(org3);
  
    Employee subject = new Employee();
    subject.setOrgs(orgs);  
    
    Org expectedPrimaryOrg = new Org();
    expectedPrimaryOrg.setCode("1234");
    assertThat(subject.getPrimaryOrg()).isEqualTo(expectedPrimaryOrg);
}

It's a relatively straightforward test hampered by smells that include:

  1. The method scope is polluted with unnecessary objects. The orgs, org1, org2, and org3 variables are ephemeral and only necessary for the setup of the Employee instance, which is the subject of the test.
  2. Since object insantiation and mutation are independent operations, I am forced to give these intermediary setup objects names. Further, because of the single scope, I must name all objects distinctly.
  3. The number of named variables in the same scope puts me at risk of making a mistake, such as adding org1 to the Set twice and failing to add org2 at all. These mistakes can be hard to spot. Did you notice the mistake in my example code?
  4. Overall, the test reads poorly. I must wade through a dozen lines of setup code code to glean the important pieces from it, and the ratio of setup code to assertion code seems out of balance.

There are some ways to allieviate these issues, at least in part. I could separate the setup logic into its own dedicated method, but that introduces two methods with a 1:1 coupling per typical test method, negatively impacting readability. The collection factory methods introduced in Java 9 could benefit here, but that only helps with the creation of the Set<Org> and not the other types. I could also add an Org(String code) convenience constructor and perhaps even a setOrgs(Org... orgs) convenience method on Employee, but this won't scale well for more complex object graphs.

Ultimately and generally speaking, I would like to consolidate instantiation and mutation concerns whenever I assign a variable reference and have these concerns be isolated in their own scope.

Is there a solution?

Tap

A number of languages offer built-in methods that help in this situation. In Kotlin it's called "apply"; in Ruby and Groovy it's called "tap".

Regardless of the name, the method takes as an argument some lambda/closure/code block that can interact with the object the method was invoked on, and ultimately returns that object.

Here's an example of how the above code could be re-written with Groovy's tap method:

@Test
public void primary_org_is_most_precise() {
    Employee subject = new Employee().tap {
      orgs = new HashSet<Org>().tap {
          add(new Org().tap {
              code = '1200'
          })
          add(new Org().tap {
              code = '1234'
          })
          add(new Org().tap {
              code = '1230'
          })
      }
    }
    assert subject.primaryOrg == new Org().tap { code = '1234' }
}

Granted, there are Groovy alternatives to tap that could be used instead, but the point is that the tap method resolves my aforementioned smells with the original code.

  1. It creates distinct scopes for each Org instance and the Set.
  2. In doing so, it eliminates the requirement to give each instance a distinct name.
  3. Since the objects are not in the same scope, I avoid setup mistakes related to naming. There's no way for me to accidentally set the code property of the second Org instance twice, like I did in my original Java method.
  4. Because all setup logic is contained with a sub-scope of the test method's scope, I'm better able to visually distinguish setup code from assertion code.

Mimicking Tap in Java

Although not as elegant as Groovy, a tap-like method can be written in Java fairly easily:

    public static <T> T tap(T object, Consumer<T> consumer) {
        consumer.accept(object);
        return object;
    }

The primary differences between this Java method and Groovy's are:

  • It's a static method versus an (extension) instance method on java.lang.Object.
  • The Java method passes the object as an argument to the consumer whereas the Groovy method will simply delegate to the object tap was invoked on. Because Java's Consumer needs an argument, I'm forced to come up with a name for it, but at least the name is tied to its own scope.

Despite these differences, I still find Java's tap to be quite useful. Here's the original test method rewritten so that the employee assignment is tapped:

@Test
public void primary_org_is_most_precise() {
    Employee subject = tap(new Employee(), employee -> {
        employee.setOrgs(tap(new HashSet<>(), orgs -> {
            orgs.add(tap(new Org(), org -> org.setCode("1200")));
            orgs.add(tap(new Org(), org -> org.setCode("1234")));
            orgs.add(tap(new Org(), org -> org.setCode("1230")));
        }));
    });
  
    assertThat(subject.getPrimaryOrg()).isEqualTo(tap(new Org(), org -> org.setCode("1234")));
}

Overall, I find this to be more readable and less risky than the original. If you're unconvinced, consider the use case of creating java.util.Date instances:

    Date date = tap(Calendar.getInstance(), cal -> {
        cal.set(Calendar.YEAR, 2021);
        cal.set(Calendar.DAY_OF_MONTH, 9);
        cal.set(Calendar.MONTH, 1);
        cal.set(Calendar.HOUR_OF_DAY, 20);
        cal.set(Calendar.MINUTE, 3);
    }).getTime();

I've also found tap useful for simply isolating concerns within unit tests. For example, consider an integration test for adding a user via a RESTful interface. There are three REST calls to make:

  1. A GET to ensure the user does not exist before attempting to create it.
  2. The PUT to add the user.
  3. Another GET to make sure the user was added.

Each of these three HTTP requests can be done within its own tap call to isolate objects related to the response and its payload.

Conclusion

I find tap to be a useful functional construct and a handy utility method in my Java toolkit. I mainly use it within unit tests but am finding occassions where it is useful outside of unit tests, too.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment