Principii SOLID în practică

Am fost întrebat mai deunăzi dacă pot îmbunătăți codul de mai jos:

class Tooltip
{
  protected $dataIsProcessed = false;

  public function __construct(array $data) {
    $this->data = $data;  
  }

  protected function processData() {
    if($this->dataIsProcessed) {
      return;
    }

    $this->dataIsProcessed = true;

    $this->data = array_merge([
      'title' => '',
      'body' => '',
      'meta' => ''
    ], $this->data);

    $this->data['body'] = stripslashes($this->data['body']);
    $this->data['title'] = do_some_stuff_with_the_title($this->data['title']);
    $this->data['meta'] = do_some_other_stuff_with_meta($this->data['meta']);
  }

  protected function getAttribute($attribute) {
    $this->processData();
    return $this->data[$attribute] ?? '';
  }

  public function style1() {
    return sprintf('<h2>%s</h2>',
      $this->getAttribute('title')
    );
  }

  public function style2() {
    printf('<h2>%s</h2><p>%s</p>',
      $this->getAttribute('body')
    );
  }

  public function style3() {
    return sprintf('<p>%s</p><div>%s</div>',
      $this->getAttribute('body'),
      $this->getAttribute('meta')
    );
  }
}


$tooltip = new Tooltip([
  'title' => 'Hello World!',
  'body' => 'Nice to meet you.',
  'meta' => 'Last update: last year'
])

echo $tooltip->style1();
echo $tooltip->style2();
echo $tooltip->style3();

Evident că se poate! Încalcă SRP și OCP și, îmbunătățind codul, putem implementa și restul principiilor SOLID.

Ca o recapitulare, SOLID este acronim pentru:

  • (SRP) Single Responsibility Principle: a class should have only a single responsibility (i.e. changes to only one part of the software’s specification should be able to affect the specification of the class).
  • (OCP) Open/Closed Principle: software entities … should be open for extension, but closed for modification.
  • (LSP) Liskov Substitution Principle:objects in a program should be replaceable with instances of their subtypes without altering the correctness of that program.
  • (ISP) Interface Segregation Principle: many client-specific interfaces are better than one general-purpose interface.
  • (DIP) Dependency Inversion Principle: one should „depend upon abstractions, [not] concretions.

sursa

Am zis la început că încalcă două principii: SRP și OCP. Să explicăm de ce:

SRP este pe cât de simplu explicat pe atât de greșit înțeles, pentru că l-am văzut de foarte multe ori explicat ca „o clasă trebuie să facă un singur lucru”. Ceea ce e adevărat pentru metode, dar o clasă e ceva mai greu să îndeplinească această sarcină.

De fapt, SRP se traduce prin „O clasă trebuie să aibă un singur motiv să se schimbe”. Subtil, nu?

Ori clasa noastră are cel puțin două motive să se schimbe:

  1. Dacă vrem să schimbăm modul în care sunt procesatele datele
  2. Dacă vrem să schimbăm markup-ul tooltip-urilor;

Apoi, OCP zice că un obiect ar trebui să fie deschis pentru extensie, închis pentru modificare. Ori noi, dacă vrem să adăugăm un stil nou, trebuie să edităm clasa.

Și, în cele din urmă, observi că style2 afișează markup, pe când celelalte metode returnează markup-ul? Nu sunt foarte sigur, dar cred că asta încalcă și LSP un pic.

Refactor

Să vedem cum putem îmbunătăți acest cod, astfel încât să respecte principiile SOLID. Ca regulă, am observat că cel mai eficient este să pornim de la cea mai mică unitate și să construim în baza ei. Pentru că vom folosi DIP, avem nevoie de interfețe.

interface TooltipInterface {
  public function getAttribute($attribute);
}

Apoi implementăm interfața și lăsăm în ea doar metodele ce țin de datele tooltipului. Fără nici un markup:

class Tooltip implements TooltipInterface {
  protected $dataIsProcessed = false;

  public function __construct(array $data) {
    $this->data = $data;
  }

  protected function processData() {
    if($this->dataIsProcessed) {
      return;
    }

    $this->dataIsProcessed = true;

    $this->data = array_merge([
      'title' => '',
      'body' => '',
      'meta' => ''
    ], $this->data);

    $this->data['body'] = stripslashes($this->data['body']);
    $this->data['title'] = do_some_stuff_with_the_title($this->data['title']);
    $this->data['meta'] = do_some_other_stuff_with_meta($this->data['meta']);
  }

  public function getAttribute($attribute) {
    $this->processData();
    return $this->data[$attribute] ?? '';
  }

În felul ăsta suntem în regulă cu SRP: clasa noastră nu are nici un alt motiv să se schimbe în afară de modul în care sunt procesate datele.

Ce facem cu stilurile? Păi… extragem fiecare stil într-o clasă. Definim întâi o interfață pentru stiluri:

interface TooltipStyleInterface {
  public function getMarkup();
}

Și implentăm interfața:

class Style1 implements TooltipStyleInterface {
  public function getMarkup() {}
}

Am zis mai sus că vom implementa și DIP. Ce înseamnă asta? Că Tooltip va fi un obiect trimis unui Style1!

class Style1 implements TooltipStyleInterface {
  public function __construct(TooltipInterface $tooltip) {
    $this->tooltip = $tooltip;
  }

  public function getMarkup() {
    return sprintf('<h2>%s</h2>',
      $this->tooltip->getAttribute('title')
    );
  }
}

Cum folosim noul cod?

// partea asta rămâne la fel:
$tooltip = new Tooltip([
  'title' => 'Hello World!',
  'body' => 'Nice to meet you.',
  'meta' => 'Last update: last year'
])

// Schimbăm doar modul de afișare:
echo (new Style1($tooltip))->getMarkup();

Ce avantaje avem acum?

  • SRP ne garantează separarea responsabilităților: o clasă este responsabilă de date, alta de markup;
  • Prin urmare, OCP ne garantează că putem folosi orice markup, cât timp implementăm interfața TooltipStyleInterface;
  • În același timp, ISP ne permite să implementăm și alt tooltip (e.g. poate vrem ca datele să fie procesate în alt mod în unele cazuri);
  • În cazul în care dorești să testezi una din clasele implicate în toată povestea asta, o poti face făra probleme.

(nu sunt chiar adeptul markup-ului în cod, dar pentru a simplifica exemplele, cred că este OK)

Publicat de

Ionuț Staicu

este frontend & WordPress developer, iar în timpul liber administrează DevForum.