środa, 9 stycznia 2013

SOLIDny kod cz. 2 - Single responsibility principle

w czym rzecz

Jak już ostatnio pisałem, Single responsibility principle (SRP), czyli zasada jednej odpowiedzialności, mówi o tym, że klasa nie powinna mieć więcej niż jednego powodu do modyfikacji.
Często spotykam się z tym, że programiści twierdzą, że takie myślenie prowadzi jedynie do niepotrzebnego rozbijania każdej klasy na kilka bądź więcej elementów składowych. Co za tym idzie? Potrzeby zarządzania wieloma klasami. A to wszystko tylko po to, żeby zrobić coś naprawdę prostego.

Cóż, można na to spojrzeć w ten sposób. Jednak jest kilka ale...

ale...

Owe wspomniane wyżej "ale", to:
  • W małych klasach ciężej popełnić błąd, niż w klasach złożonych z setek/tysięcy linii kodu. Po prostu łatwiej jest spojrzeć na całość i nie trzeba się przegrzebywać przez x długości ekranów, aby dotrzeć do pożądanego miejsca.
  • Małe klasy łatwiej się testuje. Jest tak dlatego, że ilość przypadków do przetestowania jest dużo mniejsza niż w przypadku skomplikowanych klas. Trudniej w takim wypadku również pominąć jakiś.
  • Wprowadzanie zmian jest proste. Po zapoznaniu się z zależnościami pomiędzy klasami i odpowiedzialnością każdej z nich, bez problemu odpowiemy sobie na pytanie, która odpowiada za co i który element wymaga modyfikacji.
  • Łatwiej dodawać nową funkcjonalność. Jeżeli opieramy się na kompozycji, a nie na wielkich klasach mogących wszystko, to rozszerzanie możliwości komponentów sprawi nam dużo mniej problemów.

apropos zarządzania

Co do zarzutu o problemach w zarządzaniu większą ilością klas, to szczerze mówiąc, wydaje mi się, że wynika on ze strachu przed klasami i obiektami. Skąd on się bierze? Z moich obserwacji wynika, że większość programistów nie zaczynała od programowania obiektowego, a strukturalnego i są przyzwyczajeni do myślenia strukturalnego. Dlatego dzielenie kodu jest dla nich czymś nienaturalnym. Stąd ten absurdalny strach.
Na szczęście większość z nich (przynajmniej Ci, którzy rzeczywiście chcą programować obiektowo) szybko zauważają, jak wiele korzyści przynosi to rozbijanie kodu na elementy składowe.

Wracając jednak do zarządzania, to jeżeli klasy są odpowiednio uporządkowane, to problem z zarządzaniem przestaje istnieć, bo w ich strukturze szybko się odnajdujemy. Dzięki odpowiedniemu przydzielaniu odpowiedzialności unikamy również wielu bloków warunkowych w kodzie, co sprawia, że kod jest czytelniejszy i przyjemniej się z nim pracuje.

ale nie samą teorią programista żyje...

... więc przejdźmy do przykładu:) Zacznijmy od tego, jak kod nie powinien wyglądać, a nastepnie stopniowo go udoskonalimy:)

Załóżmy, że mamy klasę, która waliduje i filtruje dane przychodzące z formularza rejestracji. Niech tymi danymi będą login i adres email. Klasa wyglądałaby mniej więcej tak:
<?php
class RegistrationForm 
{
  private $_rawData = array();

  public function areValid(array $data)
  {
    $this->_rawData = $data;
   
    if (!isset($data['login']) || !isset($data['email'])
      return false;

    return 
      $this->_isEmailAddress($data['email']) && 
      $this->_isLogin($data['login']);
  }

  public function getFiltered()
  {
    return array(
      'login'  => $this->_getFilteredName($this->_rawData['login']),
      'email' => $this->_getFilteredEmail($this->_rawData['email']),
    );
  }  

  /** more code */
}

W powyższym kodzie pominąłem metody filtrujące i walidujące. Wystarczy założenie, że działają w jakiś z góry określony sposób:)

Przyjrzyjmy się tej klasie. Ile rzeczy może wpłynąć na jej modyfikację?
  • zmiana ilości pól formularza
  • zmiana ich nazw
  • zmiana sposobu walidacji adresu mailowego
  • zmiana sposobu walidacji loginu
  • zmiana sposobu filtrowania adresu mailowego
  • zmiana sposobu filtrowania loginu

Sporo, nieprawdaż:)

co dalej?

Zastanówmy się, czym tak naprawdę nasza klasa jest, z jakich elementów się składa, jak je można (i czy w ogóle da się) je w jakiś sposób pogrupować. Jednym słowem - przeanalizujmy ją:)

