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.











Brak komentarzy:

Prześlij komentarz