3 Functions

Functions are the first line of organization in any program.

Small!

The first rule of functions is that they should be small, very small. The blocks within if statements, else statements, while statements, and so on should be one line long. Probably that line should be a function call. Not only does this keep the enclosing function small, but it also adds documentary value because the function called within the block can have a nicely descriptive name.

Do one thing

Functions should do one thing. They should do it well. They should do it only. 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. Another way to know that a function is doing more than “one thing” is if you can extract another function from it with a name that is not merely a restatement of its implementation. Functions that do one thing cannot be reasonably divided into sections.

One level of abstraction per function

In order to make sure our functions are doing “one thing,” we need to make sure that the statements within our function are all at the same level of abstraction. We want the code to read like a top-down narrative. We want every function to be followed by those at the next level of abstraction so that we can read the program, descending one level of abstraction at a time as we read down the list of functions.
It turns out to be very difficult for programmers to learn to follow this rule and write functions that stay at a single level of abstraction.

Switch statements

It’s hard to make a small switch statement. It’s also hard to make a switch statement that does one thing. By their nature, switch statements always do N things. We can’t always avoid switch statements, but we can make sure that each switch statement is buried in a low-level class and is never repeated. We do this with polymorphism.
My general rule for switch statements is that they can be tolerated if they appear only once, are used to create polymorphic objects, and are hidden behind an inheritance relationship (abstract factory pattern) so that the rest of the system can’t see them.

Use descriptive names

It is hard to overestimate the value of good names. Remember Ward’s principle: “You know you are working on clean code when each routine turns out to be pretty much what you expected.” A long descriptive name is better than a short enigmatic name. A long descriptive name is better than a long descriptive comment. Be consistent in your names. Use the same phrases, nouns, and verbs in the function names you choose for your modules.

Function arguments

The ideal number of arguments for a function is zero (niladic). Next comes one (monadic). More than three (polyadic) requires very special justification—and then shouldn’t be used anyway.
Arguments are hard. They take a lot of conceptual power. The argument is at a different level of abstraction than the function name and forces you to know a detail that isn’t particularly important at that point. Arguments are even harder from a testing point of view. Output arguments are harder to understand than input arguments.

There are two very common reasons to pass a single argument into a function. You may be asking a question about that argument, as in boolean fileExists(“MyFile”). Or you may be operating on that argument, transforming it into something else and returning it. For example, InputStream fileOpen(“MyFile”).
A somewhat less common, but still very useful form for a single argument function, is an event. Try to avoid any monadic functions that don’t follow these forms.

Flag arguments are ugly. Passing a boolean into a function is a truly terrible practice. It immediately complicates the signature of the method, loudly proclaiming that this function does more than one thing. The method call render(true) is just plain confusing to a poor reader. We should have split the function into two: renderForSuite() and renderForSingleTest().

A function with two arguments is harder to understand than a monadic function. There are times, of course, where two arguments are appropriate. For example, Point p = new Point(0,0); is perfectly reasonable.
In this case the two arguments are ordered components of a single value! Even obvious dyadic functions like assertEquals(expected, actual) are problematic. The two arguments have no natural ordering. The expected, actual ordering is a convention that requires practice to learn.

When a function seems to need more than two or three arguments, it is likely that some of those arguments ought to be wrapped into a class of their own. Sometimes we want to pass a variable number of arguments into a function. If the variable arguments are all treated identically then they are equivalent to a single argument of type List.

Choosing good names for a function can go a long way toward explaining the intent of the function and the order and intent of the arguments. In the case of a monad, the function and argument should form a very nice verb/noun pair. For example, write(name) is very evocative. An even better name might be writeField(name). This last is an example of the keyword form of a function name. Using this form we encode the names of the arguments into the function name. For example, assertEquals might be better written as assertExpectedEqualsActual(expected, actual).

Have no side effects

Side effects are lies. Your function promises to do one thing, but it also does other hidden things. In either case they are devious and damaging mistruths that often result in strange temporal couplings and order dependencies. For example, a checkPassword function should not initialize a session.

Arguments are most naturally interpreted as inputs to a function. In general output arguments should be avoided. If your function must change the state of something, have it change the state of its owning object.

Command query separation

Functions should either do something or answer something, but not both. Doing both often leads to (verb/adjective) confusion.

Prefer exceptions to returning error codes

Returning error codes from command functions is a subtle violation of command query separation. It promotes commands being used as expressions in the predicates of if statements. It also creates the problem that the caller must deal with the error immediately.

Try/catch blocks are ugly in their own right. It is better to extract the bodies of the try and catch blocks out into functions of their own. Functions should do one thing. Error handing is one thing. Thus, a function that handles errors should do nothing else.

Returning error codes usually implies that there is some class or enum in which all the error codes are defined. Classes like this are a dependency magnet; many other classes must import and use them. Thus, when the Error enum changes, all those other classes need to be recompiled and redeployed.

Don’t repeat yourself

Duplication may be the root of all evil in software. Many principles and practices, including Object-Oriented programming, Structured programming, Aspect Oriented Programming, Component Oriented Programming have been created for the purpose of controlling or eliminating it.

Structured programming

Edsger Dijkstra’s rules of structured programming say that every function, and every block within a function, should have one entry and one exit. If you keep your functions small, then the occasional multiple return, break, or continue statement does no harm and can sometimes even be more expressive than the single- entry, single-exit rule. On the other hand, goto only makes sense in large functions, so it should be avoided.

How do you write functions like this?

Writing software is like any other kind of writing. When I write functions, they come out long and complicated. But I also have a suite of unit tests that cover every one of those clumsy lines of code. So then I massage and refine that code all the while keeping the tests passing.

Conclusion

Master programmers think of systems as stories to be told rather than programs to be written. They use the facilities of their chosen programming language to construct a much richer and more expressive language that can be used to tell that story. The functions you write need to fit cleanly together into a clear and precise language to help you with that telling.

Previous: 2 Meaningful NamesUp: ContentsNext: 4 Comments