🧠 Writing Clean Functions & Methods

Any fool can write code that a computer can understand. Good programmers write code that humans can understand. — Martin Fowler

Functions and methods are the fundamental building blocks of any codebase. Every line of code we write lives within a function. Writing clean, well-designed functions is essential for creating maintainable, readable, and scalable software.

⚙️ Anatomy of a Function

Every function consists of three essential components:

Component
Description
Coverage

Name

How the function is identified

See Naming section

Parameters

The data inputs it accepts

Covered here

Body

The implementation logic

Covered here

This guide focuses on crafting clean parameters and function bodies.

🧩 Minimize the Number of Parameters

Core Principle: The fewer parameters a function has, the easier it is to read, call, and understand.

❌ What Not to Do

Problems:

  • ⚠️ Must memorize parameter order

  • ⚠️ Unclear why values are duplicated ('Max' appears twice)

  • ⚠️ Hard to parse at a glance

  • ⚠️ Prone to errors when calling

📊 Parameter Guidelines

Count
Rating
Description
Example

0

⭐⭐⭐⭐⭐

Ideal clarity

createSession()

1

⭐⭐⭐⭐

Straightforward

isValid(email)

2

⭐⭐⭐

Acceptable if intuitive

login(email, password)

3+

⚠️

Avoid when possible

createRect(10, 9, 30, 12)

Examples by Parameter Count

Zero Parameters — Perfect Clarity

One Parameter — Simple & Flexible

Two Parameters — Context Matters

Clear and intuitive:

⚠️ Ambiguous ordering:

Hard to use:

✅ Solution: Use Structured Objects

Transform multiple parameters into self-documenting objects:

Before:

After:

Benefits:

  • ✅ Self-documenting code

  • ✅ No ordering dependencies

  • ✅ Easy to add optional parameters

  • ✅ Better IDE autocomplete support

🪶 Keep Functions Small

Core Principle: Smaller functions are easier to read, test, maintain, and reuse.

Why Small Functions Matter

  • 📖 Less cognitive load — Easier to understand at a glance

  • 🧪 Better testability — Simpler to write unit tests

  • ♻️ Higher reusability — More focused, single-purpose functions

  • 🐛 Easier debugging — Smaller surface area for bugs

  • 📝 Self-documentation — Good names describe what the function does

Real-World Example

❌ Before: Long & Complex

Problems:

  • Too many responsibilities mixed

  • Hard to test individual steps

  • Difficult to maintain or modify

  • Not reusable

✅ After: Small & Focused

Benefits:

  • ✨ Clear, scannable logic

  • ✨ Each step can be tested independently

  • ✨ Easy to modify validation or session logic

  • ✨ Functions can be reused elsewhere

🎯 Do One Thing

Core Principle: Each function should do one thing, do it well, and do it only.

Understanding "One Thing"

Consider this function:

Question: Is this doing "one thing"?

You might argue it does three things:

  1. Validate user input

  2. Verify credentials

  3. Create a session

Answer: This function IS doing one thing — logging in a user. All three operations are steps required to accomplish that single goal.

🔑 The Key Principle

A function does "one thing" if all operations in the function body are:

  1. On the same level of abstraction

  2. One level below the function name

Good Example

All operations are:

  • ✅ At the same abstraction level (high-level orchestration)

  • ✅ One level below "process order"

  • ✅ Necessary steps for the single goal

Bad Example

Problems:

  • ❌ Mixing abstraction levels (high-level charging with low-level loops)

  • ❌ Implementation details exposed

  • ❌ Hard to test individual pieces


🧱 Levels of Abstraction

Core Principle: Maintain consistent abstraction levels within functions.

Understanding abstraction is crucial for writing clean, maintainable code.

What Are Abstraction Levels?

High-Level Operations — Abstract away implementation details

Low-Level Operations — Close to language/system primitives

Neither is "better" — but mixing them confuses.

❌ Mixed Abstraction Levels

Problem: Jumping between abstraction levels makes code harder to read.

✅ Consistent Abstraction Level

Benefit: All operations at the same level create smooth, readable code.

Real-World Example

❌ Mixed Levels

Issues:

  • Low-level: Manually configuring file system options

  • High-level: Using printer abstraction

  • The reader must switch mental contexts

✅ Consistent Level

Benefits:

  • All operations are at the same high level

  • readFromFile() handles configuration internally

  • Smooth reading experience

📐 Operations One Level Below Function Name

