Flog-Driven Development

kevin_j_m

Kevin Murphy

Posted on May 15, 2023

Flog-Driven Development

Remembering Refactoring

In a prior post, we extracted the details of how to tune a guitar out of the Guitar class. We moved it to a separate Tuner class. In that telling, we did so because it spoke to our sensibilities. It afforded us more space in the Guitar class to focus on other responsibilities. It gave a central location to focus on tuning.

What if those justifications weren't enough? What if we needed metrics to give an explanation for our refactoring? Let's use the guidance from a tool called flog to guide our changes.

Flog

Flog describes itself as a tool that:

reports the most tortured code in an easy to read pain report. The higher the score, the more pain the code is in.

You point flog to a file, or directory, and it provides you with a score. The higher the score, the more attention you might want to pay to it. As for how flog calculates the number, I'll let flog summarize itself again:

Flog essentially scores an ABC metric: Assignments, Branches, Calls, with particular attention placed on calls.

Let's look at the flog score of our Guitar class with all the tuning details inside the class:

⇒ flog guitar_with_tuning.rb -a
65.6: flog total
9.4: flog/method average

16.8: Guitar#standard_tuning
16.8: Guitar#down_half_step_tuning
13.0: Guitar#tune
6.6: Guitar#pick
5.2: Guitar#initialize
4.3: Guitar#restring
3.0: Guitar#none
Enter fullscreen mode Exit fullscreen mode

Let's investigate the method with the biggest flog score.

def standard_tuning
  @strings[5].tune(note: :e, octave: 2)
  @strings[4].tune(note: :a, octave: 2)
  @strings[3].tune(note: :d, octave: 3)
  @strings[2].tune(note: :g, octave: 3)
  @strings[1].tune(note: :b, octave: 3)
  @strings[0].tune(note: :e, octave: 4)
end
Enter fullscreen mode Exit fullscreen mode

We know that flog is particularly attuned to looking for calls. Each line in this method has a call to access an element in an array, and then another call to the tune method. That's two calls per line, with six lines in the method.

What do we do with the number?

So, bigger is worse, but how big is bad? At what number should you take action? Thoughtbot's Ruby Science book suggests a method is long or complex with a flog score above 10. It also posits that a class is long or complex with a flog score above 50.

With those heuristics in mind, our Guitar class is long, and all the methods related to tuning are long as well. Let's start by looking at the public method in that class, the method to tune the guitar:

def tune(tuning = :standard)
  case tuning
  when :standard
    standard_tuning
  when :down_half_step
    down_half_step_tuning
  when :drop_d
    drop_d_tuning
  else
    raise "unknown tuning"
  end

  @tuning = tuning
end
Enter fullscreen mode Exit fullscreen mode

The technique I'd start with to simplify a complex method is to extract smaller methods from it. However, there doesn't seem to be much gained from that here. This happens to be a really long case statement, each branch of which calls another method.

In an attempt to achieve a lower flog score, I'm going to rely on the similar naming of each of these tuning methods. I'll reach for a different tool: metaprogramming.

def tune(tuning = :standard)
  raise "unknown tuning" unless VALID_TUNINGS.include?(tuning)

  send("#{tuning}_tuning")

  @tuning = tuning
end
Enter fullscreen mode Exit fullscreen mode

What does flog think about this change?

⇒ flog guitar_tuning_metaprogramming.rb
58.1: flog total
8.3: flog/method average

5.6: Guitar#tune
Enter fullscreen mode Exit fullscreen mode

The flog score decreased from 13 to 5.6. A clear improvement - in the eyes of the metric. Is it better though? I'd argue that's still a matter of taste. How do you feel about the metaprogramming? How comfortable will you and your team be maintaining this? What pressure does the required naming scheme for any of the tuning methods place on your system? Consider these questions to decide whether to make this change, regardless of the flog score.

Extract Class

Even with this change, we still have a class that has a flog score over 50. That puts it in the "too long" category, according to Ruby Science. So, let's do what we did in the prior post and move everything related to tuning into a Tuner class.

class Tuner
  def initialize(guitar)
  def tune(tuning = :standard); end
  def standard_tuning; end
  def down_half_step_tuning; end
  def drop_d_tuning; end
  def open_a_tuning; end
  def modal_c_tuning; end
  def all_fourths_tuning; end
  def all_fifths_tuning; end
