Object Calisthenics

How to train your inner senior

Who am I?

Technical coach & content creator
at Dev Dojo IT

Mentor at Tomorrow Devs

Bug producer for ~25 years 

Christian Nastasi

What does Calisthenics mean?

The word “calisthenics”, originates from the Greek terms “Kalos” and “Sthenos”, meaning “beauty” and “strength”. 

What does Calisthenics mean?

In the sport discipline called Calisthenics, practitioners aim for maximum efficiency through the repetition of complex exercises close to their maximum.

What is Object Calisthenics?

They are programming exercises based on a set of very strict rules.

Learning to write good code is not immediate, so... practice is necessary

What is Object Calisthenics

By practicing them consistently, one can significantly improve its coding skills and develop discipline in applying good practices.

What is Object Calisthenics

Repetita iuvant

What skills are trained?

  • Encapsulation

  • Maintainability

  • Readability

  • Testability

  • Reusability

But what are these 9 rules?

At most one level of indentation per method

I

Each level of indentation represents a new mental context that the programmer must always keep in mind.

The more levels there are, the greater the cognitive load.

At most one level of indentation per method

Benefits

Simpler methods, easier to understand, test, and maintain.

The code becomes self-documenting as each complex operation is described with a meaningful name.

At most one level of indentation per method

 function checkUser(user) {
  if (user.isActive()) {
    if (user.hasPermission('admin')) {
      if (user.getLoginAttempts() < 3) {
        return true;
      }
    }
  }
   
  return false;
}

At most one level of indentation per method

function checkUser(user) {
  if (!user.isActive()) return false;
  if (!user.hasPermission('admin')) return false;
  if (user.getLoginAttempts() >= 3) return false;
  
  return true;
}

Refactoring strategies:

  • Use early returns to avoid unnecessary indentation

  • Apply the Extract Method pattern for every loop or nested condition

  • Break complex methods into a sequence of calls to smaller methods

At most one level of indentation per method

Do not use "ELSE"

II

If/else structures create multiple execution paths that increase complexity and make it difficult to follow the program flow.

Do not use "ELSE"

Benefits

Promotes a linear control flow, reduces code nesting, and encourages the use of guard clauses, polymorphism, and strategy patterns.

The code becomes more predictable and easier to read.

Do not use "ELSE"

function processPayment(payment) {
  if (payment.isValid()) {
    return payment.process();
  } else {
    throw new Error("Pagamento non valido");
  }
}

Do not use "ELSE"

function processPayment(payment) {
  if (!payment.isValid()) {
    throw new Error("Pagamento non valido");
  }
  
  return payment.process();
}

Do not use "ELSE"

function calculateShipping(order) {
  if (order.isInternational()) {
    return order.getWeight() * 10;
  } else if (order.isExpress()) {
    return order.getWeight() * 5;
  } else {
    return order.getWeight() * 2;
  }
}

Do not use "ELSE"

function calculateShipping(order) {
  if (order.isInternational()) return shippingRates.international(order);
  if (order.isExpress()) return shippingRates.express(order);
  return shippingRates.standard(order);
}
const shippingRates = {
  international: order => order.getWeight() * 10,
  express: order => order.getWeight() * 5,
  standard: order => order.getWeight() * 2
};
Strategy
Pattern
// order.type = "international" | "express" | "standard"
function calculateShipping(order) {
  const shippingRateStrategy = shippingRates[order.type] 
                            ?? shippingRates["standard"];
  
  return shippingRateStrategy(order);
}

Refactoring strategies:

  • Use early returns (guard clauses) at the beginning of the method
  • Replace if/else with polymorphism when dealing with different behaviors
  • Use the Strategy pattern to encapsulate variable algorithms
  • Apply the State pattern to manage state-dependent behaviors
  • Use ternary operators for simple assignments
  • Create lookup tables or map objects to replace chains of if/else

Do not use "ELSE"

Use meaningful names

III

