Object Calisthenics in PHP

Object Calisthenics in PHP

Callistenia in PHP: 5 indicazioni di stile per scrivere codice più pulito, semplice da leggere, manutenibile e testabile.

Con il termine callistenia si indicano degli esercizi ginnici che usano il peso del proprio corpo come unico attrezzo. L’accoppiamento al mondo della programmazione è stato fatto da Jaff Bay nel libro The ThoughtWorks Anthology dove fornisce, più che degli esercizi, delle indicazioni di stile per scrivere codice più pulito, semplice da leggere, manutenibile e testabile.

  1. Un solo livello di rientro per metodo
  2. Non usare ELSE
  3. Incapsula le variabili primitive
  4. Collezioni di prima classe
  5. Un punto per riga
  6. Non abbreviare
  7. Mantenere piccole tutte le entità
  8. Nessuna classe deve avere più di due variabili di istanza
  9. Nessun Getters/Setters

Di queste 9 regole, 5 sono applicabili a PHP. Vediamo nello specifico come applicare queste regole al nostro codice.

1. Un solo livello di rientro per metodo

Lo scopo di questa regola, come dice il nome stesso, è quello di avere un solo livello di indentazione in ogni metodo di una classe. Vediamo un esempio:

class PlayerRole 
{
    protected $players;

    function __construct($players)
    {
        $this->players = $players;
    }

    public function filterBy($playerType)
    {
        $filtered = [];

        // 0
        foreach ($this->players as $player)
        {
            // 1
            if ($player->type() == $playerType)
            {
                // 2
                if ($player->isActive())
                {
                    // 3
                    $filtered[] = $player;
                }   
            }
        }

        return $filtered;
    }
}

In questo metodo relativamente semplice, vediamo che ci sono tre livelli di indentazione. Chiaro segnale che potremmo estrarre qualche metodo. Vediamo come si dovrebbe riscriverlo rispettando la regola. Partendo dall’interno, per prima cosa potremmo mettere in linea le due condizioni if:

if ($player->type() == $playerType && $player->isActive())
{
    $filtered[] = $player;
}

Un livello eliminato... ma quella condizione si può scrivere meglio. Estraiamola in un metodo auto esplicativo nel modello:

if ($player->isOfType($playerType))
{
    $filtered[] = $player;
}

public function isOfType($playerType)
{
    return $this->type() == $playerType && $this->isActive());
}

Molto meglio... ma abbiamo ancora un livello di indentazione di troppo. Come possiamo eliminare quel ciclo foreach? Visto che stiamo filtrando un array, PHP ci viene in aiuto con una funzione specifica: array_filter. Il ciclo foreach, che dopo il primo refactoring, al momento risulta così:

public function filterBy($playerType)
{
    $filtered = [];

    // 0
    foreach ($this->players as $player)
    {
        // 1
        if ($player->isOfType($playerType))
        {
            $filtered[] = $player;
        }
    }

    return $filtered;
}

Può essere riscritto così:

public function filterBy($playerType)
{
    return array_filter($this->players, function() use ($playerType)
    {
        return $player->isOfType($playerType);
    });
}

Ed ecco un solo livello di indentazione. Molto più leggibile, no?

2. Non usare ELSE

Questa seconda regola è molto semplice da spiegare. Ogni volta che in un metodo vedi un else, dovresti ri-scriverlo per evitarlo. Come? Invertendo il controllo e interrompendo l’esecuzione con un return. Vediamo un esempio:

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'type' => 'string'
    ]);

    if ($validator->passes())
    {
        if ($request->type == 'post')
        {
            Post::create($request);

            return redirect('posts/index');
        }
        else
        {
            throw new Exception("Type not supported.");
        }
    }
    else 
    {
        return redirect('posts/create')
            ->withErrors($validator)
            ->withInput();
    }
}

Applicando quanto detto, potremmo ri-scriverlo, in maniera molto più leggibile, così:

public function store(Request $request)
{
    $validator = Validator::make($request->all(), [
        'title' => 'required|unique:posts|max:255',
        'body' => 'required',
        'type' => 'string'
    ]);

    if ($validator->fails())
    {
        return redirect('posts/create')
            ->withErrors($validator)
            ->withInput();
    }

    if ($request->type != 'post')
    {
        throw new Exception("Type not supported.");
    }

    Post::create($request);
    return redirect('posts/index');
}

Tutto il metodo potrebbe essere ulteriormente semplificato in laravel, estraendo la validazione e assegnandola alla request, o meglio ad una custom request. Allo stesso modo potremmo estrarre il filtro sul tipo in un middleware.

3. Incapsula le variabili primitive