end
Enter fullscreen mode Exit fullscreen mode

We still need to tune our guitar. We achieve that by delegating this responsibility to our Tuner class.

class Guitar
  def tune(tuning = :standard)
    Tuner.new(self).tune(tuning)
  end
end
Enter fullscreen mode Exit fullscreen mode

What does that mean for our flog score?

⇒ flog guitar_separate_tuner.rb
18.9: flog total
3.8: flog/method average
Enter fullscreen mode Exit fullscreen mode

Unsurprisingly, deleting most of the code in a class reduces the complexity of that class. We didn't get rid of the complexity though. We just moved it somewhere else. Let's check the flog score of our new Tuner class.

⇒ flog tuner.rb
58.6: flog total
11.7: flog/method average

25.2: Tuner#standard_tuning
25.2: Tuner#down_half_step_tuning
5.3: Tuner#tune
Enter fullscreen mode Exit fullscreen mode

The complexity of our tuning methods actually got worse, increasing from 16.8 to 25.2. That's because we added another call on each of our lines.

Tuning one string used to look like this:

@strings[5].tune(note: :e, octave: 2)
Enter fullscreen mode Exit fullscreen mode

But now we need to find the string from the guitar that's passed into the tuner.

@guitar.strings[5].tune(note: :e, octave: 2)
Enter fullscreen mode Exit fullscreen mode

Extracting new concepts

To handle the complexity, we're going to reshuffle the responsibility of these ideas. Right now the methods for the different tunings tune each string of the guitar. Instead, let's have the tunings only know what notes to tune the strings to.

We'll also extract each of these tunings out to a separate module.

module StandardTuning
  def self.pitches
    [
      { note: :e, octave: 4 },
      { note: :b, octave: 3 },
      { note: :g, octave: 3 },
      { note: :d, octave: 3 },
      { note: :a, octave: 2 },
      { note: :e, octave: 2 },
    ]
  end
end
Enter fullscreen mode Exit fullscreen mode

Now when we want to add a new tuning, we add a new module, rather than another method on the Tuner class.

The act of tuning is now isolated into the Tuner#tune method.

class Tuner
  def tune(tuning = :standard)
    raise "unknown tuning" unless VALID_TUNINGS.include?(tuning)

    pitches_for(tuning).each_with_index do |pitch, index|
      @guitar.strings[index].tune(**pitch)
    end
  end

  private

  def pitches_for(tuning)
    Object.const_get(tuning_class_name(tuning)).pitches
  end

  def tuning_class_name(tuning)
    "#{tuning}_tuning".split("_").map(&:capitalize).join
  end
end
Enter fullscreen mode Exit fullscreen mode

Keeping with our original refactor, this still uses metaprogramming. This time we use it to recall the appropriate module to get the pitches from.

What does flog think about this?

⇒ flog tuner_without_tunings.rb
21.9: flog total
4.4: flog/method average

8.5: Tuner#tune
6.8: Tuner#tuning_class_name
3.6: Tuner#pitches_for
Enter fullscreen mode Exit fullscreen mode

Our Tuner class has a flog score under 50, and all the methods are under 10. But, like last time, did we just shift the complexity into our extracted tuning modules?

⇒ flog standard_tuning.rb -a
1.5: flog total
1.5: flog/method average

1.5: StandardTuning::pitches

⇒ flog down_half_step_tuning.rb -a
1.5: flog total
1.5: flog/method average

1.5: DownHalfStepTuning::pitches
Enter fullscreen mode Exit fullscreen mode

Each of these is lightweight - a single method returning an array of hashes. According to Ruby Science's heuristics, we no longer have complex methods or classes!

Flog as a forcing function

At the time I didn't refer to flog when making these changes. But it was interesting to reconstruct them with a dedicated focus on their flog scores. When you don't have any intuition around the complexity of a piece of code, it can help as an objective source.

Choosing to lean on some of these changes would require more than only the score. Introducing the metaprogramming would depend on my team's attitude towards that language feature.

Those high flog numbers may have driven me to explore different options. I may not have gotten to the point of building separate modules for the tunings. That freed any given class to know about every different tuning. Paying attention to the flog score pushed me in ways I may not have otherwise considered.

💖 💪 🙅 🚩
kevin_j_m
Kevin Murphy

Posted on May 15, 2023

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related