Kod efektywny czy efektowny ? Alternatywa dla unset() w PHP

Jakiś czas temu zostałem zmuszony do skasowania pewnego elementu w tablicy, który oczywiście musiał być tak głęboko ukryty w całej tej hierarchii bym łatwo i przyjemnie nie mógł tego zrobić. W efekcie końcowym uzyskałem kod, który w pewnym sensie narusza prawo demeter[1], a symptomy są podobne do opisanego przez Wujka Boba problemu z wrakami pociągów (ang. Train Wrecks) [2]. O ile takie porównanie ma jakiś sens w kontekście operowania tablicami w PHP. Tak czy siak, być może poniższy kod jest efektywny, ale czy jest on efektowny ?

unset($carsArray['accessories'][0]['cockpit'][0]['mirror'])

Tak właściwie jest to częsta przypadłość języków dynamicznie typowanych. Postanowiłem przemyśleć sprawę raz jeszcze. Czy aby na pewno nie można by zrefaktoryzować tego kodu tak, by wyglądał choć odrobinę lepiej?

Struktury zmienić nie mogę, natomiast tą część logiki odpowiedzialnej za wyszukanie odpowiedniego klucza i jego skasowanie już tak. Stąd pierwszym pomysłem jaki mi wpadł do głowy to wykorzystanie pętli foreach na wielu poziomach, gdzie każda klasa znałaby strukturę tablicy wyłącznie dla swojego poziomu (zgodnie z prawem demeter). Można by też to oprzeć o kompozyt[3], ale gdybyśmy byli w świecie obiektów. Niestety funkcja unset() nie zadziała dla referencji uzyskanej z pętli foreach. Dlatego też rozwiązania musiałem szukać w wbudowanych w PHP funkcjach na tablicach [4]. W szczególności array_filter() [5].

class MirrorCleaner
{
    private $carsArray = [];

    public function __construct(array $carsArray)
    {
        $this->carsArray = $carsArray;
    }

    public function cleanMirror()
    {
        $newCarsArray = [];

        foreach ($this->carsArray as $value) {
            $accessoriesArray = $value['accessories'] ?? [];
            $newCarsArray[]['accessories'] = $this->parserAccessories($accessoriesArray);
        }

        return $newCarsArray;
    }

    private function parserAccessories(array $accessoriesArray)
    {
        $newAccessoriesArray = [];

        foreach ($accessoriesArray as $value) {
            $cockpitsArray = $value['cockpit'] ?? [];
            $newAccessoriesArray[]['cockpit'] = $this->parserCockpits($cockpitsArray);
        }

        return $newAccessoriesArray;
    }

    private function parserCockpits(array $cockpitsArray)
    {
        $newCockpitsArray = [];

        foreach ($cockpitsArray as $element) {
            $newCockpitsArray[] = $this->removeMirror($element);
        }

        return $newCockpitsArray;
    }

    private function removeMirror(array $value)
    {
        return array_filter($value, function ($key) {
            return !($key == 'mirror');
        }, ARRAY_FILTER_USE_KEY);
    }
}

Pełny kod na https://github.com/domino91/ArrayFilter

Nie jest to rozwiązanie idealne. Klasa MirrorCleaner narusza zasadę pojedynczej odpowiedzialności (parsuje tablice, kasuje elementy). Nadal złamane jest prawo demeter (klasa zna algorytm przetwarzania całej struktury). Nie jest to też rozwiązanie optymalne dla naprawdę sporych tablic czy aplikacji, w których czas wykonania ma kluczowe znaczenie (kopiujemy tu tablice). Pewnym krokiem naprzód byłoby zrefaktoryzowanie tej klasy o wydzielenie na kilka klas kompozytów i wstrzykiwanie do nich zależności…Mimo to jednak uważam obecne rozwiązanie za całkiem efektowne 😉 Co Wy o tym sądzicie ?

Źródło:
[1] https://www.tripled.io/25/08/2016/The-anemic-domain-model/
[2] https://hackerchick.com/clean-code/
[3] https://www.geeksforgeeks.org/composite-design-pattern/
[4] https://carlalexander.ca/php-array-functions-instead-loops/
[5] https://www.php.net/manual/en/function.array-filter.php