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();
    }
}