Published on

A function should do one thing

5 min read - 801 words
Authors
  • avatar
    Name
    Cédric RIBALTA
    Twitter
post image

Introduction

Do you know what the human brain and a perfect function have in common?

They both do just one thing at a time.

Why Should a Function Do Just One Thing?

In software development, simplicity is the ultimate goal.
As developers, we want to write code that is:

  1. Efficient
  2. Readable
  3. Maintainable
  4. And most importantly, easy to understand

This is where one of the most fundamental principles of Clean Code by Robert C. Martin (aka Uncle Bob) comes in: a function should do one thing, and do it well.

A Matter of Clarity and Readability

When we write a function that focuses on a single task, it makes the code much easier to read. Clarity is one of the first casualties when a function starts to "do too many things."

Imagine a function that opens a file, reads it, parses it, and saves a modified copy somewhere else. At first glance, it may not seem so bad. But over time, with additions and changes, this function can become a maintenance nightmare.

In contrast, by breaking these steps into multiple dedicated functions, each focused on a single task, you get code that is far more understandable.

Reusability

A function that does one thing is also much more reusable.

For instance, if you have a function that only opens a file, you can reuse it in different contexts without having to copy/paste code.

It becomes much harder to reuse a function if it does a whole set of things because it will often be too specific to a particular use case.

One Function, One Level of Abstraction

Another key point that Uncle Bob emphasizes is that a function should not mix multiple levels of abstraction.

If you call a function, it should operate at one level:

  • Process raw data
    or
  • Orchestrate high-level operations

But not both at the same time.

Mixing abstraction levels in a single function complicates the reading and understanding of the code because it forces the reader to juggle between different concepts at different levels.

Easier to Test

Functions that focus on a single task are also much easier to test. By isolating each behavior, you can write precise, targeted, and robust unit tests.

The more concise and specific the function is, the more confident you can be that each test is valid and maintainable in the long term.

The Temptation to Do It All

It can be tempting to group several small actions into a single function to "save time" or out of convenience.

However, this ends up being counterproductive. This kind of practice complicates the code and makes it harder to maintain, and can also introduce hidden bugs.

A well-thought-out function that does one thing is much easier to fix, adjust, and improve.

A Concrete Example

// Bad example: a function that does too many things at once
function processFile(filePath: string): void {
  const fs = require('fs')

  // Read the file
  const fileContent = fs.readFileSync(filePath, 'utf-8')

  // Parse the content (let's assume it's JSON)
  const parsedContent = JSON.parse(fileContent)

  // Transform the data (e.g., add a property to each object)
  const transformedContent = parsedContent.map((item: any) => {
    item.newProperty = 'value'
    return item
  })

  // Save the new data
  fs.writeFileSync(filePath, JSON.stringify(transformedContent, null, 2))
}

In this example, the processFile function does several things:

  1. Read the file
  2. Parse the content
  3. Transform the data
  4. Write the file.

This mixture of responsibilities makes it hard to test, maintain, and reuse.

Now let's see how we can improve this by following the principle of "doing one thing":

const fs = require('fs')

// Function that reads a file
function readFile(filePath: string): string {
  return fs.readFileSync(filePath, 'utf-8')
}

// Function that parses the content of a file
function parseFileContent(content: string): any[] {
  return JSON.parse(content)
}

// Function that transforms the data
function transformData(data: any[]): any[] {
  return data.map((item: any) => {
    item.newProperty = 'value'
    return item
  })
}

// Function that writes to a file
function writeFile(filePath: string, content: any): void {
  fs.writeFileSync(filePath, JSON.stringify(content, null, 2))
}

// Main function that orchestrates the operations
function processFile(filePath: string): void {
  const content = readFile(filePath)
  const parsedData = parseFileContent(content)
  const transformedData = transformData(parsedData)
  writeFile(filePath, transformedData)
}

Explanation:

  • readFile: This function does one thing: it reads the file and returns its content as a string.
  • parseFileContent: It performs one task, parsing the JSON content.
  • transformData: It transforms the data and adds a property to each element.
  • writeFile: This function writes the transformed data to the file.
  • processFile: It orchestrates the entire process, but each step is delegated to a function that focuses on a single task.

Conclusion

The idea that a function should do one thing is a fundamental principle of clean code. It encourages writing code that is simple, readable, maintainable, and reusable.

Robert Martin often reminds us that "Clean code is simple and direct. Clean code reads like well-written prose."

By applying this principle, you're not only improving the quality of your code but also the quality of life for the developers who will work on it after you — including your future self!