Abbreviations create a cryptic language that requires implicit knowledge to be understood.

Use meaningful names

Benefits

  • The code becomes self-documenting, easier to read and understand
  • Reduces the learning curve for new team members
  • Improves searchability in the code.

Use meaningful names

function procPymnt(usr, amt, curr, pType) {
  const tx = curr === "EUR" ? amt * 0.22 : amt * 0.1;
  const tamt = amt + tx;
  
  if (usr.pHist && usr.pHist.lstPymt > 0 && pType === "cc") {
    return { sts: "ok", ttl: tamt, fee: tamt * 0.03 };
  }
  
  return { sts: "err", ttl: tamt, msg: "inv pymt method" };
}

Use meaningful names

function processPayment(user, amount, currency, paymentType) {
  const tax = currency === "EUR" ? amount * 0.22 : amount * 0.1;
  const totalAmount = amount + tax;
  
  if (user.paymentHistory && user.paymentHistory.lastPayment > 0 && paymentType === "creditCard") {
    return { status: "ok", total: totalAmount, fee: totalAmount * 0.03 };
  }
  
  return { status: "error", total: totalAmount, message: "Invalid payment method" };
}

Use meaningful names

function processPayment(user, amount, currency, paymentType) {
  const tax = currency === "EUR" 
            ? amount * 0.22 
            : amount * 0.10;

  const totalAmount = amount + tax;
  
  if (
    user.paymentHistory 
    && user.paymentHistory.lastPayment > 0 
    && paymentType === "creditCard"
  ){
    return { 
      status: "ok", 
      total: totalAmount, 
      fee: totalAmount * 0.03 
    };
  }
  
  return { 
    status: "error", 
    total: totalAmount, 
    message: "Invalid payment method" 
  };
}

Refactoring strategies:

  • Rename variables, methods, and classes with full and descriptive names
  • Use modern IDE's Rename Method/Variable/Class refactoring
  • Create a shared glossary for domain-specific terms
  • Follow consistent naming conventions throughout the codebase
  • Prefer compound names when greater clarity is needed

Use meaningful names

One point per line

IV

Long chains of methods create hidden dependencies and violate the Law of Demeter ("talk only to your immediate friends").

One point per line

Benefits

  • Reduces coupling between objects
  • Improves encapsulation
  • Facilitates testing and makes the code more resilient to changes in object structure.

One point per line

function calculateShippingCost(order) {
  const shippingCost = order.getCustomer().getAddress().getCountry().getShippingRate() * order.getTotalWeight();
  
  if (order.getCustomer().getMembershipLevel().hasDiscount()) {
    return shippingCost * 0.9;
  }
  
  return shippingCost;
}

One point per line

function calculateShippingCost(order) {
  const customer = order.getCustomer();
  const address = customer.getAddress();
  const country = address.getCountry();
  const shippingRate = country.getShippingRate();
  const totalWeight = order.getTotalWeight();
  
  const shippingCost = shippingRate * totalWeight;
  const membership = customer.getMembershipLevel();
  
  return membership.hasDiscount() 
       ? shippingCost * 0.9
       : shippingCost;
}

Refactoring strategies:

  • Introduce intermediate variables to break method chains
  • Hide complexity behind helper methods
  • Encapsulate sequences of operations in a separate function
  • Implement the Tell, Don't Ask pattern to move behavior into the appropriate object
  • Reorganize responsibilities among classes to reduce the need for navigation chains

One point per line

Keep all entities small

V

Large entities (classes, packages) tend to accumulate responsibilities and become difficult to manage.

Keep all entities small

Benefits

  • Promotes responsibility segregation
  • Facilitates understanding of the code
  • Improves testability
  • Allows for better organization of code according to modular design principles.

Keep all entities small

class UserService {
  constructor(database) {
    this.database = database;
  }
  
