Object Calisthenics

9 regole per un codice che spacca

Chi sono?

Sensei / Co-host at Dev Dojo IT

Mentor at Tomorrow Devs

Produttore di bug da ~25 anni 

Christian Nastasi

Che vuol dire Calisthenics?

La parola “calisthenics”, trae origine dai termini greci “Kalos” e “Sthenos” , ovvero “bellezza” e “forza”. 

Che vuol dire Calisthenics?

Nella disciplina sportiva chiamata Calisthenics i praticanti mirano alla massima efficienza attraverso la ripetizione di esercizi complessi e vicini al proprio massimale.

Cos'è l'Object Calisthenics?

Sono degli esercizi di programmazione basati su una serie di regole molto stringenti.

Imparare a scrivere del buon codice non è immediato, è necessario quindi... esercitarsi

Cos'è l'Object Calisthenics

Praticandoli con costanza si migliora di molto le proprie capacità di coding e si acquisisce la disciplina nell'applicare le buone pratiche.

Cos'è l'Object Calisthenics

Come per tutto

repetita iuvant!

Quali skills si allena?

  • Incapsulamento

  • Manutenibilità

  • Leggibilità

  • Testabilità

  • Riusabilità

Ma quali sono queste 9 regole?

Ogni livello di indentazione rappresenta un nuovo contesto mentale che il programmatore deve tenere sempre presente.

Più livelli ci sono, maggiore è il carico cognitivo.

Massimo un livello di indentazione per metodo

Benefici

Metodi più semplici, più facili da comprendere, testare e manutenere.

Il codice diventa auto-documentante poiché ogni operazione complessa viene descritta con un nome significativo.

Massimo un livello di indentazione per metodo

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

Massimo un livello di indentazione per metodo

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

Strategie di refactoring:

  • Utilizzare early returns per evitare indentazioni superflue

  • Applicare il pattern Extract Method per ogni ciclo o condizione nidificata

  • Suddividere metodi complessi in una sequenza di chiamate a metodi più piccoli

Massimo un livello di indentazione per metodo

Le strutture if/else creano percorsi di esecuzione multipli che aumentano la complessità e rendono difficile seguire il flusso del programma.

Non utilizzare gli "ELSE"

Benefici

Promuove un flusso di controllo lineare, riduce il nesting del codice e incoraggia l'uso di guard clauses, polimorfismo e pattern strategy.

Il codice diventa più prevedibile e facile da leggere.

Non utilizzare gli "ELSE"

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

Non utilizzare gli "ELSE"

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

Non utilizzare gli "ELSE"

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

Non utilizzare gli "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);
}

Strategie di refactoring:

  • Utilizzare early returns (guard clauses) all'inizio del metodo
  • Sostituire if/else con polimorfismo quando si tratta di comportamenti differenti
  • Utilizzare il pattern Strategy per incapsulare algoritmi variabili
  • Applicare il pattern State per gestire comportamenti dipendenti dallo stato
  • Utilizzare operatori ternari per assegnazioni semplici
  • Creare lookup tables o oggetti map per sostituire catene di if/else

Non utilizzare gli "ELSE"

Le abbreviazioni creano un linguaggio criptico che richiede conoscenze implicite per essere compreso.

Usa nomi che hanno senso

Benefici

  • Il codice diventa auto-documentante, più facile da leggere e comprendere
  • Riduce la curva di apprendimento per i nuovi membri del team
  • Migliora la ricercabilità nel codice.

Usa nomi che hanno senso

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" };
}

Usa nomi che hanno senso

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" };
}

Usa nomi che hanno senso

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" 
  };
}

Strategie di refactoring:

  • Rinominare variabili, metodi e classi con nomi completi e descrittivi
  • Utilizzare il refactoring Rename Method/Variable/Class degli IDE moderni
  • Creare un glossario condiviso per i termini di dominio specifici
  • Seguire convenzioni di naming coerenti in tutto il codebase
  • Preferire nomi composti quando serve maggiore chiarezza

Usa nomi che hanno senso

Lunghe catene di metodi creano dipendenze nascoste e violano la Law of Demeter ("parla solo con i tuoi amici immediati").

Un solo punto per riga

Benefici

  • Riduce l'accoppiamento tra oggetti
  • Migliora l'incapsulamento
  • Facilita il testing e rende il codice più resiliente ai cambiamenti nella struttura degli oggetti.

Un solo punto per riga

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

Un solo punto per riga

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;
}

Strategie di refactoring:

  • Introdurre variabili intermedie per spezzare le catene di metodi
  • Nascondere la complessità dietro metodi accessori
  • Incapsulare sequenze di operazioni in una funzione a parte
  • Implementare il pattern Tell, Don't Ask per spostare il comportamento nell'oggetto appropriato
  • Riorganizzare le responsabilità tra le classi per ridurre la necessità di catene di navigazione

