Object Calisthenics

il tuo codice fa schifo

Giovanni Battista Lenoci / @gianiaz

PUG SONDRIO - 23 Gennaio 2019

Chi sono?

  • Giovanni Battista Lenoci
  • Senior developer @
  • @gianiaz
  • @gianiaz
  • Coordinatore PUG Sondrio

Il tuo codice fa schifo?

Cerchiamo di capire se il tuo codice fa schifo e perchè.

Il tuo codice è leggibile e comprensibile?

                    
$error = "";
$datetime_inizio = date('Y-m-d H:i', strtotime($_POST['datetime_inizio']));
$datetime_fine = date('Y-m-d H:i', strtotime($_POST['datetime_fine']));
if ($_POST['tipologia_incarico'] == 1) {
    if ($_POST['nome'] != "") {
        $nome = mysqli_real_escape_string(Connection::getConnection(), $_POST['nome']);
        $sql = "SELECT COUNT(*) AS num FROM incarichi WHERE nome='" . $nome . "' AND id_commessa='" . intval($_POST['id_commessa']) . "' ";
        if ($_POST['id'] != "") {
            $sql .= " AND id!='" . intval($_POST['id']) . "' ";
        }
        $sql .= " LIMIT 1";
        $res = mysqli_query(Connection::getConnection(), $sql);
        if ($row = mysqli_fetch_array($res)) {
            if ($row['num'] > 0) {
                $error = 'inuso';
            }
        }
    }
}

if ($datetime_fine <= $datetime_inizio) {
    $error = "time";
}

echo $error;

                

Il tuo codice è manutenibile?

Il tuo codice è testabile?

Il tuo codice è riutilizzabile?

Object Calisthenics

Calisthenics è un termine inglese che identifica un insieme di esercizi fisici a corpo libero, generalmente eseguiti senza l'ausilio di attrezzi, alternativi a quelli classici della ginnastica. ... La parola calisthenics deriva da due termini greci, “kalos” che significa bellezza e “sthenos” ovvero forza.
The ThoughtWorks Anthology

Gli Object Calisthenics sono esercizi di programmazione, formalizzati in 9 regole, inventati da Jeff Bay nel suo libro "The ThoughtWorks Anthology"

Disclaimer!

Le regole non sono da prendere alla lettera, sono solo regole da tenere sempre presente, inoltre sono state pensate

1. Un solo livello di indentazione

    
function validaProdotti($prodotti)
{
    $campiObbligatori = [
        'prezzo',
        'nome',
        'descrizione',
        'tipo'
    ];

    $valido = true;

    foreach ($prodotti as $prodotto) {
        $campi = array_keys($prodotto);
        foreach ($campiObbligatori as $campoObbligatorio) {
            if(!in_array($campoObbligatorio, $campi)) {
                $valido = false;
            }
        }
    }

    return $valido;
}

    

Togliamo il secondo livello di indentazione, collegandoci anche ai principi SOLID, ogni funzione/metodo deve svolgere un singolo compito.

    
function validaProdotti($prodotti)
{
    $campiObbligatori = [
        'prezzo',
        'nome',
        'descrizione',
        'tipo'
    ];

    $valido = true;

    foreach ($prodotti as $prodotto) {
        $prodottoValido = validaProdotto($prodotto, $campiObbligatori);
        if(!$prodottoValido) {
            $valido = false;
        }
    }

    return $valido;
}
    
    
function validaProdotto($prodotto, $campiObbligatori)
{

    $valido = true;

    $campi = array_keys($prodotto);

    foreach ($campiObbligatori as $campoObbligatorio) {
        if (!in_array($campoObbligatorio, $campi)) {
            $valido = false;
        }
    }

    return $valido;
}

    

Possiamo fare meglio?

Early return fail
    
function validaProdotti($prodotti)
{
    $campiObbligatori = [
        'prezzo',
        'nome',
        'descrizione',
        'tipo'
    ];

    foreach ($prodotti as $prodotto) {
        if(! validaProdotto($prodotto, $campiObbligatori)) {
            return false;
        }
    }

    return true;
}


function validaProdotto($prodotto, $campiObbligatori)
{
    $campi = array_keys($prodotto);

    $campiMancanti = array_diff($campiObbligatori, $campi);

    return count($campiMancanti) === 0;
}
    
            

Possiamo fare meglio?

    
function validaListaProdotti($prodotti)
{
    $prodottiValidi = array_filter($prodotti, 'isValid');

    return count($prodottiValidi) === count($prodotti);
}


function isValid($prodotto)
{
    $campiObbligatori = ['prezzo', 'nome', 'descrizione', 'tipo'];

    $campi = array_keys($prodotto);
    $campiMancanti = array_diff($campiObbligatori, $campi);

    return count($campiMancanti) === 0;
}
    
            

Cosa abbiamo ottenuto?

  • Abbiamo rispettato la prima regola dei principi SOLID
    Single Responsibility principle
  • Riutilizzo del codice!

2. Non usare la keyword else

    
private function handleInvoiceCreation(Request $request, AbstractInvoice $invoice)
{
    $form = $this->formFactory->create(InvoiceType::class, $invoice);
    $form->handleRequest($request);

    if($this->user->canCreate()) {

        if ($form->isSubmitted() && $form->isValid()) {

            $this->entityManager->persist($invoice);
            $this->entityManager->flush();
            $this->flashBag->addSuccessMessage('Fattura creata con successo');

            $this->entityManager->refresh($invoice);

            return new RedirectResponse(
                $this->router->generate('panel_detail', ['invoice' => $invoice->getId()])
            );
        } else {
            return [
                'form' => $form->createView(),
                'invoice' => $invoice,
            ];
        }

    } else {
        // qualcos altro
    }

}
    
            
                    
