Why I Don't Do Work in Constructors

2024-09-27
4 min read

In java, the only thing I’ll do in a constructor is assign a parameter to a field. Sounds strict? Let me explain.

The canonical anti-pattern

This is where our story begins.

class Thingy
{
    private final FileInputStream inputStream;

    public Thingy() throws FileNotFoundException
    {
        this.inputStream = new FileInputStream("/tmp/my-very-special-file.txt");
    }

    ...

This is the sort of class that I used to find a lot in my early career, but less so now.

It does a few things I don’t like:

  • It opens a FileInputStream in the constructor
  • It hard codes where that FileInputStream comes from
  • The field declaration is for a FileInputStream rather than an InputStream

All of these dislikes are driven by the same underlying desire: I want to unit test this code. Those three mistakes mean I have to use files, in a specific location. Even without a strict definition of unit test (they really shouldn’t talk to the file system, particularly in a hermetically sealed bazelesque build world), that just makes the test hard to write.

Ideally that class would look like this:

final class Thingy implements AutoCloseable
{
    private final InputStream inputStream;

    Thingy(InputStream inputStream) {
        this.inputStream = inputStream;
    }

    public static Thingy openThingy() throws FileNotFoundException {
        return new Thingy(new FileInputStream("/tmp/my-very-special-file.txt"));
    }

    ...

Now we can approach unit testing armed with a ByteArrayInputStream, and we push the convenience hard-coding of the default implementation into a handy factory method.

I don’t think I’ve said anything particularly controversial. Yet.

Becoming radicalised

I definitely don’t have a case for “assignment and nothing else” at this point.

Here are some statements I might have a case for:

  • “depend on interfaces, not implementations” (this one is quite famous).
  • “listen to the tests - if writing the test is hard, examine why” (also famous).
  • “minimise (and ideally completely evict) side effects in constructors”. Mostly a corollary of 2.

If folks left this post here with those reasonably pragmatic thoughts I would be satisfied - if we achieve all three of these things we’re at a solid 9/10 on my hygiene scale. Having gotten there - why not push for the perfect score?

To move forward, we need to take a trip into my brain.

A model of how I work

Programming is tiring. It notably isn’t maths. The numbers don’t even work properly, it’s disgusting.

It’s tiring because going fast is a product of navigating the correct branches of a decision tree. Decisions aren’t free - there’s a daily budget for them (for me anyway), and I don’t always notice when it gets exhausted (and I start making bad ones).

We can make decisions cheaper by turning them into non-decisions; by having a system of rules that can, as much as possible, be blindly followed, rather than requiring us to actually engage with a problem. That system of rules comes with a few “bad luck - now you have to think” escape hatches (thanks for nothing, Gödel).

The first two of our defensible statements are definitively not that - they are abstract “food for thought” things. There are times when #1 is not true and you want to be explicit about coupling, for example.

The third is actionable but still a bit abstract - what is a side effect? Is it writing to disk? What about allocation?

The ruthless teenager operating the controls of this lumbering robot wants something more enforceable, and asks instead: why not avoid all constructor work, just to simplify things? What have we got to lose?

The price of simplicity

Let’s return to Thingy.

final class Thingy
{
    private final Map<String, String> state;

    Thingy() 
    {
        this.state = new HashMap<>();
    }
}

Hard line constructor hygienists would call that work in a constructor. What’s the alternative?

final class Thingy
{
    private final Map<String, String> state;

    Thingy(final Map<String, String> state)
    {
        this.state = state;
    }

    public static Thingy newInstance()
    {
        return new Thingy(new HashMap<>());
    }
}

Wordier. It’s still convenient to construct for the outside user, but there’s more code. We do get a nice side benefit - if a test wanted to snoop on the state, it now can, via that package-private constructor.

For this example, the cost seems minimal, and we get an unexpected benefit. Most importantly our very simple rule is intact.

What else might we do in a constructor that isn’t assignment? I think it mostly falls into those two camps; avoidance of work is cheap, so adherence to the simple rule is easy.

Join me.

Avatar
James Byatt I was a mathematician but then I sold out