17 stycznia 2020

PHP: SOLID [S] Zasada pojedynczej odpowiedzialności

W oryginale „Single Responsibility Principle„.

Wstęp

Zasada z pozoru prosta, natomiast stosowanie jej w praktyce wymaga przelania odrobiny potu.

Autor zasady pojedynczej odpowiedzialności twierdzi, że jedna klasa powinna mieć jeden powód do zmiany, powinna zatem robić jedną konkretną rzecz.

Kto z nas nie pisał na początku swej przygody z programowaniem klas typu „scyzoryk szwajcarski„, oczywiście taki kod jest niezgodny z pierwszą zasadą SOLID.

Praktyka

Aby klasy miały jedną odpowiedzialność należy pochylić się na początku i poświęcić sporo czasu na odpowiednim zaprojektowaniu aplikacji. Proponuję na początku spisać wszystkie klasy jakie będą nam potrzebne i zastanowić się nad ich relacją.
Każda klasa powinna ściśle być odpowiedzialna za jedną rzecz, najlepiej gdyby każdą klasę udało nam się opisać maksymalnie w 25 słowach, jeśli w opisie naszej klasy znajduje się zbyt wiele spójników „i”, „lub”, powinniśmy rozważyć przeniesienie części odpowiedzialności do oddzielnych klas.

Poniżej przedstawię ilustrację pewnej klasy typu „scyzoryk szwajcarski„:

class Basket
{
    private $allProducts = [];

    public function __construct(array $data)
    {
        if (isset($data['name']) && isset($data['price'])) {
            $this->allProducts[] = ['name' => $data['name'], 'price' => $data['price'], 'bruttoPrice' => (23 / 100) * $data['price']];
        }
    }
    
    public function getAllProducts() : array
    {
        return $allProducts;
    }

    private function erase()
    {
        $this->allProduct = [];
    }
}

Powyższa klasa przechowuje listę produktów, oblicza cenę brutto, posiada metody odpowiedzialne za czyszczenie koszyka zakupowego.
Możemy do niej dodać kolejne metody związane z koszykiem, bowiem jest to przecież klasa o nazwie „Basket„.

Możemy również dodać kolejne indeksy w tablicy produktów jak na przykład „Opis produktu„. A co możliwością wyświetlania cen po zastosowaniu kodu rabatowego ? Również dodamy tutaj.

Proszę zobaczyć, w jaki sposób jest obliczana kwota brutto, takie podejście będzie z pewnością wymagało powielenia kodu. Prawidłowe podejście wymaga, aby operacje obliczenia ceny odbywały się w osobnym do tego przeznaczonym miejscu.

Przedstawię poniżej bardzo prosty przykład rozdzielenia powyższych funkcjonalności.

trait PriceUtilities
{
    private $tax = 23;

    public function getBruttoPrice(float $price) : float
    {
        return ($this->tax / 100) * $price;
    }
}

class EntityProduct
{
    use PriceUtilities;

    private $name = null;
    private $price = null;

    public function __construct (string $name = null, float $price = null)
    {
        $this->name = $name;
        $this->surname = $price;
    }
}

class Basket
{
    private $products = [];

    public function erase()
    {
        $this->products = [];
    }

    public function getAll() : array
    {
        return $this->products;
    }
}

Jak wspomniałem wcześniej, przykład ilustruje pewien problem który możemy napotkać w sytuacji, gdy pewna klasa zaczyna się niebezpiecznie rozrastać. Najlepiej w takiej sytuacji zapobiec ewentualnym błędom i rozdzielić funkcjonalności.

Powyżej obsługę koszyka zawarliśmy w klasie „Basket„.
Reprezentację pojedynczego rekordu przenieśliśmy do osobnej klasy „EntityProduct„.
Obliczanie ceny brutto w postaci trait umieściliśmy również osobno, dzięki temu kod zawarty wewnątrz nie będzie powielany. Można się pokusić o zastąpienie trait klasą, jeśli zamierzamy wprowadzić różnego rodzaju promocje, czyli kolejne warunki.

Podsumowanie

Zasady SOLID zostały wymyślone, aby ułatwić życie programistom a nie utrudniać. Nie ma sensu stosowanie ich na siłę.
Oczywiście powinniśmy się do nich stosować, ale każdy problem rozpatrywać indywidualnie, czasem umieszczanie jednej linii kodu w osobnej klasie nie jest dobrym pomysłem.