private function handleInvoiceCreation(Request $request, AbstractInvoice $invoice)
{
    $form = $this->formFactory->create(InvoiceType::class, $invoice);
    $form->handleRequest($request);

    if ($this->user->canCreate()) {
        throw new Exception('Non puoi creare una invoice');
    }

    if ($form->isSubmitted() && $form->isValid()) {

        $this->entityManager->persist($invoice);
        $this->entityManager->flush();
        $this->entityManager->refresh($invoice);

        $this->flashBag->addSuccessMessage('Fattura creata con successo');

        return new RedirectResponse(
            $this->router->generate('panel_detail', ['invoice' => $invoice->getId()])
        );
    }


    return [
        'form' => $form->createView(),
        'invoice' => $invoice,
    ];


}


                    
                

3. Wrappa i primitivi in oggetti

non si applica molto al PHP ¯\_(ツ)_/¯

                    
$component->repaint(false);
                    
                
                    
class UiComponent {
    public function repaint($animate = false)
}
                    
                
                    
class Animate {
    public $animate = true;
    public function __construct($animate = true){
        $this->animate = $animate;
    }
}

$component->repaint(new Animate(false));
                    
                

4. Un punto (o ->) per linea

                    
$this->base_uri = $this->CI->config->site_url() . '/' . $this->CI->uri->segment('./') . $this->CI->uri->slash_segment(2, 'both');

$this->base_uri = $this->CI->config->iro->segment(1) . $this->CI->uri->slash_segment(2, 'leading');
                    
                

D: Legge di Demetra

La legge di Demetra afferma che un oggetto A può chiamare un metodo di un altro oggetto B ma l'oggetto A non può usare l'oggetto B per raggiungere un terzo oggetto C che possa soddisfare le sue richieste. Questo infatti implicherebbe una conoscenza dei dettagli interni di B (nello specifico, dei suoi componenti) da parte di A.

Soluzione

Anziché consentire ad A di interagire con un oggetto ottenuto da B (uno "sconosciuto"), il progettista dovrebbe modificare la classe B in modo da fornire direttamente nell'interfaccia di B il servizio di C che serve ad A.

Vantaggi

Il vantaggio nel seguire la legge di Demetra consiste nel fatto che il software così creato tende ad essere più mantenibile ed adattabile. Visto che gli oggetti sono meno dipendenti dalla struttura interna degli altri oggetti, i contenitori di oggetti possono essere modificati senza dover ristrutturare i chiamanti.

Svantaggi

Uno svantaggio della legge è che richiede la scrittura di una grande quantità di metodi wrapper per propagare le chiamate a metodo. Ciò può aumentare il tempo di sviluppo, almeno inizialmente, e sicuramente aumenta la quantità di codice necessario e peggiora le performance.

                    

$pathMock->getExportFilename(Argument::type(InvoicePassive::class))
    ->shouldBeCalled()
    ->willReturn(self::FILE_NAME_INVOICE);




$user = $this->get('security.context')->getToken()->getUser();
                    

                    
                

Benefici

  • Leggibilità
  • Mockare gli oggetti è più facile (TEST)
  • E' più facile fare debug
  • Legge di Demetra

5. Non abbreviare

                    
$products = [
    [
        'nome' => 'Pippo',
        'prezzo' => '10'
    ],
    [
        'nome' => 'Pluto',
        'prezzo' => '5'
    ]
];

foreach ($products as $p) {
    list($n, $prz) = $p;

    echo 'Nome: ' . $n . ', prezzo' . $prz;
}
                    
                
                    
foreach ($products as $product) {
    list($name, $price) = $product;

    echo 'Nome: ' . $name . ', prezzo' . $price;
}
                                            
                
                    
function processResponseHeaderAndDefineOutput($response) { ... }
                    
                
                    
function getPageData($data) { ... }
                    
                

Benefici

  • Maggiore comunicazione agli altri programmatori
  • Può mostrare problemi nascosti

6. Le tue classi devono essere piccole! (adattata)

  • 200 linee per 1 classe
  • 10 metodi per classe
  • 10 righe per metodo

Benefici

  • Single responsibility
  • Metodi chiari
  • Riutilizzo

7. Limitare il numero di istanze negli oggetti

                    
public function __construct(
    BlameBot $blameBot,
    SingleInvoiceChecker $singleInvoiceChecker,
    ExportShutdownRepository $exportShutdownRepository,
    InvoicePassiveRepository $invoicePassiveRepository,
    EntityManager $entityManager,
    PdfGenerator $pdfGenerator
    LoggerInterface $logger,
    Mailer $mailer
    .....
) {
                    
                

8. Usare collections pure

                    
public function add($element);
public function clear();
public function contains($element);
public function isEmpty();
public function remove($key);
public function removeElement($element);
public function containsKey($key);
                    
                

9. NON Usare getter and setters

Devo usarli o no?

In php è obbligatorio, ad esempio per Doctrine.
                    
class User {
    /** @var int $id */
    private $id;
    /** @var string $name */
    private $name;

    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }

    public function getId(): int {
        return $this->id;
    }
    public function setId(int $id): void {
        $this->id = $id;
    }

    public function getName(): string {
        return $this->name;
    }
    public function setName(string $name): void {
        $this->name = $name;
    }
}
                    
                
Le cose cambieranno con php 7.4
                    
class User {
    public int $id;
    public string $name;

    public function __construct(int $id, string $name) {
        $this->id = $id;
        $this->name = $name;
    }
}
                    
                

Grazie per l'attenzione!

Contatti

Domande?