DEV Community

Cover image for Understanding Clean Code: Systems ⚡️
Ali Samir
Ali Samir

Posted on

Understanding Clean Code: Systems ⚡️

When building software systems, it's crucial to manage codebase complexity.

Clean Code's Chapter 11 discusses designing modular systems that are easier to maintain and adapt over time.

We can use JavaScript examples to illustrate these concepts.


📌 The Problem with Large Systems

As systems grow, they naturally become more complex. This complexity can make it difficult to:

  • Understand the system as a whole.
  • Make changes without causing unintended side effects.
  • Scale the system with new features or requirements.

A well-designed system should be easy to modify, testable, and scalable. The secret to achieving this lies in modularity and careful separation of concerns.


📌 Modularity: Divide and Conquer

At the heart of clean systems design is the principle of modularity. You make the system more manageable by breaking a large system into smaller, independent modules, each with a clear responsibility.

Each module should encapsulate a specific functionality, making the overall system easier to understand and change.

Example: Organizing a Shopping Cart System

Let’s imagine a shopping cart system in JavaScript. Instead of lumping all logic into a single file, you can break the system into several modules:

// cart.js
export class Cart {
    constructor() {
        this.items = [];
    }

    addItem(item) {
        this.items.push(item);
    }

    getTotal() {
        return this.items.reduce((total, item) => total + item.price, 0);
    }
}

// item.js
export class Item {
    constructor(name, price) {
        this.name = name;
        this.price = price;
    }
}

// order.js
import { Cart } from './cart.js';
import { Item } from './item.js';

const cart = new Cart();
cart.addItem(new Item('Laptop', 1000));
cart.addItem(new Item('Mouse', 25));

console.log(`Total: $${cart.getTotal()}`);
Enter fullscreen mode Exit fullscreen mode

The responsibilities are divided here: Cart manages the items, Item represents a product, and order.js orchestrates the interactions.

This separation ensures that each module is self-contained and easier to test and change independently.


📌 Encapsulation: Hide Implementation Details

One of the goals of modularity is encapsulation—hiding the internal workings of a module from the rest of the system.

External code should only interact with a module through its well-defined interface.

This makes changing the module’s internal implementation easier without affecting other parts of the system.

Example: Encapsulating Cart Logic

Let’s say we want to change how we calculate the total in the Cart. Maybe now we need to account for sales tax. We can encapsulate this logic inside the Cart class:

// cart.js
export class Cart {
    constructor(taxRate) {
        this.items = [];
        this.taxRate = taxRate;
    }

    addItem(item) {
        this.items.push(item);
    }

    getTotal() {
        const total = this.items.reduce((sum, item) => sum + item.price, 0);
        return total + total * this.taxRate;
    }
}

// Now, the rest of the system does not need to know about tax calculations.
Enter fullscreen mode Exit fullscreen mode

Other parts of the system (like order.js) are unaffected by changes in how the total is calculated. This makes your system more flexible and easier to maintain.


📌 Separation of Concerns: Keep Responsibilities Clear

A common problem in large systems is that different parts of the system get entangled.

When a module starts taking on too many responsibilities, it becomes harder to change or reuse in different contexts.

The separation of concerns principle ensures that each module has one specific responsibility.

Example: Handling Payment Separately

In the shopping cart example, payment processing should be handled in a separate module:

// payment.js
export class Payment {
    static process(cart) {
        const total = cart.getTotal();
        console.log(`Processing payment of $${total}`);
        // Payment logic goes here
    }
}

// order.js
import { Cart } from './cart.js';
import { Payment } from './payment.js';

const cart = new Cart(0.07); // 7% tax rate
cart.addItem(new Item('Laptop', 1000));
cart.addItem(new Item('Mouse', 25));

Payment.process(cart);
Enter fullscreen mode Exit fullscreen mode

Now, the payment logic is separated from cart management. This makes it easy to modify the payment process later (e.g., integrating with a different payment provider) without affecting the rest of the system.


📌 Testing Modules Independently

One of the greatest benefits of modularity is that you can test each module independently.

In the example above, you could write unit tests for the Cart class without needing to worry about how payments are processed.

Example: Unit Testing the Cart

// cart.test.js
import { Cart } from './cart.js';
import { Item } from './item.js';

test('calculates total with tax', () => {
    const cart = new Cart(0.05); // 5% tax
    cart.addItem(new Item('Book', 20));

    expect(cart.getTotal()).toBe(21);
});
Enter fullscreen mode Exit fullscreen mode

With a clear separation of concerns, each module can be tested in isolation, making debugging easier and development faster.


📌 Handling Dependencies: Avoid Tight Coupling

When modules depend too heavily on each other, changes in one part of the system can have unexpected consequences elsewhere.

To minimize this, aim for loose coupling between modules.

This allows each module to evolve independently.

Example: Injecting Dependencies

Instead of hardcoding dependencies inside a module, pass them in as arguments:

// cart.js
export class Cart {
    constructor(taxRateCalculator) {
        this.items = [];
        this.taxRateCalculator = taxRateCalculator;
    }

    addItem(item) {
        this.items.push(item);
    }

    getTotal() {
        const total = this.items.reduce((sum, item) => sum + item.price, 0);
        return total + this.taxRateCalculator(total);
    }
}
Enter fullscreen mode Exit fullscreen mode

This approach makes the Cart class more flexible and easier to test with different tax calculations.


Conclusion: Keep Systems Modular, Flexible, and Easy to Change

Happy Coding! 🚀

Top comments (0)