sobota, 20 października 2012

o fabrykach i interfejsach historia krótka

usprawiedliwienie

Na wstępie zaznaczam, że wpis nie jest przede wszystkim o wzorcu Factory (choć pojawia się on tutaj), wątkiem przewodnim nie są również interfejsy (chociaż i one się pojawią:). Najistotniejszym elementem tego wpisu jest próba przedstawienia procesu analizy, dzięki któremu usuwamy zbędne zależności pomiędzy klasami, a powiązania przestają opierać się na konkretnych implementachach.

wyobraźcie sobie...

... kod, gdzie macie kilka klas tworzących instancje obiektów:
<?php
class ProductFactory
{
  public function create($name, Money $price) { /* CODE */ }
}

class CustomerFactory
{
  public function create($name, Nip $nip) { /* CODE */ }
}

class InvoiceFactory
{
  public function create(InvoiceNumber $number) { /* CODE */ }
}

Jak widać w powyższym kodzie, do utworzenia produktu jest nam potrzebna jego nazwa i cena, do dodania nowego klienta potrzebujemy jego nazwy i nipu, a żeby wystawić nową fakturę potrzebujemy jej numeru.
Każda z tych klas jest wykorzystywana w innym kontrolerze, podczas wykonywania konkretnych operacji.

Patrząc na powyższy kod raczej nie może być mowy o żadnym interfejsie (deklaracje metod są różne) ani o żadnej klasie abstrakcyjnej (tworzymy instancje różnych klas, które w konstruktorze przyjmują różne parametry).

coś się jednak podobnego dzieje...

Pomimo tego, że metody klas posiadają różną implementację oraz deklarację, z logicznego punku widzenia pełnią one dokładnie tą samą funkcję, a to jest już coś, co daje nam podstawy do dalszych rozmyślań:) Nawet pomimo tego, że ich implementacja nie posiada żadnej części wspólnej.

część wspólna istnieje! tylko gdzieś indziej

W pewnym momencie zauważamy (czy to podczas projektu, czy podczas implementacji), że obiekty naszych klas są wykorzystywane w podobny sposób:
  • wyciągnięcie danych z żądania
  • walidacja danych
  • zwrócenie odpowiednich komunikatów w przypadku błędnych danych
  • stworzenie nowego obiektu
  • zapisanie go do repozytorium
W powyższych krokach mamy dwie zmienne: walidacja dla każdego zestawu danych jest inna oraz wymagane parametry również są różne.

Ok, no to mamy punkt wyjścia, co dalej?

walidacja danych

Najpierw skupmy się na klasie walidującej, co jej potrzeba? Jakiejś metody, która przyjmuje dane z żądania oraz zwróci nam informacje, czy te dane są poprawne, czyli potrzebujemy takiej metody:
<?php
/**
 * @return boolean
 */
public function areValid(RequestParams $params);

A skoro wymagamy takiej metody i nie obchodzi nas jej implementacja, to czy nie brzmi to jak interfejs?
<?php
interface ValidatorInterface
{
  /**
   * @return boolean
   */
  public function areValid(RequestParams $params);
}

Czym różnią się poszczególne walidatory? Muszą sprawdzić, czy każdy parametr jest poprawny, czyli muszą mieć zbiór reguł sprawdzających, czy dane parametry są poprawne. Każda reguła musi być przyporządkowana do odpowiedniego parametru, czyli rodzi nam się coś takiego:
<?php
class ValidatorElement
{
  public function __construct($name, RulesCollection $rules) { /* CODE */ }

  // ta metoda sprawdza, czy parametr spełnia określone reguły
  public function isValid($value) { /* CODE */ }
}

Każdy walidator posiada kolekcję obiektów klasy ValidatorElement, więc widzimy, że posiadają część wspólną (klasa abstrakcyjna?). Dodatkowo implementacja metody areValid() to nic innego jak przejście przez kolekcję elementów i sprawdzenie, czy konkretne parametry są poprawne (klasa abstrakcyjna??).

