Clean Code by Robert C. Martin: summary with TypeScript examples

by Dan Edwards, 10 September 2024

Clean Code by Robert C. Martin: summary with TypeScript examples

Clean Code: A Handbook of Agile Software Craftsmanship(opens in a new tab) by Robert C. Martin is probably the closest a software development book can get towards being a timeless classic. Published in 2008, it emphasizes writing readable, maintainable, and elegant code.

The book covers several key areas of software craftsmanship and is not specific to any particular programming language. Here's what I found most useful when reading it, with some additional tips for working with TypeScript.

1. Meaningful Names

Martin stresses the importance of choosing clear, intention-revealing names for variables, functions, and classes. Well-chosen, expressive names can make code self-explanatory, reduce the need for comments, and reduce the number of things you need to remember.

Finding the perfect expressive name for a variable or function can take a long time, so feel free to rename things if you're hit with inspiration later on. Modern IDEs make this easy, and your teammates won't complain if it's a genuine improvement.

I want to add that it's okay if function names are pretty long, if necessary, and that single-letter variable names are usually terrible. e could mean error, event, or evaluate, so it can be confusing for someone (your future self included) reading your code.

For variables

badExample.ts
TypeScript
1let d: number; // elapsed time in days

This example uses a single-letter variable name, which doesn't convey any meaning about its purpose or content.

improvedExample.ts
TypeScript
1let elapsedTimeInDays: number;

This improved version indicates the variable's value, making the code readable and self-documenting.

For functions

badExample.ts
TypeScript
1function getThem(): number[][] {
2    const list1: number[][] = [];
3    for (const x of theList)
4        if (x[0] === 4)
5            list1.push(x);
6    return list1;
7}

This function uses vague names and doesn't clearly communicate its purpose or the nature of the data it's processing.

improvedExample.ts
TypeScript
1interface Cell {
2    isFlagged(): boolean;
3}
4
5function getFlaggedCells(gameBoard: Cell[]): Cell[] {
6    return gameBoard.filter(cell => cell.isFlagged());
7}

The improved version uses descriptive names and leverages TypeScript's type system and array methods to clearly express the function's intent.

2. Functions

Martin advocates for small, focused functions that do one thing well. He provides guidelines on function length, parameter lists, and the Single Responsibility Principle.

badExample.ts
TypeScript
1function payEmployee(e: Employee): void {
2    if (e.isActive()) {
3        const salary = calculateSalary(e);
4        const tax = calculateTax(salary);
5        const net = salary - tax;
6        saveToDB(e, net);
7        generatePayslip(e, net);
8        sendEmail(e, "Your payment has been processed");
9    } else {
10        sendEmail(e, "You are not an active employee");
11    }
12}

This function violates the Single Responsibility Principle by handling multiple concerns: calculation, database operations, document generation, and email notifications.

improvedExample.ts
TypeScript
1function processPayment(employee: Employee): void {
2    if (employee.isActive()) {
3        const netSalary = calculateNetSalary(employee);
4        recordPayment(employee, netSalary);
5        notifyEmployee(employee, netSalary);
6    } else {
7        notifyInactiveEmployee(employee);
8    }
9}
10
11function calculateNetSalary(employee: Employee): number {
12    const grossSalary = calculateGrossSalary(employee);
13    const taxAmount = calculateTax(grossSalary);
14    return grossSalary - taxAmount;
15}
16
17function recordPayment(employee: Employee, netSalary: number): void {
18    saveToDB(employee, netSalary);
19    generatePayslip(employee, netSalary);
20}
21
22function notifyEmployee(employee: Employee, netSalary: number): void {
23    sendEmail(employee.getEmailAddress(), `Your payment of ${netSalary} has been processed`);
24}
25
26function notifyInactive Employee(employee: Employee): void {
27    sendEmail(employee.getEmailAddress(), "You are not an active employee");
28}

This improved version breaks down the colossal function into smaller, more focused functions. Each function has a single responsibility, making the code more modular and easier to maintain.

3. Comments

While not dismissing comments entirely, Martin argues for code that is so clear and expressive that it requires minimal additional explanation. He distinguishes between necessary clarifications and redundant noise.

badExample.ts
TypeScript
1// Check if the user is logged in
2if (user.isLoggedIn()) {
3    // ...
4}

This comment is unnecessary as the code clearly expresses what's being checked.

improvedExample.ts
TypeScript
1// IMPORTANT: Do not change the order of these operations.
2// The API expects the data to be sent in this specific sequence,
3// or it will reject the entire batch.
4function processUserData(users: User[]): void {
5    for (const user of users) {
6        sendBasicInfo(user);
7        updatePreferences(user);
8        recordLoginTime(user);
9        notifyConnectedServices(user);
10    }
11}

This imaginary API is probably not very well coded and should be rewritten. However, if it were an external API that you couldn't change, this comment would be justified.

JSDoc Comments

One TypeScript-specific feature I learned from another book, Effective TypeScript, by Dan Vanderkam(opens in a new tab) is using JSDoc comments. You can put these in your types and interfaces, creating helpful popups when filling your variables with data.

articles.ts
TypeScript
1import { StaticImageData } from 'next/image';
2
3export interface Article {
4title: string;
5description: string;
6writer: string;
7
8/** Lowercase, separated with a comma and space
9 * 	Example; 'react, next.js, front-end'
10 */
11keywords: string;
12
13/** Landscape meta image, PNG exactly 1,200 x 675px */
14featuredImage: StaticImageData;
15
16/**Year-Month-Day: '2024-09-04' */
17date: string;
18}

