sobota, 6 kwietnia 2013

SOLIDny kod cz. 3 - Open/closed principle

łatwo powiedzieć

W najprostszej i najkrótszej postaci definicja drugiej zasady SOLID, to: elementy systemu powinny być otwarte na rozszerzenia, ale zamknięte na zmiany. Proste, jasne i przejrzyste, czyż nie? No dobra, może nie takie jasne i przejrzyste, ale przynajmniej proste. Łatwo zapamiętać:)

Może przesadzam z tym, że nie jest ona (definicja) równie przejrzysta jak prosta, bo w większości przypadków programista jest w stanie wytłumaczyć o co chodzi - "O to, żeby łatwo było rozszerzać bez mieszania w kodzie". Problemy zaczynają się wtedy, gdy docieramy do projektowanie czy też tworzenia kodu, bo to, co prosto powiedzieć, już wcale takie banalne w realizacji nie jest.

Jak umożliwić rozwój, który nie wymaga zmian?

zachowanie i interakcje ponad budowę

Tak naprawdę ciężko jest dać dobrą radę, której przestrzeganie spowoduje, że Wasz kod będzie zgodny z zasadą open/closed, bo wyrażenie z nagłówka akapitu, które jest moim zdaniem najlepszą radą z możliwych, jest równocześnie taką, która jest równie przejrzysta i jasna, jak sama druga zasada SOLID.
Więc co robić? Jak tworzyć kod skupiając się na zachowaniu, a nie na budowie?

Po pierwsze, tak, zasada open/closed każe nam skupiać się na interfejsach (abstrakcjach), a nie na konkretnych klasach. Dlaczego? Ponieważ przy takim podejściu przy stosunkowo małym wysiłku możemy zmieniać elementy bez ingerencji w kod. Skupiamy się na relacjach (które możemy podmieniać), a nie na kodzie, w którego budowę musielibyśmy ingerować.

Ale zaraz, przecież miało być o rozbudowie, a ja piszę o podmienianiu. Przecież podmiana nie jest synonimem rozbudowy!
Jasne, że nie jest, co jednak nie oznacza, że te dwie aktywności nie mogą iść ze sobą w parze.

daj mnie ten kawałek kodu

Wyobraźmy sobie, że mamy aplikację, która co 5 godzin musi wykonywać następujące operacje:
  • Odświeżyć dane dotyczące użytkowników, które w czasie rzeczywistym są przechowywane przez inną usługę.
  • Sprawdzić stan wykonywanych operacji i wysłać odpowiednie notyfikacje w przypadku, gdy któraś z nich trwa zbyt długo.

Możemy to zrobić w taki sposób:
<?php
class EventController
{
  public function executeAction()
  {
    EventsExecutor_Factory::create()->execute();
  }

  /** more code */
}

class EventsExecutor_Factory
{
  public static function create()
  {
    $executor = new EventsExecutor();
    $executor
      ->addUsersUpdater(new UsersUpdater())
      ->addNotifier(new Notifier());

    return $executor;
  }

  /** more code */
}

class EventsExecutor
{
  public function execute()
  {
    if ($this->_usersUpdater->isServiceAvaliable())
      $this->_usersUpdater->updateUsers();

    if ($this->_notifier->areOperationsHanging())
      $this->_notifier->notifyAboutHanginOperations();
  }

  public function addUsersUpdater(UsersUpdater $usersUpdater) {/** code */}
  public function addNotifier(Notifier $notifier) {/** code */}

  /** more code */
}

class UsersUpdater {/** code */}
class Notifier {/** code */}

Działa? Świetnie, czyli zadanie wykonane. Możemy nawet z dumą stwierdzić, że pierwsza zasada SOLID została zachowana - każda klasa jest odpowiedzialna za pojedynczą rzecz:)

Ale... przyszłość nadciąga...

rozwój gwarantowany

Jestem bardzo pragmatyczny i minimalistyczny zarówno w projektach, jak i przy implementacji kodu, więc unikam jak ognia nadmiarowości w jakiejkolwiek formie. Co jednak nie oznacza, że nie myślę o przyszłości, o zmianach, które są przed nami. Po prostu nie zastanawiam się nad tym, CO może się przydarzyć, staram się jednak, aby kod był prosty w rozwoju. Jak? Funkcjonalność ma być zależna od zachowania, a nie od implementacji. Co to oznacza w praktyce?
Relacje pomiędzy klasami powinny opierać się na interfejsach, a nie na konstrukcjach.

Popatrzcie na powyższy kod. Nie jest zły, ale zwróćcie uwagę na zależności pomiędzy klasami. Każda wie o istnieniu pozostałych. Jaka jest tego konsekwencja? Zmiana w którejkolwiek, pociąga zmiany w innych.

