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.