czwartek, 31 maja 2012

Jak programować obiektowo cz. 10 - final

final - what for is that?

Wielu programistów zapewne nigdy nie użyła słowa final w celu oznaczenia klasy lub metody. Dlaczego? Z prostego powodu, a raczej jego braku. "Po co?" W końcu, jeżeli ktoś będzie chciał sobie dziedziczyć bądź nadpisywać niech sobie robi to, co mu się żywnie podoba. W czym problem? Jak na ironię ten argument wcale nie jest 'przeciw' stosowaniu final, jak błędnie uważają niektórzy. Jest to najważniejszy powód, dla którego to słowo kluczowe powinno być wykorzystywane, ponieważ przemyślane jego zastosowanie może opłacić się w przyszłości.

o co tyle szumu?

Na pierwszy rzut oka słowo final wcale nie wydaje się czymś istotnym, co powoduje, że przez wielu (nawet doświadczonych) programistów jest spychanych do otchłani zapomnienia. Do czego może zostać wykorzystane? Do ograniczenia dalszego dziedziczenia bądź nadpisywania metody. Czy rzeczywiście jest to tak istotna kwestia, że warto zawracać sobie tym głowę?

Odpowiedź na to pytanie jest twierdząca. Poprawnie wykorzystane słowo final może sprawić, że unikniemy wielokrotnego dziedziczenia czy też zmiany logiki potomka względem klasy nadrzędnej. Ma ono duże znaczenie ze względu na zależności pomiędzy klasami, ich spójność, ogólny projekt aplikacji.

metody finalne

Wyobraźmy sobie, że implementujemy złożoną operację, która pozwoli nam zmodyfikować w określony sposób pewne przechowywane przez nas dane. Te dane jednak są przechowywane w różnych miejscach: baza, pliki, sesja. Cała operacja przebiega zawsze tak samo (jest zawsze taka sama kolejność i ilość kroków) pomimo odczytu danych i ich zapisu.
Wywoływanie operacji odpowiedzialnej za modyfikacje odbywa się w obiekcie innej klasy, a o tym, na których danych (z bazy, z pliku ...) ma być przeprowadzona, odpowiada użytkownik aplikacji. Kod klasy wykonującej modyfikację będzie wyglądał mniej więcej tak:
<?php
abstract class DataManagerAbstract
{

  public function modify()
  {
    $this->_step1();
    $this->_step2();
    $this->_step3();
    $this->_step4();
  }

  abstract protected function _step3();

  protected function _step1() {/* ... */}
  protected function _step2() {/* ... */}
  protected function _step4() {/* ... */}
}
Implementacja metody, która będzie wykorzystywała konkretne implementacje tej klasy:
<?php
class DataController
{
  /* ... */

  public function execute(DataManagerAbstract $dataManager)
  {
    /* ... */
    $dataManager->modify();
    /* ... */
  }

  /* ... */
}
Powyższy kod jest bardzo uproszczony, ale główna idea pozostaje:)

Załóżmy, że pewnego dnia ktoś dochodzi do wniosku, że rozszerzyłby klasę DataManagerAbstract tylko, że w metodzie modify() nie jest mu potrzebne do szczęścia wywołanie metody _step2(). Co w takich sytuacjach robi przeciętny programista?
Tworzy klasę pochodną, rozszerza DataManagerAbstract i nadpisuje metodę modify(). Jest z siebie zadowolony, puszcza testy i nawet wszystkie przechodzą (jeżeli pominął dokładne przetestowanie swojego kodu, co zdarza się częściej niżbyśmy chcieli). Aż pewnego dnia przychodzi zgłoszenie od klienta, że "aplikacja zwariowała i generuje losowe rzeczy". Nikt nie wie, gdzie tkwi błąd. Po pewnym czasie spędzonym na próbach reprodukcji i szukaniu przyczyny okazuje się, że metoda _step2(), tak jak i każda kolejna, jest nieodłączną częścią operacji i nie może ona (operacja) przebiec prawidłowo (bądź nie we wszystkich sytuacjach), bez wywołania każdej z nich.

I w tym miejscy przydałoby się, przy tworzeniu metody modify(), sprawić, że byłaby ona finalna. Daje to programistom wyraźny znak ostrzegawczy: "ta metoda nie może być nadpisana!".

Oczywiście, nie zmienia to faktu, że praca wykonana przez programistę z przykładu i tak musiałaby zostać wykonana. Jednak z pewnością pozwoliłoby to uniknąć ewentualnych zgłoszeń o bugach od klienta.

Kiedy warto się zastanowić nad utworzeniem metody finalnej? Zawsze wtedy, gdy metoda przeprowadza złożone operacje, które są ściśle ze sobą powiązane.

klasy finalne

Załóżmy, że stworzyliśmy sobie klasę JsonView, która służy nam do wyświetlania modeli w formacie JSON:
<?php
class JsonView
{
  private  $_model;

  public function __construct(Model $model)
  {
    $this->_model = $model;
  }

  public function getRenderedView() {/* ... */}
}
Pewnego dnia natrafiamy na przypadek, że dane z modelu muszą dodatkowo zostać przetłumaczone. Programista, który jest odpowiedzialny za implementację dochodzi do wniosku, że najlepiej byłoby dodać obiekt klasy translacyjnej przy tworzeniu widoku. Idąc po najmniejszej linii oporu tworzy kolejną klasę, dzięki czemu unika refaktoryzacji (czyli, w jego odczuciu, straty czasu):
<?php
class JsonViewTranslable extends JsonView
{
  private  $_translator;

  public function __construct(Model $model, Translator $translator)
  {
    parent::__construct(Model $model);
    $this->_translator= $translator;
  }

  //@overriden
  public function getRenderedView() {/* ... */} 
}
Po pewnym czasie, już inny programista, używając klasy JsonViewTranslable, odkrywa, że w jego implementacji musi istnieć możliwość ustawienia, które pola modelu mogą zostać zwrócone, a które nie. Rozwiązuje ten problem po raz kolejny poprzez dziedziczenie.

Zdaję sobie sprawę, że powyższy przykład nie jest zbyt wyszukany, ale dzięki temu na pierwszy rzut oka widać, że dało się to wszystko rozwiązać inaczej, niż poprzez dziedziczenie.
Jaki tak naprawdę jest problem z tym dziedziczeniem? Jeżeli jest klasa z kilkoma metodami i podczas dziedziczenia (kilkukrotnego) zostają one nadpisane (czasami po kilka razy), różne metody w różnych klasach, to zarządzanie czymś takim, to prawdziwy koszmar. Z drugiej strony mamy w miarę spójny logicznie kod rozsiany po kilku klasach, a to z pewnością dobre nie jest.

Czasami opłaca się stworzyć klasę finalną, żeby uzmysłowić programiście, że jeżeli myślał nad rozszerzeniem tej klasy to najprawdopodobniej nie tędy droga.

na koniec

Tym razem podaruję sobie dalsze przykłady, ponieważ powyższe w wystarczający sposób obrazują istotę klas i metod finalnych.

Chciałbym jeszcze dodać, że tego słowa nie należy nadużywać, ponieważ jeżeli w aplikacji co druga klasa będzie finalna lub będzie miała finalną metodę, to ostrzeżenie straci na mocy. Jednak w istotnych lub/i skomplikowanych miejscach warto czasami poświęcić 5 minut i się nad tym zastanowić.

Poza tym, jeżeli projekt aplikacji jest dobry, to z dużą dozą prawdopodobieństwa można założyć, że nie będzie potrzeby dziedziczenia poza miejscami (klasami), które są do tego celu stworzone.