Wyobraźmy sobie teraz, że musimy dodać kolejne zdarzenie do istniejącej już listy. Aby to zrealizować musimy wykonać poniższe czynności:
  • Utworzyć klasę dla nowego zdarzenia.
  • Zmodyfikować ciało klasy EventsExecutor_Factory.
  • Zmodyfikować ciało metody execute() w klasie EventsExecutor.
  • Dodać nową metodę do klasy EventsExecutor, która pozwoli nam dodać nowe zdarzenie.

Wszystkich punków nie wyeliminujemy, bo to jest nie możliwe. Zarówno utworzenie nowej klasy, jak i zmiana w klasie EventsExecutor_Factory są wymagane. Bez nowej klasy nie będzie nowego zdarzenia, a bez dodania tego zdarzenia do klasy EventsExecutor nie może być mowy o jego wykonaniu.
Jednak te zmiany są następstwem rozwoju i tego typu rzeczy nie należy się obawiać:)

Dobrze, ale co z pozostałymi punktami?

podobieństwa są wśród nas

Zwróćcie uwagę, że klasy UsersUpdater oraz Notifier są zdarzeniami, czyli logicznie mają dużo wspólnego:) Poza tym oba muszą zostać wykonane. Gdy patrzymy na zdarzenie, to nie ważne czego ono dotyczy, żeby mogło zaistnieć musi zostać wykonane. Idąc tym tokiem myślenia możemy odrobinę zmodyfikować istniejący kod:
<?php
class EventsExecutor
{
  public function execute()
  {
    if ($this->_usersUpdater->isServiceAvaliable())
      $this->_usersUpdater->execute();

    if ($this->_notifier->areOperationsHanging())
      $this->_notifier->execute();
  }

  public function addUsersUpdater(UsersUpdater $usersUpdater) {/** code */}
  public function addNotifier(Notifier$notifier) {/** code */}

  /** more code */
}

interface Event
{
  public function execute();
}

class UsersUpdater implements Event {/** code */}
class Notifier implements Event {/** code */}

Idealnie, to może pójdziemy za ciosem?

Zauważcie, że każde zdarzenie, aby mogło zostać wykonane musi spełnić określone warunki, więc można pokusić się o dalsze modyfikacje:
<?php
class EventsExecutor
{
  public function execute()
  {
    if ($this->_usersUpdater->isValid())
      $this->_usersUpdater->execute();

    if ($this->_notifier->isValid())
      $this->_notifier->execute();
  }

  public function addUsersUpdater(UsersUpdater $usersUpdater) {/** code */}
  public function addNotifier(Notifier$notifier) {/** code */}

  /** more code */
}

interface Event
{
  public function execute();
  public function isValid();
}

Mając taki kod, na pierwszy rzut oka widać, że wykonywanie zdarzeń nie zależy już od konkretnego zdarzenia, ale od naszego interfejsu. Tak, więc możemy jeszcze odrobinę zmodyfikować klasę EventsExecutor:
<?php
class EventsExecutor
{
  public function execute()
  {
    foreach ($this->_events as $event)
    {
      if ($event->isValid())
        $event->execute();
    }
  }

  public function addEvent(Event $event) {/** code */}

  /** more code */
}

Oczywiście metody addNotifier() i addUsersUpdater() zostają usunięte.

cel osiągnięty?

Udało się? Wydaje mi się, że tak, poszło całkiem nieźle. W tym momencie EventsExecutor nie wie absolutnie nic o tym, jakie zdarzenia będzie wykonywał. To wiedza nie jest do niczego potrzebna.

Jak widzicie, udało się stworzyć kod otwarty na rozszerzenia, ale zamknięty na zmiany:)

nigdy nie jest tak dobrze...

Pamiętajcie jednak, że rozwój funkcjonalności to jedno i możemy stworzyć taki kod, który będzie rozszerzalny, co innego ze zmianami, które również czasami są wymagane.

I tu niekiedy zauważam problemy. Programiści zastanawiają się, co może się zdarzyć i jak się przed skutkami tych zmian obronić, bo wydaje im się, że to jest właśnie sens zasady Open/Close. Nie, to jest zwyczajne przewidywanie przyszłości. Wasz kod będzie otwarty na rozszerzenia, jeżeli oprzecie zależności pomiędzy klasami na interfejsach, wtedy dodawanie kolejnych elementów skupia się na dodaniu tych elementów, a nie na grzebaniu w bebechach innych klas, tylko po to, aby można było wszystko poskładać w spójną całość.