ABC score - Reduction by Combining
Previously, I discussed how to reduce an ABC score of a method by extracting out high scoring blocks of code. This time, let’s work on reducing an ABC metric score by combining high scoring parts.
This time, the code is from my version of
Karger’s minimum cut. The
main method which contracts a list of nodes is the min_cut
:
1
2
3
4
5
6
7
8
9
10
11
def min_cut(list)
return 0 if list.keys.count < 1
return 0 unless connected?(list, list.keys.first, list.keys.last)
return 1 if list.keys.count < 3 && connected?(list, list.keys.first, list.keys.last)
node1 = list.key.first
node2 = list[node1].first
contract(list, node1, node2)
end
It’s a pretty straight forward block of code and I wrote it with specs. When I check with Rubocop, it gave this warning:
Assignment Branch Condition size for min_cut is too high, [21.47/15]
Wow, that’s a pretty high score for so few lines. What gives?! Time to investigate and find out how this score can be lowered.
Note: if you have about five to ten minutes, calculate the ABC score of each line in the example and compare with actual results. I bet the results will surprise you.
ABC metric score: min_cut
This is min_cut
’s ABC metric scored, line-by-line:
1
2
3
4
5
6
7
8
9
10
11
def min_cut(list) # A B C
return 0 if list.keys.count < 1 # 0 3 1
return 0 unless connected?(list, list.keys.first, list.keys.last) # 0 6 1
return 1 if list.keys.count < 3 && connected?(list, list.keys.first, list.keys.last) # 0 8 2
node1 = list.key.first # 1 2 0
node2 = list[node1].first # 1 1 0
contract(list, node1, node2) # 0 1 0
end
One thing that surprised me from the last time I calculated the count:
return
counts as a branch.
The breakdown of Rubocop’s ABC score:
ABC Score | |
---|---|
Assignments | 2 |
Branches | 21 |
Conditionals | 4 |
Total (Math.sqrt(a*a + b*b + c*c) ) |
21.47 |
Target: High Scoring Lines
Follow similar approach as last time: focus on lowering the score on high scoring lines. In this method, the highest scoring lines are:
return 0 unless connected?(list, list.keys.first, list.keys.last) # 0 6 1
return 1 if list.keys.count < 3 && connected?(list, list.keys.first, list.keys.last) # 0 8 2
The ABC score for these lines is 14.31, which is very close to
Rubocop’s ABC size default of 15. On trying to reduce the ABC score of
these two lines, one thought that comes to mind: list.keys
is used a
lot. Four times in two lines. Each list.keys
adds another branch
call to the ABC score.
Reduce by Combining
Let’s reduce the number of list.keys
call by combining list.keys
calls into a single assignment, let’s it nodes
since that is what
they represent here, and use that variable in place of these calls.
This will add another assignment to the ABC metric, but it will reduce four branch calls in these two lines alone.
1
2
3
4
5
6
7
8
9
10
11
12
def min_cut(list) # A B C
nodes = list.keys # 1 1 0
return 0 if nodes.count < 1 # 0 2 1
return 0 unless connected?(list, nodes.first, nodes.last) # 0 4 1
return 1 if nodes.count < 3 && connected?(list, nodes.first, nodes.last) # 0 5 2
node1 = nodes.first # 1 1 0
node2 = list[node1].first # 1 1 0
contract(list, node1, node2) # 0 1 0
end
Overall, list.keys
was used in more than those two lines, so a
single assignment has reduced the number of branches by more than
four, from 21 to 15:
ABC Score | |
---|---|
Assignments | 3 |
Branches | 15 |
Conditionals | 4 |
Total | 15.81 |
Combine further?
Reducing an ABC score from 21 to 15 is pretty good, but can we do better?
Yes, there are still more branches than assignments and conditionals. Going back and focusing on the highest scoring lines:
return 0 unless connected?(list, nodes.first, nodes.last) # 0 4 1
return 1 if nodes.count < 3 && connected?(list, nodes.first, nodes.last) # 0 5 2
By combining list.keys
into nodes
helped a lot. Can a similar
process be applied to combine excess branch calls?
Combine: nodes.first
and nodes.last
As nodes.first
and nodes.last
are called quite a few times in this
block. Let’s combine the calls by creating new variables, first_node
and last_node
, and assign those values to it:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
def min_cut(list) # A B C
nodes = list.keys # 1 1 0
first_node = nodes.first # 1 1 0
last_node = nodes.last # 1 1 0
return 0 if nodes.count < 1 # 0 2 1
return 0 unless connected?(list, first_node, last_node) # 0 2 1
return 1 if nodes.count < 3 && connected?(list, first_node, last_node) # 0 3 2
node1 = first_node # 1 0 0
node2 = list[node1].first # 1 1 0
contract(list, node1, node2) # 0 1 0
end
Similar to the first refactor, first_node
was used outside of the
high scoring line as well. Now the score is:
ABC Score | |
---|---|
Assignments | 5 |
Branches | 12 |
Conditionals | 4 |
Total | 13.60 |
Adding first_node
and last_node
variables only reduced the ABC
score by a few points. Most importantly, the ABC score is lower than
Rubocop’s default of 15! So, there won’t be any warnings like this
from Rubocop:
Assignment Branch Condition size for min_cut is too high
Reducing: when to stop?
With a score of 13.60, can we do better? Hmm… that’s a good question at this point. In a way, there will always be some way to improve, but looking at the ABC scores of each line, the highest scoring lines are still:
return 0 unless connected?(list, first_node, last_node) # 0 2 1
return 1 if nodes.count < 3 && connected?(list, first_node, last_node) # 0 3 2
which is a vast improvement over the initial version:
return 0 unless connected?(list, list.keys.first, list.keys.last) # 0 6 1
return 1 if list.keys.count < 3 && connected?(list, list.keys.first, list.keys.last) # 0 8 2
Can this be reduced? Yes, it can. Can it be done simply? Not without
some restructuring of the code. At this point, my first thought is to
extract the block into a separate method to lower the ABC score of
min_cut
.
Sensible Default
Looking at the refactored version of min_cut
, I find it more
readable than the initial version. The ABC score default of 15 was
helpful as it made my code more readable.
Rubocop’s default of 15 for ABC metric feels like a good balance of getting work done in a block but also forcing out lazy coding, which I did initially.
In this case, I just wrote the code very directly, just throwing in anything I needed as I needed. The ABC score shot up because of this.
Getting the ABC score back down to 15 really cleaned up the code without major restructuring and in a way, made the code easier to read by adding a few assignments and significantly reducing branches.
Conclusion
When Rubocop gives a warning of:
Assignment Branch Condition size for min_cut is too high. [21.47/15]
There are a few ways to deal with it.
The most important is to understand how the ABC metric is counting. Practice by counting ABC line by line.
Once high scoring lines have been identified, reduce the ABC score either by extracting blocks or by combining common elements together.