Each problem has smaller problems inside.
—MartinFowler
• Limit the number of branch points per unit to 4.
• Do this by splitting complex units into simpler ones and
avoiding complex units altogether.
• This improves maintainability because keeping the number of
branch points low makes units easier to modify and test.
Complexity is an often disputed quality characteristic. Code that appears complex to an outsider or novice developer can appear straightforward to a developer that is intimately familiar with it. To a certain extent, what is “complex” is in the eye of the beholder. There is, however, a point where code becomes so complex that modifying it becomes extremely risky and very time-consuming task, let alone testing the modifications afterward. To keep code maintainable, we must put a limit on complexity.Another reason to measure complexity is knowing the minimum number of tests we need to be sufficiently certain that the system acts predictably. Before we can define such a code complexity limit, we must be able to measure complexity.
A common way to objectively assess complexity is to count the number of possible paths through a piece of code. The idea is that the more paths can be distinguished, the more complex a piece of code is. We can determine the number of paths unambiguously by counting the number of branch points. A branch point is a statement
where execution can take more than one direction depending on a condition. Examples of branch points in Java code are if and switch statements (a complete list follows later). Branch points can be counted for a complete codebase, a class, a package,or a unit. The number of branch points of a unit is equal to the minimum number of paths needed to cover all branches created by all branch points of that unit. This is called branch coverage. However, when you consider all paths through a unit from the first line of the unit to a final statement, combinatory effects are possible. The reason is that it may matter whether a branch follows another in a particular order. All possible combinations of branches are the execution paths of the unit—that is, the maximum number of paths through the unit.
As explained at the beginning of this chapter, we need to limit the number of branch points to four. In Java the following statements and operators count as branch points:
• if
• case
• ?
• &&, ||
• while
• for
• catch
So how can we limit the number of branch points? Well, this is mainly a matter of identifying the proper causes of high complexity. In a lot of cases, a complex unit consists of several code blocks glued together, where the complexity of the unit is the sum of its parts. In other cases, the complexity arises as the result of nested if-then else statements, making the code increasingly harder to understand with each level of nesting. Another possibility is the presence of a long chain of if-then-else statements or along switch statement, of which the get FlagColors method in the introduction is an example.
Each of these cases has its own problem, and thus, its own solution. The first case, where a unit consists of several code blocks that execute almost independently,is a good candidate for refactoring using theExtract Method pattern.This way of reducing complexity is similar to Chapter2. But what to do when faced with the other cases of complexity?
A chain of if-then-else statements has to make a decision every time a conditional if is encountered. An easy-to-handle situation is the one in which the conditionals are mutually exclusive; that is, they each apply to a different situation. This is also the typical use case for a switch statement,like the switch from the getFlagColors method.
There are many ways to simplify this type of complexity, and selecting the best solution is a trade-off that depends on the specific situation. For the getFlagColors method we present two alternatives to reduce complexity. The first is the introduction of a Map data structure that maps nationalities to specific Flag objects. This refactoring reduces the complexity of the getFlagColors method from McCabe 7 to McCabe 2.
(是不是觉得mybatis3中TypeHandlerRegistry代码中有这样的结构)
private static Map<Nationality,List<Color>>FLAGS =
new HashMap<Nationality,List<Color>>();
static {
FLAGS.put(DUTCH,Arrays.asList(Color.RED,Color.WHITE,Color.BLUE));
FLAGS.put(GERMAN,Arrays.asList(Color.BLACK,Color.RED,Color.YELLOW));
FLAGS.put(BELGIAN,Arrays.asList(Color.BLACK,Color.YELLOW,Color.RED));
FLAGS.put(FRENCH,Arrays.asList(Color.BLUE,Color.WHITE,Color.RED));
FLAGS.put(ITALIAN,Arrays.asList(Color.GREEN,Color.WHITE,Color.RED));
}
public List<Color>getFlagColors(Nationalitynationality) {
List<Color>colors = FLAGS.get(nationality);
return colors!= null? colors: Arrays.asList(Color.GRAY);
}
A second, more advanced way to reduce the complexity of the getFlagColors method is to apply a refactoring pattern that separates functionality for different flags in different flag types. You can do this by applying theReplaceConditional with Poly-morphismpattern: each flag will get its own type that implements a general interface.The polymorphic behavior of the Java language will ensure that the right functionality is called during runtime
For this refactoring, we start with a general Flag interface:
public interface Flag{
List<Color>getColors();
}
and specific flag types for different nationalities, such as forthe Dutch:
public class DutchFlagimplements Flag{
public List<Color>getColors() {
return Arrays.asList(Color.RED,Color.WHITE,Color.BLUE);
}
}
and the Italian:
public class ItalianFlagimplements Flag{
public List<Color>getColors() {
return Arrays.asList(Color.GREEN,Color.WHITE,Color.RED);
}
}
The getFlagColors method now becomes even more concise and less error-prone:
private static final Map<Nationality,Flag> FLAGS=
new HashMap<Nationality,Flag>();
static {
FLAGS.put(DUTCH,new DutchFlag());
FLAGS.put(GERMAN,new GermanFlag());
FLAGS.put(BELGIAN,new BelgianFlag());
FLAGS.put(FRENCH,new FrenchFlag());
FLAGS.put(ITALIAN,new ItalianFlag());
}
public List<Color>getFlagColors(Nationalitynationality) {
Flag flag =FLAGS.get(nationality);
flag = flag!= null? flag: newDefaultFlag();
return flag.getColors();
}
This refactoring offers the most flexible implementation. For example, it allows the flag type hierarchy to grow over time by implementing new flag types and testing these types in isolation. A drawback of this refactoring is that it introduces more code spread out over more classes. The developer much choose between extensibility and conciseness.
Suppose a unit has a deeply nested conditional, as in the following example. Given a binary search tree root node and an integer, the calculate Depth method determines whether the integer occurs in the tree. If so, the method returns the depth of the integer in the tree; otherwise, it throws a TreeException:
public static intcalculateDepth(BinaryTreeNode<Integer>t,intn) {
int depth= 0;
if (t.getValue()==n) {
return depth;
} else {
if (n< t.getValue()) {
BinaryTreeNode<Integer>left = t.getLeft();
if (left==null) {
throw new TreeException("Value not found in tree!");
} else {
return 1+ calculateDepth(left,n);
}
} else {
BinaryTreeNode<Integer>right = t.getRight();
if (right==null) {
throw new TreeException("Value not found in tree!");
} else {
return 1+ calculateDepth(right,n);
}
}
}
}
To improve readability, we can get rid of the nested conditional by identifying the distinct cases and insert return statements for these. In terms of refactoring, this is called theReplace Nested Conditional withGuard Clauses pattern. The result will be
the following method:
public static intcalculateDepth(BinaryTreeNode<Integer>t,intn) {
int depth= 0;
if (t.getValue()==n)
return depth;
if (n< t.getValue() &&t.getLeft() !=null)
return 1+ calculateDepth(t.getLeft(),n);
if (n> t.getValue() &&t.getRight() !=null)
return 1+ calculateDepth(t.getRight(),n);
throw new TreeException("Value not found in tree!");
}
Although the unit is now easier to understand, its complexity has not decreased. In order to reduce the complexity, you should extract the nested conditionals to separate methods. The result will be as follows:
public staticint calculateDepth(BinaryTreeNode<Integer>t, intn) {
int depth= 0;
if (t.getValue()==n)
return depth;
else
return traverseByValue(t,n);
}
private static inttraverseByValue(BinaryTreeNode<Integer>t,intn) {
BinaryTreeNode<Integer>childNode =getChildNode(t,n);
if (childNode==null) {
throw new TreeException("Value not found in tree!");
} else {
return 1+ calculateDepth(childNode,n);
}
}
private static BinaryTreeNode<Integer>getChildNode(
BinaryTreeNode<Integer>t, intn) {
if (n< t.getValue()) {
return t.getLeft();
} else {
return t.getRight();
}
}
This actually does decrease the complexity of the unit.Now we have achieved two things: the methods are easier to understand, and they are easier to test in isolation since we can now write unit tests for the distinct functionalities.
Of course, when you are writing code, units can easily become complex. You may argue that high complexity is bound to arise or that reducing unit complexity in your codebase will not help to increase the maintainability of your system. Such objections are discussed next.
“Our domain is very complex, and therefore high code complexity is unavoidable.”
When you are working in a complex domain—such as optimizing logistical problems, real-time visualizations, or anything that demands advanced application logic—it is natural to think that the domain’s complexity carries over to the implementation, and that this is an unavoidable fact of life.
We argue against this common interpretation. Complexity in the domain does not require the technical implementation to be complex as well. In fact, it is your responsibility as a developer to simplify problems such that they lead to simple code. Even if the system as a whole performs complex functionality, it does not mean that units on the lowest level should be complex as well. In cases where a system needs to process many conditions and exceptions (such as certain legislative requirements), one solution may be to implement a default, simple process and model the exceptions explicitly.
It is true that the more demanding a domain is, the more effort the developer must expend to build technically simple solutions. But it can be done! We have seen many highly maintainable systems solving complex business problems. In fact, we believe that the only way to solve complex business problems and keep them under control is through simple code.
“Replacing one method with McCabe 15by three methods with McCabe 5 each meansthat overall McCabe is still 15 (and therefore, there are 15 control Šfow branches overall).So nothing is gained.”
Of course, you will not decrease the overall McCabe complexity of a system by refactoring a method into several new methods. But from a maintainability perspective, there is an advantage to doing so: it will become easier to test and understand the code that was written. So, as we already mentioned, newly written unit tests allow you to more easily identify the root cause of your failing tests.
Put your code in simple units (at most four branch points) that have carefully chosen names describing their function and cases.