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.