Object Calisthenics

Improve your code with Object Calisthenics

Posted by Martín Peveri on June 17, 2024 · 6 mins read

Object Calisthenics

Improve your code with Object Calisthenics

Object Calisthenics is a set of rules designed to encourage writing cleaner and more maintainable object-oriented code. These rules were originally proposed by Jeff Bay in "The ThoughtWorks Anthology".

These restrictions are intended to instill good practices when writing object-oriented code, aiding in the creation of higher-quality code and helping to identify the characteristics of well-designed code.

  1. Only One Level of Indentation per Method
  2. Don’t Use the else Keyword
  3. Wrap All Primitives and Collections in Classes
  4. First Class Collections
  5. One Dot per Line
  6. Don’t Abbreviate
  7. Keep All Entities Small
  8. No Classes with More Than Two Instance Variables
  9. No Getters/Setters/Properties

1. Only One Level of Indentation per Method

Limiting yourself to one level of indentation per method encourages short and easy-to-understand methods.


class OrderProcessor {
    public processOrders(orders: Order[]): void {
        for (const order of orders) {
            this.processSingleOrder(order);
        }
    }

    private processSingleOrder(order: Order): void {
        if (!this.isOrderValid(order)) {
            this.handleInvalidOrder(order);
            return
        }

        this.processOrder(order);
    }

    private isOrderValid(order: Order): boolean {
        return order !== null && order.isValid();
    }

    private processOrder(order: Order): void {
        this.executePayment(order);
        this.sendConfirmation(order);
        this.recordOrder(order);
    }

    private executePayment(order: Order): void {
        console.log("Payment executed for order with items:", order.items);
    }

    private sendConfirmation(order: Order): void {
        console.log("Confirmation sent for order with items:", order.items);
    }

    private recordOrder(order: Order): void {
        console.log("Order recorded with items:", order.items);
    }

    private handleInvalidOrder(order: Order): void {
        console.log("Invalid order with items:", order.items);
    }
}

In this example, we extracted the code to some private methods.

2. Don't Use the else keyword

Eliminating else can lead to clearer code and a more linear control flow.


class DiscountCalculator {
    calculateDiscount(order: Order): number {
        if (order.isHolidaySeason()) {
            return order.total * 0.1;
        }

        if (order.isMember()) {
            return order.total * 0.05;
        }
        return 0;
    }
}

3. Wrap All Primitives and Collections in Classes

Primitives and collections should be encapsulated in classes to improve semantics and validation.


class Email {
    private value: string;

    constructor(email: string) {
        if (!this.validate(email)) {
            throw new Error("Invalid email");
        }
        this.value = email;
    }

    private validate(email: string): boolean {
        // Validate email format
        return /\S+@\S+\.\S+/.test(email);
    }

    toString(): string {
        return this.value;
    }
}

class EmailList {
    private emails: Email[];

    constructor(emails: Email[]) {
        this.emails = emails;
    }

    add(email: Email): void {
        this.emails.push(email);
    }
}

4. First Class Collections

Each class should have a single responsibility, making it easier to maintain and test.


class Orders {
    private orders: Order[];

    constructor(orders: Order[]) {
        this.orders = orders;
    }

    add(order: Order): void {
        this.orders.push(order);
    }

    getTotalValue(): number {
        return this.orders.reduce((total, order) => total + order.total, 0);
    }
}

5. One Dot per Line

This encourages the Law of Demeter, avoiding deep dependencies.


class OrderSummary {
    constructor(private order: Order) {}

    getTotal(): number {
        return this.order.getTotal();
    }

    getItemCount(): number {
        return this.order.getItemCount();
    }
}

As we can see, we create methods to access the methods of the child class in order to avoid talking to strangers.

6. Don’t Abbreviate

It is tempting to abbreviate class names, methods, or variables. Resist the temptation, as abbreviations confuse and tend to hide more serious problems. For example, it can help you recognize single responsibility problems, etc.


class CustomerAddress {
    constructor(
        public street: string,
        public city: string,
        public state: string,
        public zip: string
    ) {}

    fullAddress(): string {
        return `${this.street}, ${this.city}, ${this.state}, ${this.zip}`;
    }
}

7. Keep All Entities Small

Small classes are easier to understand and maintain.


class Item {
    name: string;
    price: number;

    constructor(name: string, price: number) {
        this.name = name;
        this.price = price;
    }
}

class Order {
    items: Item[];

    constructor() {
        this.items = [];
    }

    addItem(item: Item): void {
        this.items.push(item);
    }

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

    isValid(): boolean {
        return this.items.length > 0;
    }
}

8. No Classes with More Than Two Instance Variables

Limiting the number of instance variables simplifies classes and makes them more cohesive.


class Coordinates {
    constructor(public latitude: number, public longitude: number) {}
}

In this rule the point for me it's to maintain the class small as possible. For example in DDD project using aggregates the point is create aggregate smallers.

9. Don’t use any getters/setters/properties

Avoid using getters, setters, or public properties. Instead, create methods that describe the behavior.


class BankAccount {
    private balance: number;

    constructor(initialBalance: number) {
        this.balance = initialBalance;
    }

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

    withdraw(amount: number): void {
        if (amount > this.balance) {
            throw new Error("Insufficient funds");
        }
        this.balance -= amount;
    }

    // Instead of a getter for balance
    getBalance(): number {
        return this.balance;
    }
}

This rule basically follows the "tell, don't ask" principle. That's why I would like to emphasize that it is not wrong to use a getter alone. The problem is using getters and setters together, since here the "tell, don't ask" principle can be broken.

Conclusion

Object Calisthenics provides a set of rules that can help write cleaner and more maintainable code. By following these practices, we can create more robust and understandable applications.