Skip to content

Martin Belev

7 tips for writing cleaner functions

Clean code2 min read

Functions

Functions are one of the fundamental building blocks of our programs.

It's our responsibility as developers to keep them concise, easy to understand, and reason about.

Let's explore 7 language-agnostic tips that will help us to do so.

Link to this heading
1. Use descriptive and intent-revealing names for your functions

Don't be afraid to use long names as well. It's better than comments, abbreviations, etc.

Naming is not an easy thing to do and it's OK if it takes more time to think about how to name something!

Intent revealing names

I've written a separate post for naming which you can find useful as well.

Link to this heading
2. Functions should do only one thing and they should do it well

💡 If another function can be extracted with a name that is not merely a restatement of its implementation, it would mean that your function is doing too much.

Functions should do one thing and do it well

Link to this heading
3. Avoid functions that depend on an external (hidden) state which if changed will result in a change of the function's behavior

You don't know when and where this external state can be changed. Therefore, you don't know what to expect from invoking your function.

1// ❌ Avoid
2// cartInformation is external state for the function.
3// External state can be changed from outside and can lead to hard to find bugs.
4
5const cartInformation = getCartInformation();
6
7const calculateTotalPrice = () => {
8 return cartInformation.items.reduce((acc, item) => item.price + acc, 0);
9};

Passing a variable as an argument to the function is a very simple, yet powerful solution in such situations to avoid external state usage.

1// ✅ Prefer
2// Pass it as argument and avoid external state.
3const calculateTotalPrice = (cartInformation) => {
4 return cartInformation.items.reduce((acc, item) => item.price + acc, 0);
5};

Link to this heading
4. Use pure functions whenever you can

Meaning given the same input, the function will always behave in the same way and return the same output.

Avoid mutating input arguments but use the returned value from your function.

1// ❌ Avoid
2const isAvailableForSale = (product) => {
3 product.availableForSale = product.numberOfItemsLeft > 0;
4};
5
6// Usage
7const product = getProduct('iPad Pro');
8isAvailableForSale(product);
9
10if (product.availableForSale) {
11 // do something
12}
13
14// ✅ Prefer
15const isAvailableForSale = (product) => product.numberOfItemsLeft > 0;
16
17// Usage
18const product = getProduct('iPad Pro');
19
20if (isAvailableForSale(product)) {
21 // do something
22}

Some of the benefits of using pure functions:

  • idempotent - produces the same output when called over and over with the same input
  • easier to understand and reason about
  • easier to test
  • easier for function composition

Link to this heading
5. Try to keep your functions with up to 2-3 arguments (ideally even less)

More arguments are making the function harder to understand and to test as well because of all of the different arguments' combinations.

Try to look for logical groupings of arguments and combine them in an object/class and then pass them as a single argument to the function.

This is a way to try and reduce the number of arguments that a function needs instead of passing separately each argument.

Configuration options can be a good example.

We start by adding just one argument to configure something, then another is introduced, etc. but some of them can be skipped.

1// ❌ From
2// true, null, false, 42, 'config' - doesn't make sense until we go and read the function itself.
3someFunc(true, null, false, 42, 'config');
4
5// ✅ To
6// By grouping the arguments in a config object, we will get better naming and readability as well and we will know what "true" means, etc.
7someFunc(config);

It also helps for:

  • consistency - as all functions will use the same new data structure
  • explicit argument relationship

Link to this heading
6. Avoid mixing different levels of abstraction in functions

If we do so, it's easier to spread around even more (like the broken windows theory) and most probably means that we have violated the single responsibility principle.

Link to this heading
7. Avoid having object state changes and retrieving information about the object in the same function

✅ Prefer separate functions - one doing state change, another one answering a question about the object.

Mixing the two can create confusion and ambiguity.

Link to this heading
Conclusion

What we've covered:

  • Use descriptive and intent-revealing names for your functions
  • Functions should do only one thing and they should do it well
  • Avoid functions that depend on an external (hidden) state which if changed will result in a change in the function's behavior
  • Use pure functions whenever you can
  • Try to keep your functions with up to 2-3 arguments (ideally even less)
  • Avoid mixing different levels of abstraction in functions
  • Avoid having object state changes and retrieving information about the object in the same function

As one of the fundamental building blocks in our software, it's essential to have and keep our functions as clean as possible.


Thank you for reading this to the end 🙌 . If you enjoyed it and learned something new, support me by clicking the share button below to reach more people and/or give me a follow on Twitter where we can catch up. I am sharing some other tips, articles, and things I learn there.
If you didn't like the article or you have an idea for improvement, please reach out to me on Twitter and drop me a DM with feedback so I can improve and provide better content in the future 💪.

© 2021 by Martin Belev. All rights reserved.