Today is your best friends birthday and she asked you to buy a turtle as a present. The problem is you have never seen a real turtle before. The only thing you know about turtles is that they can swim, have four legs and are reptiles. As you walk into the petstore you see a lot of different species of animals. You decide to write a simple program to determine whether an animal is a turtle or not.
The program you write looks something like this:
Maintenance nightmare
After having checked all animals in the shop you are still left with two. They both can swim, have four legs and are reptiles.
You try to add one more condition, but because there are so many conditions already you find it hard to modify your program. You want to make sure you don’t make any mistake. The horror if your present happens to be a crocodile and the guests will be eaten instead of the cake.
How would you change the code to make this program more readable?
Refactor to a recipe
The first thing you realize is that when an animal cannot swim it cannot be a turtle. Therefore we can safely take out te canSwim() condition and use an early return when the animal cannot swim.
After you have rewritten the checks on canSwim() it becomes clear you can do the same with the check on hasFourLegs() as well.
You already cleaned up your code a lot. Only the check on isReptile() is left.
Adding new requirements
Finally to distinguish the turtle from the crocodile you come up with one final check. Does the animal have a shell? Now that you have written your code like a recipe it becomes trivial for you to add this new condition. A recipe can easily be read from top to bottom.
Now you know the basics of rewriting a nested solution to a recipe. In this particular case there is an even more clean solution.
But, aren’t early returns a bad thing?
Some would argue that early returns make your code harder to understand because there are multiple places where you method could end. In my experience this is never a problem until your methods become very long and complex. When this happens you should probably deal with those long methods instead i.e. by reducing their length and complexity first.
The Recipe
The general recipe is as follows:
Try to do X
If X didn’t work -> stop
continue with the assumption that X worked
In the following example you can see this pattern at work. The fridge in this example is a simple state machine and all illegal transitions will throw an exception.
To Clean up this code even further, you can use Kotlins require(value:Boolean) and check(value:Boolen) methods. These methods assert that a certain condition has been met and throw an IllegalArgumentException or IllegalStateException when it hasn’t been. Let’s see how our fridge looks like with the right checks. Notice that the condition has been negated and only throws an exception when the condition is false.
Today you learned how to write software like you write a recipe. I hope it tastes good.