wtorek, 17 lipca 2012

ten pieprzony init()

ten pieprzony init

Zend w wersji 1.x obfitował w klasy, które posiadały deklarację pustej metody init(), która była wywoływana w konstruktorze.

Do czego jest ona wykorzystywana? Twórcy Zenda doszli do wniosku, że jeżeli chcesz np. stworzyć odpowiednio skonfigurowany formularz (np. do dodawania produktów), to idealnym rozwiązaniem będzie rozszerzenie klasy Zend_Form i umieszczenie całej kofiguracji owego formularza w nadpisanej metodzie init(), która wykona się przy tworzeniu nowego obiektu.

Sprytne, no nie? I jeszcze w dodatku można pokusić się o stwierdzenie, że jest to implementacja wzorca template method.

Chciałbym jednak zasmucić wszystkich tych, którzy praktykują takie rozwiązanie. Ani to sprytne nie jest, a użycie wzorca jest niepoprawne i niepotrzebne.

problem jest dużo większy

Przykład z Zenda, to tylko wstęp, demonstracja problemu. W ogóle, problemem są wszelkie dziedziczenia, które mają na celu stworzenie klasy, której obiekty będą po prostu odpowiednio skonfigurowanymi instancjami rodzica np.
<?php
class Field
{
  private $_name;

  public function __construct($name)
  {
    $this->_name = $name;
  }
  /* code */
}

class Form
{
  private $_fields = array();

  public function addField(Field $field)
  {
    $this->_fields[] = $field;
  }
  /* code */
}

class ProductForm extends Form
{
  public function __construct()
  {
    $this->addField(new Field('name'));
    $this->addField(new Field('description'));
    $this->addField(new Field('price'));
  }
}
Czym jest klasa ProductForm? Dlaczego rozszerza klasę Form? Czym się różni? Jaką nową funkcjonalność dodaje? Modyfikuje?

never do that again

Jeżeli zdarzyło Ci się stosować takie rozwiązanie, to przestań!
Dziedziczenie to bardzo silna i wiążąca zależność pomiędzy klasami i powinno się ją wykorzystywać tylko i wyłącznie w sytuacjach, gdy jest naprawdę konieczna.

co dalej?

Czy to oznacza, że tego typu obiekty należy tworzyć tam, gdzie są wykorzystywane? Budować je w tych miejscach? A co, gdy są wykorzystywane dwukrotnie, w dwóch różnych miejscach? Co gdy ich konfiguracja, to 20-30 i więcej linijek kodu?

Jest doskonałe rozwiązanie tego problemu. Nazywa się Abstract Factory:)

Kod z przykładu, bez dziedziczenia, z użyciem wzorca, po refaktoryzacji, wygląda tak:
<?php
class ProductFormFactory
{

  /**
   * @return Form
   */
  public function create()
  {
    $form = new Form();

    $form->addField(new Field('name'));
    $form->addField(new Field('description'));
    $form->addField(new Field('price'));

    return $form;
  }
}
Czy tracimy coś na takim rozwiązaniu? Nie. Co zyskujemy? Unikamy dziedziczenia, klasa tworząca instancje klasy Form nie jest w tak dużym stopniu od niej zależna. Nie tworzymy zależności, której nie ma potrzeby tworzyć.

