DEV Community

Cover image for How to Get Rid of Annoying IFs Forever
Maxi Contieri
Maxi Contieri

Posted on • Updated on • Originally published at maximilianocontieri.com

How to Get Rid of Annoying IFs Forever

Why the first instruction we learn to program should be the last to use.

Nobody uses GOTO instruction anymore and few programming languages still support it.

We have matured and confirmed spaghetti code is unmaintainable and error prone.
Structured Programming solved that problem years ago.

We got rid of the sentence thanks to Edsger Dijkstra's incredible paper: Go To Statement Considered Harmful.

Next evolution step will be removing most IF statements.

IFs / Cases and Switches are GOTOs disguised as structured flow.

Our tool will be Object Oriented Programming principles.

forest-road-5228673_1920.jpg

Photo Przemysław Bromberek en Pixabay

# The Problem

Most IF sentences are coupled to accidental decisions. This coupling generates ripple effect and make code harder to maintain.

IFs are considered as Harmful as GOTOs.

IF sentences violate open/closed principle. Our designs will be less extensible and closed to extension.

What is more, IFs are open doors to even worse problems, like switches, cases, defaults, return, continue and breaks.

They make our algorithms darker and force us to build accidentally complex solutions.

People out of software development cannot explain why we use this branching sentence. This is a code smell.

Solutions

Before we move on and remove IF sentences we should decide if its an essential one or an accidental If.

To check this out we will look for answers in real world through bijection.

Essential Ifs

Let's see an essential IF statement

We should decide whether to remove this if sentence or not.

We must understand whether it represents a business rule (essential) or an implementation artifact (accidental).

In the case above we will honor our bijection. So we will NOT replace the if.

People In real world describe age constraints in natural language using IFs

Accidental Ifs

Let us dive now into bad IFs.

The movie rating IF is not related to a real world If but to accidental (and coupled) implementation.

Our design decision was to model ratings with strings.

This is a classic neither open to extension, nor closed to modification solution.

Let's see what happens with new requirements.

We can detect some Code Smells:

  1. Code is polluted with IFs.
  2. A default statement is missing.
  3. New ratings will bring new IFs.
  4. The strings representing ratings are not first class objects. A typo will introduce hard to find errors.
  5. We are forced to add getters on Movies to take decisions.

The Recipe

