“Clean Code” Book Club: Chapter 8, Boundaries

Posted on Mon 17 June 2024

Continuing with our book club on Robert Martin’s “Clean Code”, last week we’ve discussed chapter 8 (“Boundaries”). (As usual, this post collects most of my notes and some points from our group discussion; completeness is explicitly a non-goal.)

Chapter 8: Boundaries

Here’s a boundary I like to set: I don’t want a sketch of a boy leering at a towel-clad woman in the bathroom in my programming books. The cover image for this chapter is kinda creepy.


This chapter, written by James Grenning, covers ways of interacting with third-party code (or even first-party code that’s not yet written; or that you don’t want to closely couple to).

One key point that came up again and again in our discussion was that not all dependencies are equal. On one hand, there are things like a logging framework—an example Grenning uses throughout this chapter—, where a typical application might use a small number of functions hundreds of times throughout the code base. On the other hand, there are dependencies like SciPy, where some piece of research software may use a mix of statistical functions, numerical integration or interpolation algorithms, constants and more: the overall API surface it’s exposed to is much larger, but most elements are used a small number of times or even just once.

Unfortunately, this chapter focusses solely on the first case; it does not discuss how to identify cases where these approaches may not be appropriate. I was also missing a more nuanced discussion of the downsides of the approaches advocated in this chapter.

Using Third-Party Code

Providers of third-party packages and frameworks strive for broad applicability so they can work in many environments and appeal to a wide audience. Users, on the other hand, want an interface that is focused on their particular needs. (p. 114)

The example here is a good illustration:

public class Sensors {
    private Map sensors = new HashMap();

    public Sensor getById(String id) {
        return (Sensor) sensors.get(id);
    }

    // ...
}

The Sensors class is a Map of individual sensors; but instead of exposing the full API of Java’s Map to the caller, it encapsulates the actual Map and provides its own, more limited API.

This interface lets the Sensors class explicitly enforce design and business rules; e.g. ensuring that any object added to the Map is of the Sensor type. It also allows the Sensors implementation to change over time, without changing the interface exposed to callers.

On the other hand, if the interface is too restricted, it could be frustrating to callers. E.g., if I had a Sensors class in Python, I’d expect it to support language idioms like

for sensor in sensors:
    # do something with each sensor here

and the class would feel terribly broken if that didn’t work.

Another potential downside is inconsistency: Over time, developers become familiar with the interface of common data structures (like maps, lists, etc.) in their language; so if Sensors inherits the interface of Map, they will be able to pick that up instantly. However, if Sensors implements its custom API that deviates from the familiar interface (e.g. by having a function getByID instead of get), it has a much steeper learning curve.

Now, whether the advantages are worth these trade-offs depends on the context: Developers on a large corporate Java app may value the enforced structure higher; whereas I, as maintainer of open-source scientific Python codes, prefer a more familiar interface for occasional users or first-time contributors. However, it’s a little disappointing that the book did not acknowledge these trade-offs.

Exploring and Learning Boundaries / Learning log4j / Learning Tests Are Better Than Free

These three closely related sections have led to extensive discussion during our book club. There were several points here, that we were really struggling with.

First, the suggestion that every project should write their own custom wrapper for log4j. A convenience function to set sensible defaults during initialization? Sure. But a full wrapper? That may make sense in some situations—but as above, it comes with trade-offs that the chapter simply doesn’t acknowledge. And for a dependency like SciPy, writing my own wrapper simply doesn’t make sense.

Next, we discussed “learning tests”. We commonly explore third-party code by playing around with it in the REPL before using it in our own code; but contrary to the chapter’s claim, writing tests still has a nontrivial overhead.1

We also weren’t sure, what benefit the “learning tests” are supposed to deliver. The sample code in the chapter only showed very basic tests that verify that the code executes successfully; i.e. that the log4j functions aren’t removed/renamed or get a completely different signature (such as additional required arguments). But in compiled languages, these are checks that a compiler would already perform, before even running the tests. More subtle semantic changes to an—outwardly identical—API may not be caught by the compiler; but even then, existing tests of your own code are effectively “integration tests” for your dependencies, that would catch such changes if they affect your own code.

So: What is the benefit of these learning tests? If I think about it for a while, I might come up with some edge case; but is that truly worth the ongoing cost (energy usage, time, complexity, …) of this significantly larger test suite?

Using Code That Does Not Yet Exist

This all seems sensible; and the Adapter Pattern introduced here can indeed be quite powerful. Unfortunately, as above, the chapter doesn’t discuss the limits of this approach. Sure, it works great if the API you’re mocking is fairly small—just a single function in this example. But what happens if you have a much more complex dependency, whose API design isn’t obvious?

  1. Though Java didn’t get an official REPL until 2017, long after “Clean Code” was written.