Red Green Repeat Adventures of a Spec Driven Junkie

ABC Metric Line-by-Line and Reducing by Extraction

In my previous article I introduced the Assignment Branch Condition metric, or ABC metric.

Let’s look at how ABC metric is calculated in detail on some code and by understanding the ABC calculation, on a line by line level, it can be used to quickly reduce the ABC score of a method by extraction.

Example from Specs

An example of ABC scoring from the specs, which is a great source of documentation, since there’s no question about how the ABC metric is calculated as this spec is used to develop the code.

1
2
3
4
5
6
                                          # A B C
my_options = Hash.new if 1 == 1 || 2 == 2 # 1 3 2
my_options.each do |key, value|           # 0 1 0
  p key,                                  # 0 1 0
  p value,                                # 0 1 0
end

The ABC score is:

ABC Score  
Assignments 1
Branches 6
Conditionals 2
Total (Math.sqrt(a*a + b*b + c*c)) 6.40

Pretty straight forward in this example. Assignments are basically any =. Branches are a bit surprising since Hash.new is also a branch. Conditionals are straight-forward as well.

Calculating the ABC Score for My Code

Now, let’s use the ABC count and apply to some messier code. Recently, I have been working on Prim’s Minimum Spanning Tree. The main method to drive the algorithm is mst and the code is:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
def mst(source, graph)
  weight = 0
  nodes = [source]
  current_graph = graph
  current_node = source
  while current_graph.keys.count > 1
    next_node = cheapest_edge(current_node, current_graph)
    puts '================================================================================'
    puts current_node
    puts next_node
    weight += if current_graph.keys.count == 2
                current_graph.values.map(&:values).min.pop
              else
                current_graph[current_node][next_node]
              end
    nodes << next_node
    current_graph = collapse_graph(current_node, next_node, current_graph)
    current_node = "#{current_node}#{next_node}".to_sym
    current_graph
  end
  puts nodes
  weight
end

Rubocop scored this as a 22.02! Not so surprising since it’s a bit messy.

Line by Line calculation of ABC score

Let’s see how Rubocop calculated the ABC score of mst

Note: If you have ten minutes, try to calculate the ABC score of mst on your own before moving on in the article. It was definitely enlightening to see how many items I missed when calculating the ABC score myself!

Ok, this is how the ABC score of 22.02 was calculated, line by line:

Results

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
def mst(source, graph)                                                                      # A B C
  weight = 0                                                                                # 1 0 0
  nodes = [source]                                                                          # 1 1 0
  current_graph = graph                                                                     # 1 0 0
  current_node = source                                                                     # 1 0 0
  while current_graph.keys.count > 1                                                        # 0 2 1
    next_node = cheapest_edge(current_node, current_graph)                                  # 1 1 0
    puts '================================================================================' # 0 1 0
    puts current_node                                                                       # 0 1 0
    puts next_node                                                                          # 0 1 0
    weight += if current_graph.keys.count == 2                                              # 1 3 1
                current_graph.values.map(&:values).min.pop                                  # 0 5 0
              else                                                                          # 0 1 0
                current_graph[current_node][next_node]                                      # 0 0 0
              end                                                                           # 0 0 0
    nodes << next_node                                                                      # 1 0 0
    current_graph = collapse_graph(current_node, next_node, current_graph)                  # 1 1 0
                                                                                            # 0 0 0
    current_node = "#{current_node}#{next_node}".to_sym                                     # 1 2 0
    current_graph                                                                           # 0 0 0
  end                                                                                       # 0 0 0
  puts nodes                                                                                # 0 1 0
  weight                                                                                    # 0 0 0
end

Some surprises when I first calculated the ABC score:

  • each object message (i.e. .keys, .values, .count, etc.) are another branch call
  • += counts twice, once as an assignment, once again as a branch
  • [source] is also a branch call (i.e. Array.new(source))

Depending on the way methods are written, branches can be hidden but at the end: there’s no hiding from Rubocop!

The total:

ABC Score  
Assignments 9
Branches 20
Conditionals 2
Total 22.02

With the breakdown of the ABC score, it’s pretty clear that Branches are the culprit for a high ABC score. Let’s see how the ABC score can be reduced using this knowledge.

Reducing ABC score of mst by Extraction

I want to reduce the ABC metric score down by extracting parts of code out from the mst method.

Remove puts

The easiest extraction that can be done is: remove puts. Each puts adds another branch call and there are four of those in mst. I used for debugging in development and can be removed without a change in functionality.

Removing four puts, reduces the ABC score from 22.02 to 18.47, a direct correlation between removing four points and ABC score decreasing by four points.

