Graduate Program KB

Clean Code Chapter 3: Functions


Long Functions

Functions must be written in a way that makes it easily readable by the user, and clearly displays the purpose of the function. Typically, if you cannot understand each aspect of what a function is doing, or get lost in a line of 'if' statements, the function is too long, and attempting to complete too many things at once. The remains of chapter 3 will discuss how we can ensure our functions follow rules to achieve the desired outcome.

Making Functions Small

Functions should be small. Small functions are easy to read with only a few lines, and will more closely follow the single responsibility principle. Having small functions that complete just one task, will often allow for reusability of that function for other purposes. Additionally, smaller functions allow bug-fixing to occur at a faster rate. The less you write in a function, the less can go wrong, and the less there will be to test if a bug occurs.

Blocks and Indenting

The text implies that block statements (such as if, else, while) should not be nested within each other, and if they are then the function is too long. If you are performing multiple operations within a block statement, this should instead be a call to another function, leaving the block just one line long.

Do One Thing

We already mentioned the SRP principle, evidently this leads into "Do one thing". This whole section is about how functions should only do one thing, and if they do more than one thing, it should be refactored with methods extracted until it meets that principle. They have a quote: "Functions should do one thing. They should do it well. They should do it only.". To determine what just 'one thing' is, the book makes reference to levels of abstraction. If a function completes things only 1 level below the name of the function, then we can say the function only does one thing. Another way to recognise if a function does only one thing, is that during refactoring, we cannot extract into another function any of the original code without simply duplicating what we already had. See Page 71 of the textbook for an example of a function doing well more than 1 thing.

The Step-down Rule

I found the way this rule was described, to be similar to how Jest tests should be written. Writing functions in a way that can be read like a narrative ensures that we will more closely follow to just one level of abstraction, as each level becomes more clear when written as a narrative. In the book they use the "TO" method of writing.

Switch Statements

Switch statements are a tricky case when applying all we have learnt previously. By nature, they do more than one thing, and are typically longer than we would like to have it in a function. In the provided example (page 38), the book states that there are several things wrong with the switch statement. Including violating multiple principles and being too large. We realise in the example that many other functions would also require this switch statement to determine what kind of employee they are dealing with, so the function can execute accordingly. The book also states that switch statements should only be used when they are hidden, appear only once and are used to create polymorphic objects (occurs in multiple different forms).

Use Descriptive Names

Choosing names that describes a functions function (funnily enough) is vital. The book makes reference to an earlier quote: "You know your working on clean code when ech routine turns out to be pretty much what you expected.". The smaller a function is, the easier it is to choose a matching descriptive name, this could be a good indicator of whether or not your function is too long if you are struggling to find an appropriate name for it. It is also important to note that a long descriptive name is better than a long descriptive comment about the function. You do not have to know the right name immediately, but as you expand out the function and it's subsequent calls, finding the correct name should become easier and easier. Being consistent in your naming conventions is just as important as choosing an appropriate name. Using the same keywords or phrases in naming helps us to tell the story we mentioned earlier.

Function Arguments

The ideal number of arguments for a function is zero. This is due to argument taking up conceptual power. Each time we read a function, (provided we have a descriptive function name), the arguments force us to pause, read their name, think about why are there, and potentially need to trace them back to their origin to better understand the context of the function.

More function arguments will typically make it harder to write tests for a function. More arguments, allows for more potential inputs, which in turn requires more thorough test cases for the multitude of values which could be passed into the function. When we do include arguments, we want them to have a name which makes sense within the context of the function. The example the book gave was: "SetupTeardownIncluder.render(pageData)". Obviously the "pageData" makes sense in this context.

Monadic (One Argument)

When passing just one argument to a function, we are typically going to do one of two things to the argument. We may be asking a question about the argument (such as passing a filename, to check if the file exists), or because we want to operate on the argument in some form. The name of our function should make it clear what we will be doing with the argument. Sometimes we may have a monadic function which has an input, but no output argument. The provided example was checking how many password attempts have been made, and then altering the system based on the result. Typically, we are advised using monadic functions which do not follow these forms.

Dyadic (Two Arguments)

Dyadic functions are typically harder to understand than monadic functions. Whilst there are use cases for dyadic functions, we should attempt to convert them to monadic functions wherever possible. This may means making a variable have global scope so that it doesn't need to be passed in, or extracting a method to a class etc.

Triads (Three Arguments)

As expected, triads are even harder to read (like a narrative) and understand when compared to dyadic and monadic functions. More arguments can simply lead to confusion, mistaking one argument for another, operating on the wrong thing, the list goes on. Less arguments = less room for error/confusion. Sometimes, if we need to pass 2+ arguments to a function, we should consider if they can be placed into an object, and pass that in instead.

Verbs and Keywords

For monadic functions, the function name and variable name should create a nice verb/noun pair which reads well. For example: "write(name)". It is simple, but we can easily infer that the argument name, is being written. We should be a little more descriptive to know, where or why name is being written.

Have No Side Effects

A side effect is an unfortunate by-product that wa not expected from our function. In the provided example (page 44), we see the function checkPassword, which as the name suggests, checks if a password is correct. However the function calls Session.initialize(); if the password is correct, yet this was not described within the scope of the function by its name. This also violates the SRP, and doing one thing rule.

Command Query Separation

Functions should either do something, or answer something but not both. For example, either altering an array, or returning information about the array. If we need to perform both operations, they should be separated from one another.

Prefer Exceptions to Returning Error Codes

In the provided examples (page 46) it becomes clear that using exceptions rather than error codes makes our code far simpler and more readable without losing any functionality. It removes nested 'if' statements (and as we mentioned earlier we do not want them to be nested where possible).

Try/Catch Blocks

Typically 'ugly' by nature, we can make them far more readable by removing their scopes into separate functions. This will result in both try and catch blocks being one line each, which should simply be a call to the function which will evaluate the block. Example of a poor try/catch:

try{
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
} catch {
    (Exception e) {
        logger.log(e.getMessage());
    }
}
// This can be re-written as below:

public void delete(Page page){
    try{
        deletePageAndAllReferences(page);
    } catch (Exception e) {
        logError(e);
    }
}

private void deletePageAndAllReferences(Page page) throws exception {
    deletePage(page);
    registry.deleteReference(page.name);
    configKeys.deleteKey(page.name.makeKey());
}

private void logError(Exception e){
    logger.log(e.getMessage());
}

As we can see, whilst the second way of writing the block takes up more physical screen space, it becomes much clearer, and ensures each function follows the SRP.

Don't Repeat Yourself

When writing code we want to ensure that we do not have to write a piece of code that does essentially the same thing over and over. Either we should find a more efficient way to complete a task, or write a function that can be re-used an adapted to multiple functions. This will save us from repeating ourselves multiple times, as we can instead just call our helper function.

Structured Programming

Dijkstra states each function, and each block within a function should have one entry and one exit. Meaning only one return statement, with no break or continue statements in a loop. These types of rules only make functional sense within a larger function, as smaller functions by nature will follow this methodology.

How Write Code Like This

The book describes wiring code to be similar to any other kind of writing. It starts as a rough draft, as we get our thoughts down, and slowly as we edit and refactor, it becomes far closer to what the end product should be. And if we follow the boyscout rule of leaving the code cleaner then when we found it, we can slowly work on improving our code over time. With practice, the 'rough draft' will look less rough each time you begin anew with the experience you can take from previous projects.