4. Formatting

This chapter is dated, as modern IDEs like VS Code and powerful plugins like Prettier and ESLint can instantly enforce beautiful formatting. However, it's still an important consideration, as coding is probably 98% reading and 2% writing, and anything you can do to make your code more accessible for other people to understand is undoubtedly a good thing.

5. Error Handling

Martin emphasizes the importance of proper error handling techniques, writing code that gracefully handles exceptions and edge cases.

badExample.ts
TypeScript
1function readFile(filename: string): void {
2    try {
3        // Read file
4    } catch (e) {
5        console.log("Error reading file");
6    }
7}

This example catches all errors and logs a generic message, losing important error details and potentially hiding serious issues.

improvedExample.ts
TypeScript
1import { promises as fs } from 'fs';
2
3async function readFile(filename: string): Promise<string> {
4    try {
5        return await fs.readFile(filename, 'utf8');
6    } catch (error) {
7        if (error instanceof Error) {
8            if ('code' in error && error.code === 'ENOENT') {
9                throw new Error(`File not found: ${filename}`);
10            }
11            throw new Error(`Error reading file ${filename}: ${error.message}`);
12        }
13        throw error;
14    }
15}

This improved version handles specific error types, provides more detailed error messages, and correctly propagates errors up the call stack.

6. Testing

Martin strongly advocates Test-Driven Development (TDD) and dedicates several chapters to writing effective unit tests. For TypeScript projects, I love using Vitest, as you can also write your tests in TypeScript with minimal or no additional configuration.

calculator.test.ts
TypeScript
1import { describe, it, expect, beforeEach } from 'vitest';
2
3import { Calculator } from './Calculator';
4
5describe('Calculator', () => {
6    let calc: Calculator;
7
8    beforeEach(() => {
9        calc = new Calculator();
10    });
11
12    it('should add two numbers correctly', () => {
13        expect(calc.add(2, 2)).toBe(4);
14        expect(calc.add(-1, 1)).toBe(0);
15        expect(calc.add(-1, -1)).toBe(-2);
16    });
17});

This test is clear and concise and covers multiple scenarios for the add function, including positive, negative, and zero-sum cases.

7. Problematic Code Patterns

Martin extensively discusses signs of suboptimal code and how to improve it. These include:

a) Rigidity

When software is difficult to change because every modification affects many other parts of the system.

badExample.ts
TypeScript
1class Report {
2    generateReport(): void {
3        this.getData();
4        this.formatData();
5        this.printReport();
6        this.emailReport();
7        this.saveToDatabase();
8    }
9    // ... other methods
10}

This class is rigid because any change to the report generation process requires modifying this class, potentially affecting all its functionalities.

improvedExample.ts
TypeScript
1interface Data {}
2interface FormattedData {}
3
4interface DataSource {
5    getData(): Data;
6}
7
8interface Formatter {
9    format(data: Data): FormattedData;
10}
11
12interface Printer {
13    print(data: FormattedData): void;
14}
15
16interface EmailService {
17    send(data: FormattedData): void;
18}
19
20interface DatabaseService {
21    save(data: FormattedData): void;
22}
23
24class Report {
25    constructor(
26        private dataSource: DataSource,
27        private formatter: Formatter,
28        private printer: Printer,
29        private emailService: EmailService,
30        private dbService: DatabaseService
31    ) {}
32
33    generateReport(): void {
34        const data = this.dataSource.getData();
35        const formattedData = this.formatter.format(data);
36        this.printer.print(formattedData);
37        this.emailService.send(formattedData);
38        this.dbService.save(formattedData);
39    }
40}

This version is more flexible as each component can be modified or replaced independently, adhering to the Dependency Inversion Principle.

b) Fragility

Changes in one part of the code unexpectedly break other seemingly unrelated parts.

badExample.ts
TypeScript
1class UserService {
2    registerUser(username: string, email: string): void {
3        // Register user
4        this.sendWelcomeEmail(username, email);
5        this.updateUserCount();
6    }
7
8    private sendWelcomeEmail(username: string, email: string): void {
9        // Send email
10    }
11
12    private updateUserCount(): void {
13        // Update count in database
14    }
15}

If the email sending fails, it will prevent the user count from being updated, even though these operations are not logically dependent.

improvedExample.ts
TypeScript
1interface EmailService {
2    sendWelcomeEmail(username: string, email: string): void;
3}
4
5interface UserCountService {
6    incrementUserCount(): void;
7}
8
9class UserService {
10    constructor(
11        private emailService: EmailService,
12        private countService: UserCountService
13    ) {}
14
15    registerUser(username: string, email: string): void {
16        // Register user
17        try {
18            this.emailService.sendWelcomeEmail(username, email);
19        } catch (e) {
20            // Log error, but don't prevent further operations
21            console.error('Failed to send welcome email', e);
22        }
23        this.countService.incrementUserCount();
24    }
25}

This version separates concerns and ensures that a failure in one operation doesn't affect others, making the system more robust.

Conclusion

'Clean Code' remains a pivotal text in software development, offering enduring principles that transcend specific programming languages. Its practical focus and concrete examples provide invaluable guidance for transforming problematic code into clean, efficient solutions. While some may find certain aspects prescriptive, the core concepts of writing clear, maintainable code are more relevant than ever in today's complex software landscape.