  createUser(userData) { /* logica per creare un utente */ }
  updateUser(id, data) { /* logica per aggiornare un utente */ }
  removeUser(id) { /* logica per eliminare un utente */ }
  authenticateUser(email, password) { /* logica per autenticare */ }
  resetPassword(email) { /* logica per reset password */ }
  sendWelcomeEmail(user) { /* logica per inviare email */ }
  generateUserReport(user) { /* logica per generare report */ }
  validateUserData(data) { /* logica per validare dati */ }
}

Keep all entities small

class UserRepository {
  constructor(database) {
    this.database = database;
  }
  
  create(userData) { /* logica per creare un utente */ }
  update(id, data) { /* logica per aggiornare un utente */ }
  remove(id) { /* logica per eliminare un utente */ }
  findById(id) { /* logica per trovare un utente */ }
}
class Authenticator {
  constructor(userRepository) {
    this.userRepository = userRepository;
  }
  
  authenticate(email, password) { /* logica di autenticazione */ }
  resetPassword(email) { /* logica per reset password */ }
}

Refactoring strategies:

  • Separate the class into smaller classes
  • Use the Single Responsibility principle to identify multiple responsibilities
  • Organize code into cohesive packages with clear responsibilities
  • Separate interfaces from implementations
  • Use composition instead of inheritance to combine behaviors
  • Identify and extract common behaviors into separate services

Keep all entities small

No more than 2 attributes per class

VI

Too many instance variables often indicate that a class is doing too much and violating the single responsibility principle.

No more than 2 attributes per class

Benefits

Enforces a rigorous breakdown of the domain, increases class cohesion, reduces coupling, and helps discover new objects and abstractions that may have been overlooked.

Increases testability and robustness of the system.

No more than 2 attributes per class

class OrderService {
  constructor(
    userRepository,
    productRepository,
    inventoryService,
    paymentGateway,
    orderRepository,
    emailService,
    logger,
    taxCalculator,
    shippingService,
    discountCalculator
  ) {
    this.userRepository = userRepository;
    this.productRepository = productRepository;
    this.inventoryService = inventoryService;
    this.paymentGateway = paymentGateway;
    this.orderRepository = orderRepository;
    this.emailService = emailService;
    this.logger = logger;
    this.taxCalculator = taxCalculator;
    this.shippingService = shippingService;
    this.discountCalculator = discountCalculator;
  }
  
  createOrder(userId, items) {
    // Implementazione con molte dipendenze
  }
}

No more than 2 attributes per class

class OrderService {
  constructor(orderFactory, orderRepository) {
    this.orderFactory = orderFactory;
    this.orderRepository = orderRepository;
  }
  
  createOrder(orderRequest) {
    const order = this.orderFactory.createFrom(orderRequest);
    return this.orderRepository.save(order);
  }
}
class OrderFactory {
  constructor(productRepository, inventoryService) {
    this.productRepository = productRepository;
    this.inventoryService = inventoryService;
  }
  
  createFrom(orderRequest) {
    // Crea l'ordine usando le dipendenze
  }
}
class OrderNotifier {
  constructor(emailService, logger) {
    this.emailService = emailService;
    this.logger = logger;
  }
  
  notifyOrderCreated(order) {
    // Invia notifiche
  }
}

Refactoring strategies:

  • Identify groups of related attributes and extract them into separate classes
  • Use composition instead of inheritance to combine behaviors
  • Review the domain model to identify missing concepts

No more than 2 attributes per class

Do not use getters/setters/properties

VII

Exposing the internal state of objects through getters and setters violates encapsulation and shifts the responsibility of manipulating data outside the object.

This generates unexpected and unforeseen behaviors.

Do not use getters/setters/properties

Benefits

Reinforces the principle "tell, don't ask", improves encapsulation, shifts behavior where the data resides, and leads to objects with richer and more cohesive behaviors.

Do not use getters/setters/properties

class BankAccount {
  constructor() {
    this._balance = 0;
  }
  
  get balance() {
    return this._balance;
  }
  
