Another refactoring topic today: dealing with nulls. There are a bunch of techniques, but they amount to a) don’t, and b) do but only one time ever.
The basic idea: a null always occurs in a particular context, and in that context, it has a meaning. When we pass it up or down our call-stack, we are changing contexts, and hence changing meanings. We’re using the same symbol to mean different things at different times.
Using the same generic symbol to mean different things in different contexts is anti-signal. It represents a loss of meaning. An example might help.
PixelBuffers update pixel grids on the screen. The signature for the update takes two arguments, the destination grid, and the rectangle within that grid that will receive the buffer’s pixels. If the rectangle is null, the meaning is "the whole grid".
But here’s thing, that signature is really one API to do two different things, and the client calling that Pixelbuffer service always knows which of the two things it wants to do. Either it wants the buffer to position & clip its contents, or it doesn’t.
So what could we do instead? The answers are multiple, and you will have to choose. (If you take one thing from all of my content, it is this: we are permanently, irretrievably, inveitably, and happily dependent in programming on individual humans using their judgment.)
- One answer: split the API. Since clients actually always know whether they want a destination rectangle or they don’t, let the client call one of two API’s, one that has it, and one that doesn’t.
- One answer: force the client to always supply the rectangle. There’s no nulls allowed, it’s on the client to decide whether it wants the whole grid changed or not and to pass a righteous rectangle into it.
- One answer. potentially a very valuable one, but a little hairier: don’t pass a rectangle at all, pass a placement strategy, and let the service use that strategy. This one will take some further explanation, which we’ll do here in a minute.
- One more answer: Introduce a named sentinel, and let the name carry the meaning. It could be either a simple alias, define EVERYTHING = null, or an actual instance with special values, define EVERYTHING = Rectangle(-infinity,-infinity,+infinity,+infinity).
So what are we aiming at, with all of these answers? We’re trying to maximize the meaning of the client’s code. At the client site, it’s far more obvious what the call means in its context. We’re preserving meaning, not losing it.
Some modern languages, Kotlin is one, actually take "don’t" as their default answer. No variable in kotlin is allowed to take the value null unless the programmer goes out of her way to indicate it in the variable’s type.
This is surprisingly powerful, by the way. Once you’ve kotlin’d for a while, you start to suspect every "nullable" indicator and wonder if there’s a better way. There usually is. When you convert java sources to kotlin, it really opens your eyes: there’s nullables everywhere.
It leads to a strategy of doing java->kotlin translations (which are automated and either perfectly reliable or hand-wavingly errored), and then immediately setting out to de-nullable-ize the code before you get serious with it.
Even in languages without that feature, you can simply adopt the convention as part of your team’s programming style. It’s less powerful, because the compiler doesn’t enforce it, but it’s still a win. Designs with no nulls are cleaner, tighter, and usually smaller.
Okay, let’s circle back and look at the variant where you pass a strategy instead of a null. Pursuing this line will lead us once again into the strategy pattern, an extremely useful concept from the GoF book. But let’s ease into it with another pattern from GoF, the Null Object.
In null-ish code, you often see this kind of thing:
Notice two features here. 1) it’s a one-legged if, with no else clause. 2) the thing that’s done is on the non-null x object.
Hmmm, a tad more clarity: that’s code in the service. The client can call it with x as a null, in which case the code means "no-op", don’t do anything. Or the client can call it with a real x, in which case the services uses the x to do something.
We can eliminate that null-check, and in so doing we’re enhancing our code. How? by having an instance of X whose doTheThing() function doesn’t do anything. Now, the client always calls the service with a valid instance of X, it’s just one that no-ops the "doTheThing()"
The GoF book calls this the "Null Object" pattern. I usually call it the "NoOp Strategy", myself. That’s because the smart woman, once realizing this is possible for null-check branching, will then wonder if it’s possible for a lot of different branching. Spoiler: it is.
So far, we’ve been talking mostly about "don’t", but once we stumble on the NoOp strategy, we see that "do but only one time ever" becomes a viable stance, too.
Consider the client code that is calling that service we just made branchless. If it’s, in turn, getting called with a null, its code will look like
if(x==null) x = X.Noop; service.call(x);
So what we did was ripple the branch out of the service into the client.
Now we introduce the concept of a call-tree gateway. Your code is always arranged in a tree-shape. Method A calls methods B and C, so if we put A on top, draw an arrow to B and to C, we get a call-tree. And here’s the thing, if A is the only user of B and C, A is a gateway.
That is, the code can’t get to B or C except by calling A. You know this: you use it all the time. Any time you vet inputs before you operate on them. The vetting is a gateway, because you literally cannot reach the operation without doing it.
So that client code is serving as a gateway to that service code. In our code so far, the gateway is immediately before the service call. But does it have to be? It does not. It could be done far far away, both in the code text and in the temporal setting.
So, "do but only one time ever", says sometimes you have no choice but to recieve a null. Perhaps it’s coming from a dirty database, or user input, or an old-style library call, it doesn’t matter what the source is.
When we first receive that null, right at that moment, we replace it with a null object. We have created a null gateway, and from that time forward, everyone below us (or guaranteed after us) has no need of null checks because there are no nulls in the system.
By far the most common place for this to happen in OO: during the construction of an object. Why? Because a constructor is always a gateway for every member function of that object. You can’t use an object you haven’t made, and you can’t make it without a constructor. Gateway!
Okay, we’re gonna wrap this up for now, but before we go, I want to remind you about the change-harvester’s mantra and how it relates to these possible refactorings, and I want to foreshadow our next topic.
Change-harvesters say "human, local, oriented, taken, iterative" change is the way forward. I want to make sure you see how these refactorings lend themselves to that mantra.
Null-killing is all about the humans, because it’s all about preserving meaning, and only the humans care about that, the computer does not.
It’s also local & oriented, because we can null-kill one null at a time. We don’t have to stop production for six weeks while we eliminate all nulls below all gateways. We can set that as a horizon goal to orient to, and inch by inch, de-null our codebase.
Null-killing is also iterative. If A calls B and B calls C, and B is a null gateway, if I can make A a null gateway, I can go back and eliminate the null-checking code from B. Iterative development means changing things we’ve already changed.
Okay, cool. Foreshadowing foreshadowing foreshadowing: we are so close to having everything we need to really grok the Strategy pattern, which is one of the most valuable OO design patterns there is. I say, soon, we should go there.
The GeePaw Podcast
If you love the GeePaw Podcast, show your support with a monthly donation to help keep the content flowing. Support GeePaw Here. You can also show your support by sending in voice messages to be included in the podcasts. These can be questions, comments, etc. Submit Voice Message Here.