Un solo punto per riga

Entità grandi (classi, pacchetti) tendono ad accumulare responsabilità e a diventare difficili da gestire.

Mantenere tutte le entità piccole

Benefici

  • Promuove la segregazione delle responsabilità
  • Facilita la comprensione del codice
  • Migliora la testabilità
  • Permette una migliore organizzazione del codice secondo i principi della progettazione modulare.

Mantenere tutte le entità piccole

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 */ }
}

Mantenere tutte le entità piccole

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 */ }
}

Strategie di refactoring:

  • Separare la classe in classi più piccole
  • Utilizzare il principio Single Responsibility per identificare responsabilità multiple
  • Organizzare il codice in pacchetti coesi con chiare responsabilità
  • Separare interfacce da implementazioni
  • Utilizzare la composizione invece dell'ereditarietà per combinare comportamenti
  • Identificare e estrarre comportamenti comuni in servizi separati

Mantenere tutte le entità piccole

Troppe variabili di istanza spesso indicano che una classe sta facendo troppo e violando il principio di responsabilità singola.

Non più di 2 attributi per classe

Benefici

Forza una rigorosa scomposizione del dominio, aumenta la coesione delle classi, riduce l'accoppiamento e aiuta a scoprire nuovi oggetti e astrazioni che potrebbero essere stati trascurati.

Aumenta la testabilità e la robustezza del sistema.

Non più di 2 attributi per classe

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
  }
}

Non più di 2 attributi per classe

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
  }
}

Strategie di refactoring:

  • Identificare gruppi di attributi correlati e estrarli in classi separate
  • Utilizzare la composizione invece dell'ereditarietà per combinare comportamenti
  • Rivedere il modello di dominio per identificare concetti mancanti

Non più di 2 attributi per classe

Esporre lo stato interno degli oggetti attraverso getter e setter viola l'incapsulamento e sposta la responsabilità di manipolare i dati al di fuori dell'oggetto.

Si generano comportamenti inaspettati e non previsti.

Non usare getter/setter/properties

Benefici

Rafforza il principio "tell, don't ask", migliora l'incapsulamento, sposta il comportamento dove risiedono i dati e porta a oggetti con comportamenti più ricchi e coesi.

Non usare getter/setter/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;
}

Non usare getter/setter/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);
  }
}

Strategie di refactoring:

  • Identificare funzionalità che comuni che vengono utilizzate sempre con un determinato oggetto, e spostarne all'interno dell'oggetto
  • Sostituire getter con metodi che esprimono l'intenzione
    (es. canAfford(amount) invece di getBalance())
  • Utilizzare il pattern Command o Strategy per incapsulare operazioni
  • Introdurre Domain Services quando un'operazione coinvolge multiple entità

Non usare getter/setter/properties

L'uso diretto di primitivi è un sintomo di "primitive obsession", dove i concetti di dominio vengono ridotti a tipi semplici senza comportamento.

Wrappa i tipi primitivi

Benefici

  • Aggiunge significato e comportamento ai dati
  • Incapsula la logica di validazione
  • Migliora il type checking e rende il codice più espressivo e orientato al dominio.

Wrappa i tipi primitivi

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";

Wrappa i tipi primitivi

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`;
  }
}

Strategie di refactoring:

  • Creare Value Objects per rappresentare concetti di dominio (Email, Money, PhoneNumber)
  • Implementare validazione all'interno dei costruttori dei Value Objects
  • Incapsulare comportamenti specifici del dominio nei Value Objects

Wrappa i tipi primitivi

Qualsiasi collezione dovrebbe essere incapsulata in una classe che fornisce comportamenti specifici per ciò che la collezione contiene, invece di manipolare direttamente array o liste grezze.

Specializza le collezioni

Benefici

  • Incapsula le operazioni sulla collezione all'interno di una classe rilevante per il dominio
  • Evita che la manipolazione delle collezioni sia dispersa in tutto il codice
  • Rende la logica relativa alle collezioni riutilizzabile e testabile
  • Aggiunge significato di dominio alle operazioni generiche sulle collezioni

Specializza le collezioni

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},
];

Specializza le collezioni

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

Strategie di refactoring:

  • Creare classi dedicate per le collezioni con nomi specifici del dominio
  • Spostare le operazioni che manipolano le collezioni all'interno di queste classi wrapper
  • Aggiungere metodi specifici del dominio che esprimono regole di business
  • Incapsulare logiche di filtraggio, ordinamento e trasformazione

Specializza le collezioni

E ora un po' di pratica!