The statements within a function should all be written at the same level of abstraction, which should be one level below the operation described by the name of the function. This may be the hardest of these heuristics to interpret and follow. Though the idea is plain enough, humans are just far too good at seamlessly mixing level of abstraction. Consider, for example, the following code taken from FitNesse:
My goal, at this point, was to create the necessary separation and get the tests to pass. I accomplished that goal easily, but the result was a function that still had mixed level of abstraction. In this case the mixed levels were the construction of the HR tag and the interpretation and formatting of the size variable. This points out that when you break a function along lines of abstraction, you often uncover new lines of abstraction that were obscured by the previous structure.
So what you ideally want is for each function/etc to descend only 1 level of abstraction.
Mixing level of abstraction within a function is always confusing. Readers may not be able to tell whether a particular expression is an essential concept or a detail. Worse, like broken windows theory, once details are mixed with essential concepts, more and more details tend to accrete within the function.
DSLs, when used effectively, raise the abstraction level above code idioms and design patterns. They allow the developer to reveal the intent of the code at the appropriate level of abstraction.
Each class and each function add a concept to your language. Every time you create one - either from primitive statements or from other concepts you've already defined - you make your language more expressive. You rise it's level of abstraction.
Separating level of abstraction is one of the most important functions of refactoring, and it’s one of the hardest to do well. As an example, look at the code below. This was my first attempt at separating the abstraction levels in the HruleWidget.render method.
If a function does only those steps that are one level below the stated name of the function, then the function is doing one thing. After all, the reason we write functions is to decompose a larger concept (in other words, the name of the function) into a set of steps at the next level of abstraction.
If you have to reach deep into some object that may serve as indication that there is something wrong with the level of abstraction of your code - two levels that try to interact are too far removed.
The percentFull function is at the wrong level of abstraction. Although there are many implementations of Stack where the concept of fullness is reasonable, there are other implementations that simply could not know how full they are. So the function would be better placed in a derivative interface such as BoundedStack.
One of the key principles of writing good software is maintaining an appropriate separation of different level of abstraction.
Large functions tend to mix code at the multitude level of abstraction (all code in the function should be at the same level of abstraction)
Succinctness is good when it is accomplished by rising level of abstraction/syntax
[[good code/names should be consistent with the level of abstraction they are used in]]
Now look again. This method is mixing at least two level of abstraction. The first is the notion that a horizontal rule has a size. The second is the syntax of the HR tag itself. This code comes from the HruleWidget module in FitNesse. This module detects a row of four or more dashes and converts it into the appropriate HR tag. The more dashes, the larger the size.
This change separates the two level of abstraction nicely. The render function simply constructs an HR tag, without having to know anything about the HTML syntax of that tag. The HtmlTag module takes care of all the nasty syntax issues.