loading...

The Hierarchy of Complexity for Conditionals

clayshentrup profile image clay shentrup Originally published at Medium ・3 min read

Originally posted on Extreme Programming

When the Ruby on Rails framework was first released, one of its biggest selling points was convention over configuration. Name your ActiveRecord model User, and it would use the users table in its generated SQL. This preempted the need for verbose configuration. Instead all you had to know was a handful of rules and your codebase would be predictable and consistent throughout.

It turns out this technique can be utilized in your own code. In her instant classic Practical Object Oriented Design in Ruby, Sandi Metz posed the following example of a case statement which follows an obvious pattern (see shock.rb at top).

# Source: https://www.youtube.com/watch?v=f5I1iyso29U&t=23m44s
class Shock
  def cost(type)
    case type
    when :front
      FrontShockCost.new.compute
    when :rear
      RearSHockCost.new.compute
    when :lefty
      LeftyShockCost.new.compute
    else
      ShockCost.new.compute
    end
  end
end
class Shock
  def cost(type)
    "#{type.to_s.titleize}Shock".constantize.new.compute
  end
end

Sandi's observation is that this code has to change any time you add or remove a type of shock. This increases the difficulty of changing code (see the Shotgun Surgery code smell). Shock has too much “Meta Knowledge”. Shock knows these four things.

1. cost method

2. four shock cost types

3. four shock cost classes

4. which type gets what class (the mapping from type to class)

We can reduce this information by replacing these rules with a convention, as shown in the second file. Sandi demonstrated this in one of her talks, saying:

This transition where you make up a class name and turn it into the object..is something that it's really useful to get comfortable with.

Some software developers I've encountered eschew this approach based on concerns like the difficulty of searching for calling code. While I think that concern has merit, the value of this refactoring adds up the more examples you have and/or the more frequently the code changes. I don't think one should reject this (or any) programming approach on principle. As with any business decision, it should be about a cost/benefit calculation. When is it worth it to apply a convention? How many conditional branches (which ideally just duplicate the same convention) must you obviate before the abstraction is worth the cost in terms of searchability?

That said, it's nice to know our alternatives. I propose something like a “hierarchy of complexity” for conditionals. It runs from simplest and least flexible to most verbose and flexible.

Create a convention.
As demonstrated above, this says there's one rule to follow, whether you have three unique cases or a thousand. This is definitively the simplest approach (simple in the Rich Hickey sense).

Use a map.
A map (like the Ruby hash below) is a little more verbose, and doesn't enforce a rule about naming conventions. Thus it can lead to an increase in information–multiple one-off rules instead of a single overarching rule forming a convention. But it does enforce part of the rule–the interface. Each class is expected to provide the compute method. This doesn't create a conditional into which future programmers might “lazily” (or simply unwittingly) shovel additional code for each specific use case. (Sandi Metz even makes the point that you can replace an if/else with a map, where the keys are true and false.)

shock_cost_class_map = {
  front: FrontShockCost,
  lefty: LeftyShockCost,
  rear: RearSHockCost,
}
shock_cost_class = shock_cost_class_map.fetch(type, ShockCost)
shock_cost_class.new.compute

Use a case statement.
Looking back at our original example above, we see that each conditional is simply a match between a type variable and a value. This is simpler (less flexible) than a series of arbitrary boolean expressions, but it still creates the tempting opportunity for a future programmer to insert new behaviors into one or more of those case statements rather than merely following the rule that each class's compute method does the relevant work. As OO practitioners we eschew differences and seek sameness where possible.

Use arbitrary boolean expressions (if/else).
This is most flexible and, in my view, the last resort. Because not only does this give us the flexibility to change behavior for each condition (e.g. replacing the class with inlined code), but it also gives us infinite flexibility in the comparisons. Any boolean expression will do.

Whenever I need conditional behavior, I start at the top of this hierarchy, and work my way down only when my requirements force me to.

Posted on by:

Discussion

markdown guide