Let's fix this mess with these steps:

  1. Create a Polymorphic Hierarchy for every IF condition (if it doesn't already exist).
  2. Move every IF Body to the former abstraction .
  3. Replace IF Call by polymorphic method call.

On our example:

With this outcome:

1- Code is polluted with IFs.

We should add no more IFs. Extending the model will be enough.

2- A default statement is missing.

In this case default behaviour is no needed since exceptions break flow. In many times a Null Object will be enough.

3- New ratings will bring new IFs.

We will address it with polymorphic new instances.

4- The strings representing ratings are not first class objects. A typo will introduce hard to find errors.

This is hidden in Ratings implementation.

5- We are forced to add getters on Movies to take decisions.

We will clear this problem favoring Demeter's Law.


Breaking this collaborator chain

Rating is private so we don't break encapsulation.

As a consequence we are safe to avoid getters.

Applying the recipe to all IF conditions

Now we have the secret formula we can go further and try to remove the essential IF condition related to age.

We replaced all IFs. In the later case using Double Dispatch Technique

We used our formula and it worked. But there's a smell of over design.

  1. Classes representing Ages are not related to real concepts on our model.
  2. Model is too complex.
  3. We will need new classes related to new age groups.
  4. Age groups might not be disjoint.

We should avoid the last design and set a clear boundary between essential and accidental ifs.

A Good design rule is to create abstractions if they belong to the same domain (movies and ratings) and don't do it if they cross domains (movies and ages).

Do Ifs stink?

According to evidence shown above. We should consider many IFs to be a code smell and tackle them with our recipe.

Why this is happening?

This article (and many others) recommend avoiding most IF sentences.
This will be very hard for all developers very comfortable with its usage.

Remember, Laziness and hidden assumptions are very rooted on our profession. We have been (ab)using IFs for decades and our software is not the best version of it. 

This is a root cause analysis of a serious SSL defect on IOS caused by a lazy case:

This article's thesis suggests there's a correlation between IFs/Switch/Case and defects.

You should give a try and avoid IF conditionals.

Conclusions

With this simple technique we will be able to remove, in a procedural way, all accidental ifs.

This will make our models less coupled and more extensive.

Null object pattern is a special case of this technique. We will be able to remove all NULLs since:

NULL ifs are always accidental.

Credits

We have been using If removal technique at Universidad de Buenos Aires for several years. Kudos to all my fellow teachers for all the experience we gathered together with it.

Egm99IwXYAI8S-G.jpg

Top comments (15)

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

This is an incredibly narrow view on programming.

  1. Ifs are performant¹, precisely because they get translated to conditional gotos, which is how CPUs make decisions.
  2. Normally if-statements are the simplest and easiest way to describe some logic². In most cases, this doesn't change, so it's much more viable to only remove them as a refactoring step or if it's very clear from the start that the logic will have to be extended.
  3. The example of the movie ratings makes the fatal mistake of confusing logic and data, which I will explain in more detail below

Have a look at the following code³:

local ratings = {
   AdultsOnly = {age = 18};
   PG13 = {age = 13};
   -- ...
}
Enter fullscreen mode Exit fullscreen mode

It should become obvious that the mapping from a rating to the minimum age should be thought of as a constant, not as program logic. Encoding this data into the source code is the actual code smell, not the statement used to do so. Building classes around this only hides the real issue under a heap of complexity instead of fixing anything.

This is a very common pattern in naive OO design: Bury obvious flaws under a convoluted class-system and hope to gain some extensibility by breaking up the code.

Here's what the code should really look like:

function moviegoer:watch(movie) -- The : can be read as a . here
   if self.age < ratings[movie.rating].age then
      error "You are not allowed to watch this movie"
   end

   error("watching movies not implemented yet")
end
Enter fullscreen mode Exit fullscreen mode

There is exactly one if, and it encodes business logic. The data is extracted into a data structure that could at any time be trivially moved into a JSON or YAML file for easier configuration.


¹ This isn't always relevant, but it often enough is that it shouldn't be ignored. It always depends on the language and compiler, but if performance is critical, the code will likely be C or something comparable.

² "if this than that" sentences are how we usually communicate conditional actions outside programming, so it's safe to assume it's a very natural way for humans to think about them.

³ Written in Lua because I don't enjoy typing as much as in my Pascal days.

Collapse
 
mcsee profile image
Maxi Contieri • Edited

Hello and thank you for your detailed comments.

1 - Performance and good design are sometimes in tension. In my experience performance issues should be delayed as possible. Unless you are building a time critical software good designs should always be prioritized.
You might be doing Premature Optimization (which is a code smell)

Modern Virtual machines make a lots of inline optimizations so I always suggest making a benchmark before guessing which one will be faster.

2 - I don't know what is normal. This is the way were taught at school and universities. But software has a lot of errors and bad quality and that is the article thesis. Don't trust normal.
Logic extension should be made following SOLID principles. Not adding IFs.

Are you very sure if this than that is common among not developers? I don't really think so on accidental IFs. Try talking with non developers on NULLs and IFs. You might get surprised.

3 - I don't know anything about logic and data. I think they are related to old school structured programming. Which is fine. I write articles con OOP and they only predicate on behaviour.

I think you are confusing accidental concepts (like JSON or YAML) with essential ones (behavior). I don't see a problem on that.
I just think we are talking with different design points of view.

Your comment states data is important and should be configured externally. I say data is accidental and we should never care about it at all. My humble opinion is that we should view objects related to behavior and we should encapsulate what you call data following information hiding guidelines.

This is my point of view on models:

Related to this topic:
This is a very common pattern in naive OO design: Bury obvious flaws under a convoluted class-system and hope to gain some extensibility by breaking up the code.

Extensibility is key when you design very large systems with millions of code lines (as I do). Breaking up the code and refactoring is the only way of evolving the system and IFs are always blockers and defects source.

I Appreciate the time you took to comment on the note.

Regards

Collapse
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️ • Edited

Performance and good design are sometimes in tension. In my experience performance issues should be delayed as possible. Unless you are building a time critical software good designs should always be prioritized.

I 100% agree. To clarify, my point was just that performane can be a reason to prefer if statements, but only after the code has been found to be a performance bottleneck. Prematurely using low-level constructs where a higher-level abstraction would improve code quality is, as always, bad style.

Are you very sure if this than that is common among not developers? I don't really think so on accidental IFs

Depending on the situation, I am very much convinced. People say "If the weather is good, we can go to the park tomorrow", not "tomorrow we can do a weather-appropriate activity, where going to the park would be appropriate for sunny weather". This obviously isn't necessarily universal, but it does seem like a tendency.

I think you are confusing accidental concepts (like JSON or YAML) with essential ones (behavior).

I did mention those, but only to give a specific example. My main point is independent of data description formats though: The mapping from Rating to minimum age, is (or rather falls on the side of) data, not logic

I say data is accidental and we should never care about it at all

On the contrary: Data is the only thing we should care about. After all, the only purpose of any program is to take input data and turning it into output data.

Extensibility is key when you design very large systems with millions of code lines (as I do). Breaking up the code and refactoring is the only way of evolving the system and IFs are always blockers and defects source.

Our opinions seem to run parallel here. Looking back at my code example, you end up getting the same level of extensibility. It is easy to add another rating by appending an entry to the map. This is certainly also achieved by your method of encoding this relationship in objects, but my main point of criticism is that it encodes what I would consider a constant¹ as an object method.

¹ note that I don't consider a numeric constant like 42 to be any different from a hugely complex constant data-structure. The defining factor is it's immutability during program execution.

What I consider naive with that pattern is that it adds complexity to the code. If you were to draw flow-charts of both implementations, one of them ends up showing the relationships, while the other treats them as additional input data.

Breaking up the code and refactoring is the only way of evolving the system and IFs are always blockers and defects source.

Not in my experience at least. Assuming performance is not critical, refactoring code with many conditionals can be done in many ways and I personally prefer representing the complex decision logic using data structures. This sometimes goes almost full-circle: depending on the language, data-structures can contain code (usually in the form of functions), which then looks surprisingly similar to an object, which is ultimately just a handle for a collection of behaviours, not unlike a data-structure containing functions.

Thread Thread
 
mcsee profile image
Maxi Contieri

Hi again.
We agree to disagree :)

