Oggetti e messaggi
Sono sicuro che la maggior parte di voi sarà d’accordo con me: nel mondo software abbiamo un problema con la Programmazione ad Oggetti. Il paradigma OOP ha origine già dagli anni ‘50 ed è stato formalizzato nella fine dei ‘60, eppure dopo tutti questi anni c’e ancora una curva di apprendimento per molti complessa ed una forte discussione su alcune tematiche fondamentali.
Secondo me, una cosa è innegabile: qualcosa è andato storto nel modo in cui nel mondo software si è diffuso questo paradigma.
Molti dev vedono la OOP in modo errato, ma non mi sento di dare totalmente loro la colpa: per qualche motivo quasi impossibile da ricostruire, nella storia del Software, le nozioni di Programmazione ad Oggetti si sono diffuse in modo poco efficace e molto impreciso.
Partiamo dalla definizione originale di Programmazione ad Oggetti:
un sistema fatto di oggetti che comunicano tra loro tramite messaggi
Prendiamo poi in considerazione anche le euristiche su cui si basa la Programmazione ad Oggetti, ancora più sottovalutate della definizione stessa:
- Tell, don’t ask
- 🔴 A chiede a B i suoi dati interni per poi fare qualche calcolo con quei dati
- ✅ A chiede a B di eseguire quel calcolo e fargli sapere il risultato
- Law of Demetra (aka: non parlare con gli sconosciuti)
- 🔴 A chiede a C di fare qualcosa tramite B (
$classA→classB→classC→doSomething()
) - ✅ A chiede a B di fare qualcosa, senza avere idea di come B sia in grado di farla perchè non gli interessa (
$classA→classB→doSomething()
)
- 🔴 A chiede a C di fare qualcosa tramite B (
Nonostante la relativa semplicità di questi concetti, ci sono tanti fraintendimenti che si sono diffusi nelal community – vediamone alcuni esempi:
- OOP = Classi. Le classi sono sicuramente un concetto fondamentale di Programmazione ad Oggetti, ma sono solo un pezzo di un puzzle molto più ampio.
- Trattare gli oggetti come semplici strutture dati: escludendo casi particolari, l’idea dell’OOP presuppone che gli oggetti abbiano uno stato (dati interni) e dei comportamenti (funzioni pubbliche), non uno solo dei due.
- Abuso di ereditarietà: Spesso l’ereditarietà viene usata con uno scopo differente da quello per cui nasce: ereditarietà significa che tra due classi c’e una relazione di tipo “essere” (un Cane “è un” Animale) mentre troppo spesso viene usata anche in situazioni di tipo “avere” (un Cane “ha” la Coda), per cui esiste invece la composizione. Come ci insegna il vecchio adagio, dovremmo sempre “preferire la composizione rispetto all’ereditarietà” – perchè le relazioni di tipo “avere” sono molto più frequenti.
- Usare getters, setters e dare accesso in modifica a proprietà pubbliche: gli oggetti devono comunicare tramite messaggi, ovvero le funzioni pubbliche, ed esporre comportamenti, non dati; i getters/setters sono un finto incapsulamento che non risolve il vero problema di esporre dati.
Ci sono ovviamente altri fraintendimenti, ma con questi esempi spero di aver reso chiaro che la maggior parte di queste situazioni sembra derivare dal dimenticarsi che gli oggetti non devnon condividere dati ma comportamenti.
Come riconoscere un codice problematico
Nel Software è molto difficile essere d’accordo su quale sia il design migliore, perchè non esiste il design perfetto – è molto più semplice, invece, essere d’accordo su cosa significhi codice “scritto male”.
In particolare, tra i principi di Clean Code esiste il concetto di Code Smells che ci aiuta a dare delle definizioni ai tipici pattern problematici che possiamo incontrare nel codice.
I Code Smells sono uno dei concetti più importanti in Programmazione ad Oggetti: una volta che hai imparato a riconoscerli, il tuo feedback loop interno mentre scrivi codice migliorerà drasticamente. Come ho scritto prima, non esiste il design perfetto: c’e sempre un compromesso da qualche parte, e conoscere i Code Smell ci permette di accettare consciamente un compromesso invece di subirlo senza renderci conto della sua presenza.
Trovo rilevante anche riflettere sul fatto che non vengano chiamati errori, ma smell
– odori, puzze. Il motivo per questo nome è che questi pattern non sono di per sè un problema, sono i sintomi di un problema – vengono infatti divisi in due categorie riferite al tipo di problema che stanno evidenziando: smell di design e smell di codice.
Design smells
Gli smells di design sono quelli più ad alto livello e sono sintomi di un problema grave – danno però poche informazioni riguardo alla causa. Se riconosciamo qualcuno di questi smell, dobbiamo mettere in discussione l’architettura ed il design del nostro sistema.
I design smell sono
- rigidità (il sistema è difficile da cambiare)
- fragilità (i cambiamenti causano errori inaspettati)
- immobilità (è difficile creare componenti riutilizzabili)
- viscosità (fare le cose bene è piu difficile di fare le cose male)
- complessità inutile (componenti o pattern implementativi che non dannobenefici al sistema o non sono usati)
- ripetizioni inutili (duplicazione di comportamenti)
- opacità (difficile da leggere e capire, non espressivo)
Code smells
I code smells sono sintomi molto più a basso livello, più facili e meno costosi da correggere, perchè danno feedback sul design locale del codice.
Possiamo riconoscere 5 categorie di code smells:
- Bloaters – sintomi legati a componenti troppo grandi, alcuni esempi: Metodi o Classi troppo grandi, utilizzo dei tipi primitivi nei metodi pubblici
- Couplers – sintomi legati ad un accoppiamento troppo alto, alcuni esempi: chiamate tra classi concatenate, o classi che dipendono da dettagli implementativi di un altra
- Object-Orientation Abusers – sintomi legati ad un utilizzo errato dei concetti di OOP, alcuni esempi: property temporanee aggiunte runtime ad un oggetto, o una classe che eredita da un altra pur avendo bisogno solo di una piccola porzione delle sue parti
- Change preventers – sintomi legati a problematiche che rendono le modifiche difficili, alcuni esempi: una classe che cambia troppo spesso per motivi diversi, oppure al contrario un cambiamento che causa tante piccole modifiche in tante classi in giro per il sistema
- Dispensables – sintomi di problemi accessori, alcuni esempi: commenti che spiegano il codice, duplicazione, o generalizzazione prematura
Come evitare di fare questi errori
Ricapitolando, vogliamo imparare a riconoscere i Code Smell per poterli evitare, rimuovere o in alcuni casi accettare come trade-off consapevole.
Ma come facciamo ad evitare di incappare in questi Code Smell?
C’è in realtà una pratica tanto semplice quanto poco conosciuta che possiamo utilizzare per assicurarci che le fondamenta dei principi di OOP vengano rispettate in modo naturale nel nostro codice: l’ Object Calisthenics.
L’Object Calisthenics è una pratica introdotta da Jeff Bay nella ThoughtWorks Anthology, e consiste in un set di 10 regole da rispettare quando scrivi il tuo codice – regole che hanno lo scopo di dare un confine alle nostre decisioni di design: non ti dicono cosa fare, ma più che altro cosa non fare.
Il nome prende ispirazione dalla pratica sportiva del Calisthenics, il cui nome nasce da due parole greche: “kalòs”, che significa bello, e “sthènos”, che significa forte – perchè il corpo di chi pratica questo sport diventerà “bello e forte”. Le caratteristiche di questo sport sono quelle di poter essere praticato con nessun attrezzo (o quasi) e che gli esercizi possono essere adattati a qualunque livello atletico. Il parallelismo con le regole di Object Calisthenics è evidente e spiega perchè abbiano derivato il nome: grazie a queste regole, il nostro codice diventerà “bello e forte”, ovvero “facile da leggere e cambiare”, ed anche queste regole non richiedono uno specifico contesto o buy in da parte del team: possiamo semplicemente iniziare ad utilizzarle nel nostro lavoro individuale quotidiano.
Credo fortemente che l’Object Calisthenics sia troppo sottovalutato, e sia anzi il miglior modo di iniziare ad imparare la Programmazione ad Oggetti mettendo delle basi chiare e solide. Queste regole nascono con l’obiettivo di prevenire i Code Smell e sono strettamente correlate ai principi SOLID, fornendo quindi un primo step verso la scrittura di un design facile da capire, mantenere ed estendere.
Le 10 regole di Object Calisthenics
1) Un solo livello di indentazione
Mantieni un solo livello di indentazione nel codice, evitando di innestare tanti costrutti uno dentro l’altro: questo favorisce sia la leggibilità che la creazione di metodi che facciano una sola cosa.
Wrong Example ❌
class WrongExample {
public function doSomething(int $a) {
if ($a > 0) {
if ($a < 10) {
return true;
}
}
return false;
}
}
Code language: PHP (php)
Correct Example ✅
class CorrectExample {
public function isBetween1And10(int $a) {
if ($a > 0) {
return $this-> checkIfLowerThan10($a);
}
return false;
}
public function checkIfLowerThan10(int $a) {
if ($a < 10) {
return true;
}
return false;
}
}
Code language: PHP (php)
2) Non usare gli “else”
Evitare gli “else” rende più leggibile il codice perchè possiamo avere una strada principale che si conclude alla fine del metodo, e nel mezzo gestire i casi particolari con delle guardie.
Wrong Example ❌
class WrongExample {
public function doSomething(int $a) {
if ($a < 0) {
// do something
} else {
// handle special case
}
}
}
Code language: PHP (php)
Correct Example (with guard) ✅
class CorrectExample {
public function doSomething(int $a) {
if ($a > 0) {
// guard to handle special case
return;
}
// do something
}
}
Code language: PHP (php)
3) Wrap all primitives and string
Nei metodi pubblici non dovremmo mai avere primitive come parametri, ma sempre oggetti.
Wrong Example ❌
class WrongShop {
public function buy(int $money, string $productId) {
// do something
}
}
Code language: PHP (php)
Correct Example ✅
class CorrectShop {
public function buy(Money $money, Product $product) {
// do something
}
}
class Money {
public function convert(Currency $from, Currency $to) { /** ... */ }
public function __toString() { /** ... for example a specific format like "$ 1.000,00" */ }
}
Code language: PHP (php)
4) First-class collections
Uguale alle 3, ma per gli array: nei metodi pubblici non dovremmo mai avere array nativi come parametri, ma sempre oggetti.
Wrong Example ❌
class WrongLeague {
public function newParticipants(array $newParticipantsList) {
// do something
}
}
Code language: PHP (php)
Correct Example ✅
class CorrectLeague {
public function newParticipants(Participants $newParticipants) {
// do something
}
}
// Participants class allow us to create a list of participants validating data and offer behavior of order and go throught all the list
Code language: PHP (php)
5) No getters/setters/properties
Come abbiamo già detto: “Tell, don’t ask”. Scriviamo classi che espongano comportamenti, non dati.
Wrong Example ❌
class WrongUser {
public Id $id;
public Email $email;
public Password $password;
public function setId(Id $newId) { /** ... */ }
public function setEmail(Email $newEmail) { /** ... */ }
public function setPassword(Password $newPassword) { /** ... */ }
public function getId() { return $this->id; }
public function getEmail() { return $this->email; }
public function getPassword() { return $this->password; }
}
Code language: PHP (php)
Correct Example ✅
class User {
private Id $id;
private Email $email;
private Password $password;
// you can only set them in constructor, then we only expose behaviors
public function login() { /** ... */ }
// ...
}
Code language: PHP (php)
6) One dot per line
Legge di Demetra, ricordate? Non devo conoscere dettagli implementativi di un metodo che utilizzo, ma solo la sua firma. Come svolge quel comportamento che mi fornisce non mi interessa.
Wrong Example ❌
class WrongDog {
private WrongDogBody $body;
public function body(): WrongDogBody { return $this->body; }
}
class WrongDogBody {
private WrongDogTail $tail;
public function tail(): WrongDogTail { return $this->tail; }
}
class WrongDogTail {
public function wag(): void { /** wag the tail action */ }
}
// used somewhere
$dog = new WrongDog();
$dog->body()->tail()->wag();
Code language: PHP (php)
Correct Example ✅
class Dog {
private DogBody $body;
public function expressHappiness(): void { return $this->body->wagTail(); }
}
class DogBody {
public function wagTail(): void { /** do something */ return; }
}
// used somewhere
$dog = new Dog();
$dog->expressHappiness();
Code language: PHP (php)
7) Don’t abbreviate
Rendi sempre i nomi espliciti, senza usare abbreviazioni che possano causare fraintendimenti.
Wrong Example ❌
class Calc {
public function calcSumTwoNums() { /** */ }
}
Code language: PHP (php)
Correct Example ✅
class Calculator {
public function calculateSumOfTwoNumbers() { /** */ }
}
// sum would be a good name for this method, just made an example to show problems with abbreviations so I didn't care about choosing the best name possible.
Code language: PHP (php)
8) Keep all entities small
Tieni tutte le “entità” di software esistenti di dimensioni piccole: classi piccole che si focalizzano su una sola cosa, metodi piccoli, moduli piccoli. In questo modo favorirai la coesione in modo naturale.
9) No classes with more than 2 instance variables
Piu variabili d’istanza significano minore coesione nella classe: tipicamente definiamo “attuatori” le classi con un parametro d’istanza e “orchestratori” quelli con due. Dovremmo evitare di averne più di due.
Wrong Example ❌
class Example {
public function __construct(string $firstName, string $lastName, string $email, string $password) { /** ... */ }
}
Code language: PHP (php)
Correct Example ✅
class Example {
public function __construct(User $user) { /** ... */ }
}
Code language: PHP (php)
10) All classes must have state
Non usare i metodi statici, ed evita di creare classi “utility”/”helpers” che raggruppano comportamenti randomici scorrelati tra loro. Crea classi con responsabilità chiare ed uno stato interno da mantenere.
Wrong Example ❌
class Utilities {
public static function log() { /** ... */ }
public static function translate() { /** ... */ }
}
Code language: PHP (php)
Correct Example ✅
class Logger {
public function log() { /** ... it's state might include the logger technique and some persisted logs ... */ }
}
class Translator {
public function translate() { /** ... it's state might include the translations and languages ... */ }
}
Code language: PHP (php)
Se questo articolo ti è piaciuto, sappi che è parte di una serie mensile a tema Agile, il che significa che puoi recuperare i precedenti e che ne arriveranno altri nei prossimi mesi! In più, se ti piace il mio stile, dai un occhiata a Learn Agile Practices, il mio ecosistema di contenuti online nei quali parlo di pratiche e metodologie Agile come TDD, CI, CD e molto altro a tema programmazione.
Scopri tutto su learnagilepractices.com e seguimi su LinkedIn!