Skip to content
SP StackPractices
intermediate By Mathias Paulenko

Clean Code: Naming, Functions, Classes, Comments

Master clean code principles: meaningful naming, small functions, single responsibility, comments, formatting, error handling, and production code quality patterns.

Topics: testing

Note: This guide follows English-language naming conventions and terminology standards common in international development teams. Examples use English identifiers and comments to maximize compatibility across codebases and tooling.

Introduction

Clean code is code that is easy to read, understand, and modify. It is written for humans, not compilers. The principles come from Robert C. Martin’s “Clean Code” and decades of software engineering practice. The following guide covers naming, functions, classes, comments, formatting, error handling, and production patterns.

Meaningful Names

Use intention-revealing names

// BAD: what does d represent?
const d = 5; // elapsed time in days
const total = d * 24 * 60 * 60;

// GOOD: name reveals intent
const elapsedTimeInDays = 5;
const totalSeconds = elapsedTimeInDays * 24 * 60 * 60;

Avoid disinformation

// BAD: accountList is not a List
const accountList = new Set<Account>();

// GOOD: name reflects the data structure
const accountSet = new Set<Account>();
// or just
const accounts = new Set<Account>();

Use searchable names

// BAD: 5 is a magic number, hard to search for
if (employee.type === 5) {
  giveBonus(employee);
}

// GOOD: named constant is searchable
const SENIOR_ENGINEER = 5;
if (employee.type === SENIOR_ENGINEER) {
  giveBonus(employee);
}

Class names should be nouns

// GOOD: nouns
class Customer { }
class Account { }
class OrderProcessor { }
class PaymentValidator { }

// BAD: verbs
class ProcessOrder { }    // should be a method
class ValidatePayment { } // should be a method

Method names should be verbs

// GOOD: verbs
function processOrder(order: Order): void { }
function validatePayment(payment: Payment): boolean { }
function calculateTotal(items: Item[]): number { }

// BAD: nouns for actions
function orderProcessing(order: Order): void { }
function paymentValidation(payment: Payment): boolean { }

Functions

Small and focused

// BAD: function does too many things
function processOrder(order: Order): void {
  // Validate
  if (!order.items || order.items.length === 0) {
    throw new Error('Order has no items');
  }
  if (order.total <= 0) {
    throw new Error('Order total must be positive');
  }

  // Calculate tax
  let tax = 0;
  for (const item of order.items) {
    if (item.category === 'food') {
      tax += item.price * 0.05;
    } else if (item.category === 'electronics') {
      tax += item.price * 0.18;
    } else {
      tax += item.price * 0.10;
    }
  }

  // Apply discount
  if (order.customer.isVip) {
    order.total *= 0.90;
  }

  // Save to database
  db.query('INSERT INTO orders ...', order);

  // Send email
  emailService.send(order.customer.email, 'Order confirmed', order);
}

// GOOD: each function does one thing
function processOrder(order: Order): void {
  validateOrder(order);
  const tax = calculateTax(order.items);
  applyDiscount(order);
  saveOrder(order);
  sendConfirmationEmail(order);
}

function validateOrder(order: Order): void {
  if (!order.items || order.items.length === 0) {
    throw new Error('Order has no items');
  }
  if (order.total <= 0) {
    throw new Error('Order total must be positive');
  }
}

function calculateTax(items: Item[]): number {
  const taxRates: Record<string, number> = {
    food: 0.05,
    electronics: 0.18,
    default: 0.10,
  };
  return items.reduce((total, item) => {
    const rate = taxRates[item.category] ?? taxRates.default;
    return total + item.price * rate;
  }, 0);
}

function applyDiscount(order: Order): void {
  if (order.customer.isVip) {
    order.total *= 0.90;
  }
}

No more than 2-3 parameters

// BAD: too many parameters, hard to understand
function createOrder(customerId: string, items: Item[], shippingAddress: Address, billingAddress: Address, paymentMethod: string, couponCode: string | null): Order { }

// GOOD: parameter object
interface CreateOrderRequest {
  customerId: string;
  items: Item[];
  shippingAddress: Address;
  billingAddress: Address;
  paymentMethod: string;
  couponCode?: string;
}

function createOrder(request: CreateOrderRequest): Order { }

No side effects

// BAD: function checks password AND initializes session (side effect)
function checkPassword(user: User, password: string): boolean {
  if (user.password === hash(password)) {
    session.initialize(user); // side effect!
    return true;
  }
  return false;
}

// GOOD: separate concerns
function validatePassword(user: User, password: string): boolean {
  return user.password === hash(password);
}

function login(user: User, password: string): Session {
  if (!validatePassword(user, password)) {
    throw new Error('Invalid credentials');
  }
  return session.initialize(user);
}

Command-query separation