Nie klasa abstrakcyjna, a raczej normalna klasa. Jedyna różnica pomiędzy konkretnymi walidatorami to lista elementów (a właściwie reguł, które są w nich zawarte), czyli wystarczy, że dodamy metodę addElement() lub addElementsCollection() czy też dodamy te elementy w konstruktorze (bo w końcu są wymagane) i po wszystkim.

To czy potrzebny jest nam interfejs? Tak, a to ze względu na obecność metody, za pomocą której dodamy elementy, ale o tym pisałem już tutaj:)

tworzenie modeli

Skoro dotarliśmy do tego momentu to znaczy, że dane z żądania (RequestParams) są poprawne, czyli możemy tworzyć modele. Tylko jak?
Wiemy, że w RequestParams (kolekcja parametrów przekazanych w żądaniu) znajdują się poprawne dane wymagane do utworzenia konkretnej instancji obiektu. Jak je otrzymać w sposób generyczny? Który obiekt wie, że dane są poprawne i wie, które to dane? Oczywiście klasa służąca do walidacji:) Czyli mamy kolejną metodę do interfejsu, który teraz wygląda tak:
<?php
interface ValidatorInterface
{
  /**
   * @return boolean
   */
  public function areValid(RequestParams $params);

  /**
   * @return ValidParams
   */
  public function getValidParams();
}

Natomiast nasze klasy wytwórcze możemy przerobić w taki sposób, aby deklaracja metody create() wyglądała tak:
<?php
public function create(ValidParams $params);

Oczywiście implementacja nadal pozostaje różna, ale deklaracja stała się tak sama, a że od obiektów tej klasy, w każdym przypadku wymagamy wytworzenia konkretnego typu obiektu, to deklarację metody create() możemy wynieść do interfejsu:
<?php
interface ModelFactoryInterface
{
  /**
   * @return Model
   */
  public function create(ValidParams $params);
}

Oczywiście każda konkretna klasa musi implementować interfejs:)

złóżmy układankę w całość

Mamy już wszystkie elementu układanki, teraz wystarczy to tylko złożyć w całość:)Sposoby są dwa.

sposób pierwszy
Tworzymy w kotrolerze, który zawierać będzie całą logikę operacji dodawania, dwie metody abstrakcyjne:
<?php
/**
 * @return ModelInterface
 */
abstract protected function _getModelFactory();

/**
 * @return ValidatorInterface
 */
abstract protected function _getValidator();

sposób drugi
Tworzymy obiekt, który będzie nam zwracał fabrykę modelu oraz walidator, czyli również potrzebujemy interfejsu, aby uwspólnić metody zwracające te instencje:
<?php
interface ModelCreatorInterface
{
  /**
   * @return ModelInterface
   */
  public function getModelFactory();

  /**
   * @return ValidatorInterface
   */
  public function getValidator();
}

natomiast w kotrolerze dodajemy jedną metodę:
<?php
/**
 * @return ModelCreatorInterface
 */
abstract protected function getModelCreator();

co wybrać?
Na pierwszy rzut oka widać, że te rozwiązania są bardzo podobne, implementacje metod nie będą się w ogóle różniły. Jednak lepsze jest rozwiązanie drugie, ponieważ przenosimy odpowiedzialność za tworzenie klas na inny obiekt niż kotroler, a jak wiadomo kotrolery pełnią rolę zarządzającą, a nie twórczą:)

na koniec

Mam nadzieję, że w poprawny sposób przedstawiłem wszystkie czynniki, które wpłynęły na poszczególne decyzje projektowe.

Oczywiście taką analizę należy przeprowadzać jak najwcześniej, czyli najlepiej na etapie projektowania:) Ustrzeże Was to przed refaktoryzacją kodu oraz ułatwi jego utrzymywanie oraz rozwijanie.