9 komentarzy:

  1. (cyt):Jeżeli zdarzyło Ci się stosować takie rozwiązanie, to przestań!

    A co w przypadku:

    class Form
    {
    (cut)
    protected function addField(Field $field)
    (cut)
    }

    ?

    Formularz, to formularz - zamknięta całość, która jako całość powinna zajmować się sama sobą. Dlaczego metoda addField jest publiczna? Żeby inne obiekty mogły modyfikować formularz? Po co? Dlaczego? Jeśli jestem formularzem, to wiem jakie pola posiadam - jeśli jestem formularzem, któremu każdy może dodawać pola, to jaki jestem w tej chwili? Co mam robić z polami których nie znam/nie rozumiem? Jak mam je walidować?

    (cyt): Czy tracimy coś na takim rozwiązaniu? Nie.
    Tak. Dostęp do metod protected.

    (cyt): Co zyskujemy? Unikamy dziedziczenia, klasa tworząca instancje klasy Form nie jest w tak dużym stopniu od niej zależna. Nie tworzymy zależności, której nie ma potrzeby tworzyć.
    Tworzymy _nową_zależność_ której też nie ma potrzeby tworzyć.

    OdpowiedzUsuń
    Odpowiedzi
    1. Formularz to klasa, posiada swoją nazwę, jakieś ustawienia i pola. Formularz z nazwą 'dodaj produkt' i polami 'nazwa', 'opis', to już konkretna instancja formularza.
      Tak, jak z użytkownikami. Możesz mieć Janka, Marka, Anię itp., ale nie tworzysz klasy dla każdego użytkownika, , która będzie ustawiać wszystkie parametry go dotyczące. Nie, tworzysz instancję klasy User dla każdego z nich.

      Całkowicie nie rozumiem skąd pomysł na zmianę widoczności metody addField()? Taki zabieg narzuca dziedziczenie, ale jest całkowicie nieuzasadniony.

      Nie tworzymy żadnej zależności. Przydzielamy klasie odpowiedzialność tworzenia obiektu.

      Usuń
  2. Jeżeli zakładamy, że formularz odpowiada tylko za renderowanie formularza, to tak naprawdę by wystarczyła tablica z konfiguracją, którą klasa formularza przyjmowałaby jako parametr konstruktora. Można by też zamiast tablicy zastosować obiekt + builder.

    OdpowiedzUsuń
  3. Dlaczego wolę metodę init niż kombinowanie z pakowaniem formularza do jakiejś metody gdzieś tam? Ponieważ gdzieś tam zależy od programisty. Dla jednego będzie to metoda w kontrolerze, dla innego w modelu, a dla jeszcze innego w helperze widoku. Mając klasę dziedziczącą po Zend_Form od razu wiem czego się spodziewać - taka konwencja.
    Poza tym mając klasę formularza mogę sprawdzić czy dziedziczy po Zend_Form, czy po jakiejś klasie pośredniej i na tej podstawie wykonać dodatkowe operacje. W Twoim przypadku jest to niemożliwe. Inny przykład - dekoratory. Domyślne nie zawsze odpowiadają. Mam z tego powodu tworzyć kolejną metodę tylko po to, by je ustawić tak jak potrzebuję? O wiele lepiej jest zrobić to w klasie dziedziczącej po Zend_Form, a potem skorzystać z tego w klasie potomnej (jeśli będzie taka potrzeba).
    Odpowiadając na pytanie "co tracimy". Tracimy metodę setOptions, którą można wykorzystać na wiele ciekawych sposobów. Tracimy również polimorfizm (przykład z dekoratorami).

    OdpowiedzUsuń
    Odpowiedzi
    1. Nie chcę dyskutować na temat klasy Zend_Form, bo wykorzystałem ją tylko jako przykład. Może rzeczywiście tracimy kilka metod i funkcjonalności, ale wynika to z faktu, że klasa była projektowana z myślą o rozszerzaniu.

      Nie rozumiem za bardzo Twojego argumentu przeciw umieszczaniu gdzieś tam. U mnie gdzieś tam, to konkretna klasa tworząca gotowy obiekt i daje Ci taki sam obraz tego, co jest stworzone, jak nowa klasa, która rozszerza.

      "mając klasę formularza mogę sprawdzić czy dziedziczy po Zend_Form, czy po jakiejś klasie pośredniej i na tej podstawie wykonać dodatkowe operacje" i jeżeli rzeczywiście dodajesz jakieś dodatkowe operacje, to nie twierdzę, że nie powinno się dziedziczyć. Po to właśnie dziedziczenie się stosuje, aby rozszerzać/zmieniać/uzupełniać funkcjonalność rodzica.

      We wpisie jednak piszę jedynie o dziedziczeniu, które ma na celu stworzenie konkretnej instancji (z polami itp.), co uważam za błędne.

      Jeżeli nie dodajemy żadnej nowej funkcjonalności, to nie tworzymy nowej klasy per formularz, tylko tworzymy instancję klasy Form. A żeby było to wszystko czytelniejsze, to budowanie owej klasy najlepiej jest przerzucić gdzieś indziej. I tutaj z pomocą przychodzi nam Fabryka:)

      Usuń
    2. Metoda init została po to stworzona, by klasa dziedzicząca mogła dodać swoją funkcjonalność, która wykona się w danym momencie. We wspomnianym Zend_Form dodatkową funkcjonalnością jest utworzenie konkretnego formularza.

      Usuń
    3. A ja nadal śmiem twierdzić, że dopięcie pól i konfiguracja formularza, to powinna być nowa instancja obiektu, a nie kolejna klasa. Bo to nie jest nowa funkcjonalność, to jedynie ustawianie atrybutów klasy Zend_Form.

      Utworzenie konkretnego formularza, to nie nowa funkcjonalność. Tworzenie nowego formularza, to użycie operatora new i odpowiednie ustawienie atrybutów klasy:)

      Usuń
  4. Damian Zientalak17 lipca 2012 18:18

    Klasa "Field" chyba nie jest dobrym pomysłem. W Symfony2 jest tzw. FormTypeInterface, który może być całym formularzem jak i poszczególnym polem. Dochodzi tam jeszcze budowniczy formularzy, który docelowo zwraca FormInterface. Warto się tym zainteresować.

    OdpowiedzUsuń
    Odpowiedzi
    1. To tylko przykład, który ma na celu zademonstrowanie problemu błędnego wykorzystania dziedziczenia.

      Usuń