poniedziałek, 25 maja 2020

Analiza Symfony - HttpKernel::handleRaw()

private function handleRaw(Request $request, int $type = self::MASTER_REQUEST): Response
{
    $this->requestStack->push($request);

    // request
    $event = new RequestEvent($this, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::REQUEST);

    if ($event->hasResponse()) {
        return $this->filterResponse($event->getResponse(), $request, $type);
    }

    // load controller
    if (false === $controller = $this->resolver->getController($request)) {
        throw new NotFoundHttpException(sprintf('Unable to find the controller for path "%s". The route is wrongly configured.', $request->getPathInfo()));
    }

    $event = new ControllerEvent($this, $controller, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
    $controller = $event->getController();

    // controller arguments
    $arguments = $this->argumentResolver->getArguments($request, $controller);

    $event = new ControllerArgumentsEvent($this, $controller, $arguments, $request, $type);
    $this->dispatcher->dispatch($event, KernelEvents::CONTROLLER_ARGUMENTS);
    $controller = $event->getController();
    $arguments = $event->getArguments();

    // call controller
    $response = $controller(...$arguments);

    // view
    if (!$response instanceof Response) {
        $event = new ViewEvent($this, $request, $type, $response);
        $this->dispatcher->dispatch($event, KernelEvents::VIEW);

        if ($event->hasResponse()) {
            $response = $event->getResponse();
        } else {
            $msg = sprintf('The controller must return a "Symfony\Component\HttpFoundation\Response" object but it returned %s.', $this->varToString($response));

            // the user may have forgotten to return something
            if (null === $response) {
                $msg .= ' Did you forget to add a return statement somewhere in your controller?';
            }

            throw new ControllerDoesNotReturnResponseException($msg, $controller, __FILE__, __LINE__ - 17);
        }
    }

    return $this->filterResponse($response, $request, $type);
}
Zależności klasy HttpKernel do innych komponentów Symfony


1. Zgłoszenie zdarzenia 


    Metoda przyjmuje jako pierwszy parametr obiekt żądania klasy Request pochodzącej z komponentu HttpFoundation, zwracając przy tym obiekt Response z tego samego komponentu.

    Parametr Request w pierwszej kolejności wrzucany jest na stos innych żądań, który jest instancją klasy HttpFoundation\RequestStack

    Następnie zgłaszane jest zdarzenie żądania za pomocą wstrzykniętego do konstruktora klasy HttpKernel,  obiektu implementującego EventDispatcherInterface pochodzącego z zewnętrznego komponentu EventDispatcher. Zdarzenie symbolizowane jest przez instancją klasy HttpKernel\Event\RequestEvent, a powiązana z nim nazwa zdarzenia to kernel.request. W przypadku gdy do aktualnego żądania istnieje powiązana odpowiedź, metoda w tym momencie ją zwróci.


2. Dobieranie kontrolera


    Za to zadanie odpowiedzialny jest obiekt ControllerResolverInterface (Constructor Injection) będący częścią komponentu jądra. Metoda getController przyjmuje jako parametr obiekt Request i w przypadku powodzenia operacji zwraca zmienną typu callable (akcja kontrolera) bądź - gdy nie zostanie sparowany odpowiedni kontroler - bool'owski false.  W klasie konkretnej resolver'a ControllerResolver,  znajdują się szczegóły implementacyjne dotyczące doboru kontrolera, zagadnienie to wymaga rozszerzenia w oddzielnym wpisie. W przypadku gdy:

  • nie dojdzie do sparowania kontrolera z obiektem żądania, wyrzucony zostanie wyjątek HttpKernel\Exception\NotFoundHttpException
  • \InvalidArgumentException będzie wyrzucony gdy dojdzie do wewnętrznych problemów resolver'a np. gdy sparowana akcja kontrolera nie będzie typu callable,
    Po pomyślnym wybraniu kontrolera na podstawie obiektu Request, zostanie zgłoszone zdarzenie kernel.controller za pośrednictwem  instancje EventDispatcher przechowywanej w chronionym polu $dispatcher

Ewentualna podmiana kontrolera


    Jako że mamy możliwość nasłuchiwania na zdarzenie kernel.controller (KernelEvents::CONTROLLER)  oraz dostęp do utworzonego obiektu Event\ControllerEvent w user land'owych klasach Listener/Subscriber, który posiada metodę publicznego interfejsu function setController(callable $controller): void, w ciele omawianej metody handleRaw musi znajdować się ewentualne pobranie innej instancji kontrolera:

$event = new ControllerEvent($this, $controller, $request, $type);
$this->dispatcher->dispatch($event, KernelEvents::CONTROLLER);
$controller = $event->getController();



3. Pobranie argumentów akcji


    Za to zadanie odpowiedzialny jest wstrzyknięty do konstruktora klasy HttpKernel obiekt implementujący interfejs ArgumentResolverInterface (należący do tego samego komponentu). W tym przypadku jako argumenty należącej do niego metody getArguments, przekazywana jest instancja klasy Request oraz callable $controllerLogika ukryta za tą warstwą abstrakcji nie będzie tutaj opisywana ponieważ zasługuje na osobny wpis - ważne jest, że metoda zwraca tablicę ze wszystkimi zadeklarowanymi w akcji parametrami.

    Następne kroki procesu przebiegają podobnie co w przypadku kontrolera: 

  1. utworzenie obiektu zdarzenia ControllerArgumentsEvent
  2. wysłanie go za pośrednictwem EventDispatcher'a z przypisaną nazwą kernel.controller_arguments 
  3. pobranie do zmiennych $controller i $arguments prawdopodobnie zmienionych instancji z obiektów typu *Event



4. Wywołanie akcji kontrolera


// call controller
$response = $controller(...$arguments);
   
    Posiadając callable akcji oraz wszystkie wymagane argumenty, nic nie stoi na przeszkodzie by pozyskać obiekt Response będący zwrotką każdej akcji kontrolera w Symfony. Mogłaby do tego procesu zostać użyta funkcja call_user_func_array() jednak autorzy zdecydowali się na wykorzystanie Operatora Variadic.


5. Zwrócenie obiektu Response


    W przypadku gdy zmienna $response nie przechowuje obiektu klasy Response, emitowane jest zdarzenie kernel.view. W tym miejscu, algorytm oczekuje, że istnieją nasłuchiwacze, które zapewnią obiektowi zdarzenia ViewEvent, pożądaną przez niego instancje klasy Response. Jeżeli takowa nie zostanie wykryta, prawdopodobnie developer nie zwrócił nic w akcji kontrolera, czego konsekwencją będzie wyrzucenie wyjątku ControllerDoesNotReturnResponseException. W procesie implementacji można uchronić się przed tego typu problemem, deklarując typ zwracany przez akcję. Dzięki temu w przypadku ewentualnego pominięcia return'a, zostaniemy o tym poinformowani już podczas kompilacji.

    Gdy mamy dostęp do obiektu Response,  metoda go zwraca i emituje - pośrednio, przez metody prywatne - dwa zdarzenia: kernel.response oraz kernel.finish_request (ściągając przy tym instancje Request ze stosu RequestStack). 


Podsumowanie wszystkich emitowanych zdarzeń w metodzie handleRaw




niedziela, 19 kwietnia 2020

Komunikacja pomiędzy komponentami (Fasady i Adaptery)

 
Platon's Cave źródło



    Tworząc aplikację opartą na komponentach - pomimo ich niezależności - czasami przychodzi potrzeba komunikacji między nimi. Zależności powinny być ograniczone do niezbędnego minimum, a liczba udostępnionych klas jak najmniejsza. Należy też dbać o to by komunikacja między komponentami była jednostronna. Przypadek w którym wymiana danych następuje dwustronnie, świadczy o niewłaściwym rozdzieleniu odpowiedzialności w modułach.

Bezpośrednie odwołania do zewnętrznego komponentu


╔════════════════════════════════════╗     ╔════════════════════════════════════╗
║          Component Order           ║     ║         Component Product          ║
╠════════════════════════════════════╣     ╠════════════════════════════════════╣
║ ┌───────┐                          ║     ║ ┌───────┐                          ║
║ │ Class │            ┌───────┐     ║     ║ │ Class │               ┌───────┐  ║
║ └───────┘┌───────┐   │ Class │     ║     ║ └───────┘   ┌───────┐   │ Class │  ║
║          │ Class │   └───────┘     ║     ║      ┌─────>│ Class │   └───────┘  ║
║          └───────┘                 ║     ║      │      └───────┘              ║
║    ┌───────┐                       ║     ║      │   ┌───────┐      ┌───────┐  ║
║    │ Class ├───────────────────────╫─────╫──────┘   │ Class │   ┌─>│ Class │  ║
║    └───────┘                       ║     ║          └───────┘   │  └───────┘  ║
║                  ┌───────┐         ║     ║ ┌────────────────────┤             ║
║      ┌───────┐   │ Class ├─────────╫─────╫─┤  ┌───────┐         │   ┌───────┐ ║
║      │ Class │   └───────┘         ║     ║ └─>│ Class │         └──>│ Class │ ║
║      └───────┘                     ║     ║    └───────┘             └───────┘ ║
╚════════════════════════════════════╝     ╚════════════════════════════════════╝


    W tym przypadku Order Component musi mieć pełną wiedzę na temat usług jakie oferuje Product Component


namespace CompanyName\OrderComponent\Service;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Model\ProductFactory;

final class CreateOrder
{
    private ProductInterface $product;

    public function __constructor(ProductFactory $productFactory)
    {
        $this->product = $productFactory->getInstance();
    }
 
    // Interfejs publiczny
}

 
⚠️ Komponent Order musi posiadać wiedzę na temat współdziałania ze sobą klas z innego komponentu, ich umiejscowienia i przeznaczenia, 
⚠️ ma dostęp do klas reprezentujących szerokie spektrum zachowań, gdy tak naprawdę potrzebuje tylko części z nich,
⚠️ jest całkowicie uzależniony od interfejsu publicznego klas zewnętrznego modułu, w przypadku jego zmiany istniałaby możliwość modyfikacji kodu który z nich korzysta.

    Komponent Produktu który pomimo, że jest niezależnym bytem udostępnia swoje usługi innym komponentom, a w związku z tym powinien w jakiś sposób demonstrować ten zamiar. Nawiązując komunikacje z zewnętrznym komponentem, nie wiemy jakie zachowania mogą wykraczać poza jego granicę, a który absolutnie nie powinny.   

    Ujawnianie zewnętrznych usług można zrealizować tworząc specjalną klasę Fasady przeznaczonej tylko w tym celu. Leży ona na granicy komponentu i zbiera wszystkie zachowania, które mogą być udostępnione klientom. Prezentując usługi w ten sposób tworzymy niejako komunikacyjny interfejs publiczny dla innych części systemu, ograniczając wiedzę na temat jego wewnętrznego działania.

    Jest to oczywiście wszystko w codziennych decyzjach (dobrej woli) zespołu deweloperskiego by przestrzegać z dyscypliną tych zasad, ponieważ nie ma fizyczny barier by ominąć Fasadę. 

Fasada po stronie usługodawcy



╔════════════════════════════════════╗     ╔════════════════════════════════════╗
║           Component Order          ║     ║          Component Product         ║
╠════════════════════════════════════╣     ╠════════════════════════════════════╣
║ ┌───────┐                          ║    ┌╨────┐ ┌───────┐                     ║
║ │ Class │            ┌───────┐     ║    │  F  │ │ Class │                     ║
║ └───────┘┌───────┐   │ Class │     ║    │     │ └───────┘          ┌───────┐  ║
║          │ Class │   └───────┘     ║    │  A  │         ┌───────┐  │ Class │  ║
║          └───────┘                 ║    │     ├────────>│ Class │  └───────┘  ║
║    ┌───────┐                       ║    │  S  │         └───────┘             ║
║    │ Class ├───────────────────────╫───>│     │      ┌───────┐     ┌───────┐  ║
║    └───────┘                       ║    │  A  │      │ Class │  ┌─>│ Class │  ║
║                  ┌───────┐         ║    │     │      └───────┘  │  └───────┘  ║
║                  │ Class ├─────────╫───>│  D  │  ┌──────────────┤             ║
║      ┌───────┐   └───────┘         ║    │     ├──┤   ┌───────┐  │   ┌───────┐ ║
║      │ Class │                     ║    │  A  │  └──>│ Class │  └──>│ Class │ ║
║      └───────┘                     ║    └╥────┘      └───────┘      └───────┘ ║
╚════════════════════════════════════╝     ╚════════════════════════════════════╝


Na fasadę InternalCommunication został nałożony interfejs InternalCommunicationInterface by ułatiwć testowanie.

namespace CompanyName\ProductComponent\Facade;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Model\ProductFactory;

final class InternalCommunication implements InternalCommunicationInterface 
{
    private ProductFactory $productFactory;

    public function __constructor(ProductFactory $productFactory)
    {
        $this->productFactory = $productFactory;
    }

    /**
     * @inheritDoc
     */
    public function getProduct(): ProductInterface
    {
        return $this->productFactory->getInstance();
    }
}

Klient Fasady nie ma wiedzy na temat tworzenia klasy ProductInterface:


namespace CompanyName\OrderComponent\Service;

use CompanyName\ProductComponent\Model\ProductInterface; 
use CompanyName\ProductComponent\Facade\InternalCommunicationInterface;

final class CreateOrder
{
    private ProductInterface $product;

    public function __constructor(InternalCommunicationInterface $productFacade)
    {
        $this->product = $productFacade->getProduct();
    }
 
    // Interfejs publiczny
}

✔️ Komponent Product udostępnia interfejs publiczny innym częścią systemu, hermetyzując wiedzę o jego wewnętrznym działaniu,
⚠️ komponenty konsumenckie w dalszym ciągu, bezpośrednio operują na obiektach z zewnętrznego komponentu, oraz wiedzą o ich klasach.


Adaptery po stronie usługobiorcy



    Aby jak najbardziej uniezależnić klasy korzystające z usług zewnętrznego komponentu, najlepiej byłoby operować w nich tylko na ich lokalnych odpowiednikach. Obiekt Product w różnych częściach systemu może charakteryzować się innymi zachowaniami determinowanymi przez kontekst komponentu, dlatego najlepiej jakby każdy z nich posiadał swój interfejs dla klas reprezentujących produkty. W tym celu, z innym komponentem komunikować się będą jedynie klasy będące Adapterami realnych obiektów zwracanych przez Fasadę zewnętrznego komponentu.


╔════════════════════════════════════╗        ╔════════════════════════════════════╗
║          Component Order           ║        ║          Component Product         ║
╠════════════════════════════════════╣        ╠════════════════════════════════════╣
║ ┌───────┐                          ║       ┌╨────┐ ┌───────┐                     ║
║ │ Class │            ┌───────┐     ║       │  F  │ │ Class │                     ║
║ └───────┘┌───────┐   │ Class │     ║       │     │ └───────┘          ┌───────┐  ║
║          │ Class │   └───────┘     ║       │  A  │         ┌───────┐  │ Class │  ║
║          └───────┘                 ║       │     ├────────>│ Class │  └───────┘  ║
║    ┌───────┐                   ┌───╨───┐   │  S  │         └───────┘             ║
║    │ Class ├──────────────────>│Adapter│──>│     │      ┌───────┐     ┌───────┐  ║
║    └───────┘                   └───╥───┘   │  A  │      │ Class │  ┌─>│ Class │  ║
║                ┌───────┐       ┌───╨───┐   │     │      └───────┘  │  └───────┘  ║
║                │ Class ├──────>│Adapter│──>│  D  │  ┌──────────────┤             ║
║      ┌───────┐ └───────┘       └───╥───┘   │     ├──┤   ┌───────┐  │   ┌───────┐ ║
║      │ Class │                     ║       │  A  │  └──>│ Class │  └──>│ Class │ ║
║      └───────┘                     ║       └╥────┘      └───────┘      └───────┘ ║
╚════════════════════════════════════╝        ╚════════════════════════════════════╝

    Adapter produktu z innego komponentu implementuje interfejs OrderedProductInterface, który znajduje się w katalogu Model wraz z innymi klasami tego typu w komponencie OrderComponent. Dzięki takiemu zabiegowi, klasy logiki biznesowej znajdujące się w przestrzeni nazw \CompanyName\OrderComponent\* (np. klasy serwisowe) nie zdają sobie sprawy, że operują na owiniętym obiekcie z innego komponentu. Aby ułatwić korzystanie z tego adaptera przez inne klasy, należy zarejestrować go w Kontenerze Zależności i ukryć go pod interfejsem który implementuje. 
    W celu zachowania przejrzystości projektu, należy umieszczać wszystkie klasy komunikujące się z innymi komponentami w jednym katalogu. Spoglądając na ich zadeklarowane importy innych klas, łatwo będzie można dostrzec zależności do klasy z poza komponentu macierzystego. 


namespace CompanyName\OrderComponent\Adapter;

use CompanyName\ProductComponent\{
    Model\ProductInterface,
    Facade\InternalCommunicationInterface
};
use CompanyName\OrderComponent\Model\OrderedProductInterface;

final class ProductAdapter implements OrderedProductInterface
{
    private ProductInterface $product;

    public function __constructor(InternalCommunicationInterface $productFacade)
    {
        $this->product = $productFacade->getProduct();
    }
 
    /**
     * @inheritDoc
     */
    public function someBehavior(): bool 
    {
        return $this->product->someBehavior();
    }
}

    Ostateczna forma serwisu domenowego z komponentu OrderComponent nie posiada żadnej wiedzy na temat innych komponentów.


namespace CompanyName\OrderComponent\Service;

use CompanyName\OrderComponent\Model\OrderedProductInterface;

final class CreateOrder
{
    private OrderedProductInterface $product;

    public function __constructor(OrderedProductInterface $product)
    {
        $this->product = $product;
    }
 
    // Interfejs publiczny
}


poniedziałek, 13 kwietnia 2020

Array jako struktury danych - konsekwencje


Główne problemy sprowadzają się do:

  • odwoływania się do nieistniejącego indeksu tablicy w kodzie klienta,
    • nie można dokumentować struktury tablicy w @docBlock,
    • brak definiowania typów,
    • tablica może być modyfikowana w czasie RunTime,
    • walidacja tablicy musiała by się znajdować w każdej metodzie która korzysta z tej tablicy,
    • trudno zdefiniować czy tablica jest w poprawnym stanie,
    • komunikat Notice zgłaszany na odwołanie się do nieistniejącego indeksu, często jest przyczyną błędnie wyprodukowanej struktury danych - gdzie powinien być zgłaszany, 
  • braku jawnego powiązania z logiką, 
    • zachowania należące do struktury mogą być rozproszone na wiele klas, 
    • odnalezienie wszystkich powiązanych zachowań wymaga przejrzenia klas zależnych (w optymistycznym przypadku) od klasy produkującej tablicową strukturę klasy, 
    • trudno zdefiniować czy dane zachowanie należy do tablicy,

Komunikaty Notice błędnej struktury


    Jeżeli obiekt typu
array zostanie utworzony w metodzie A i przerzucimy go do metody B, potem C by dopiero tam odwołać się do nieistniejącego klucza, komunikat NOTICE będzie wyrzucony w tym właśnie miejscu. Zgłoszony komunikat informuje tylko o nieistniejącym kluczu tablicy, linii i pliku w którym wystąpi, nie zawiera on informacji o prawdziwej przyczynie błędu więc albo jest to nieprawidłowość po stronie klasy klienckiej bo oczekuje innej struktury, albo array został niepoprawnie utworzony lub przekazany z niewłaściwym stanem. Tak czy inaczej, może to prowadzić do żmudnego ustalania przyczyny problemu, w kilku metodach przez które został przerzucany od momentu jego utworzenia. Problem ten występuje głównie w tablicach ze skomplikowaną strukturą, często przechowujących wartości z kilku dziedzin domenowych.

                       ╔═════════════════════════════╗
                       ║createProductArray(): array  ║
                       ╟─────────────────────────────╢
                       ║return [name => 'something'];║
                       ╚══════════════╤══════════════╝
                                      │
                  ┌───────────────────┴─────────────────────┐
                  │                                         │
                  V                                         V
┌────────────────────────────────────┐ ┌─────────────────────────────────────────┐
│              Use Case 1            │ │                Use Case 2               │
├────────────────────────────────────┤ ├─────────────────────────────────────────┤
│╔══════════════════════════════════╗│ │╔═══════════════════════════════════════╗│
│║addPriceKey(array $product): array║│ │║processSomething(array $product): array║│
│╟──────────────────────────────────╢│ │╟───────────────────────────────────────╢│
│║$product['price'] = $totalPrice;  ║│ │║// some implementation                 ║│
│╚════════════════╤═════════════════╝│ │╚═══════════════════╤═══════════════════╝│
│                 │                  │ │                    │                    │
│                 V                  │ │                    V                    │
│╔══════════════════════════════════╗│ │  ╔═══════════════════════════════════╗  │
│║usePriceKey(array $product): void ║│ │  ║usePriceKey(array $product): void  ║  │
│╟──────────────────────────────────╢│ │  ╟───────────────────────────────────╢  │
│║$totalPrice = $product['price'];  ║│ │  ║$totalPrice = $product['price'];   ║  │
│╚══════════════════════════════════╝│ │  ║//Notice: Undefined index          ║  │
│                                    │ │  ╚═══════════════════════════════════╝  │
└────────────────────────────────────┘ └─────────────────────────────────────────┘


    W powyższym przypadku głównym problemem jest sposób tworzenia przez metodę createProductArray() struktury z niepełnym stanem. Jeżeli liczba metody przez które przechodzi utworzony array jest duża, trudno ustalić w którym miejscu stan tablicy jest uzupełniany o wymagany element. 


Mutowalność tablic    


    Array może być zmieniany w czasie RunTime'a i nie mamy nad tym absolutnie ŻADNEJ kontroli. Jeżeli przechodzi on przez kilka metod, w każdym z tych miejsc może dojść do jego modyfikacji co stoi w opozycji do podejścia niemutowalnych obiektów, które są tworzone w pełni kompletnym-niemodyfikalnym stanem. W przypadku gdy metoda tworząca array'a jest wykorzystywana w kilku innych metodach klienckich może to znacznie spotęgować podatność na błędy. W przypadku obiektów jeżeli źródło danych zawiedzie, o niepoprawnie utworzonym obiekcie dowiemy się we właściwej klasie implementującej wzorzec Factory/Builder i warstwie systemu:
  • Infrastruktury - na styku bazy danych,
  • Aplikacji - dane z Request'a HTTP,

    Jeżeli zdecydujemy się walidować tablicę przed wykonaniem operacji, czynność tą będziemy musieli powtarzać w każdy miejscu w którym będziemy z niego korzystać. Można to działanie oddelegować do jakiejś metody statycznej dzięki czemu kod nie musiał by być powielany, ale zachowanie to tak czy inaczej nie byłoby w żadnym wypadku jawnie powiązane z tą strukturą danych. Deweloperzy musieliby zawsze pamiętać by użyć tej metody przed przystąpienie do działania na tej tablicy. W przypadku klas sytuacja jest jasna - publiczny interfejs mówi o tym do czego mamy dostęp, a projektując wedle zasady niemutowalności, mamy zapewnione, że operujemy ZAWSZE na poprawnym obiekcie.

    Jeżeli tworzymy tablicową strukturę danych z kluczami typu
Integer - kod klienta pobierającego/zapisującego do array'a jest niejasny, nieczytelny, wymaga przeanalizowania kodu w którym został utworzony.


Zachowania powiązane ze strukturami tablicowymi


    Logika związana z tablicową strukturą danych nie jest w stanie być ściśle z nią związana. Może być rozproszona w kilku 
prywatnych/publicznych metodach jednej klasy bądź w skali globalnej w wielu klasach. W związku z tym może dochodzić do - z pozoru niezauważalnych -duplikacji zachowań co sprawia trudności w późniejszym utrzymaniu aplikacji. Aby wyselekcjonować zachowania należące do jednej konkretnej tablicowej struktury danych należy prześledzić flow programu co jest czasochłonną czynnością. 

    Jeżeli deweloper niepracujący wcześniej z daną struktura danych, nie znajdzie powiązanej z nią określonego zachowania, będzie implementował podobną bądź identyczną logikę biznesową. Koniec końców będzie to zmarnowany czas na implementacje zduplikowany zachowania.  Gdy biznes zażąda modyfikacji tej funkcjonalności, trzeba będzie pamiętać o aktualizacji kodu w dwóch miejscach. Stosowanie tablicowych struktur danych niewątpliwie działa na niekorzyść projektu jak i wszystkich członków zespołu deweloperskiego. 


Schemat


    Poniżej znajduje się schemat metod ściśle powiązanych metod ze tablicową strukturą danych ProductArray zwracanej przez metodę createProductArray.  Jak widać struktura wykorzystywana jest (pośrednio i bezpośrednio) także poza macierzystą klasą co sprawia, że ewentualna refaktoryzacja nie będzie miała lokalnego charakteru. Można wyróżnić kilka typów metod:

  •      metody używające ProductArray tylko do utworzenia nowej struktury danych, lub wykonania innych działań, 
  •      metody zmieniające stan ProductArray dla istniejących kluczy,
  •           metody zmieniające strukturę ProductArray (poniekąd pod typ tablicowej struktury).  Dysponując w kodzie kilkoma typami jednej tablicowej struktury danych zwiększamy skomplikowanie korzystania z jej zachować. W tym przypadku trzeba wprowadzić dla każdej metody obsługującej konkretny podtyp - odpowiednią walidację kluczy oraz odpowiednie nazewnictwo tworzące symboliczne powiązanie
Można też rozróżnić sposób przekazywania struktury do zachowań:

  • pobranie struktury wewnątrz zachowania,
  • przekazanie struktury jako parametr metody, 



╔═════════════════════════════════════════════════════╗
║                   ProductService                    ║
╟─────────────────────────────────────────────────────╢
║   ┌───────────────────────────────────┐             ║
║        Metoda tworząca strukture                  ║
║   ├───────────────────────────────────┤             ║
┌─┤ public createProductArray(): array             ║
└───────────────────────────────────┘             ║
           ┌───────────────────────────────────┐   ║
           │public getOtherStructure(): array  │   ║
           ├───────────────────────────────────┤   ║
├──────────>│//call createProductArray()        │   ║
           └───────────────────────────────────┘   ║
                                                   ║
           ┌────────────────────────────┐          ║
           private addsNewKeys(): array          ║
           ├────────────────────────────┤          ║
├──────────>//call createProductArray()           ║
           └─────────────────────────┬──┘          ║
   ┌─────────────────────────────────┘             ║
          ┌─────────────────────────────────────┐ ║
          │public getDifferentStructure(): array│ ║
          ├─────────────────────────────────────┤ ║
   ├──────>│//call addsNewKeys()                 │ ║
          └─────────────────────────────────────┘ ║
          ┌──────────────────────────────┐        ║
          public addsAnotherKey(): array        ║
          ├──────────────────────────────┤        ║
   └──────>//call addsNewKeys()          ├────┐   ║
           └──────────────────────────────┘       ║
                                                  ║
╚═══════════════════════════════════════════════════╝
                                                 
                                                  
                                                 
╔═════════════════════════════════════════════╗ 
               OrderService                 ║ 
╟─────────────────────────────────────────────╢ 
└────────────────────────┐                   ║ 
║                          V                   ║ 
║┌───────────────────────────────────────┐     ║ 
║│public getOrder(array $product): array │     ║ 
║└───────────────────────────────────────┘     ║ 
║  ┌─────────────────────────────────────────────┘
║        ┌───────────────────────────┐       ║
║        public increasePrice(): void       ║
║        ├───────────────────────────┤       ║
║  └─────>//call addsAnotherKey()            ║
║         └───────────────────────────┘       ║
╚══════════════════════════════════════════════╝


Odnajdywanie zachowań struktury tablicowej 


    Wszystko zależy od modyfikatora dostępu metody tworzącej ProductArray,  zachowań zwracających strukturę, oraz tego które klasy zależą od klasy zawierającej powyższe metody.
Korzystając z IDE należy znaleźć wszystkie wywołania tych metod wewnątrz klasy.  Niestety, korzystając z tablic nie mamy szybkiego wglądu w kompletny zestaw zachować. W wielu przypadkach, klasa produkująca array ma wiele innych odpowiedzialności przez co jej całkowity rozmiar może przekraczać kilka tysiące linii kodu. Metody związane ze tablicową strukturą danych mieszają się z innymi przez co na pierwszy rzut oka nie można stwierdzić jakimi zachowaniami dysponujemy.

    Jak wcześniej wspomniałem, istnieje rodzaj metod, których zadaniem będzie dodanie nowych kluczy do struktury, co możemy identyfikować jako strukturę rozwiniętą bądź inny podtyp struktury. W związku z wyselekcjonowaniem wszystkich dostępnych zachowań należy mieć na uwadze, że niektóre metody będą należały tylko do struktur rozwiniętych.



Utrzymanie


    Skłonność do modyfikowania array'a, dodawania pól które musimy gdzieś przemycić i nie do końca pasują do reszty danych w array'u. Może się wydawać, że modyfikacja w ten sposób nie będzie niosła ze sobą większych konsekwencji, ale zapominamy że operujemy na strukturze danych. Deweloperzy w zespole zapewne poważniej przemyślą modyfikację klas i interfejsów pod tym kątem.

    Brak ściśle określonego miejsca dla nowych zachowań tablicy sprawi, że będą one dodawane głównie w klasie w której jest ona tworzona co poskutkuje nieustannie rosnącym jej rozmiarem.   



    Tablice nie przechowują ŻADNEJ wiedzy na temat ich poprawnej struktury co sprawia, że programiści zaczynający prace nad nieimplementowanym przez nich kodem nie mają 100% pewności, że zmieniając strukturę tablicy nie uszkodzą innych części systemu (o ile nie są te części zabezpieczone testami automatycznymi). Wiedza domenowa na temat struktury/zachowań tablicy może zostać zatarta przez lata, zmianę zespołu deweloperskiego jak i liczne przekształcenia wprowadzane na przestrzeni lat. 

    Jeżeli metoda zwraca array strukturalny ZAWSZE będzie to implikować wyżej opisane problemy.











niedziela, 5 kwietnia 2020

Alternatywa od tablic

    Jedną z najczęstszych czynności w implementowaniu rozwiązań biznesowych jest iterowanie przez kolejne elementy tablicy za pomocą konstruktu foreach.  Tablice te często są składane tuż przed samym ich wertowaniem przez co metody w których się znajdują są długie, nieczytelne i trudne w utrzymaniu/rozwoju. Rozwiązanie to nie jest zbytnio modułowe bo nawet jeżeli metoda zwraca poskładaną tablice jako jej Type Hint możemy zadeklarować tylko array, co sprawia, że tak naprawdę nie mamy pewność czy w po kilku modyfikacjach metody tworzącej tablicę, wartość zwracana będzie kompatybilna z wcześniej napisanym kodem klienta. 

    Jednym z największy problemów, który dotyka kod oparty o tablicę jest fakt, że nie mogą one zawierać w sobie żadnych zachowań - bo jest to tylko struktura danych. W konsekwencji, cała logika obróbki elementów tablicy (formatowanie poszczególnych wartości pojedynczych elementów czy filtrowanie elementów) spada do odpowiedzialności klasy, która je przetwarza. Sprawia to trudności w tworzeniu uniwersalnych rozwiązań, gotowych do wykorzystania w wielu miejscach. Gdy taki kod nie jest tworzonych z myślą o modułowości i przestrzeganiu zasady DRY, dochodzi do duplikacji (jawnych lub ukrytych), które mogą nieść ze sobą poważne konsekwencje w przyszłości, związane ze zmianą jednego globalnego zachowanie ich dotyczącego.  

    W codziennej pracy, możemy zauważyć ograniczenia klucza tablicy jakie narzuca sama struktura danych zaimplementowana w PHP. Wartość jaką może przyjąć może być jedynie typu String i Integer. Programiści dostosowując się do tej zasady umieszczają tam (jeżeli w ogóle) zwykle ID znajdującej się w wartości klucza encji/modelu bądź np datę w formacie 'Y-m-d'. W tym drugim przypadku implikuje to zwykle powtarzalną pracę, polegającą na tworzeniu wewnątrz pętli instancji DateTime na podstawie obecnie iterowanego elementu. Nie uważam, że jest to uciążliwość najwyższego priorytetu, ale w przypadkach gdy widzę tego typu niewielkie nieudogodnienia, staram się znaleźć i stosować lepsze rozwiązania.

    Programiści PHP często decydują się na stosowanie tablic, ze względu na szybkość wdrożenia rozwiązania pewnego problemu. Wystarczy jedynie poskładać array'a tuż przed jego wykorzystaniem, w metodzie realizującą pewne zachowanie biznesowe. Jak się okazuje, jest to tylko pozorna szybkość bo wraz ze stosowaniem tablic, przemycamy do kodu inne problemy-spowalniacze, które są jedynie odroczone w czasie, ale prędzej cz później będziemy musieli się zmierzyć z ich refaktoryzacją. O ile z tej sytuacji można byłoby jeszcze wyjść obronną ręką i oddelegować implementację do klas wielokrotnego użytku, często w dalszym ciągu podejmowane są złe decyzje. Deweloperzy brną w stosowanie licznych instrukcji warunkowych komplikujących sprawę coraz bardziej, bo uważają, że sprawy zaszły za daleko i naprawą będzie zbyt czasochłonna (opcja pesymistyczna: gdy brak im wiedzy/doświadczenia i nie potrafią inaczej). Wszystko to mogłoby się nigdy nie wydarzyć, gdyby od razu stosowane były dobre wzorce - niekoniecznie idealne - sprawiające, że kod napisany dzisiaj jest przygotowany na to co może nadejść jutro.  

Stosowanie tablic, które utrudnia pracę w projekcie mogę podzielić na dwa typy:

  1. tablice tworzące struktury danych,
  2. tablice w kodzie klienta, przechowujące elementy będące tablicową-strukturą danych (z podpunktu pierwszego) tworzone w celu przetwarzania ich w pętli
W tym wpisie odniosę się do podpunktu drugiego.

Oddelegowanie array'a do oddzielnej klasy


use App\Model\Product;

final class Products 
{
    /** @var array|Product[] */ private $list = [];
}

klasa wielokrotnego użytku, możliwa do zarejestrowania w Kontenerze Zależności, 
 komentarz @docBlock wskazujący IDE i inny programistą jakiego typu obiekty przechowywane są w tablicy,
✔ kod kliencki może Type Hint'ować na tą klasę, dzięki czemu mamy pewność, że operujemy w nim na pożądanej strukturze,
✔ testowalny,


Metoda dodająca element do tablicy


use OverflowException;
use App\Model\Product;

[...]

public function add(Product $product): void 
{
    if ($this->has($product)) {
        throw new OverflowException;
    }
    $this->list[$product->id()] = $product;
}

private function has(Product $product): bool
{
    return array_key_exists($product->id(), $this->list);
}

✔ daje pewność, że obiekty znajdujące się w tablicy będą tylko jednego typu,
✔ nie pozwala na dodanie takiego samego elementu do tablic drugi raz, a więc zawsze będziemy mogli operować na walidowalnej strukturze,

Umożliwienie klientom iterowania instancji w pętli foreach 


    Aby możliwe byłby iterowanie się w pętli foreach po obiekcie w ustalony przez nas sposób, klasa musi implementować wewnętrzny interfejs silnika - Traversable.  Nie można go jednak implementować bezpośrednio do klasy, musimy zatem skorzystać z jednego z dwóch interfejsów zapewnionych przez PHP:

  1. Iterator,
  2. IteratorAggregate,

           ╔═══════════════════╗
           ║      Iterable     ║
           ╚═════════╤═════════╝
          ┌──────────┴───────────┐
╔═════════╧═════════╗  ╔═════════╧═════════╗
║       Array       ║  ║    Traversable    ║
╚═══════════════════╝  ╚═════════╤═════════╝
                      ┌──────────┴───────────┐
            ╔═════════╧═════════╗  ╔═════════╧═════════╗
            ║      Iterator     ║  ║ IteratorAggregate ║
            ╚═══════════════════╝  ╚═══════════════════╝


W przypadku klasy Products wykorzystam IteratorAggregate by sam proces iteracji był odseparowanym od operacji uzupełniania listy.


use IteratorAggregate;
use Iterator;
use ArrayIterator;
use EmptyIterator;

final class Products implements IteratorAggregate
{
    [...]

    /**
     * @inheritDoc
     * @return Iterator<int, Product>
     */
    public function getIterator(): Iterator
    {
        return $this->list ? new ArrayIterator($this->list)
                           : new EmptyIterator;
    }
}


Schemat wykorzystanych klas:


                ╔═══════════════════╗
                ║   <<Interface>>   ║
                ╟───────────────────╢
                ║      Iterator     ║
                ╚═════════╤═════════╝
                          │
╔══════════════════════╗  │
║Biblioteka Standardowa║  │
╠══════════════════════╩══╪═══════════════════════╗
║           ┌─────────────┴────────────┐          ║
║           │                          │          ║
║ ╔═════════╧═════════╗                │          ║
║ ║   <<Interface>>   ║                │          ║
║ ╟───────────────────╢                │          ║
║ ║ SeekableIterator  ║                │          ║
║ ╚═════════╤═════════╝                │          ║
║           │                          │          ║
║ ╔═════════╧═════════╗     ╔══════════╧════════╗ ║
║ ║   ArrayIterator   ║     ║   EmptyIterator   ║ ║
║ ╚═══════════════════╝     ╚═══════════════════╝ ║
║                                                 ║
╚═════════════════════════════════════════════════╝

    
    Kiedy lista posiada jakieś elementy, zwracana jest instancja SPL'owej klasy ArrayIterator. Co prawda klasa Biblioteki Standardowej posiada inne zachowania poza metodami iteracyjnymi jak sortowania czy serializacja, ale metoda (return Type HintgetIterator(): Iterator zapewnia nam płaszcz ochronny dzięki któremu klasy klienckie będą wiedzieć tylko o tym, że korzystają z podstawowego iteratora.  Jeżeli lista jest pusta zwrócony jest obiekt SPL'owej klasy EmptyIterator będący implementacją wzorca projektowego Null Object.

    To w zasadzie tylko jedyne zachowanie, które wychodzi na zewnątrz i udostępnia dane klasie klienta. Nie dodaję tutaj żadnych innych metod w rodzaju getById(int $id): Product ponieważ nie takie są intencje tej klasy. Jeżeli naprawdę potrzebujesz jakiegoś konkretnego elementu z listy, próba pozyskiwania go w taki sposób jest nadużyciem - w tym przypadku powinno się przemyśleć inne rozwiązanie niż na siłę implementować je tutaj. 


Implementacja interfejsu Countable z Biblioteki Standardowej (opcjonalnie)


    Aby zapewnić funkcjonalność tablic PHP'owych i w klasach klienckich używać na instancji klasy Products funkcji count(), musimy zaimplementować SPL'owy interfejs Countable z jedną metodą count(): int.


use IteratorAggregate;
use Countable;


final class Products implements IteratorAggregate, Countable
{
    [...]

    /**
     * @inheritDoc
     */
    public function count(): int
    {
        return count($this->list);
    }
}

    W tym przypadku logika jest bardzo prosta, być może w innych wypadku obliczenia będą naprawdę skomplikowane (listy generowane w locie). Jeżeli będziemy tworzyli bardziej wyspecjalizowaną klasę np. ActiveProducts będącą implementacją wzorca Dekorator,  będzie miała ona dostęp do wszystkich elementów listy (aktywnych/nieaktywnych), wystarczy jedynie zapewnić nową implementacji metody count(): int

 kod klienta opiera się na natywnej funkcji PHP,
 przeniesienie logiki obliczania długości z klienta do klasy listy, 



Modyfikacja zachowań poprzez użyciu klas dekorujących


Obiekt DateTime jako klucz iterowanego elementu.


Schemat klasy IteratorIterator (wzorzec Dekorator)


    ╔═════════════╗
    ║<<Interface>>║
    ╟─────────────╢
    ║  Iterator   ║
    ╚══════╤══════╝

           │            
╔══════════╧═══════════╗
║Biblioteka Standardowa║
╠══════════╤═══════════╣
║          │           ║
║          │           ║
║ ╔════════╧════════╗  ║
║ ║  <<Interface>>  ║  ║
║ ╟─────────────────╢  ║
║ ║  OuterIterator  ║  ║
║ ╚════════╤════════╝  ║
║          │           ║
║ ╔════════╧════════╗  ║
║ ║IteratorIterator ║  ║
║ ╚═════════════════╝  ║
║                      ║
╚══════════════════════╝


use IteratorIterator;
use DateTime;
use App\Products;

/**
 * @interface Iterator<DateTime, Product>
 */
final class DateTimeAsKeyDecorator extends IteratorIterator
{
    public function __construct(Products $products)
    {
        parent::__construct($products);
    }

    public function key(): DateTime 
    {
        /** @var Product */
        $productparent::current();

        return new DateTime(
            $product->rawStringCreationDate()
        );
    }
}

    Zdecydowałem się nadpisać konstruktor klasy by przyjmował ona jako parametr tylko obiekty klasy Products, by mieć pewność że przekazywana instancja Traversable posiada tylko obiekty Product jako iterowane elementy. 

Kod klienta (hipotetyczna akcji kontrolera we framework'u Symfony):

public function someAction(Products $products): Response
{
    /**
     * @var DateTime $date - product creation date time
     * @var Product $pro
     */
    foreach (new DateTimeAsKeyDecorator($products) as $date=>$pro) {
        // implementacja
    }
}


Filtrowanie tylko aktywnych produktów


Schemat klasy FilterIterator (wzorzec Dekorator)


    ╔═════════════╗
    ║<<Interface>>║
    ╟─────────────╢
    ║  Iterator   ║
    ╚══════╤══════╝
           │            
╔══════════╧═══════════╗
║Biblioteka Standardowa║
╠══════════╤═══════════╣
║          │           ║
║          │           ║
║ ╔════════╧════════╗  ║
║ ║  <<Interface>>  ║  ║
║ ╟─────────────────╢  ║
║ ║  OuterIterator  ║  ║
║ ╚════════╤════════╝  ║
║ ╔════════╧════════╗  ║
║ ║ IteratorIterator║  ║
║ ╚════════╤════════╝  ║
║ ╔════════╧════════╗  ║
║ ║  <<Abstract>>   ║  ║
║ ╟─────────────────╢  ║
║ ║  FilterIterator ║  ║
║ ╚═════════════════╝  ║
╚══════════════════════╝


use FilterIterator;
use App\Products;

/**
 * @interface Iterator<int, Product>
 */
final class ActiveProductsDecorator extends FilterIterator 
{
    public function __construct(Products $products)
    {
        parent::__construct($products);
    }

    public function accept(): bool 
    {
        /** @var Product */
        $product = parent::current();

        return $product->isActive();
    }
}