  set balance(amount) {
    if (amount < 0) throw new Error('Balance cannot be negative');
    
    this._balance = amount;
  }
}
function deposit(bankAccount, amount) {
	bankAccount.balance = bankAccount.balance + amount;
}
function withdraw(bankAccount, amount) {
    if (amount > bankAccount.balance)
      throw new Error('Insufficient funds');
    
    bankAccount.balance = bankAccount.balance - amount;
}

Do not use getters/setters/properties

class BankAccount {
  constructor(initialBalance = 0) {
    this.balance = initialBalance;
  }
  
  deposit(amount) {
    if (amount <= 0) throw new Error('Deposit amount must be positive');
    this.balance += amount;
    return this.balance;
  }
  
  withdraw(amount) {
    if (amount <= 0) throw new Error('Withdrawal amount must be positive');
    if (amount > this.balance) throw new Error('Insufficient funds');
    
    this.balance -= amount;
    return this.balance;
  }
  
  transferTo(destinationAccount, amount) {
    this.withdraw(amount);
    destinationAccount.deposit(amount);
  }
}

Refactoring strategies:

  • Identify common functionalities that are always used with a certain object, and move them inside the object
  • Replace getters with methods that express intention
    (e.g. canAfford(amount) instead of getBalance())
  • Use the Command or Strategy pattern to encapsulate operations
  • Introduce Domain Services when an operation involves multiple entities

Do not use getters/setters/properties

Wrap primitive types

VIII

The direct use of primitives is a symptom of "primitive obsession", where domain concepts are reduced to simple types without behavior.

Wrap primitive types

Benefits

  • Adds meaning and behavior to data
  • Encapsulates validation logic
  • Improves type checking and makes the code more expressive and domain-oriented.

Wrap primitive types

const age = 30;
const email = "christian.nastasi@email.it"
const age = 440;

const age2 = "Ventiquattro";

const email = "christian.nastasi@email.it@qualcosa.it"

const email2 = "pippo";

Wrap primitive types

class Age {
  constructor(value) {
    if (!Number.isInteger(value)) throw new Error('Age must be an integer');
    if (value < 0) throw new Error('Age cannot be negative');
    if (value > 120) throw new Error('Age cannot be greater than 120');
    
    this.value = value;
    
    Object.freeze(this);
  }
    
  equals(other) {
    return other instanceof Age 
        && this.value === other.value;
  }
  
  isAdult() {
    return this.value >= 18;
  }
  
  toString() {
    return `${this.value} years old`;
  }
}

Refactoring strategies:

  • Create Value Objects to represent domain concepts (Email, Money, PhoneNumber)
  • Implement validation within the constructors of Value Objects
  • Encapsulate domain-specific behaviors in Value Objects

Wrap primitive types

Specialize collections

IX

Any collection should be encapsulated in a class that provides specific behaviors for what the collection contains, instead of directly manipulating raw arrays or lists.

Specialize collections

Benefits

  • Encapsulates collection operations within a class relevant to the domain
  • Prevents collection manipulation from being scattered throughout the code
  • Makes collection-related logic reusable and testable
  • Adds domain meaning to generic operations on collections

Specialize collections

function calculateTotalPrice(products) {
  let total = 0;
  
  for (const product of products) {
    total += product.price
  }
  
  return total;
}
const products = [
  {name: 'Apple', price: 0.50},
  {name: 'Orange', price: 0.70},
  {name: 'Banana', price: 0.90},
];

Specialize collections

class ShoppingCart {
  constructor(products = []) {
    this.products = products;
  }
  
  addProduct(product) { 
    this.products.push(product); 
  }
  
  calculateTotal() { 
    return this.products.reduce((t, p) => t + p.price, 0); 
  }
  
  /* ... */
}

Refactoring strategies:

  • Create dedicated classes for collections with domain-specific names
  • Move operations that manipulate collections into these wrapper classes
  • Add domain-specific methods that express business rules
  • Encapsulate filtering, sorting, and transformation logic

Specialize collections

And now a bit of practice!