keyboard-shortcut
d

One Function, One Job

5 min read

There is a familiar piece of programming advice: a function should do one thing. The trouble is that a function rarely starts out doing several things. It gets there one reasonable change at a time.

Imagine a function that takes several positional arguments:

function createInvoice(customerId, address, currency) {
  // ...
}

Later, somebody already has a Customer object containing all of that information. Passing each value separately feels wasteful, so the function is extended to accept the object in place of its first argument:

function createInvoice(customerIdOrCustomer, address, currency) {
  if (typeof customerIdOrCustomer === "object") {
    const customer = customerIdOrCustomer;
    customerIdOrCustomer = customer.id;
    address = customer.address;
    currency = customer.currency;
  }

  // ...
}

This can look like a tidy, backwards-compatible change. Existing callers keep working, new callers become shorter and no logic is duplicated. But the function now has two jobs and two calling conventions.

In one mode, its arguments mean exactly what their names suggest. In the other, the first argument changes type and the remaining arguments become irrelevant. The function has acquired a fork in its logic and some combinations of arguments no longer make sense. Every reader now has to understand both paths.

Complexity leaks out

The cost becomes clearer when every call that passes an object must be updated. Searching for createInvoice finds both calling conventions, while searching for object literals or variables is hopeless: objects are everywhere in JavaScript, and their names do not reliably reveal their shape.

In one real case, distinguishing the problematic calls required parsing the codebase with an abstract syntax tree. What should have been an editor search became a static-analysis project because two operations shared one name.

Give the new idea a new name

The alternative is wonderfully unremarkable. Add an independent function:

function createInvoice(customerId, address, currency) {
  // ...
}

function createInvoiceForCustomer(customer) {
  return createInvoice(customer.id, customer.address, customer.currency);
}

The wrapper contains a little extra code, but each function now has one clear contract. Either can evolve independently, and every use of the object-based API can be found with a plain text search. If they later diverge, the new function can own its implementation instead of wrapping the old one.

One Reason to Change

This is the practical benefit of the Single Responsibility Principle: code with one responsibility has one reason to change. Its contract is easier to name, test and search for; changes stay local; and callers are not forced to understand unrelated branches. A little duplication is often cheaper than coupling two ideas that only happen to look similar today.

How Far Should You Take It?

A Reddit discussion of this principle highlights a provocative example from Clean Code: because error handling is one job, a function containing try/catch delegates the actual work to another function.

function deletePage(page) {
  try {
    deletePageAndAllReferences(page);
  } catch (error) {
    logError(error);
  }
}

function deletePageAndAllReferences(page) {
  deletePageFromStore(page);
  registry.deleteReference(page.name);
  configKeys.deleteKey(page.name);
}

Applying that rule to every try block may create more indirection than value, but it asks a useful question: are error handling and the operation itself likely to change for different reasons? As one reply put it:

That particular example is pretty ugly, I'll admit. But the philosophy still makes sense to me. What is a system if not a million little functions working in tandem?

A million small functions that each do one thing are preferable to 500,000 functions with two responsibilities. Clear names and narrow contracts win every time.