Refactoring: semplifichiamo la leggibilità del nostro codice

Refactoring: semplifichiamo la leggibilità del nostro codice

Ti è mai capitato di riprendere in mano del codice che hai scritto tempo fa e di non capire cosa facesse una determinata variabile/funzione? Probabilmente perchè non hai fatto un buon factoring.

Questo articolo è stato scritto molto tempo fa. Potrebbe essere stato superato dalla tecnologia.

Programmo in PHP ormai da più di 15 anni e se c'è una cosa che ho imparato dall’esperienza, è che leggibilità e semplicità sono le chiavi per un codice manutenibile e duraturo. 

Ogni primo tentativo di scrivere codice dovrebbe riguardare il farlo funzionare a dovere. Una volta ottenute le funzionalità desiderate, ci si dovrebbe dedicare al refactoring, mirando alla leggibilità e alla semplicità del codice. Ovviamente, all’aumentare dell’esperienza, il codice che butterai giù di prima gittata avrà sempre meno bisogno di un refactoring profondo ma qualche aggiustamento sarà sicuramente necessario. 

In questo post, provo a spiegarti in cosa mi focalizzo quando devo rivedere il codice PHP.

Il refactoring è il processo di modifica e ristrutturazione del codice senza cambiarne la funzionalità. Perché dovresti perdere tempo a modificare del codice che funziona già perfettamente?  Se ti stai ponendo questa domanda, probabilmente non hai mai preso in mano un tuo vecchio progetto. Le reazioni classiche che ho io sono 2: la prima “Chi è il pirla che ha scritto ‘sto codice? Ah, io.” e la seconda “Ma cosa fa questa funzione? E quella variabile lì, cosa significa?” Con il refactoring, puoi rendere migliore il codice funzionante e rendere più esplicito il suo funzionamento, evitando di dover aggiungere commenti ovunque. Anzi, il mio obiettivo è di non avere commenti ad esclusione dei Docblock.

Premessa: il refactoring va a braccetto con i test

Visto che il refactoring non deve modificare la funzionalità del codice è necessario che il corretto funzionamento sia provato da test rigorosi, altrimenti la possibilità di introdurre bug e malfunzionamenti è alta. Se non esistono i test, ti consiglio di partire creandoli.

Fatta questa doverosa premessa, vediamo con degli esempi qualche metodo per avere un codice migliore.

Sii espressivo

Questo potrebbe essere un suggerimento banale, ma nella pratica vedo che non viene usato. Rendi sempre il tuo codice autoesplicativo in modo che tu, il tuo sé futuro o qualsiasi altro sviluppatore che inciampa nel tuo codice sappia cosa sta succedendo in quel determinato frammento di codice.

Investi del tempo per la denominazione di funzioni e variabili: è una delle cose più difficili nella programmazione.

Esempio 1: denominazione

Prima
// Non è chiaro cosa stia facendo questo metodo con la stringa ‘active’
// Stiamo impostando o stiamo controllando lo stato?
$status = $user->status(‘active’);
Dopo
// Aggiungendo "is" ai nomi rendiamo più chiare le nostre intenzioni
// Stiamo verificando se lo stato dell'utente è uguale alla stringa passata
// Anche il nuovo nome della variabile ci fa supporre che sarà un booleano
$isUserActive = $user->isStatus(‘active’);

Esempio 2: denominazione

Prima
// Cosa otteniamo da questo metodo? Il nome della classe o il percorso?
return $factory->getTargetClass();
Dopo

// È il percorso che stiamo riprendendo
// Se l'utente cerca il nome della classe, questo è il metodo sbagliato
return $factory->getTargetClassPath();

Esempio 3: estrai il codice duplicato

