Mindful Functions

TL;DR: break code into short functions with scalar parameters and good names, and coordinate them #

Mindful means, for a human, to focus on the immediate environment, task, and intention: to be “present in the moment”. For a function, it means to do one operation and to return the consequence of that operation. This is quite close to Functional Programming’s definition of a pure function (side-effects withstanding). F.P.‘s prime benefit is the isolation of the operation; mindfulness’ prime benefits are attention, modularity, and scope

Each function or method should do one thing1. The implementation of that one thing might be calling other functions. For example, a function prepare_order() calls other functions to process every step of the order; it never actually changes or creates data itself, yet it completes a single conceptual task by orchestrating sub-functions

Hierarchical Code #

After one distributes the code into smallish functions (which do one thing each) and coordinating functions that call the others, ensure that within each function all the code and calls are at the same level of abstraction. Coordinating functions are all coordination; they do not coordinate some things and do other things themselves. They focus on which functions they call, high-level looping, handling errors, and passing data from one function to another

             data1, error1 := doHighLevelThing(data)
             if error1 != nil ...

             data2, error2 := doOtherThing(data1)
             if error2 != nil   ...

            for normalizationIndex, datum2 := range data2 {
                data2[normalizationIndex].price = 
                                              normalizePrices(datum2)

            // IMPROVEMENT: move this into a function
            for dateAfterIndex, item := range data2 {
                if item.Date.After(cutoff_date) {
                         data2[dateAfterIndex].status = TooLateSorry
                }
            }

Sub-functions do their one thing and return, e.g. one function should not have the code to format data and to save it; those are two separate things that should live in two separate functions. The formatter should not care about files and the saver should not care about formats. Functions should not coordinate with sibling sub-functions; that’s their caller’s job

The pattern of a coordinating function calling a series of other functions and managing their inputs and outputs has several names: top‑down structure, king-pinning, stepwise design, etc. This is the strategy behind the Open-Closed principle: make a process’ steps discrete, replaceable, and augmentable (via injection or subclassing) by making them separate

Good code reads like well-written prose —Grady Booch

Higher-level functions #

Lower-level functions #

Pass In Only What The Function Needs #

Instead of passing in objects as arguments and having the function pull attributes from that object, pass in the attributes themselves to the function (e.g. scalars and collections). Simpler parameters make for simpler testing and debugging (see example below). The less a function knows about its environment, classes, and globals, the more portable it is

For example:

function1(objectA)

def function1(an_object: AClass):
    if an_object.items == []:  // tightly linked to the AClass structure
        return

    # do something with the items

vs.

if an_object.items != []:
    function1(an_object.items)

def function1(some_items: List[Items]):
    # do something with the items

Minimize globals within functions. If a function needs a value that comes from a singleton, context information, and other global, the caller should pass it in

try:
    sable_local_value = somePackage.customer_configuration()
except:
    return "Nope, didn't work"

nextValue = function1(datum, sable_local_value.currency_code)

Even instance methods should not use instance fields except at the highest level (e.g. the protected virtual methods designed to be overridden). Instances fields are globals (albeit in a much smaller, encapsulated globe) with all the drawbacks of globals

    def prepare_total(self):
         sable_local_value= somePackage.CustomerConfiguration()

# we're passing in the part of self that _prepare_items cares about
         self._prepare_items(self.items, sable_local_value.currency_code)
         self._prepare_shipping(sable_local_value.currency_code)
         self._prepare_discounts(sable_local_value.currency_code)

Ideally, these high-level methods access the instance fields and call functions (or static methods) which are happily ignorant of the instance

Break It Down #

Breaking long blocks of procedural code into smaller functions makes them easier to read, easier to understand, easier to debug, and easier to see design and context problems. Perhaps the process of breaking up large blocks of code itself allows the authors to step back from writing the next line of code to think about organization and naming, and that exercise might allow them to catch problems

Fold Code into Functions #

An analogy is code-folding in an IDE: wherever one would want to fold up chunks of code to see an overview, the folded code might be better in a function. Likewise, if a block of code has a comment explaining what it does, it might well become a separate function with a name[3] that explains what it does instead of the comment

[3]: Nomenclature is the best documentation

Don’t Tell a Function What To Do #

Beware of functions that look like

def do_something(please_do_details : bool)

If a function does one thing, it does not need parameters to tell it what to do. Behavioural parameters are a code smell or perhaps a design smell. The caller decides what behaviours it wants, then calls different functions to get that behaviour

if url_queries["details"] is not None:
    return do_something(data)
else:
    return do_something_with_details(data)

def do_something(foo):
    bar = do_something_nice(foo)
    baz = do_something_well(bar)
    return baz

def do_something_with_details(foo):
    bar = do_something_nice(foo)
    baz = do_something_well(bar)
    foz = do_something_with_details(baz)
    return foz

Don’t Call For No Reason (aka Ding-Dong Ditch) #

Callers should determine that they need to call a function before calling it; the function should not have to check its arguments to see if it should have been called[4] (e.g. passing an empty array). This is not the same as validating the arguments (e.g. Guards); functions should not trust their callers even though they must obey all arguments or return an error

[4]: many optional parameters is a code smell

Bringing an Outline to Life #

If one lays out a list of comments in a high-level function as an outline or roadmap for future development, turn that list into a series of function calls with names at least as descriptive as the outline, and remember to remove the comments. A function with a good name does not need a comment, especially in code that calls it

And Do This Other Thing #

Good function names describe what they do. Reading calling code is like a narration: we do this thing (by calling this function) and then that thing (by calling that function). If a function name has the conjunction “And” in it (e.g. ReadAndFormatData()), then it is doing two things that might be better done in two separate functions. An “Or” or “But” in a name implies that the function makes a decision that its caller (or a wrapping function) should make rather than passing a behavioural parameter
For example, BackupDataButNotOnTuesday() might have the test for Tuesday and call a function that does the backup. A better name is RegularBackup() as that is the conceptual thing we want to do. Within RegularBackup(), separate the test for Tuesday and the call to the function that does backups

See Also #

See also CODE IS NOT LITERATURE


  1. SRP: Single Responsibility Principle can apply to functions as well as classes and modules (and even individual lines of code – See Single Responsibility Lines‽ 

 
0
Kudos
 
0
Kudos

Now read this

Walk, Don’t Race

TL;DR # Cha-Cha-Changes # Breaking changes happen, and the breaking occurs between the layers (e.g. UI and API, API and database). Rather than trying to release new layers simultaneously (especially with edge distribution), commit the... Continue →