In the last installment of this series, I talked a good bit about lines of code. As it turns out, the question, “what is a line of code?” is actually more complex than it first appears. Of the three different ways I mentioned to regard a line of code, I settled on “logical lines of code” as the one to use as part of assessing time to comprehend.
As promised, I sent code to use as part of the experiment, and got some responses. So, thanks to everyone who participated. If you’d like to sign up for the experiment, but have yet to do so, please feel free to click below.
Here is the code that went out for consideration. I’m not posting the results yet so that people can still submit without any anchoring effect and also because I’m not, in this installment, going to be updating the composite metric just yet.
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 26 27 28 29 30 31 32 33 34 35 36 37 |
using System; using System.Collections.Generic; using System.Linq; namespace CodeReadability { public class Arithmetic { public static int FirstProcessNumberA(int number) { return number * 12; } public static int SecondProcessNumber(int number) { int a = number * 12; int b = a - 4; int c = 2 * b; int d = 3 - c; return 14 + d; } public static int ThirdProcessNumber(int number) { int a = 3 * number; int b = a + 5; int c = b - 3; int d = 2 * c; int e = d - 10; int f = e - 2; int g = 25 - f; int h = 12 + g; int i = 2 * h; return i + 6; } } } |
The reason that I’m discussing this code is to show how simple it was. I mean, really, look at this code and think of all that’s missing.
- There are no control flow statements.
- There are no field accesses.
- There is no interaction with collaborators.
- There is no interaction with global state.
- There is no internal scoping of any kind.
These are purely functional methods that take an integer as input, do things to it using local declarations, and then return it as output. And via this approach, we’ve fired the first tracer bullet at isolating logical lines of code in a method. So let’s set that aside for now and fire another one at an orthogonal concern.
Before, I talked about the meaning of a line of code. Now I’d like to talk about the meaning of complexity in your methods. Specifically here, I’m referring to what’s called “cyclomatic complexity.” Cyclomatic complexity is a measure of the number of path’s through a piece of source code. Let’s see a few examples to get the hang of it.
Consider the following method from the Pawn class in the Chess TDD codebase.
1 2 3 4 |
public void ClearEnPassant() { _enPassantTarget = null; } |
This method has a cyclomatic complexity of 1 because there is only one path through the code. Contrast that with the following method from the Board class.
1 2 3 4 5 6 7 |
public void RemovePiece(BoardCoordinate coordinateForRemoval) { if (!DoesPieceExistAt(coordinateForRemoval)) throw new ArgumentException("coordinateForRemoval"); SetPiece(null, coordinateForRemoval); } |
The cyclomatic complexity of this method is 2 because there are two paths through it.
- The if condition evaluates to true and the method throws an exception.
- The if condition evaluates to false and the method finishes executing.
Be mindful that “if” isn’t the only way to create multiple paths through the code. For instance, this method also has a cyclomatic complexity of 2 because of the ternary operator creating two different execution paths.
1 2 3 4 |
public bool CanPerformEnPassantOn(BoardCoordinate enPassantTarget) { return _enPassantTarget != null && enPassantTarget.Matches(_enPassantTarget.Value); } |
Cyclomatic complexity can increase quite rapidly, particularly when nested conditionals enter the equation. This method has a cyclomatic complexity of 4, and you can see it already is starting to get hard to figure out exactly why.
1 2 3 4 5 6 7 8 9 |
private void CleanEnPassantForPlayerThatJustMoved(Piece pieceToMove) { foreach (var piece in _pieces) { var pawn = piece as Pawn; if (pawn != null && pawn.IsFirstPlayerPiece == pieceToMove.IsFirstPlayerPiece) pawn.ClearEnPassant(); } } |
Imagine what it starts to look like as methods have things like large degrees of nesting, switch statements, and conditional after conditional. The cyclomatic complexity can soar to the point where it’s unlikely that every path through the code has even ever been executed, let alone tested.
So it stands to reason that something pretty simple to articulate, like complexity, can have a nuanced effect on the time to comprehend a code base. In the upcoming installment of our experiments, I’d like to focus on cyclomatic complexity and its effect on method time to comprehend.
But I’ll close out this post by offering up a video showing you one of the ways that NDepend allows you to browse around your code by cyclomatic complexity.