I think the main disagreement comes from this perception of software.

On the contrary: Data is the only thing we should care about. After all, the only purpose of any program is to take input data and turning it into output data.

I don't care about data because I see software as a model and predictor and data is accidental.
If we were to design a weather forecaster I would not see temperature and pressure indicators as data flowing into the system. I'd like to see the model as a simulator and predictor of actual conditions.
So out forecasted data like estimated temperature, wind speed and thus would be accidental. What would be essential (IMHO) is the model behaving like real weather.
If you get too coupled to data you cannot change them as long as requirements change. And I personally think end users don't care about data. It is us, developers, who care too much about that. User wish a system do what they would need to achieve manually.

You prefer representing the complex decision logic using data structures and this is fine. you want to see all the information centralized and this is tradeoff decision. This is the structural programming approach and is very good.
I'm just pointing out there other ways of simulating real world.

Again. All my opinions

Thread Thread
 
darkwiiplayer profile image
𒎏Wii 🏳️‍⚧️

We agree to disagree :)

Definitely. But either way, I still find it very interesting to see how someone else thinks about software systems completely differently, as I don't believe neither of us is inherently more "right" or "wrong" than the other.

To end on a positive conclusion: I think we can extrapolate from this that OOP, with its emphasis on systems and behaviours and FP, with its contrasting emphasis on data flow and transformations can both be the right or wrong tool for any individual developer and/or team, depending largely on how they tend to think about computation.

Not that this is a groundbreaking deduction, but it's something that's easily forgotten :D

Thread Thread
 
mcsee profile image
Maxi Contieri

It is very nice learning with you

I think there are no silver bullets.
different projects need different solutions

