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 digetBalance()
) - 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!

Object Calisthenics: 9 Regole per un codice che spacca
By Nastasi Christian
Object Calisthenics: 9 Regole per un codice che spacca
- 155