One morning, as I was warming my breakfast in the office kitchen, a colleague of mine walked in and we started engaging in some small talk. I’m going to call this colleague Freddie. Freddie had been at the company for a few of weeks, so naturally, I asked him how things had been going. What he went on to say has stuck with me since. He started with a sigh and spoke about how he had been having trouble understanding the codebase he had inherited on the particular project he was working on.
Freddie then spent a good deal of time telling me about how had become irritated and weary from staring at a behemoth of a function that made no sense. I asked him if he tried checking with one of his teammates who had been working on the software before him, to which he responded with a slight chuckle and said the following, “(Teammate’s name) had no clue either. He stared at it as intently as I did and simply said that he didn’t write it.”
I told myself two things after my conversation with Freddie. The first one was, “Don’t be that guy!”. That is, don’t be like the person who made Freddie suffer. The second was, “Write better functions”. That’s the only way to not be like that guy. I’m sure there are countless Freddie’s out there who have to inherit, make sense of and refactor badly written functions on software projects. Well crafted software takes concern for the small units (or methods) such as functions at the micro level and not just the overall functionality at the macro level.
I’ve written a lot bad functions in my short coding time, and so I became deliberate about improving in this particular area. Below are some guidelines and approaches that I’ve learned (and am still learning) to apply from experienced professionals, colleagues, and other recommended sources.
Defining things is always a good place to start. Functions are programmed procedures. If you’re looking for something more verbose than that, you’ll have to go to Google. Software systems will comprise of functions to varying degrees. It may be that you’re developing software with an Object Oriented design in which the functions will live inside the classes that make up the system, and those functions act on the state of the classes in which they live. Or maybe your system is a Function oriented design, in which the system is decomposed into a set of interacting functions acting on centralised state. Regardless of the approach, functions exist because we need to decompose our solution concept, and at a very low level of this decomposition, we find these small units that serve a specific purpose.
Functions Should Be Small
Keeping things small makes functions easier to read, understand, test and debug. I’m not going to give you a magic number. Some experts would say not more than 15 lines, others would say not more than 25. It’s probably something you’ll have to decide within your team. The important thing is to remember the reasons for the principle of keeping functions small.
Readability: A function will typically have a signature and a block code which is executed when the function is called or invoked. Having fewer lines of code in the function’s block helps to easily read and get the gist of what the function is supposed to do.
Understandability: Smaller functions help reduce the likelihood of deviating from the main purpose of a function. The more linear the concept or purpose of the function is, the more comprehensible it will be.
Testability: Short methods have fewer variations which means they are easier to test
Here’s an example of a function that is meant to check the validity of a bearer token:
Functions Should Be Clean
It probably doesn’t get more ambiguous than that. However, this isn’t so much about code styles, indentations or variable name lengths. It’s about understandability. Would Freddie be able to look at your function, figure out it’s intent and be able to make modifications with losing a days worth of work?
The boils down to the measure of how maintainable your code is, and maintainable code forms a great deal of the backbone of maintainable software. I understand that are other attributes that would be used to define clean code that are subjective and that’s something you and your team can decide on.
Functions Should Be Simple
Something my Tech Lead would often say to me is, “If it (the function) requires a lot of effort, you stop and rethink your solution”. In our field, effort shouldn’t always be applauded, because more often than not, effort produces something complex.
“In software development, effort doesn’t grow linearly with complexity — it grows exponentially. Therefore, it is easier to manage two sets of four scenarios each than one with six.” — Abraham Marín-Pérez
If we can write functions based on a modularised solution, and reduce the paths of execution that the function has, it will be a lot easier to make sense of what they should be doing. When code isn’t simply written, it’s a lot harder to make sense of and these kinds of misunderstandings often lead to bugs.
Here’s an example of a function that checks if a received argument is an array of strings:
Functions Should Have One Job (No Side Effects)
Robert Martin put it best in Clean Code, “Your function promises to do one thing…”, and therefore it should. Having side effects only makes our code less readable because of the variations in the code block that don’t serve that one specific purpose. Our functions should be based on a deterministic algorithm, given a certain input, it always returns the same output.
Take the following example, the function is meant to receive a particular date and return the week that the date occurs in the form of an array with date objects.
It could be argued that the function generally has a single purpose. However, you may have noticed that there’s a point at which we are generating the week based on two arguments, an object (Moment object in this case) and the days of the week (i.e. Sunday, Monday, Tuesday, etc.). So we can actually create a new function from this one to simply things and make our methods more linear in their purpose.
When we split our function into two, we have the following:
As a result, it is now easier for a random programmer to grasp the intent of our functions, make test cases for them and modify if necessary.
That being said…
These guidelines are not the only ones to be followed, but they certainly lay a good enough foundation in helping us produce high quality code when we write our functions. Furthermore, writing good functions will take practice, deliberate refactoring, and another set of eyes (peer reviews). It might seem like extra work to produce this kind of code, but the returns are well with it. Edsger Dijkstra, a programming godfather, said the following,
“In programming, elegance is not a dispensable luxury but a quality that decides between success and failure.”
Don’t be that guy, write better functions.
Clean Code by Robert Martin