Recently, I wrote a post explaining the basics of cyclomatic complexity. You can read that for a deep dive, but for our purposes here, let’s be brief about defining it. Cyclomatic complexity refers to the number of “linearly independent” paths through a chunk of code, such as a method. Understand this by thinking in terms of debugging. If you could trace only one path through a method, it has a cyclomatic complexity of one. But throw a conditional in there, introducing a second path you could trace, and the complexity grows to two.
Today, I’ll talk specifically about C#. Cyclomatic complexity in C# is just, well, cyclomatic complexity applied to the language C#. No big mystery there.
But what I want to talk about today is not cyclomatic complexity — not per se. Today, I’d like to talk about how you can go beyond cyclomatic complexity in C# to get some even more intelligent metrics. How can you really zero in on sources of risk and complexity in your code?
Wait, What’s Wrong with Cyclomatic Complexity?
Let me be clear. There’s absolutely nothing wrong with tracking cyclomatic complexity.
It’s a great aid and shorthand for reasoning about your code’s relative complexity and for understanding where testing challenges lie. You can use it to locate complexity “hot spots” in your code and then to address them in ways that make sense. So no criticism whatsoever. I’m just advocating that you go beyond it.
Think of it this way. When I encourage you to install a Visual Studio plugin, I’m not knocking Visual Studio. Visual Studio is a wonderful and productive IDE, in my estimation. Instead, I’m encouraging you to make it even better — to enhance your experience.
The same sort of reasoning applies here. Cyclomatic complexity is a great start for reasoning about your code’s complexity. But we can add some considerations to make your life even better. Let’s take a look at those.
The rest of this post will show you in detail what that looks like. But if you want to try it out for yourself, you’ll need to download a copy of NDepend.
Worsening Cyclomatic Complexity
When we think of a codebase’s cyclomatic complexity, there’s a tendency to think statically. For instance, if the complexity is worse than, say, five, then we have a problem. Otherwise, life is good.
But reality intrudes in subtle ways. Often, you’ll start tracking complexity only in a well-established codebase. And that codebase might have complex methods, but they’re ones that people have tested extensively and not changed for a long time. When that happens, the complexity presents less of an immediate problem.
The real issue with these complex methods is the risk at change time. That’s because they’re hard to test and easy to get wrong. So what if, instead of just flagging complex methods, we also flagged complex methods growing more complex as a disproportionate risk?
Doing this is pretty simple with NDepend.
1 2 3 4 5 6 7 8 |
// <Name>Cyclomactic Complexity got worse</Name> warnif count > 0 from m in JustMyCode.Methods where m.CodeWasChanged() && m.OlderVersion().CyclomaticComplexity < m.CyclomaticComplexity && m.OlderVersion().CyclomaticComplexity > 10 select new { m, OldComplexity = m.OlderVersion().CyclomaticComplexity, m.CyclomaticComplexity } |
This takes advantage of NDepend’s CQLinq functionality, which lets you write custom queries about your code. Basically, you generate a warning when anyone adds additional complexity to a method that already has a complexity of more than 10. (You could also opt to generate the warning for any changes to such a risky method).
Pair Test Coverage with Cyclomatic Complexity
If you’re someone that follows this blog regularly, you’ll know that I’m not a fan of test coverage as an organizational metric. Coverage is great for developers to understand where holes exist in their unit test suites. But people overuse it, which makes me leery. So you might think it’s curious that I’m stumping for it here.
Well, I’m stumping for it as a pure development tool and a second data point for risky, complex methods. Think of it this way. If you have two methods with really high cyclomatic complexity and one has 100% coverage while the other has none, which do you consider to be the biggest risk? I’d say that’s a no-brainer. The second data point of coverage shows you that not all complexity is created equal.
1 2 3 4 5 |
// <Name>Uncovered code in complex methods</Name> warnif count > 0 from m in JustMyCode.Methods where m.CyclomaticComplexity > 10 && m.PercentageBranchCoverage < 100 select new { m, m.PercentageBranchCoverage, m.CyclomaticComplexity } |
You could certainly get more elaborate with the query, if you were so inclined. But let’s keep it simple here. What I’m doing is raising a warning for any complex methods without total branch coverage. This is a more nuanced look at complexity risk, and you could use it instead of or in addition to raising warnings just about cyclomatic complexity.
The CRAP Metric
I get a childish kick out of the naming of this. I think the authors probably did that intentionally. Anyway, CRAP is actually an acronym for “Change Risk Analyzer and Predictor,” meaning that it drives at what we’ve been discussing here.
You can read about this metric in depth, if you’d like. But it does a more rigorous version of the off-the-cuff pairing of coverage and complexity that I did above. It defines a specific mathematical formula to relate the two items. Expressed in CQLinq, that formula is as follows.
1 2 3 4 5 6 7 8 |
// <Name>CRAP</Name> from m in JustMyCode.Methods let CC = m.CyclomaticComplexity let uncov = (100 - m.PercentageCoverage) / 100f let CRAP = (CC * CC * uncov * uncov * uncov) + CC select new { m, CRAP } |
As you can see, it offers a score that varies with the square of complexity and the cube of uncovered percentage, and it uses the method’s basic complexity as a starting point. Based on the authors’ study and experimentation, that provides a good scale for riskiness.
IL Cyclomatic Complexity
The final consideration that I’ll offer switches gears a bit. The previous three have all been aimed at taking a different, more nuanced look at your own code’s complexity. And that’s genuinely helpful.
But it can also prove helpful to understand others’ code this way as well. Any given programmer spends an awful lot of time finding and making use of third-party libraries. You download them, try them out, struggle with their APIs, read their documentation and generally wrangle them to fit within your mission. Sometimes this goes well. Other times, it frustrates you. We can all appreciate that struggle, but it doesn’t occur to most people that you can evaluate third-party libraries with more than a simple, black box approach.
NDepend’s CQLinq offers a property on methods called IL Cyclomatic Complexity. The intermediate language in .NET is WAY beyond the scope of this post, but suffice it to say that the framework compiles your code into a different programming language. Well, as it turns out, cyclomatic complexity applies just as easily to that language. And better still, the DLLs of the third-party libraries we use are all in this language. So you can size up their cyclomatic complexity by method, using NDepend.
Why does this matter? Well, just as with your own code, you can look for problematically high levels of cyclomatic complexity in their IL code. If you have a candidate third-party library and you analyze it to find that they have extremely complex methods, you might want to run away.
Reasoning About Your Code
I’ll wrap by offering some perspective. If you measure or worry about your application’s cyclomatic complexity at all, you’re already way ahead of the general curve with code quality. But that doesn’t mean that you can’t go further.
The game of software architecture and design is all about tradeoffs and nuance. And the more tools you give yourself for exploring those tradeoffs and that nuance, the better you’ll do.