Home >Web Front-end >JS Tutorial >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.
As systems grow, they naturally become more complex. This complexity can make it difficult to:
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.
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()}`);
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.
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.
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.
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);
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.
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); });
With a clear separation of concerns, each module can be tested in isolation, making debugging easier and development faster.
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); } }
This approach makes the Cart class more flexible and easier to test with different tax calculations.
Happy Coding! ?
The above is the detailed content of Understanding Clean Code: Systems ⚡️. For more information, please follow other related articles on the PHP Chinese website!