ifs
only as a guard clause is one of the most impactful things that can be done to reduce complexity in code.
I'm a very strong advocate for a lot of Object Oriented Practices. Most of them, really. I'm not great at all the buzzwords... or words I should know... But I understand the idea, and that's what allows me to produce highly maintainable code in a fraction of the dev hours.
This post is about minimizing branching. There's other ways to look at it, but branching is one of the largest contributors to code. It's the basis for the determination of Cyclomatic Complextity - How many paths exist through the code?
When we branch, we increase the cognitive load the developer endures to work in that space. When we branch inside branches, the cognitive load grows exponentially.
We don't want exponentially harder to understand code.
The methods I really like to deal with are a single line. They do one thing.
Methods should do one thing
That methods should do a single thing, is a key point to this. It will tie into how I name methods, but when we focus a method to doing a single thing there are a few things that happen.
- Primary Purpose
- Related Behavior
- Extractable Behavior
When we refactor methods to do a single thing we can easily see what the method is supposed to do. What I call the methods "Primary Purpose". The "THING" that the method does will just LEAP out at us. It helps with naming A LOT.
If we have a few methods that are 20+ lines long. They may have the same behavior in a couple different forms. If we can extract the behaviors into single methods, we'll find the related behavior - Probably be able to get it into a single method and BAM - Our code is simpler.
We can extract the behavior into a class with much less hassle when it's isolated in a single method, and that's all the method does. It's litterally a few hot-keys via ReSharper. Any good IDE will support this refactoring. That behavior can then be used anywhere in the code and there's no Copy/Paste needed.
Yes, this is the basic driver for the creation of MicroObjects. ABSOLUTELY. I say this repeatedly - MicroObjects are the result of actually applying the known good practices of Object Oriented Programming. Good Software Development practices. I'm not far from functional code, and the applicable classes will improve that code as well. My 13 practices are just good software development practices and will help improve any code.
Let's look at how we can get a twisty-turny bit of code into one were it does a single thing, where we don't have branches of logic.
For this, I'll turn to my trust experimental and education foundation - FizzBuzz.
I'm going to jump straight to the string concatination solution
public string FizzBuzzer(int toFizzBuzz){
string result = string.Empty;
if(toFizzBuzz % 3 == 0){
result += "Fizz";
}
if(toFizzBuzz % 5 == 0){
result += "Buzz";
}
return result == string.Empty ? toFizzBuzz.ToString() : result;
}
I like to ask
What does this method do?
I'm still waiting for a good answer. :)
The point here is that we have branching logic. We can do A or B in the flow of the method. Three times.
To know the state of result
at any point, we have to know the input, walk through the code, and THEN we can know what will be returned. It's common practice, it's what most code looks like and is worked with... but it's way more than we need.
The goal here is to only have if
as a Guard Clause.
I'm more than 1/2 way through this post before even getting here as this is more of a mind set. Single Responsibility. It's harder to really transform the code to only have if
's in a guard clause when you're not also striving for the method to do a single thing. As all the books I've read talk about - these practices reinforce and raise up each other. The code's so much easier when they all get there... and yet it's so hard to get them all there. :) I know.
Another aspect of the example method - It mutates. Immutability.
Code that doesn't change is code that can change.
It's the mutation that drives the branching here. Getting rid of mutation in your method often causes a lot of the branching to fall out. Let's remove the mutation.
public string FizzBuzzer(int toFizzBuzz){
string result = string.Empty;
if(toFizzBuzz % 3 == 0){
result += "Fizz";
}
...
}
In the multiple of 3 case, it'll return fizz. This is a known result. Let's not mutate it, let's just return it.
public string FizzBuzzer(int toFizzBuzz){
...
if(toFizzBuzz % 3 == 0){
return "Fizz";
}
...
}
And we can do the same thing for multiples 5, return Buzz
.
public string FizzBuzzer(int toFizzBuzz){
...
if(toFizzBuzz % 5 == 0){
return "Buzz";
}
...
}
This will have us at something, incomplete, looking like this:
public string FizzBuzzer(int toFizzBuzz){
if(toFizzBuzz % 3 == 0){
return "Fizz";
}
if(toFizzBuzz % 5 == 0){
return "Buzz";
}
return toFizzBuzz.ToString();
}
We're a bit smaller and missing the 3&5 case. It was previously 'built up' from the Fizz and Buzz cases. While it worked, the code becomes very interwoven. Adding new conditions becomes much harder, and will lead to many more branches if continued in that style. If I see code in that style, I assume the authors will continue that style.
If we have specific returns for 3 & 5, let's do it for 15 as well.
public string FizzBuzzer(int toFizzBuzz){
if(toFizzBuzz % 15 == 0){
return "FizzBuzz";
}
if(toFizzBuzz % 3 == 0){
return "Fizz";
}
if(toFizzBuzz % 5 == 0){
return "Buzz";
}
return toFizzBuzz.ToString();
}
And with a slight bit of tweaking to the format
public string FizzBuzzer(int toFizzBuzz){
if(toFizzBuzz % 15 == 0) return "FizzBuzz";
if(toFizzBuzz % 3 == 0) return "Fizz";
if(toFizzBuzz % 5 == 0) return "Buzz";
return toFizzBuzz.ToString();
}
We now, clearly, only have guard clauses. Saddly, no - it's not always this neat looking. Especially when Logging is involved.
When the method is in this form, with well defined guard clauses - The "Primary Purpose" or "Primary Behavior" of the method kinda leaps out.
If I ask, "What does this method do?", could you give me an answer?
The method converts the int to it's string form... with some special cases.
public string StringOfInteger(int stringOf){
if(stringOf % 15 == 0) return "FizzBuzz";
if(stringOf % 3 == 0) return "Fizz";
if(stringOf % 5 == 0) return "Buzz";
return stringOf.ToString();
}
I like to name methods for what they return. This returns the String of the Integer. Sure, sometimes it'll return "Fizz" or "Buzz" or "FizzBuzz" - but mostly, it returns the string of the Integer.
I've renamed things that make more sense with this approach. When we see the methods primary behavior, we can name it for that. We can more effectively convey the intent of our methods and parameters.
Of course, I'm not done. I'm probably never done. Oh no...
What does the following snippet do?
if(stringOf % 3 == 0)
Try it out...
...
...
OK - If you used any form of the word 'modulus' or 'divide' then you're just telling me what code is written. I want to know what it's there for? Why does this method exist?
What do we do when the condition is true?
We'll return Fizz.
So... the boolean check determines... if we ... should.
This snippet determines if we should return Fizz.
That's a behavior. StringOfInteger
now has many behaviors. We use those behaviors in guard clauses, but how can we get that knowledge and behavior out of the same method...
How do we name blocks of code? By extracting a method.
public bool ShouldReturnFizz(int evaluate){
return evaluate % 3 == 0;
}
We do this to all of them...
public string StringOfInteger(int stringOf){
if(ShouldReturnFizzBuzz(stringOf)) return "FizzBuzz";
if(ShouldReturnFizz(stringOf)) return "Fizz";
if(ShouldReturnBuzz(stringOf)) return "Buzz";
return stringOf.ToString();
}
...
Because we extract the mechanics of how we determine if we should return FizzBuzz, the StringOfInteger
method no longer knows how to calculate that, but does know WHO can do that.
I know what I want, you know how to do it.
The behavior the method does know - how to get a string of the integer is all[^1](Little bit of a lie, but that's a bit more advanced practice) the knowledge left in the method.
ifs
only as a guard clause is one of the most impactful things that can be done to reduce complexity in code.
If you care about the maintainability of your code, this is a technique to reduce the complexity to increase the maintainability.