Continuing my chapter-by-chapter review of A Philosophy of Software Design By John Ousterhout, today I’m going to review Chapter 9.
Chapter 9: Better Together or Better Apart
Often times a decision needs to be made about two bits of functionality. Do they belong together or do they belong apart?
the goal is to reduce the complexity of the system as a whole and improve its modularity. It might appear that the best way to achieve this goal is to divide the system into a large number of small components … However, the act of subdividing creates additional complexity
It’s at this part of the discussion that words like “complexity” stop having any real meaning to me. A large number of small, simple components is considered “complex” in John’s view, but a small number of large, “deep” modules aren’t. This works if “complexity” is redefined to be synonymous with “number of classes”, but if it’s defined in any meaningful way it doesn’t work for me.
A class is just an organizational structure. It should do one thing and do it well. If that requirement is met in a small class, that’s the class you want to have. If it requires a bigger class, then that’s what you need. Classes are the nouns in your program and their public methods are the verbs, and by stringing the two together you write the prose of the program. The behavior of your program should be clear from reading this prose, and so long as the meaning and intent are clear, dogmatic avoidance of one size of class or another seems like wasted effort to me.
Some complexity comes just from the number of components … every new interface adds complexity.
I’m not sure that it does. I mean, it’s clearly something John believes, he’s repreated it several times. I just don’t accept it as an axiom or as a fundamental truth. If a class has a name, using the established verbage of the common pattern language or the ubiquitous language of the project, and if you can easily find classes you’re looking for using a text search or your IDE, I don’t see more classes as being an inherent source of complexity. Can more classes be complex? Yes. I’ve seen programs with many thousands of classes that were a complexity nightmare, but perhaps the most complex and unmaintainable piece of software I’ve ever worked on had exactly 5 classes! The complexity in this case came from individual classes doing too much, and deep interconnectedness of features. The solution to decrease complexity in that case was to increase the number of classes and methods. More classes are good? Not necessarily. Fewer classes are good? Not necessarily. Blindly following rules like this will get you into trouble, and an unsophisticated programmer will make a mess of a system no matter how it’s arranged.
Bring It Together
John lists three big reasons why we should merge code together into a single module: when information is shared, when the interface will be simplified, and when duplication will be eliminated. Conversely, he only lists one scenario when code can be separated: When we want to separate special-purpose code from general-purpose code.
If a module contains a mechanism that can be used for several different purposes, then it should provide just that one general-purpose mechanism. It should not include code that specalizes the mechanism for a particular use.
This is good advice, and I’ve never thought about it in quite these terms before. Separate out your code to a general purpose module, and a separate module to orchestrate that mechanism for the particular case. I’m not sure this is the only reason why we would break up a module, but it’s a good start.
If the same piece of code appears over and over again, that’s a red flag that you haven’t found the right abstractions
It’s an indication, but it’s certainly not a rule. There’s always the hardcore DRY folks, but simple duplication of code also needs to consider the purposes to which that code is applied and the masters that the different pieces of code serve. If code which looks duplicated serves different purposes, and may be changed for different reasons, it shouldn’t be de-duplicated. Over-aggressive de-duplication of code which looks similar syntactically but is separate semantically causes huge problems. I don’t think from the text that John would necessarily disagree with this, but his text on this point was sufficiently thin that I thought it worth a mention.
John also seems to leave out the case where we can separate a large class into smaller classes and make the resulting interfaces simpler. I do this a lot with the Strategy Pattern, for example. Instead of a single large class where we have to setup state and settings beforehand, we can instead break up the different options into separate Strategy classes, each of which is extremely simple and straightforward. Passing a simple Strategy instance into a simple method which performs some work with that Strategy can be quite simple indeed. Or separating complex object-graph traversals into a Visitor Pattern implementation can do the same thing: More classes but with a simpler interface overall. Break mapping logic into a Mapper Pattern implementation to remove that code from a larger class which needs to focus on other things. These are only a few examples. I list them here not to be exhaustive, but just to give an idea that we can have more classes and increase overall simplicity.
On goto
goto statements … are useful in situations like this where they are used to escape from nested code.
I haven’t written a goto
statement in nigh on a decade or more, and I don’t miss it. If you’re in a situation where you feel like a goto
statement is a necessary improvement, chances are pretty good that your code isn’t structured correctly. I won’t be writing goto, and I wont accept code from others which uses it.
If the programming language supports
goto
, you can move the cleanup code to the very end of the method and thengoto
that snippet at each of the points where an error return is required
Or you could have a method called Cleanup()
and call that method from anywhere. This has the benefit that it separates the “happy path” code which communicates intent, from the cleanup or error-handling code which doesn’t.
if (something.WentWrong())
{
HandleError(neccessaryContextInformation);
return;
}
I don’t know, I just can’t imagine that having a goto
in here and smashing cleanup code into the end of the method instead of locating it close to the code that creates the mess, or having error handling code in the same method but not being near the point where the errors are created seems like it makes matters worse, not better.
Example: Separate class for logging
Rather than logging the error at the point where it was detected, a separate method in a special error logging class was invoked. … contained several methods … each of which logged a different kind of error. This separation added complexity with no benefit
I’ve used this technique before and with good results. The separatation can add benefit because the error-logging code often serves a different purpose and serves a different master from the code which generates the errors. The calling code must perform the necessary business logic and serves the interests of the business stake holders. The error-logging code performs a maintenance task and serves the software maintainers, which may be some beleaguered DevOps engineer or customer service representative. Good error reporting tells not just what happened, but where, why and how to address it. Notice that the happy-path code can serve the business requirement without any error reporting at all. We have error logging because of a separate requirement from a separate group of people. Because the two pieces of code serve different masters, it’s fine and sometimes even preferred to live in separate locations. These two pieces of code will change separately for different reasons. Whether these become separate methods in a single class or separate classes is an implementation detail which should probably take Conway’s Law into account.
Splitting and joining methods
John starts the section with this doozie:
Long methods tend to be more difficult to understand than shorter ones
This is exactly my point, and the reason why I tend to prefer short and simple classes and methods over the “deep” methods that John recommends. Isn’t “more difficult to understand” synonomous with “complexity”? If we make something easier to understand, haven’t we decreased complexity?
Students in classes are often given rigid criteria, such as “Split up any method longer than 20 lines”.
I’ve discovered in my time as a programmer than any rigid rule is often more trouble than it’s worth, which is why I replace this same idea with the more nimble “a method should fit entirely on your screen”. Sometimes I’ll even follow up the ever-important “should” from that sentence with this caveat: “You better have a darn good reason if your method needs to be larger”. It’s not about following rigid rules without thinking. It’s about understanding that every decision comes with trade-offs, and if you’re going to go against the grain you better be able to justify it. Being able to justify your decisions is everything.
suppose a method contains five 20-line blocks of code that are executed in order. If the blocks are relatively independent then the method can be read and understood one block at a time
Or you could break each block into a method with a descriptive name, and understand the method by reading 5 lines of code. The “descriptive name” bit is, of course, of utmost importance.
The method should have a clean and simple interface, so that users don’t need to have much information in their heads in order to use it correctly. The method should be deep: its interface should be much simpler than it’s implementation.
Simple interfaces are absolutely a good thing, but whether the method performs a lot of work or delegates large chunks of it out to private methods or even separate classes is an implementation detail. A method which orchestrates a large operation can be understood by reading the lines like a checklist, and the methods which perform the subtasks can each be simple, uncluttered, undistracted, and easy to understand.
Conclusion
I often think about Legos: small, simple, standardized building blocks that each are nothing but together they can create some huge and amazing works of art and feats of engineering. I think of software in these same terms; lots of little building blocks which are each simple and obvious, but which can be combined to make bigger and more impressive things. John really seems to want to go the opposite route, increasing the complexity of each block in order to reduce the overall number of blocks. I guess what I am missing is a clear statement of why this helps. Why is a single large method less complex than a sequence of smaller, simpler methods? What is it about the count of classes and methods which makes it more predictive of software quality than Cyclomatic Complexity or other measures?