Writing clear, scalable, and maintainable code is equally as crucial in the software development industry as resolving business issues. However, how can you make sure that your object-oriented programming stays organized and extensible? Object Calisthenics can help with it.
Object Calisthenics is a collection of nine guidelines that enforce better design and maintainability in order to enhance object-oriented programming (OOP) techniques. Similar to how calisthenics exercises increase muscular strength, these guidelines, which were first put forth by Jeff Bay in The ThoughtWorks Anthology, serve as exercises for producing better code.
In this article, we’ll explore:
✓ What Object Calisthenics is and why it matters.
✓ The 9 rules with practical examples in TypeScript/Node.js.
✓ How following these principles leads to better OOP design.
9 Rules of Object Calisthenics
Here’s a breakdown of each rule and a TypeScript example to illustrate how to apply them effectively.
Rule: One Level of Indentation per Method
Avoid deep nesting to improve readability and maintainability.
❌ Bad Example:
function validateUser(user: User): boolean {
if (user) {
if (user.age > 18) {
if (user.hasSubscription) {
return true;
}
}
}
return false;
}
✅ Good Example: (Refactoring with early returns)
function validateUser(user: User): boolean {
if (!user) return false;
if (user.age <= 18) return false;
if (!user.hasSubscription) return false;
return true;
}
By reducing nesting, the function becomes easier to read and understand.
Rule: Don’t Use the ELSE Keyword
Else statements often introduce unnecessary complexity and can be refactored with early returns.
❌ Bad Example:
function getDiscount(price: number, isMember: boolean): number {
if (isMember) {
return price * 0.9;
} else {
return price;
}
}
✅ Good Example:
function getDiscount(price: number, isMember: boolean): number {
if (isMember) return price * 0.9;
return price;
}
This reduces cognitive load and makes the function more maintainable.
Rule: Wrap Primitives in Objects
Use value objects instead of raw primitives to enforce domain rules.
❌ Bad Example:
class Order {
constructor(private price: number) {}
}
✅ Good Example:
class Price {
constructor(private value: number) {
if (value <= 0) throw new Error('Price must be positive');
}
getAmount(): number {
return this.value;
}
}
class Order {
constructor(private price: Price) {}
}
By wrapping primitives, we ensure data integrity at the object level.
Rule: Use First-Class Collections
Instead of passing around raw arrays, encapsulate them in objects.
❌ Bad Example:
class Cart {
constructor(public items: string[]) {}
}
✅ Good Example:
class CartItems {
constructor(private items: string[]) {}
addItem(item: string): void {
this.items.push(item);
}
getItems(): string[] {
return this.items;
}
}
class Cart {
constructor(private items: CartItems) {}
}
Encapsulating collections prevents direct manipulation and improves data encapsulation.
Rule: One Dot Per Line (Law of Demeter)
A class should only talk to direct dependencies, not deeply nested objects.
❌ Bad Example:
const city = user.getAddress().getCity();
✅ Good Example:
class Address {
constructor(private city: string) {}
getCity(): string {
return this.city;
}
}
class User {
constructor(private address: Address) {}
getCity(): string {
return this.address.getCity();
}
}
const city = user.getCity();
This reduces coupling and makes code easier to refactor.
Rule: Don’t Abbreviate
Code should be self-explanatory; avoid unnecessary abbreviations.
❌ Bad Example:
class Prsn {
constructor(public nm: string) {}
}
✅ Good Example:
class Person {
constructor(public name: string) {}
}
Rule: Keep Entities Small
Each class should have a single responsibility.
❌ Bad Example: (Mixing concerns in one class)
class User {
constructor(private name: string, private email: string) {}
sendEmail(message: string): void {
console.log(`Sending email to ${this.email}: ${message}`);
}
}
✅ Good Example: (Separate concerns into different classes)
class EmailService {
sendEmail(email: string, message: string): void {
console.log(`Sending email to ${email}: ${message}`);
}
}
class User {
constructor(private name: string, private email: string) {}
getEmail(): string {
return this.email;
}
}
This ensures that each class has a single responsibility.
Rule: No Classes with More than Two Instance Variables
Limit dependencies to keep classes small and focused.
❌ Bad Example:
class Order {
constructor(
private id: string,
private customer: string,
private items: string[],
private status: string
) {}
}
✅ Good Example: (Use separate classes to manage complexity)
class OrderId {
constructor(private value: string) {}
}
class OrderStatus {
constructor(private status: string) {}
}
class Order {
constructor(
private id: OrderId,
private customer: string,
private items: string[],
private status: OrderStatus
) {}
}
By breaking down complexity, we improve maintainability.
Rule: No Getters/Setters
Encapsulate logic within domain objects instead of exposing raw properties.
❌ Bad Example:
class Account {
constructor(private balance: number) {}
getBalance(): number {
return this.balance;
}
}
✅ Good Example:
class Account {
constructor(private balance: number) {}
withdraw(amount: number): void {
if (amount > this.balance) throw new Error('Insufficient funds');
this.balance -= amount;
}
}
This enforces domain-driven behavior.
Conclusion
Object Calisthenics provides practical exercises to improve code quality, readability, and maintainability in OOP. By following these rules, you can:
→ Write cleaner and more maintainable object-oriented code.
→ Reduce complexity and coupling.
→ Improve code scalability for large applications.
Top comments (0)