Statyczne metody w PHP

koddlo.pl 3 miesięcy temu

Pomimo tego, iż temat statycznych metod w PHP został przeorany na wiele różnych sposobów to przez cały czas dostrzegam, iż nie dla wszystkich jest on zrozumiały. Czy statyczne metody są złe? Nie da się odpowiedzieć na to pytanie bez kontekstu. Gdybym musiał udzielić zero-jedynkowej odpowiedzi to powiedziałbym, iż tak. Problem w tym, iż są sytuacje, gdzie faktycznie ich użycie może mieć sens. Wrócę więc do podstaw i w tym materiale raz jeszcze spróbuję rozprawić się ze statycznymi metodami w PHP.

Język ewoluuje, a tak zwane dobre praktyki bywają różnie rozumiane przez społeczność. Zauważam dwie grupy osób dla których ten materiał może okazać się pomocny. Do pierwszego worka wrzucam tych, którzy usłyszeli, żeby unikać statycznych wywołań i kurczowo się tego trzymają, choć nie do końca rozumieją dla czego. Drugi worek to osoby, które namiętnie używają statycznych wywołań i nie widzą w tym niczego złego, a wręcz twierdzą iż tak jest łatwiej. Prawda jest taka, iż nie jest… o ile jesteście w tej grupie, a nie chcecie zrozumieć sedna problemu to polecam Wam bezzwłocznie przeskoczyć do worka z numerem jeden.

Generalnie słowo static w PHP nie dotyczy tylko i wyłącznie metod, ale to przy ich używaniu zauważam największe niezrozumienie. Moim zdaniem wynika ono z tego powodu, iż w pewnych sytuacjach statyczne metody są w porządku. Inaczej ma się sprawa chociażby z polami klas. Te z kolei, oznaczone jako statyczne w moim rozumieniu są błędem. Dlaczego? Każdy byt zadeklarowany jako statyczny jest tak naprawdę współdzielony. Zamiast static, równie dobrze można byłoby użyć słowa shared.

Czy widzicie jakikolwiek powód, dla którego pole klasy powinno być współdzielone? W programowaniu obiektowym dochodzi do konkretyzacji klasy. Każdy obiekt jest osobną instancją, której cały stan jest unikalny i niewspółdzielony. Mimo wszystko, zdarza się, iż w klasie jest potrzeba opisania wartości, która jest wspólna dla wszystkich obiektu. Zwykło się to robić dzięki stałych (const). I dobrze, bo te jak sama nazwa wskazuje są niemutowalne. Statyczne własności klasy można zmieniać. Nie mogą zostać oznaczone jako tylko do odczytu (readonly). Niestety, ich modyfikacja może prowadzić do wielu nieoczekiwanych zachowań. Ewentualnym rozwiązaniem tego problemu może być wzorzec projektowy Singleton, który idealny nie jest, ale w kilku specyficznych sytuacjach może się sprawdzić.

To, iż macie w kodzie klasy i obiekty, nie oznacza jeszcze iż programujecie obiektowo.

Zwróćcie uwagę, iż cały czas poruszam się w kontekście programowania obiektowego. Statycznym wywołaniom jest zdecydowanie bliżej do programowania strukturalnego. To, iż macie w kodzie klasy i obiekty, nie oznacza jeszcze iż programujecie obiektowo. Przypominam, iż esencją programowania obiektowego są cztery filary: abstrakcja, hermetyzacja, polimorfizm i dziedziczenie. Te nie mają nic wspólnego ze statycznymi metodami.

Dlaczego statyczne metody są złe?

Głównym problemem statycznych metod jest współdzielony globalny stan. W jawny sposób łamie on enkapsulację. To z kolei prowadzi do zbyt dużego poplątania (coupling). Nie ma możliwości separacji warstw. Taki kod jest ciężki w utrzymaniu i rozwijaniu. Dobra architektura nie pozwoli na tego rodzaju fikołki. Co więcej, kod jest dużo mniej czytelny. Wchodząc w klasę, na pierwszy rzut oka nie widać jej zależności. Trzeba dokładnie sprawdzić przestrzenie nazw albo przelecieć kod od góry do dołu i sprawdzić wszystkie odwołania.

