What Are Small Functions?
Small functions follow this idea that functions should:
Be small as possible (very few lines of code)
Self-describing what it does by the name
Functionally discrete. The goal is to do one thing in a small function
The goal with self-describing functions, by function name, is that it should create a very readable flow with most of the business logic encapsulated in these micro functions. Testing would not be applied to the small functions, but to the overall larger, incorporating integrating function, i.e. the parent function.
The principle is that the business logic and how it’s organized is irrelevant as long as the function itself, behaves in a specified way, e.g. it’s API, and therefore, the small functions used both in quantity and what they do are mostly irrelevant. Instead, the small functions in use provide a descriptive set of self-documenting actions that provide better clarity as to the steps.
In some practices, small functions are not required for testing.
Example 1
Let’s take a look at an example:
function openDoor( desireDegrees )
{
currentDegrees = determineDoorPosition()
newPosition = moveDoor( deltaCurrentToNew( currentDegrees, desiredDegrees ) )
}
This is clearly readable but the reader has to make some assumptions:
deltaCurrentToNew( ) should only be doing a delta, basically a subtraction
moveDoor( ) relies on the degrees to move as a parameter.
Exceptions (if in use) should be assumed to be thrown on multiple levels
Example 2
Let’s take a different take on this. Programming languages are a language and they have a grammar. Imagine a story written in English like this using something akin to small functions:
A reader must tell me about this cat, the house color, and if the forest was green.
Chapter 1
There once was a cat.
Chapter 2
That cat lived in a small house (see appendix A) and it enjoy living in a nice forest (see Appendix C)
Appendix A
The house was a certain color (see appendix B)
Appendix B
The color was the second color in the list in Appendix D.
Appendix C
The forest is green and lush
Appendix D
White, Black, Red
Let’s redo this so it’s one paragraph.
There once was a call that lived in a small, black house and it enjoyed living in a nice, green and lush, forest.
Which is easier to read? Which is easier to maintain? Which style of writing mitigates problems if an editor changes something?
Problems in Small Functions Paradigm?
In this small case there are already some potential problems here:
The logic is buried in sub-functions. While the reader may assume the logic in the sub-functions is correct, they shouldn’t as they did not write the code, and during feature extension or modifications, it’s beneficial not to make any assumptions unless there are tests that assert said assumptions.
Other users may be enticed to reuse small functions and potentially, mutate the behavior such that there are unwanted side effects that unit testing must be relied upon to assert. At the point of failure, all the tester would know is the openDoor() as well as any top-level functions, have failed assertions. Now the tester must determine at what point the shared logic failed.
Error control is buried. The principle that errors can be thrown at any level is problematic with code that is buried in nested small functions. The parent function's total expectation of possible errors in the summation of all errors that its small functions can throw independently. Implementers need to be aware that producing new exceptions or errors in potentially nested calls is changing the API surface subtly. This requires an extensive understanding of when failures may occur and how those failures are manifested.
Context switching between functions is mentally difficult. As readers of the code parse the logic, each shift in the function tree is a mental stack movement to fully grasps the call chain. Such small functions may be located in different files or within different parts of the file. Locating the function, then returning back to the caller is a mental exercise that if severely abused makes the code hard to understand. At face value, the functions seem reasonable and well understood, but that can be a false assumption.
A Compromise
Instead of a pedantic application of small functions, I argue that developers should not rely on dogma, but think through why this is being done. Dogma doesn’t create good, maintainable, testable code.
If the business logic is better served not to be encapsulated and transparent, then do not apply small functions. Expose the logic so tracing and stepping through code is easier requiring less context switching. You should not write code such that it becomes a memory game for the reader and remember, while you may understand how you built your call stack, others will not.
If the business logic is not reused, then reconsider the use of small functions. Are you just using this method to be “pretty” or does it serve a greater purpose?
If you are using small functions whose input and output are considered an API, you must test them. The risk of assuming some parent function will catch mistakes means you are catching failures later rather than at the point to which the error is being caused.
The end result of all the code we write is use a language to communicate a story. The story is a sequence of events with outcomes. If how we write the story inhibits the ability to clearly edit, maintain, or understand the story, then we need to be careful which dogmatic principles to apply. We need to remember we are not the sole authors of the code we write. We aren’t the sole maintainers either.