// BAD: function both sets and returns a value
function setAttribute(name: string, value: string): boolean {
  if (isValid(name, value)) {
    attributes[name] = value;
    return true;
  }
  return false;
}

// GOOD: separate command from query
function setAttribute(name: string, value: string): void {
  if (!isValid(name, value)) {
    throw new Error(`Invalid attribute: ${name}=${value}`);
  }
  attributes[name] = value;
}

function isValidAttribute(name: string, value: string): boolean {
  return isValid(name, value);
}

Classes

Single Responsibility Principle

// BAD: class does too many things
class OrderManager {
  processOrder(order: Order) { /* ... */ }
  calculateTax(order: Order) { /* ... */ }
  sendEmail(order: Order) { /* ... */ }
  generateInvoice(order: Order) { /* ... */ }
  saveToDatabase(order: Order) { /* ... */ }
}

// GOOD: each class has one responsibility
class OrderProcessor {
  constructor(
    private readonly taxCalculator: TaxCalculator,
    private readonly orderRepository: OrderRepository,
    private readonly emailService: EmailService,
    private readonly invoiceGenerator: InvoiceGenerator,
  ) {}

  process(order: Order): void {
    const tax = this.taxCalculator.calculate(order);
    order.tax = tax;
    this.orderRepository.save(order);
    this.emailService.sendConfirmation(order);
    this.invoiceGenerator.generate(order);
  }
}

class TaxCalculator {
  calculate(order: Order): number { /* ... */ }
}

class OrderRepository {
  save(order: Order): void { /* ... */ }
}

class EmailService {
  sendConfirmation(order: Order): void { /* ... */ }
}

class InvoiceGenerator {
  generate(order: Order): Invoice { /* ... */ }
}

Encapsulation

// BAD: public fields expose internal state
class BankAccount {
  public balance: number = 0;

  deposit(amount: number) {
    this.balance += amount;
  }
}

// Anyone can modify balance directly
account.balance = 1000000; // bypassing deposit logic

// GOOD: private fields with controlled access
class BankAccount {
  private _balance: number = 0;

  get balance(): number {
    return this._balance;
  }

  deposit(amount: number): void {
    if (amount <= 0) {
      throw new Error('Deposit amount must be positive');
    }
    this._balance += amount;
  }

  withdraw(amount: number): void {
    if (amount <= 0) {
      throw new Error('Withdrawal amount must be positive');
    }
    if (amount > this._balance) {
      throw new Error('Insufficient funds');
    }
    this._balance -= amount;
  }
}

Comments

Comments do not compensate for bad code

// BAD: comment explains unclear code
// Check if employee is eligible for bonus
if (employee.flags & 0x80 && employee.yearsOfService > 5) {
  giveBonus(employee);
}

// GOOD: code is self-documenting
if (employee.isEligibleForBonus()) {
  giveBonus(employee);
}

Good comments: explain intent

// GOOD: explains why, not what
// Using binary search because the list is sorted and large (>10k items)
// Linear scan was 50ms slower in benchmarks
function findItem(sortedItems: Item[], target: string): Item | null {
  let low = 0;
  let high = sortedItems.length - 1;

  while (low <= high) {
    const mid = Math.floor((low + high) / 2);
    const comparison = sortedItems[mid].id.localeCompare(target);

    if (comparison === 0) return sortedItems[mid];
    if (comparison < 0) low = mid + 1;
    else high = mid - 1;
  }

  return null;
}

Good comments: warnings

// GOOD: warns about consequences
// This function is called on every keystroke — keep it under 5ms
function autoComplete(input: string): string[] {
  // ...
}

Bad comments: obvious comments

// BAD: states the obvious
// Increment i by 1
i++;

// BAD: commented-out code
// const oldCalculation = calculateOldWay(order);
// if (oldCalculation > 0) { ... }

Error Handling

Use exceptions, not error codes

// BAD: error codes force caller to check
function deletePage(page: Page): number {
  if (page.isLocked) {
    return E_LOCKED;
  }
  if (!page.exists) {
    return E_NOT_FOUND;
  }
  page.delete();
  return E_OK;
}

const result = deletePage(page);
if (result === E_OK) {
  // success
} else if (result === E_LOCKED) {
  // handle locked
} else if (result === E_NOT_FOUND) {
  // handle not found
}

// GOOD: exceptions separate happy path from error handling
function deletePage(page: Page): void {
  if (page.isLocked) {
    throw new PageLockedException(page.id);
  }
  if (!page.exists) {
    throw new PageNotFoundException(page.id);
  }
  page.delete();
}

try {
  deletePage(page);
} catch (error) {
  if (error instanceof PageLockedException) {
    handleLockedPage(error);
  } else if (error instanceof PageNotFoundException) {
    handleMissingPage(error);
  }
}

Don’t return null