Prima
// Codice duplicato (metodi "file_get_contents", "base_path" e l’estensione del file)
// Non ci interessa come si ottengono gli esempi di codice
public function setCodeExamples(string $exampleBefore, string $exampleAfter)
{
    $this->exampleBefore = file_get_contents(base_path("$exampleBefore.md"));
    $this->exampleAfter = file_get_contents(base_path("$exampleAfter.md"));
}
Dopo
public function setCodeExamples(string $exampleBefore, string $exampleAfter)
{
    // Il nostro codice ora dice cosa stiamo facendo: ottenere un esempio di codice (non ci interessa come)
    $this->exampleBefore = $this->getCodeExample($exampleBefore);
    $this->exampleAfter = $this->getCodeExample($exampleAfter);
}
// Il nuovo metodo può essere utilizzato più volte ora
private function getCodeExample(string $exampleName): string
{
    return file_get_contents(base_path("$exampleName.md"));
}

Esempio 4: estrai

Prima
// Più clausole where rendono difficile la lettura
// Qual è lo scopo?
User::whereNotNull('subscribed')->where('status', 'active');
Dopo
// Questo nuovo metodo di scope ci dice cosa sta succedendo
// Se abbiamo bisogno di maggiori dettagli, controlliamo il metodo
// e può essere utilizzato anche altrove
User::subscribed();

Ritorna il prima possibile (guard clauses)

Il concetto di ritorno anticipato si riferisce a una pratica in cui si cerca di evitare l'annidamento suddividendo una struttura in casi specifici. In cambio, otterremo un codice più lineare, molto più facile da leggere e comprendere. Ogni caso è separato e semplice da capire. Non aver paura di utilizzare più dichiarazioni di return.

Questo metodo è noto con il termine "guard clauses", perchè con i return anticipati stai, di fatto, proteggendo il tuo metodo da condizioni indesiderate.

Esempio 1: mai più ELSE

Prima
public function sendInvoice(Invoice $invoice): void
{
    if ($user->notificationChannel === 'Telegram')
    {
        $this->notifier->telegram($invoice);
    } else {
        // Anche un semplice ELSE influisce sulla leggibilità del codice
        $this->notifier->email($invoice);
    }
}
Dopo
public function sendInvoice(Invoice $invoice): bool
{
    // Ogni condizione è facile da leggere
    if ($user->notificationChannel === 'Telegram')
    {
        return $this->notifier->telegram($invoice);
    }
    // Basta pensare a quello cui si riferisce ELSE
    return $this->notifier->email($invoice);
}

Esempio 2: IF annidati

Prima
public function calculateScore(User $user): int
{
    if ($user->inactive) {
        $score = 0;
    } else {
        // Altro "se"?
        if ($user->hasBonus) {
            $score = $user->score + $this->bonus;
        } else {
            // Inizi a cercare le parentesi di inizio per i diversi livelli di rientro? 
            // E per fortuna che il codice è indentato...
            $score = $user->score;
        }
    }
    return $score;
}
Dopo
public function calculateScore(User $user): int
{
    // I casi limite vengono controllati per primi
    if ($user->inactive) {
        return 0;
    }
    // Ogni caso ha la sua sezione che lo rende facile da seguire passo dopo passo
    if ($user->hasBonus) {
        return $user->score + $this->bonus;
    }
    return $user->score;
}

Usa le Collections

In PHP, lavoriamo molto con array di dati diversi. Le funzionalità disponibili per gestire e trasformare questi array sono tuttora piuttosto limitate in PHP e non forniscono una buona esperienza. (array_map, usort, ecc.)

Per superare questo problema, nei moderni framework, è stato introdotto il concetto di Collection, una classe che ti aiuta a manipolare gli array. Essendo io innamorato di Laravel, userò l'helper collect() nei miei esempi ma è facilmente replicabile anche in altri framework.

Esempio 1

Prima
// Qui abbiamo una variabile temporanea
$score = 0;
// Va bene usare un ciclo, ma potremmo renderlo più leggibile
foreach($this->playedGames as $game) {
    $score += $game->score;
}
return $score;
Dopo
// La collezione è un oggetto con metodi
// Il metodo sum lo rende più espressivo
return collect($this->playedGames)->sum('score');