Operations within

A function should be one abstraction level below what its name implies.

Each operation is:

  • A necessary step for login

  • More specific than "login"

  • At the same level as its siblings

⚠️ Questionable Example

Discussion:

  • verifyCredentials() seems more abstract than the if check

  • showError() might be too granular for login()

  • Debatable whether all operations are at the same level

Key Insight: There's always room for interpretation. Use your judgment!

✂️ When to Split Functions

Don't overthink it. Use these practical rules of thumb.

Extract code that works on the same goal or is closely related.

Before

Notice: Lines 3-6 all modify the user object—related functionality.

After

Benefit: Implicitly fixed mixed abstraction levels!

Rule #2: Extract Code Requiring More Interpretation

Extract code that needs more mental processing than the surrounding code.

Before

Issue: The validation check requires interpretation, while processPayment() is immediately clear.

After

Benefit: All operations now read at the same level — no extra interpretation needed.

⚖️ Split Functions Reasonably

Warning: Over-extraction is as harmful as under-extraction.

The Danger of Over-Extraction

, More functions ≠ better code. Extracting everything leads to:

  • 🔀 Excessive jumping between functions

  • 📜 Needless scrolling

  • 🤔 Harder to follow logic

  • 🏷️ Naming difficulties

❌ Over-Extracted Example

Problems:

  • throwError() just renames throw new Error()

  • buildUser() just renames new User()

  • Must scroll through 5 functions for simple logic

✅ Reasonably Extracted Example

Benefits:

  • Clear logical flow

  • Appropriate level of abstraction

  • No pointless extractions

🚨 Signs You're Over-Extracting

Warning Sign
Example
Why It's Bad

Just renaming

function throwError(msg) { throw new Error(msg); }

Adds no value

Excessive scrolling

Must jump through 10 files for 5 lines of logic

Hurts comprehension

Naming difficulties

Can't name without reusing existing names

Function too granular

Finding the Balance

Ask yourself:

  1. Does this extraction make the code easier to understand?

  2. Does it improve testability or reusability?

  3. Can I give it a meaningful, distinct name?

If you answer "no" to these questions, the extraction may not be worth it.

⚠️ Avoid Unexpected Side Effects

Core Principle: Function names should accurately reflect what the function does, including all side effects.

What Is a Side Effect?

A side effect is any operation that changes the state of the application:

Category
Examples

Data Mutation

Modifying variables, updating objects, changing arrays

External Systems

Database operations, API calls, file I/O

User Interface

Console output, DOM manipulation, alerts

System State

Setting cookies, updating cache, creating sessions

Side effects are normal and necessary — applications exist to cause effects!

The problem: Side effects that are unexpected or hidden.

❌ Unexpected Side Effect

Problem: The name validateUserInput implies only validation, but it also creates a session (a major side effect).

Consequences:

  • Confusing for developers calling this function

  • Can't validate input without creating a session

  • Makes testing harder

  • Violates the principle of least surprise

✅ Solution 1: Move the Side Effect

Benefit: Each function does what its name promises.

✅ Solution 2: Rename to Reflect Side Effects

Benefit: Name accurately describes all operations.

More Examples

❌ Hidden Side Effects

✅ Explicit Side Effects

✅ Quick Reference Summary

Principle
Guideline
Example

Parameters

Keep to 0-2; use objects for more

create({ name, email, age })

Function Size

Small, focused functions

Aim for <10-15 lines

Single Responsibility

Do one thing well

login() handles login, not reporting

Abstraction

Keep consistent levels

Don't mix high and low-level code

Splitting

Extract thoughtfully

Don't over-extract or under-extract

Side Effects

Make them explicit

Name should reflect all effects

💡 Key Takeaways

  1. Fewer parameters = clearer code — Use objects for complex inputs

  2. Small functions = maintainable code — Break down complex logic

  3. One thing per function — Single, well-defined purpose

  4. Consistent abstraction = readable code — Stay at one level

  5. Split wisely — Extract for clarity, not ceremony

  6. Explicit side effects = predictable code — Name what you do

🎯 Final Thoughts

Writing clean functions is an art that requires:

  • Practice — You'll improve with every function you write

  • 🧠 Judgment — Context matters; use your experience

  • 👥 Empathy — Write for the humans who will read your code

  • 🔄 Iteration — Refactor as you learn and grow

The ultimate goal: Write code that is easy to read, easy to change, and easy to maintain.

Last updated