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 null
s 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.
- What's Given
- What's Returned
- 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