I tipi di variabile primitive, in PHP, sono Bool, String, Integer, Float, Array, Object, Callable, Iterable, Resource e Null.

Secondo questa regola, sarebbe da evitare di usare questi tipi di dato nella logica di dominio a favore di classi specifiche.

Questa regola, che a primo avviso può sembrare di sovra ingegnerizzazione, si rivela molto utile soprattutto su codice legacy. Quando si tornerà a leggere il codice a distanza di mesi, usare dei dati tipizzati in classi ne agevolerà la comprensione. Vediamo un esempio.

function cache($data, $duration) {
    // ...
}

cache([], 50);

Leggendo la chiamata a cache, riuscite a capire cosa indica quel 50? Secondi? Minuti? Ore? Giorni? Certo, potremmo rinominare il secondo parametro in maniera più funzionale, tipo $seconds ma quanto più chiaro è il seguente modus operandi?

class Second
{
    protected $seconds;

    function __construct($seconds)
    {
        $this->seconds = $seconds;
    }
}

function cache($data, Second $seconds) {
    // ...
}

cache([], new Second(50));

Questa regola ovviamente non va applicata per ogni variabile ma è bene usarla ogni volta che per il tipo di dato da trattare ci sono comportamenti dedicati (metodi includibili nella classe di definizione); quando rende più chiaro il codice; quando serve consistenza (ad esempio una validazione in creazione come potrebbe essere per un tipo di dato EmailAddress) e quando la variabile in oggetto è importante per il dominio di progetto.Vediamo un secondo esempio che forse renderà più chiaro il concetto:

class TimeLenght
{
    protected $seconds;

    private function __construct($seconds)
    {
        $this->seconds = $seconds;
    }

    public static function fromMinutes($minutes)
    {
        return new static($minutes * 60);
    }

    public static function fromHours($hours)
    {
        return new static($hours * 60 * 60);
    }
}

cache([], TimeLenght::fromHours(6));

6. Non abbreviare

La programmazione una volta aveva grossi limiti legati alle risorse a disposizione (RAM, filesystem, bit gestiti dalla cpu...). Se a questo aggiungiamo il fatto che un bravo programmatore deve essere pigro, il risultato è che metodi e variabili nel codice erano il più corto possibile, penalizzandone la lettura. Vediamo due esempi:

foreach ($data as $x)
{
    echo $x->name;
}

Perché usare questa sintassi quando con pochi caratteri in più potremmo avere questa?

foreach ($people as $person)
{
    echo $person->name;
}

Oppure, perchè usare un metodo non autoesplicativo, tipo:

class UserRepo {
    public function fetch($billingId) {
        
    }
}

$userRepo->fetch(13);

A distanza di tempo, se ti ritrovi davanti a questa chiamata al metodo fetch, come fai a sapere a cosa si riferisce? Con un buon IDE, certo... ma:

class UserRepository {
    public function fetchByBillingId($billingId) {
        
    }
}

$userRepository->fetchByBillingId(13);

Così è leggibile anche senza IDE.

8. Nessuna classe deve avere più di due variabili di istanza

Questa regola era riferita a Java. In PHP può essere leggermente adattata arrivando ad accettare 4 variabili di istanza ma da 3 in su è indicatore di un possibile refactoring. Ad esempio basta controllare se stiamo rispettando il principio di singola responsabilità.

class UserController
{
    protected $userService;
    protected $registrationService;
    protected $userRepository;
    protected $logger;

    function __construct(
        UserService $userService,
        RegistrationService $registrationService,
        UserRepository $userRepository,
        Logger $logger
    )
    {
      // ...
    }   
}

Questo controller nella realtà dovrebbe essere splittato in 3:

class UserController
{
    protected $userService;

    function __construct(UserService $userService)
    {
      // ...
    }   
}

class UserService
{
    protected $userRepository;
    protected $logger;

    function __construct(UserRepository $userRepository, Logger $logger)
    {
      // ...
    }   
}

class AuthController
{
    protected $registrationService;
   function __construct(RegistrationService $registrationService)
    {
      // ...
    }
}

Ecco, questi sono degli esempi di come 5 semplici regole possono semplificare la lettura del codice, soprattutto a distanza di tempo. Le conoscevi già? Ma soprattutto, le usi?

Ultima modifica: domenica 11 aprile 2021

Cosa ne pensate:

Leonardo Citton

Leonardo Citton 5 mesi fa

Bell articolo, grazie! 🎉

Rispondi
Riccardo Slanzi

Riccardo Slanzi 5 mesi fa

Grazie a te Leonardo!

Rispondi

Aggiungi il tuo commento

Iscriviti alla mia newsletter

Resterai informato sugli ultimi post, appena verranno pubblicati