Lesson 10 of 54

Functions That Do One Thing

The Single Responsibility Principle for Functions

I once spent three hours debugging a function called processOrder. It was 400 lines long. It validated the order, calculated shipping, applied discounts, charged the payment, updated inventory, sent confirmation emails, and logged analytics.

The bug was in line 287. Finding it required understanding all 400 lines.

This is what happens when functions do too much.

The Core Principle

A function should do one thing. It should do it well. It should do it only.

This is Robert C. Martin's formulation, and it's deceptively simple. The challenge is defining "one thing."

Here's a useful definition: a function does one thing if you can describe it with a single sentence that doesn't use "and."

  • "Calculate the total price" ✓
  • "Calculate the total price and apply discounts" ✗
  • "Send a confirmation email" ✓
  • "Send a confirmation email and update the order status" ✗

One Level of Abstraction

Another way to think about it: a function should operate at one level of abstraction.

Here's a function mixing levels:

Javascript
async function createUser(userData) {
  // High level: orchestration
  const validatedData = validateUserData(userData);
  
  // Low level: implementation detail
  if (!validatedData.email.match(/^[^\s@]+@[^\s@]+\.[^\s@]+$/)) {
    throw new Error('Invalid email format');
  }
  
  // High level again
  const user = await saveUser(validatedData);
  await sendWelcomeEmail(user);
  
  // Low level again
  console.log(`User ${user.id} created at ${new Date().toISOString()}`);
  
  return user;
}

The function jumps between orchestrating operations and implementing details. This makes it hard to understand the flow.

Better:

Javascript
async function createUser(userData) {
  const validatedData = validateUserData(userData);
  const user = await saveUser(validatedData);
  await sendWelcomeEmail(user);
  logUserCreation(user);
  return user;
}

Now the function tells a clear story at one level. Details are pushed into sub-functions.

Signs Your Function Does Too Much

1. It's Hard to Name

If you can't name a function clearly, it probably does too much.

  • handleUserStuff — what stuff?
  • processAndValidateAndSave — three things!
  • doEverything — literally too much

2. It Has Many Parameters

Functions doing one thing rarely need many inputs.

Javascript
// This function does too much
function processOrder(order, user, discount, shippingMethod, 
                     paymentInfo, sendEmail, logAnalytics) { }

// Split it
function calculateOrderTotal(order, discount) { }
function chargePayment(order, paymentInfo) { }
function fulfillOrder(order, shippingMethod) { }
function notifyCustomer(user, order) { }

3. It Has Multiple Sections

If you can draw horizontal lines through a function to separate "phases," each phase is a separate function.

Javascript
function handleCheckout(cart, user) {
  // Phase 1: Validation
  if (!cart.items.length) throw new Error('Empty cart');
  if (!user.paymentMethod) throw new Error('No payment');
  
  // ----- could be validateCheckout() -----
  
  // Phase 2: Calculation
  const subtotal = cart.items.reduce((sum, i) => sum + i.price, 0);
  const tax = subtotal * TAX_RATE;
  const total = subtotal + tax;
  
  // ----- could be calculateTotal() -----
  
  // Phase 3: Processing
  chargePayment(user.paymentMethod, total);
  updateInventory(cart.items);
  sendConfirmation(user.email);
  
  // ----- could be processPayment(), updateInventory(), notifyUser() -----
}

4. It's Long

There's no magic number, but:

  • 5-15 lines: Probably right
  • 15-30 lines: Review carefully
  • 30+ lines: Almost certainly too much

Code Complete found that routines under 200 lines had the fewest bugs per line. But shorter is generally better for comprehension.

The "And" Test in Practice

Describe your function. If it contains "and," extract.

DescriptionAssessment
"Parse the JSON input"✓ One thing
"Parse the JSON and validate the email"✗ Extract validation
"Calculate the order total"✓ One thing
"Calculate the order total and apply discounts and add tax"✗ Probably too much
"Send a welcome email"✓ One thing
"Send a welcome email and update the user's last contact date"✗ Extract the update

Refactoring Example

Before:

Javascript
async function handleUserRegistration(input) {
  // Parse
  const data = JSON.parse(input);
  
  // Validate
  if (!data.email?.includes('@')) {
    throw new Error('Invalid email');
  }
  if (!data.password || data.password.length < 8) {
    throw new Error('Password too short');
  }
  
  // Transform
  const user = {
    ...data,
    passwordHash: await hashPassword(data.password),
    createdAt: Date.now(),
    status: 'pending'
  };
  
  // Persist
  await db.users.insert(user);
  
  // Notify
  await sendWelcomeEmail(user.email);
  await logAnalytics('user_registered', { userId: user.id });
  
  return user.id;
}

After:

Javascript
async function handleUserRegistration(input) {
  const data = parseRegistrationInput(input);
  validateRegistrationData(data);
  const user = await createAndSaveUser(data);
  await notifyUserCreated(user);
  return user.id;
}

function parseRegistrationInput(input) {
  return JSON.parse(input);
}

function validateRegistrationData(data) {
  validateEmail(data.email);
  validatePassword(data.password);
}

function validateEmail(email) {
  if (!email?.includes('@')) {
    throw new Error('Invalid email');
  }
}

function validatePassword(password) {
  if (!password || password.length < 8) {
    throw new Error('Password too short');
  }
}

async function createAndSaveUser(data) {
  const user = {
    ...data,
    passwordHash: await hashPassword(data.password),
    createdAt: Date.now(),
    status: 'pending'
  };
  await db.users.insert(user);
  return user;
}

async function notifyUserCreated(user) {
  await sendWelcomeEmail(user.email);
  await logAnalytics('user_registered', { userId: user.id });
}

More functions, but each is:

  • Easy to name
  • Easy to test
  • Easy to modify
  • Easy to understand

The Objection: "More Functions = More Overhead"

I hear this a lot. "Won't all these small functions make the code harder to navigate?"

No. They make it easier.

When you read the top-level function, you see the story without implementation details. You only dive into sub-functions when you need to.

This is like reading a book's table of contents vs. reading every page. The table of contents gives you structure; the pages give you details when you need them.


Key insight: A function should do one thing. Use the "and" test: if your description contains "and," extract. Operate at one level of abstraction. When a function is hard to name, it's doing too much.