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.
else
KeywordLimiting 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.
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;
}
}
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);
}
}
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);
}
}
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.
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}`;
}
}
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;
}
}
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.
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.