ABC Score  
Assignments 9
Branches 16
Conditionals 2
Total 18.47

A slight improvement, and now the code looks like:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
def mst(source, graph)                                                      # A B C
  weight = 0                                                                # 1 0 0
  nodes = [source]                                                          # 1 1 0
  current_graph = graph                                                     # 1 0 0
  current_node = source                                                     # 1 0 0
  while current_graph.keys.count > 1                                        # 0 2 1
    next_node = cheapest_edge(current_node, current_graph)                  # 1 1 0
    weight += if current_graph.keys.count == 2                              # 1 3 1
                current_graph.values.map(&:values).min.pop                  # 0 5 0
              else                                                          # 0 1 0
                current_graph[current_node][next_node]                      # 0 0 0
              end                                                           # 0 0 0
    nodes << next_node                                                      # 1 0 0
    current_graph = collapse_graph(current_node, next_node, current_graph)  # 1 1 0
                                                                            # 0 0 0
    current_node = "#{current_node}#{next_node}".to_sym                     # 1 2 0
    current_graph                                                           # 0 0 0
  end                                                                       # 0 0 0
  weight                                                                    # 0 0 0
end

Targeting High scoring ABC Lines

mst’s ABC score of 18 is an improvement over 22, but is there something that can be extracted to reduce the ABC score significantly?

Scanning over the scores, this line sticks out to me:

                                           # A B C
current_graph.values.map(&:values).min.pop # 0 5 0

This is the highest score for a single line in this method. If this code can be extracted, the overall ABC score will probably drop by five points!

Looking at the block that encapsulates the line closer:

1
2
3
4
5
6
                                                       # A B C
weight += if current_graph.keys.count == 2             # 1 3 1
            current_graph.values.map(&:values).min.pop # 0 5 0
          else                                         # 0 1 0
            current_graph[current_node][next_node]     # 0 0 0
          end                                          # 0 0 0

This block of code is just figuring out the cheapest weight to add to the graph and has an ABC score of 9.11 (a = 1, b = 9, c = 1), If this block can be extracted into it’s own method, it would reduce the whole method’s ABC score signficantly and maintain functionality.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
def mst(source, graph)                                                      # A B C
  weight = 0                                                                # 1 0 0
  nodes = [source]                                                          # 1 1 0
  current_graph = graph                                                     # 1 0 0
  current_node = source                                                     # 1 0 0
  while current_graph.keys.count > 1                                        # 0 2 1
    next_node = cheapest_edge(current_node, current_graph)                  # 1 1 0
    weight += min_weight(current_graph, current_node, next_node)            # 1 1 0
    nodes << next_node                                                      # 1 0 0
    current_graph = collapse_graph(current_node, next_node, current_graph)  # 1 1 0
                                                                            # 0 0 0
    current_node = "#{current_node}#{next_node}".to_sym                     # 1 2 0
    current_graph                                                           # 0 0 0
  end                                                                       # 0 0 0
  weight                                                                    # 0 0 0
end

private
def min_weight(graph, current_node, next_node)                              # A B C
  if graph.keys.count == 2                                                  # 0 2 1
    graph.values.map(&:values).min.pop                                      # 0 5 0
  else                                                                      # 0 1 0
    graph[current_node][next_node]                                          # 0 0 0
  end                                                                       # 0 0 0
end

Now, the ABC score for mst is reduced from 18.47 to: 12.08 and there is a new method: min_weight which has an ABC score of 8.06.

ABC Score mst  
Assignments 9
Branches 8
Conditionals 1
Total 12.08

and the ABC score of min_weight is:

ABC Score min_weight  
Assignments 0
Branches 8
Conditionals 1
Total 8.06

Although the mst method had nine points removed, it did not reduce its ABC score by nine points. Branches are no longer the highest scoring item in mst’s ABC score. The highest component are now Assignments.

The result is now when Rubocop checks over these methods, there will not be a warning of:

Assignment Branch Condition size for mst is too high

Extracting high ABC scoring lines are an easy way to lower ABC scores.

Conclusion

In this example, I shifted the out a high scoring ABC block into its own method so the ABC score of its method would be reduced significantly.

As I understood how Rubocop was calculating the ABC score, I can reduce the ABC score of a method quickly, by targeting high ABC scoring lines.

Previously, it took a significant amount of effort reduce the ABC score of a method because I was really confused on where to target to reduce the ABC score.

Calculating the ABC score line by line is a great way to understand the ABC metric and also the code under examination.

Next time, I will show how ABC score can be reduced by using assignments.