Collapse
 
fp2sec profile image
cd /

This solution is way cleaner, and easier to understand than the OP. I created an account just for this post. Over engineering a solution just to get rid of some if statements is not an improvement.

Collapse
 
mcsee profile image
Maxi Contieri • Edited

It is just an example.
Replacing one IF with a hierarchy might seem over engineering.
But this is one short example to fit a small article.
In big systems (I will not put tons of code here) the impact is huge.

I'm showing a technique with its benefits and the second example is definitively over design. I also provided a rule to decide (IMHO) when it's worth it and when it isn't

IFs seem to be very easy and more readable to read. This is exactly what happen with all coupling issues like Nulls, Globals, Singletons and so. They are very easy to read on a isolated piece of code. But this coupling brings lots of defects and prevent big systems to evolve.

Collapse
 
codemouse92 profile image
Jason C. McDonald • Edited

While there may be some good ideas here, I'm concerned that this is going to promote "Real Programmers Use Butterflies"-flavored overengineering. I've seen countless atrocities committed in the name of eliminating perfectly reasonable if statements.

But then, one should be wary of anything stated as "<x> Considered Harmful". It's almost always either (1) an overbroad generalization based on a specific problem, or (2) an attempt to promote the latest Clever Solution™.

The author may say, "Well, no, I'm not really advocating getting rid of all if statements", but if that's the case, the title and article structure aren't a responsible or accurate reflection of the point, even to the brink of being guilty of "clickbait".

Collapse
 
mcsee profile image
Maxi Contieri

Thank you, Jason

I'm aware of over engineering and overbroad generalization. That's why the article has a counterexample on it and warns against abuse.

Title is a bit clickbait. But the word 'annoying' has two meanings
1) All the Ifs are annoying and should all be removed.
2) We need to remove JUST the annoying ones.

The article's spirit is the second one.

Collapse
 
codemouse92 profile image
Jason C. McDonald • Edited

I figured as much, but you have to consider how your readers MIGHT perceive it. Skimming is prevalent, and this doesn't skim the way you'd want.

Thread Thread
 
mcsee profile image
Maxi Contieri

Certainly I'll edit the conclusion to stress this out.

Thank you for your feedback!

Collapse
 
gwangjinkim profile image
Gwang-Jin Kim • Edited

Congrats! You created OOP Spaghetti code!
Leaving the If Else would have been more readable and thus more maintainable than this forest of Classes.

As for me, in the FP style - following Wii's answer:

// classes contain only attributes (container classes, structures)
class Person {
    constructor(name, age) {
        this.name = name;
        this.age = age;
    }
}

class Movie {
    constructor(name, rating) {
        this.name = name;
        this.rating = rating;
    }
}

// as Wii pointed out, underlying logic is mapped into a data structure (map)
let rating2minAge = {
    AdultsOnly: 18,
    PG13: 13,
}

// typical for FP: Predicate functions (returning booleans 
// which can then be used for filtering or other decisions) -
// They make the conditions more readable and 
// provide an abstraction layer (more readable and easier refactoring later on).
function is_allowed(person, movie) {
    return rating2minAge[movie.rating] <= person.age;
}


// Call it by:
let movies = [new Movie("The Exorcist", "AdultsOnly"), new Movie("Gremlin", "PG13")];
let persons = [new Person("Jane", 12), new Person("Joe", 16)];

persons.forEach((person) => {
    movies.forEach((movie) => {
        console.log(`${person.name} (${person.age}) watching \"${movie.name}\" allowed: ${is_allowed(person, movie)}`)
    })
})

/* 
Jane (12) watching "The Exorcist" allowed: false
Jane (12) watching "Gremlin" allowed: false
Joe (16) watching "The Exorcist" allowed: false
Joe (16) watching "Gremlin" allowed: true
*/

Enter fullscreen mode Exit fullscreen mode
Collapse
 
mcsee profile image
Maxi Contieri

How about all the other arguments about extensibility, new business rulet etc? Are they also 'Pasta' ?

Collapse
 
mjgs profile image
Mark Smith

I dunno man, your making the if statements look pretty simple.