Lambda to Reduce Coupling

Coupling occurs when one part of a system depends on another part. That is, if one part changes, the other must be adjusted too. Lambda expressions can reduce coupling by needing to refer to fewer things.

Instead of calling a method on an object, pass in a lambda and call it

Dependencies on Objects

Consider this code:

function f ( a, b, c ) {
  :
  var value = obj.method(a,b,c)
  :
}

How is f coupled to other code? There is a surprising amount of shared context:

  • There is an object obj, of a particular type or supporting a particular interface. We depend on whatever type or interface we know, plus any of its parent types.
  • Object obj must be initialized, either by us (meaning we depend on constructors or factories) or by someone else who gives it to us.
  • The object has a method named method(); we must know its name to call it.
  • The method takes three arguments; we have to know their types too.
  • The method returns a value, and we need to know its type.
  • Depending on the method and language, it may also throw exceptions of a certain type; we may depend on that type or any of its parents.

Statics and Singletons

Things aren’t any better if we have a reference to a static method or singleton, e.g., Obj.method(a,b,c). We still have similar dependencies on the types involved. We may not need to create an object, but we’ve made it far more difficult to substitute a different type.

Introducing Lambda

Instead of calling a method on an object, a lambda expression lets us pass in an anonymous function.

This reduces coupling. The calling site still needs to know the argument and return types, but there’s no longer any need to know the object (or its parents) or the name of the method that provides the function. Since there’s no object, there’s no need to create and connect up an object.

Reduce Coupling → Less Mocking

When we need a stand-in for an awkward collaborator, or we need to generate or monitor a conversation between objects, we often reach for a mock object.

Mocks do require care. I’ve seen too much code where the mocks obscure what’s really being tested, occasionally to the point where you find that nothing is actually being tested. Mocks are also a little indirect – checking that a conversation happened the way you expect doesn’t really assure you that it works correctly.

Since lambdas don’t need objects, you won’t need mock objects for tests. That’s often an improvement.

In the context of reducing coupling, I’m happy to avoid mocks where possible. Just like using a real object, a mock requires that you tell what type it’s acting like, and which method name you will use. By using lambdas, you eliminate both these pieces of information.

What’s left is so much simpler, I don’t tend to use mocking packages for it. We’ll look at how lambdas can generate responses, and how they can monitor what’s called.

Generating Responses

If only one response is needed (or the same one can be repeated), you can pass in a lambda that just returns a fixed value. If more than one response is needed, the test can hold data values that the lambda returns in order.

Here’s the original code. To test the summer() method, we’d also implicitly test the Primer.

public class Primer {
    :
    int nextPrime() {
        // complicated code to generate primes
    }
}

public int summer(int count, Primer primer) {
    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += primer.nextPrime();
    }
    return sum;
}

Let’s change summer() to take a lambda expression instead. We’ve separated the processing from the generation of values, so we can test it more easily. We do have a little overhead to manage the list of values; if we could return the same value every time it would be even simpler.

public int summer(int count, Supplier<Integer> fn) {
    int sum = 0;
    for (int i = 0; i < count; i++) {
        sum += fn.get();
    }
    return sum;
}

@Test
public void doesTheRightThingWithValues() {
    ArrayList<Integer> data = new ArrayList(List.of(1, 2, 3));

    int result = summer(3, () -> data.remove(0));

    assertEquals(result, 6);
}

Monitoring Calls

The flip side is that sometimes you want to make sure the mock was called, perhaps with certain arguments. To do this, the test can hold a string or structure that the lambda appends to with the function name and optionally its arguments. Then the test can check that the right values were captured.

Here’s the original code. We want to verify that sut2original() calls handle() with three particular strings in order.

public class Save {
    public ArrayList<String> transformedValues = new ArrayList();

    public void handle(String s) {
        transformedValues.add("<<" + s + ">>");
        // and whatever else save requires 
    }
}

public int saver() {
    var save = new Save();  // or better, pass it in
 
    var strings = new String[] {"open", "talk", "close"};
    for (String s : strings) {
        save.handle(s);
    }
    return strings.length;
}

Here, we’ve switched to calling a lambda expression rather than the Save class. We still could use the Save class (calling handle in the production lambda expression), but our system under test no longer knows the class or method name. Our lambda saves the arguments we call it with, so we can verify them.

public int saver(Consumer<String> fn) {
    var strings = new String[]{"open", "talk", "close"};
    for (String s : strings) {
        fn.accept(s);
    }
    return strings.length;
}

@Test
public void saysTheRightThings() {
    var args = new ArrayList<String>();

    int count = saver(msg -> args.add(msg));

    assertEquals(count, 3);
    assertEquals(args, List.of("open", "talk", "close"));
}

We’ve reduced dependencies, and have a simpler test than if we’d had to create and inject a mock object.

Conclusion: Reduce Coupling

By switching to a lambda, we simplify the calling code and reduce its dependencies. The code still depends on argument and return values, but the object is gone. Therefore, it no longer needs to know about the class type or method name (since they no longer exist). By reducing dependencies, we can have a simpler test, and rely less on mock objects.

Related Reading

Lambda for Control Structures, a Refactoring“, by Bill Wake. Retrieved 2021-10-26. Describes a way to extract a lambda expression.

Using ApprovalTests in .Net 14 Peel & Slice“, by Llewellyn Falco. Retrieved 2021-10-26. Video demonstrating using C# delegates as a variant lambda to “slice” out the hard part.