// BAD: caller must check for null
function getCustomer(id: string): Customer | null {
  return db.query('SELECT * FROM customers WHERE id = ?', id);
}

const customer = getCustomer(id);
if (customer !== null) {
  // 20 lines of code using customer
} else {
  // handle missing
}

// GOOD: return empty object or throw
function getCustomer(id: string): Customer {
  const customer = db.query('SELECT * FROM customers WHERE id = ?', id);
  if (!customer) {
    throw new CustomerNotFoundException(id);
  }
  return customer;
}

// or use Option/Maybe pattern
function findCustomer(id: string): Option<Customer> {
  const customer = db.query('SELECT * FROM customers WHERE id = ?', id);
  return customer ? Option.some(customer) : Option.none();
}

Don’t pass null

// BAD: method must handle null parameter
function calculateDiscount(customer: Customer | null): number {
  if (customer === null) {
    return 0;
  }
  if (customer.isVip) {
    return 0.10;
  }
  return 0;
}

// GOOD: use null object or overload
function calculateDiscount(customer: Customer): number {
  if (customer.isVip) {
    return 0.10;
  }
  return 0;
}

// Null object pattern
const NO_CUSTOMER: Customer = {
  id: 'none',
  isVip: false,
  name: 'Guest',
};

Formatting

Vertical formatting: read top to bottom

// GOOD: high-level concepts at top, details at bottom
class OrderService {
  // Public interface first
  process(order: Order): void {
    this.validate(order);
    this.calculate(order);
    this.save(order);
  }

  // Private helpers below
  private validate(order: Order): void {
    this.validateItems(order);
    this.validateTotal(order);
  }

  private calculate(order: Order): void {
    order.tax = this.calculateTax(order);
    order.discount = this.calculateDiscount(order);
  }

  private save(order: Order): void {
    this.repository.save(order);
  }

  // Lowest-level details at the bottom
  private validateItems(order: Order): void { /* ... */ }
  private validateTotal(order: Order): void { /* ... */ }
  private calculateTax(order: Order): number { /* ... */ }
  private calculateDiscount(order: Order): number { /* ... */ }
}

Horizontal formatting: keep lines short

// BAD: line too long
const result = await orderService.processOrderWithPaymentAndShippingAndTaxCalculation(order, paymentMethod, shippingAddress, billingAddress);

// GOOD: break into readable lines
const result = await orderService.processOrder({
  order,
  paymentMethod,
  shippingAddress,
  billingAddress,
});

Best Practices

  • For a deeper guide, see Refactoring Guide: Extract Method, Replace Conditional,.

  • Name things to reveal intent — a good name replaces a comment

  • Keep functions small — 4-20 lines, do one thing

  • Limit parameters to 2-3 — use parameter objects for more

  • Follow command-query separation — a function either does something or returns something, not both

  • Use exceptions for error handling — don’t return error codes

  • Don’t return null — use the Option pattern or throw

  • Don’t pass null — use null objects or default values

  • Write comments that explain why, not what — the code shows what

  • Keep one level of abstraction per function — don’t mix high-level logic with low-level details

  • Organize code top to bottom — high-level concepts first, details below

  • Use consistent formatting — follow the project’s style guide

  • Refactor before adding features — clean code is easier to extend

Common Mistakes

  • Functions that do too many things: hard to test, hard to reuse, hard to understand. Extract responsibilities into separate functions.
  • Meaningless names: data, info, temp, manager, helper don’t tell the reader anything. Use specific, descriptive names.
  • Commented-out code: dead code that nobody remembers why it’s there. Delete it — version control has the history.
  • Returning null: forces every caller to check. Use exceptions or the Option pattern.
  • Deep nesting: more than 3 levels of indentation is hard to follow. Extract nested logic into functions or use early returns.
  • Large classes: classes with 500+ lines violate SRP. Split into smaller, focused classes.

FAQ

What is clean code?

Code that is easy to read, understand, and modify. It follows principles like meaningful naming, small focused functions, single responsibility, and clear error handling. Clean code is written for the next developer who reads it.

How small should functions be?

Small enough that they do one thing. Typically 4-20 lines. If a function has multiple levels of abstraction, extract the lower levels into helper functions. The test: can you describe what the function does in one sentence without using “and”?

Should I write comments?

Write comments that explain why something is done, not what the code does. The code should be self-documenting through good naming. Comments that state the obvious (i++ // increment i) are noise. Comments that warn about consequences or explain non-obvious decisions are valuable.

What is the Single Responsibility Principle?

A class or function should have one reason to change. If a class handles order processing, tax calculation, and email sending, it has three reasons to change. Split it into three classes, each with one responsibility.

How do I handle errors without returning null?

Throw exceptions for exceptional conditions. Use the Option/Maybe pattern for operations that might not return a value. Use the Result/Either type for operations that might fail. Never return null from a function — it forces every caller to check.