Technical Practices: Trust Your Code

There's a few ways I've talked about this historically. One of the biggest that is probably well recognized in the industry

No Nulls

Do not let nulls into your code.

Clean Code touches the idea

All it takes is one missing null check to send an applicaation spinning out of control.

Java by Comparison mentions it

Instead of returning null, you return a null object

Elegant Objects has rejection of null's well called out

[Y]ou're making a big mistake if you use NULL anywhere in your code. Anywhere - I mean it.

I'm sure there are more, but these are the three books I know off the top of my head (sadly two for less than ideal reasons).

Despite the weak treatment in a nearly universally recommended to read book, My stance is that, null in your code IS WRONG.

If we don't let null into our code we never have to check for it. This is like the Wizard of Oz - Great and Powerful. It is one of the strongest code simplification techniques I have.

We need to exclude nulls, absolutely. I'll cover a couple practices to do so further down. Just removing nulls gives us some trust to our code.

I see three ways we need to trust our code.

  1. What's Given
  2. What's Returned
  3. What's Called

We need to trust what we're passed

When we're passed an argument, we need to trust it's a valid object. This doesn't mean it's entirely valid. Yegor talks about constructors having zero logic, but sometimes validation (of state) is OK. I don't fully agree with this particular approach. I think you can solve the issues that might come up through other means - but that's a different discussion for another time - and there's probably always exceptions; for the broader context I understand the value.

When we eliminate null, we can trust that nothing given the object will be null. We don't have to validate every object we're being constructed with.

Our (internally) public API doesn't have to protect itself against null with null-guards for every variable.
We eliminate all of the "Illegal Argument" exceptions that have to be caught and handled. A lot of branching and error handling that's required in imperitive code ... gone. We don't throw exceptions because we're given null - we're never given it.

When an object is given a value - it can trust that they values are all actual objects. There will never be a 'null reference' exception from asking an object to do something. The power this has to simplify the code was mind blowing for me.

We need to trust what's returned

Just like what we're passed, when we get an object back - trust it. We're getting back something and we need to trust that the code is not going to break us. We can use the object returned. When null is a returnable "value" then all code must start doing null-checks and branch to make a decision on how to handle that situation. Someone else knew it was going to be null, but didn't make the decision and we're left without an object and have to start writing procedural code to handle the situation.

When we can't trust that we're given something valid from our method call, we're going to have a code explosion of validation efforts.

Trust what you call

This is the realization that drove me to call this "Trusting your code". In a lot of the code we write we'll do things like

if(foo.SomeCondition()){
  bar.DoSomeThing();
}

it doesn't matter what foo or bar are here or how the code is constructed - there's some check if it's OK to call some method on something else.
The worst offenders of this are when you check the object, then call the object

if(foo.SomeCondition()){
  foo.DoSomeThing();
}

This is reprehensible - the other example is just terrible.
Both of these have the same solution. If the method should only be invoked in certain situations, let it make that decision.
This is a behavior, "to do or not to do" - let the object with the desired behavior do the check if it should execute or not.

We need to trust the code we call can make the appropriate decision about what to do. This leads to other changes in the code - absolutely. These changes, what this forces starts to highlight commonality - starts to FORCE commonality. Once these commonalities are identified, refactorings are easier to see and apply.

Let's take the situation where two different objects need to call the same DoSomeThing base on different checks.

if(bar.StuffCount() > 1){
  baz.DoSomeThing();
}
...
if(foo.SomeCondition()){
  baz.DoSomeThing();
}

I want to have the "condition methods" not have the same signature. This makes it a little less clear. But really... It's a simple refactor to make them the same.

if(bar.HasStuff()){
  baz.DoSomeThing();
}
...
if(foo.SomeCondition()){
  baz.DoSomeThing();
}

which is better, because we rarely care about the actual count for something. It's data - we don't work with data.

Now we have a very obvious boolean quetion happening. We need to trust our baz to make the decision for us.
We need to trust the objects we interact with to make the appropriate decisions of when to do something, or not. In these examples, two classes have to know the condition of and contain the code of asking baz to DoSomeThing. There's a lot of what if paths we can go down around changes that would have a very broad scope of change, let's avoid the word bloat and look at how this is better handled.

We handle it by trusting baz.
Instead of what this method would look like in our existing scenario

class Baz : IBaz{
  public void DoSomeThing(){...}
}

we need to update it to be able to make the decision itself

class Baz : IBaz{
  public void DoSomeThing(){
    if(SOMETHING_HERE.Not()) return;
    DoSomeThingForReal();
  }
}

What do we do for SOMETHING_HERE? We need to provide the method with an object to ask. (Note: See that I'm using if as a guard clause.)

class Baz : IBaz{
  public void DoSomeThing(ICanSomething){
    if(ICanSomething.Can().Not()) return;
    DoSomeThingForReal();
  }
}

and our callers can now trust Baz and just call the method.

baz.DoSomeThing(this);

Clearly the callers need to implement the ICanSomething interface. Put what would have been in the if condition as the result of the ICanSomething#Can method. But that's the point. Baz now has an object that knows if it Can DoSomeThing and it communicates with that collaborator. It is responsible for determining if it should run or not by asking a question of another object.
Other objects no longer determine if baz should be interacted with, they just do - providing baz the collaborators to make the appropriate decisions itself.

We TRUST the object to make the right decisions with the collanborators we provide it.

Trust the objects passed in.
Trust the objects passed back.
Trust the objects invoked.

When you can trust your code - it simplifies.

Summary

I mentioned that I'd talk about the ways to get rid of null... Sorta lied. I'm over the count I like for blog posts, so I'll just toss them out and link to where I've written about them before.
Null Object Pattern
Avoid Asymmetric Marriage

Show Comments