Naszą klasę można podzielić na trzy części:
  • kontener trzymający wszystko w całości
  • funkcjonalność dotycząca loginu (nazwa, walidator, filtr)
  • funkcjonalność dotycząca adresu mailowego (nazwa, walidator, filtr)

Czyli już mamy potencjalne trzy klasy, ale... nie tak szybko. Pomyślmy dalej:)

krok pierwszy

Klasa będąca kontenerem zawiera listę pól. To, że w tym przypadku jest to login i adres email, nie ma większego znaczenia. Idąc tym tropem, możemy stworzyć klasę będącą kontenerem:
<?php
class Form 
{

  /**
   * @var FormElementInterface[]
   */
  private $_elements = array();

  public function addElement($name, FormElementInterface $element) 
  {
    $this->_elements[$name] = $element;
  }

  public function areValid(array $data) 
  {
    foreach ($this->_elements as $name => $element)
    {
      if (!isset($data[$name])
        return false;

      $element->setValue($data[$name]);

      if (!$element->isValid())
        return false;
    }

    return true;
  }

  public function getFiltered() 
  {
    $result = array();

    foreach ($this->_elements as $name => $element)
      $result[$name] = $element->getValue();

    return $result;
  }
}

i interfejs będący wynikiem analizy operacji, które musimy posiadać w klasie reprezentującej konkretny element:
<?php
interface FormElementInterface 
{
  public function setValue($value);
  public function isValid();
  public function getValue();
}

idąc za ciosem

Co teraz? Należy stworzyć dwie klasy implementujące interfejs i po sprawie. Ich instancje dodajemy do obiektu klasy Form i jesteśmy zadowoleni.
Ale nie do końca ;)

Zwróćmy uwagę na część wspólną elementów - oba posiadają walidator i filter i to są jedyne rzeczy, które je różnią i tak naprawdę je definiują jako obiekty. Więc może warto pójść tym tropem?
<?php
class FormElement implements FormElementInterface
{
  /** implementation of methods from interface */

  public function setFilter(FilterInterface $filter) {/** code */}
  public function setValidator(ValidatorInterface $validator) {/** code */}
}

interface FilterInterface
{
  public function getFilteredValue($value);
}

interface ValidatorInterface
{
  public function isValid($value);
}

class EmailValidator implements ValidatorInterface {/** code */}
class EmailFilter implements FilterInterface {/** code */}
class LoginValidator implements ValidatorInterface {/** code */}
class LoginFilter implements FilterInterface {/** code */}

i jeszcze jedno...

To już prawie koniec. Pozostała jedynie kwestia nazwy. Obecnie przechowujemy ją w klasie Form, ale czy rzeczywiście jest to odpowiednie miejsce? W końcu to element powinien wiedzieć jaką ma nazwę, a kontener powinien przechowywać jedynie listę tych elementów. Tak więc dodajmy do klasy FormElement jeszcze jedną metodę:
<?php
class FormElement implements FormElementInterface
{
  private $_name

  public function __construct($name) {/** code */}
  /** more code */
}

Oczywiście kontener musi mieć możliwość poznania tej nazwy:) Tak więc dodamy jeszcze jedną metodę do naszego interfejsu:
<?php
interface FormElementInterface 
{
  public function getName();
  /** more code */
}

Na koniec pozostało nam zmienić jeszcze ciało dwóch metod w klasie Form:
<?php
class Form 
{
  public function addElement(FormElementInterface $element) 
  {
    $this->_elements[] = $element;
  }

  public function areValid(array $data) 
  {
    foreach ($this->_elements as $element)
    {
      if (!isset($data[$element->getName()])
        return false;

      $element->setValue($data[$element->getName()]);

      if (!$element->isValid())
        return false;
    }

    return true;
  }

  public function getFiltered() 
  {
    $result = array();

    foreach ($this->_elements as $element)
      $result[$element->getName()] = $element->getValue();

    return $result;
  }

  /** more code */
}

wnioski

Popatrzcie teraz ponownie na listę zmian, które mogą wpłynąć na formularz rejestracji jako całość. Można łatwo zauważyć, że każda zmiana dotyczy innego obiektu, czyli udało nam się osiągnąć zamierzony cel i stworzyć kod, który stosuje się do zasady jednej odpowiedzialności.

Mam nadzieję, że patrząc na cały proces powstawania kodu, łatwo zauważycie, że korzyści ze stosowania tej zasady wymienione przeze mnie w początkowych akapitach wpisu są czymś rzeczywistym i namacalnym, a nie jedynie teoretycznym gadaniem fanatyków OO:)