Statyczne wywołania są ciężkie do przetestowania. Samą metodę statyczną można przetestować bardzo łatwo. Może choćby prościej, niż standardową metodę klasy, jako iż nie ma potrzeby tworzenia instancji. Problemem są natomiast wywołania statyczne w kodzie klienckim, gdzie to odwołania stanowią zależność. Kod nie jest polimorficzny. Nie da się w żaden sposób podmienić zależności, czy użyć dublerów (test doubles/mocks). Poza tym, testy jednostkowe w przypadku statycznych wywołań nie pokrywają scenariuszy, gdzie globalny współdzielony stan może zostać zmodyfikowany w innym miejscu.

Generalnie, zależności stają się dużo trudniej utrzymywalne. Zawsze występuje powiązanie z konkretnym typem. Nie da się nadpisać statycznych metod dzięki dziedziczenia. Wszystko zależy od wszystkiego. Nie ma możliwości zaadoptowania reguły odwrócenia zależności (Dependency Inversion Principle). Dlaczego? W normalnych warunkach używa się do tego celu abstrakcji, głównie interfejsów. Nie da się wdrożyć interfejsu dla statycznej metody. Z tego właśnie powodu, dużo lepszym rozwiązaniem jest używanie wzorca DI (Dependency Injection), który eliminuje wszystkie wyżej wspomniane problemy.

Kiedy statyczne metody mogą mieć sens?

Jest kilka sytuacji, gdzie statyczne metody w PHP zdają egzamin. Moim absolutnym faworytem są tak zwane named constructors. Rozwiązują one problem braku możliwości przeciążania metod w PHP, co z kolei prowadzi do braku możliwości posiadania kilku konstruktorów. Obiekt może być wytwarzany na kilka sposobów, a poprawności stanu powinien pilnować sam konstruktor. Właśnie dlatego ma to tak ogromne znaczenie. W tym wypadku sam konstruktor staje się prywatny, co blokuje możliwość utworzenia obiektu przez new.

Widać to w poniższym przykładzie, gdzie przy tworzeniu pracownika data jego utworzenia jest równa aktualnej, status jest ustawiony jako nowy oraz emitowane jest zdarzenie o utworzeniu pracownika. Inaczej jest przy odtwarzaniu pracownika, gdzie nie powinno się opublikować zdarzenie o jego utworzeniu, a data utworzenia i status mogą przyjmować różne wartości. dzięki samego konstruktora byłoby to trudne do osiągnięcia. Trzeba byłoby wprowadzić parametry sterujące i dołożyć ifologię. Ewentualnie można zrezygnować z pilnowania poprawności stanu, co jak wspomniałem wyżej, jest niewłaściwe.

final class Employee { private function __construct( private EmployeeId $id, private Email $email, private FirstName $firstName, private LastName $lastName, private DateTimeImmutable $createdAt, private Status $status ) { } public static function create( EmployeeId $id, Email $email, FirstName $firstName, LastName $lastName, ClockInterface $clock ): self { $employee = new self($id, $email, $firstName, $lastName, $clock->now(), Status::NEW); $employee->raise(new EmployeeCreated($employee)); return $employee; } public static function restore( EmployeeId $id, Email $email, FirstName $firstName, LastName $lastName, DateTimeImmutable $createdAt, Status $status ): self { return new self($id, $email, $firstName, $lastName, $createdAt, $status); } }

Kolejna sytuacja, gdzie statyczne wywołania mogą się przydać to przy użyciu wzorca projektowego prosta fabryka. Jedną z możliwych implementacji jest właśnie statyczna fabryka. Pod warunkiem, iż fabryka nie korzysta z żadnych zewnętrznych zależności. Wówczas lepiej jest zrobić ją przy użyciu wstrzykiwania zależności.

