Get Rid of That Code Smell - Control Couple

This is a post from the Get Rid of That Code Smell series.

If you are serious about Object Oriented Design and respecting Single Responsibility Principle then you definitely want to get rid of Control Couple code smells. In this post I will show you a simple example explaining how to identify and remove control coupling from your code. I like to think about that code smell also in the context of SRP because I like to apply it to every piece of my system - whether it’s a method, a class or a whole library. I like to be able to describe each piece with a simple sentence saying what it does, what’s the responsibility. With control coupling you introduce multiple responsibilities in a single method which is against SRP and against Object Oriented Design.

Detecting Control Couple Smell

Reek can help you with that - detecting Control Couple is turned on by default. I believe that Pelusa’s “Else Clause” and “Case Statement” lints can also be used to find places in your code with potential control coupling.

A dead-simple example of control coupling could like this:

def say(sentence, loud = false)
  if loud
    puts sentence.upcase
  else
    puts sentence
  end
end

Which is pretty self-explanatory. The say method puts an upcased sentence if loud parameter is set to true.

Let me show you a real-world example though. Let’s bring it to the class level - here’s a small snippet from Virtus:

class DefaultValue

  def initialize(value)
    @value = value
  end

  def evaluate(instance)
    if callable?
      call(instance)
    elsif duplicable?
      @value.dup
    else
      @value
    end
  end

end

DefaultValue is a class responsible for evaluating a default value of an attribute in Virtus. Its #evaluate method, depending on @value ivar, is deciding how to evaluate the value. We have multiple cases here and all of them are handled within one method. You cannot easily describe this method’s responsibility because it does different things depending on what the @value ivar actually is. If it’s a proc-like object responding to #call we call it, if it’s a duplicable object then we dup it, else we just return it as is.

I found that code pretty ugly. The repercussion of Control Couple smell in this example was that every time we were setting the default value we were performing this if/else check on @value.

Removing Control Couple Smell

The DefaultValue class was refactored by splitting the evaluate logic into 3 sub-classes. Every sub-class implements a self.handle? method which checks if its instance can actually handle the given value. This means that the logic inside #evaluate is now performed only once, prior to deciding which DefaultValue sub-class we want to initialize.

Here’s how it looks like:

class DefaultValue
  DESCENDANTS =  [ FromSymbol, FromCallable, FromClonable ].freeze

  def self.build(*args)
    klass = DESCENDANTS.detect { |descendant| descendant.handle?(*args) } || self
    klass.new(*args)
  end

  def initialize(value)
    @value = value
  end

  # default implementation - simply return the value as is
  def evaluate(*)
    @value
  end
end

Now for example FromCallable class implementation looks like that:

class FromCallable < DefaultValue

  def self.handle?(value)
    value.respond_to?(:call)
  end

  # evaluates the value via value#call
  def evaluate(*args)
    @value.call(*args)
  end

end

I liked that refactor because I could remove the if/else clause from #evaluate method, split responsibilities across 3 sub-classes and gain a small performance boost too.

Summing Up

Even if removing Control Couple smell requires writing a bit more code - it’s worth that price. Getting rid of that smell leads to better Object Oriented Design and helps you with respecting Single Responsibility Principle. I also like that it’s easier to understand what the code does because responsibilities are shared across the objects rather than jamming everything into a few methods that couple the logic to the received arguments.

In the next post we’ll see how to deal with Duplication in our code. Stay tuned.