Esempio 2

$users = [
    [ 'id' => 1, 'name' => 'Mario', 'score' => 505, 'active' => true],
    [ 'id' => 2, 'name' => 'Cecilia', 'score' => 904, 'active' => false],
    [ 'id' => 3, 'name' => 'Pietro', 'score' => 704, 'active' => true],
];
// Risultato richiesto: solo utenti attivi, ordinati per punteggio ["Cecilia (904)", "Pietro (704)"...]
Prima
$users = array_filter($users, fn ($user) => $user['active']);
// usort? Come funziona la condizione?
usort($users, fn($a, $b) => $a['score'] < $b['score']);
// Tutte le trasformazioni sono separate, tuttavia riguardano tutti gli utenti
$userHighScoreTitles = array_map(fn($user) => $user['name'] . '(' . $user['score'] . ')', $users);
return $userHighScoreTitles;
Dopo
return collect($users)
    ->filter(fn($user) => $user['active'])
    ->sortBy('score')
    ->map(fn($user) => "{$user['name']} ({$user['score']})"
    ->values()
    ->toArray();

Sii coerente

Ogni riga di codice aggiunge una piccola quantità di disturbo visivo. Più codice c’è, più è difficile leggerlo. Ecco perché è essenziale stabilire delle regole: mantenere coerenti oggetti simili ti aiuterà a riconoscerli, rendendo più semplice la lettura. Se esistono name convention per il tuo framework, impara ad usarle. Qui la name convention per Laravel.

Esempio 1

Prima
class UserController
{
    // Decidi come denominare le TUTTE le variabili (camelCase, snake_case, ecc)!
    public function find($userId)
    {

    }
}

// Scegli la forma tra singolare e plurale per tutti i controller e rispettala
class InvoicesController 
{
    // I cambiamenti nello stile, come la posizione delle parentesi graffe, rendono il codice difficile da leggere (usa gli ndard PSR)
    public function find($user_id) {
    
    }
}
Dopo
class UserController
{
    // camelCase per tutte le variabili
    public function find($userId)
    {

    }
}

// Nomi dei controller coerenti (singolare come suggerito dalla convenzione Laravel)
class InvoiceController 
{
    // Posizione coerente delle parentesi graffe rende il codice più leggibile
    public function find($userId)
    {

    }
}

Esempio 2

Prima
class PdfExporter
{
    // "handle" ed "export" sono metodi simili con nomi diversi
    public function handle(Collection $items): void
    {
        // export...
    }
}
class CsvExporter
{
    public function export(Collection $items): void
    {
        // export...
    }
}
// Durante l'utilizzo ti chiederai se svolgono anche attività simili
// Scommetto che cercherai le classi per esserne sicuro
$pdfExport->handle();
$csvExporter->export();
Dopo
// Un'interfaccia può aiutare con la coerenza fornendo regole comuni
interface Exporter
{
    public function export(Collection $items): void;
}
class PdfExporter implements Exporter
{
    public function export(Collection $items): void
    {
        // export...
    }
}
class CsvExporter implements Exporter
{
    public function export(Collection $items): void
    {
        // export...
    }
}
// Gli stessi nomi di metodo per attività simili renderanno più facile la lettura
// Sono abbastanza sicuro che, senza dare un'occhiata alle classi, entrambi esportano dati
$pdfExport->export();
$csvExporter->export();

Bene, questi sono i principi che uso per fare refactoring. Tu sei solito fare refactoring? E se si, che altri stratagemmi utilizzi?

Ultima modifica: venerdì 6 novembre 2020

Ancora nessun commento presente

Che ne dici di essere il primo?

Aggiungi il tuo commento

Iscriviti alla mia newsletter

Resterai informato sugli ultimi post, appena verranno pubblicati