Inną sensowną opcją mogą okazać się bezstanowe funkcje. Nie mają one żadnych skutków ubocznych i nie przechowują stanu. zwykle nie ma też potrzeby podmiany implementacji na potrzeby testów. Równie dobrze, mogłaby to być prywatna metoda w tej samej klasie, ale żeby nie powielać kodu zostaje wyniesiona do osobnego bytu. Mimo wszystko, niektóre architektury mogą wymuszać posiadanie interfejsu, co w tym wypadku nie jest możliwe. Co więcej, niektóre przypadki wymagają polimorficznej wymiany komponentów. Posługując się poniższym przykładem, w dwóch różnych przypadkach mogą być aplikowane inne strategie generowania kodu.

final class RandomCodeGenerator { public const DEFAULT_LENGTH = 10; public const DEFAULT_CHARACTERS = 'aAbBcCdDeEfFgGhHiIjJkKlLmMnNoOpPrRsStTuUvVwWxXyYzZ0123456789'; public static function generate( int $length = self::DEFAULT_LENGTH, string $allowedCharacters = self::DEFAULT_CHARACTERS ): string { if ($length < 3) { throw new \InvalidArgumentException('Code cannot be shorter than 3 chars'); } $code = ''; $allowedCharactersLength = mb_strlen($allowedCharacters); $codeLength = $length; while ($codeLength !== 0) { $randomIndex = mt_rand(0, $allowedCharactersLength - 1); $code .= $allowedCharacters[$randomIndex]; $codeLength--; } return $code; } }

Mała uwaga: statyczne wywołanie z założenia jest szybsze. Przyspieszenie to wynika z braku potrzeby tworzenia instancji klasy. Są to jednak tak niewielkie wartości skorelowane z wieloma innymi czynnikami, iż spokojnie można uznać je za pomijalne. Wspominam o tym dla kompletności materiału, ale w obecnych czasach nie jest to argument, który warto podnosić.

Czy należy używać metod statycznych w PHP?

Każdy istniejący koncept ma swoje miejsce i zastosowanie. Dobre praktyki czy antywzorce są adekwatne w konkretnym kontekście, a w innych niekoniecznie. To właśnie dlatego nazwy „dobre”, czy „anty” są pewną generalizacją i uproszczeniem. Nie oznaczają, iż zawsze tak jest. Oznaczają, iż w większości przypadków tak jest. Okazuje się to szczególnie pomocne na wczesnych etapach nauki.

Kiedy nie rozumiecie pewnych konceptów to generalizacja na zasadzie „używać” i „nie używać” może pomóc. A dla statycznych metod brzmi ona „nie używać”. Tyle tylko, iż skoro czytacie ten fragment to powinniście mieć już wiedzę, kiedy statyczne metody w PHP mają sens. Osobiście, używam ich często jako gwarancje wielu konstruktorów, gdyż rozwiązują konkretne ograniczenie języka PHP. W pozostałych przypadkach sięgam po nie bardzo rzadko.

Pamiętajcie jednak, iż podejście które prezentuję w tym podsumowaniu może okazać się adekwatne dla wszystkich innych konceptów. Idealnie byłoby nie używać mechanizmów, których się nie rozumie. Realia są takie, iż nie da się dogłębnie zrozumieć wszystkiego, bo jest tego za dużo. Ciężko jednak wskazać konkretną rzecz, której nigdy nie należy używać. Finalnie, zasady można łamać, ale w moim odczuciu tylko wtedy o ile dobrze się je rozumie i bierze się konsekwencje na klatę.

Nie bądź statyczny tylko dynamiczny!

Wpis Statyczne metody w PHP pojawił się pierwszy raz pod Koddlo.

Idź do oryginalnego materiału