The Cost of If Comments September 16, 2013

Complexity.

If you read this word, and you write software, then you probably considered whether I meant computational complexity, as commonly expressed using big O notation by mathematicians, computer scientists, and those who just want to sound really smart.

Otherwise, (again, if you write software) you might assume that I am referring to cyclomatic (or conditional) complexity, a measure of the independent paths through a computer program.

Otherwise, you probably just thought that these first few sentences are a great illustration of complexity, and wondered why don't I just say what I'm trying to say, and you'd be right.

If you're still reading, then I'm about to get to my point.

Complicating Things

When reading the above sentences, you considered whether the condition applied to you, then evaluated the rest of the sentence (or maybe skipped it) based on that. The conditions were really not part of the definitions of complexity, at all, and basically amounted to a distraction from the real point.

Turns out, that really takes its toll on reading comprehension. If the above sentences were Ruby code that was being graded by Code Climate, they'd almost certainly have gotten an F. While the sentences might have been easier for me to write, since I didn't have to stop and consider what it was I really wanted to say, they place an unwelcome burden on the reader.

There's a pattern that we employ when refactoring object-oriented code that we call "replace conditionals with polymorphism." That sounds really fancy, but the general idea is really simple: instead of asking questions about the state of our program in order to determine the correct action to take, we ensure the correct object is in place to receive a message, and let it take the appropriate action.

For instance, consider the following code, written by a naive (and hypothetical) Rubyist:

class Foo def important_activity unless do_important_thing if Application . production? $logfile . puts ( 'Warning: important thing was unsuccessful.' ) elsif Application . development? $stdout . puts ( 'Warning: important thing was unsuccessful.' ) end end rescue StandardError => e if Application . production? $logfile . puts ( "Exception: important thing raised an exception. Error: #{ e . message } " ) elsif Application . development? $stderr . puts ( "Exception: important thing raised an exception. Error: #{ e . message } " ) end raise if $reraise_exceptions end end

This code is bad in a lot of ways. Let's just look at how using polymorphism (and DI) addresses our logging needs without complicating life for the reader:

class Foo def initialize ( logger = NullLogger . new ) @logger = logger end def important_activity do_important_thing or @logger . warn ( 'Important thing was unsuccessful.' ) rescue StandardError => e @logger . error ( e , 'Important thing raised an exception.' ) end end

Now, when we read the code, the behavior we care about understanding at this point isn't drowned out by all those conditionals. As long as our logger knows how to warn and error , we can fire-and-forget those messages.

I've inherited a lot of codebases in my time, and I always appreciate seeing code that makes it easier to gradually peel away at the layers of the application. I don't need (or want) to see all of an application's naughty bits when I first open a file.

The term for these kinds of conditionals is "branching," and there's a time and place for them. I'm not advocating a cargo-cult application of this refactoring, but it can really help to make the important stuff stand out, when used appropriately.

Branching's Hidden Costs

This is not news. I'd guess that 9 out of 10 of those reading this post already knew about this refactoring pattern. Then why am I writing about it? Because I think that branching has hidden costs.

We say that "branching is expensive" when writing code. When we say this, it's because compilers (or VMs) have a difficult time optimizing for code paths that diverge based on information that isn't available at the time of compilation.

Yet, it's incredibly easy to describe things in terms of if/then logic. So easy that we might miss the real cost of "if."

The value of the "replace conditionals with polymorphism" refactoring isn't just that it helps readability, testability, and maintainability. It's also that it encourages responsibility. It forces us to recognize that what we're describing may not be the evolution of an existing thing, but perhaps an entirely new thing (or set of things) altogether. It forces us to own that decision.

When we say "if," it's easy to miss an opportunity to recognize the emergence of something new. And, absent that recognition, we miss the chance to question whether that "something" should exist at all.

On the object level: Do we really need to branch here, or are these behaviors variations on a theme? Is that theme coherent?

On the feature level: Do we really need to display this element inconsistently based on application state? Would a change in wording accomplish the same goal while avoiding the need for cache invalidation?

On the workflow level: Does an entirely different checkout flow need to exist based on the type of user? Are we willing to absorb the maintenance overhead that is bound to create?

On the application level: Does the application that manages e-mail templates really need to know about the things it's including in the e-mail, or can it dictate that they conform to an interface, and consume them from other applications while remaining blissfully ignorant?

On the company level: Does providing this new service fit within our vision, or is it just a money grab? Are we willing to do something we aren't prepared to execute with excellence for the long haul?

On the personal level: Does making a special case for this company or this situation involve compromising something I've always considered key to my identity?

The differences between products, companies, and people are hidden in the answers they give to the questions that "if" only hints at.

"If" is the enemy of purpose.

Please enable JavaScript to view the comments powered by Disqus.

Disqus