Jak programować obiektowo? cz. 5 – ach ten static…
|
We wpisie dotyczącym metod wspomniałem m.in. o metodach statycznych oraz wspominałem również, że do tego tematu jeszcze wrócimy. Dzisiaj przyszła na to pora. Jednak nie skupię się jedynie na metodach. Omówimy sobie również statyczne atrybuty i postaramy się odpowiedzieć na pytania: czy są nam w ogóle potrzebne takie metody/atrybuty? Kiedy, o ile, powinniśmy ich używać? Dlaczego programiści tak lubią “statiki” i dlaczego nie powinni?
Atrybuty i metody statyczne… że co?
Czym tak w ogóle są atrybuty i metody statyczne? Jeżeli skupimy się jedynie na kwestiach konstrukcyjnych, to są to wszystkie atrybuty oraz metody, które nie wymagają stworzenia obiektu danej klasy, aby można było z nich korzystać. Wystarczy nam jedynie odwołanie się do danej klasy. To tyle jeżeli chodzi o kwestie konstrukcyjne.
Takie byty wyglądają mniej więcej tak:
class UserFactory {
private static $usersAmount = 0;
public function create($name, $login) {
self::$usersAmount++;
// create and return user
}
}
class JsonEncoder {
public static function encode($decoded) {
// some code
}
}
Jeżeli już same podstawy mamy za sobą to przejdźmy do ciekawszych zagadnień dodyczączych statycznych konstrukcji.
Metoda statyczna - kiedy i gdzie?
Czy każda metoda może być statyczna? Cóż, może retoryczne pytanie, bo wątpię, że ktokolwiek by bez wahania odpowiedział, że tak. Aby do metody można było (zaznaczam, że piszę o możliwości, a nie konieczności) dodać to jakże wdzięczne słowo static musi ona spełniać jeden z dwóch warunków:
- Metoda dla takich samych parametrów wejściowych zawsze zwróci ten sam wynik albo…
- Potrzebujemy tylko jednej instancji klasy i dlatego też decydujemy się na to aby zarówno jej atrybuty jak i metody były statyczne.
Do konsekwencji powyższych rozwiązań przejdę za chwilę, a w między czasie warto jeszcze napisać o tym…
Kiedy atrybut statyczny mieć bym mógł?
Atrybut statyczny przydaje się nam wtedy, gdy jakieś informacje są powiązane z klasą, a nie z jej poszczególnymi instancjami. Często wykorzystuje się go również jako atrybut powiązany z nimi wszystkimi (instancjami) np. do generacji prostych identyfikatorów lub jako zliczanie ilości obiektów danej klasy.
Wygoda przede wszystkim!
Czy metody oraz atrybuty statyczne są niezbędne? Czy bylibyśmy się w stanie bez nich obejść? Odpowiedź jest prosta - tak. Prawda jest taka, że na to, aby konstrukcję zdefiniować jako statyczną decydujemy się z czystej wygody i chyba nie ma przypadku, kiedy by istniała rzeczywista konieczność wykorzystania statica. Skorygujcie mnie, jeśli jestem w błędzie.
Czym charakteryzuje się owa wygoda? Przede wszystkim chodzi tutaj o brak jakiejkolwiek konieczności myślenia o zależnościach (czyli o projekcie), nie zastanawiamy się gdzie dana konstrukcja będzie nam potrzebna, bo gdy dotrzemy do takiego miejsca, po prostu ją wykorzystamy. Nie będzie potrzeby robienia refaktoryzacji w celu jej przekazania, nie będziemy poświęcali czasu na zmiany - po prostu wywołamy daną metodę bądź wykorzystamy atrybut.
Zabawa w chowanego
Jednym z głównych następstw korzystania ze staticów jest (nieświadome?) ukrywanie zależności pomiędzy obiektami. Choć w tym przypadku chyba trafniej byłoby napisać - pomiędzy klasami, a obiektami.
Zwróćcie uwagę, że o wykorzystaniu metody statycznej jakiejś klasy dowiem się dopiero w chwili, gdy spojrzę na ciało metody, która ją wywołuje. Do tego momentu będę żył w błogiej nieświadomości, nie wiedząc o istnieniu żadnej zależności. Oczywiście w dobie dzisiejszych IDE, które powiązania znajdą za nas, problem jest odrobinę mniejszy, jednak nadal ma to negatywny wpływ na czytelność kodu.
Jak żyć?
Od dawna słyszy się, że testowanie (w szczególności technika TDD) to nie tylko narzędzie, które ułatwia refaktoryzację, ale również wpływa na poprawę jakości projektu. Kod, który można w łatwy sposób przetestować, bez wykorzystywania hacków czy jakichś wyszukanych sztuczek jest zazwyczaj prostszy w rozwoju jak i w zarządzaniu. To bezpośrednio przekłada się na tempo dostarczania nowych funkcjonalności, a co za tym idzie na zadowolenie naszych klientów, co w następstwie pozwala nam zarabiać więcej 🙂 Ok, może nie jest to aż tak proste i piękne, ale jest w tym sporo prawdy.
Jak do powyższego ma się kod, który wykorzystuje metody statyczne? Już wcale taki prosty do przetestowania nie jest, a problem rośnie wraz ze wzrostem skomplikowania danej metody statycznej. Takiej metody nie „zamockujemy” i najprawdopodobniej będziemy musieli posunąć się do jednego z dwóch rozwiązań:
- Skonstruujemy testy w taki sposób, aby nie było potrzeby „mockowania” metody. Niestety w takim przypadku testy jednostkowe mogą przestać być jednostkowe. Wszystko zależy od tego co dzieje się za wywołaniem metody statycznej.
- Dołożymy jakieś ustawienia do klasy posiadającej ową metodę statyczną. Ustawienia, które pozwolą nam “poinformować ją”, że teraz działa w środowisku testowym i powinna działać “trochę” inaczej.
Powyższe rozwiązania są tymi, z którymi miałem okazję spotykać się najczęściej i jak widzicie nie są one tym, na co programista powinien decydować się, gdy ma możliwość wybrania innego rozwiązania.
Wszystko w jednym miejscu
Zdarza się czasami, że wpadamy na pomysł, żeby do w pełni działającej i funkcjonalnej klasy, której obiekty z powodzeniem wykorzystujemy w kodzie, dodać jeszcze metodę statyczną. Świetnym przykładem takiego zabiegu jest implementacja wzorca Factory Method, która może wyglądać następująco:
class User {
private $name;
private $address;
public static function aUser(Name $name, Address $address) {
return new User($name, $address);
}
public function changeLeavingPlace(Address $address) {
$this->address = $address;
}
private function __construct(Name $name, Address $address) {
$this->name = $name;
$this->address = $address;
}
}
Takie klasy można spotkać w wielu aplikacjach i z dużym prawdopodobieństwem możecie je również znaleźć u siebie, w projektach, nad którymi obecnie pracujecie.
W takim razie, do czego chcę się znowu przyczepić? Powyższy przykład jest naprawdę prosty, ponadto wykorzystaliśmy wzorzec projektowy, encja rownież w miarę rozsądnie skonstruowana, bez nadmiarowych metod oraz atrybutów. Więc gdzie tkwi problem? Otóż owa klasa jest odpowiedzialna zarówno za tworzenie swoich instancji jak i za logikę poszczególnych obiektów. Czyli właśnie pogwałciliśmy jedną z najważniejszych (o ile nie najważniejszą) zasadę SOLID - Single Responsibility Principle.
Oczywiście nie twierdzę, że z takim kodem nie można żyć i trzeba zrobić wszystko aby się go zmienić. Niemniej jednak, warto być świadomym takich rzeczy i posiadając tą wiedzę, decydować się na takie albo inne rozwiązanie z pełnym wachlarzem konsekwencji przed oczyma.
Klasa, która mogła być obiektem
Pisałem wcześniej o dwóch powodach, które sprawiają, że jest w ogóle możliwe rozważanie wykorzystania konstrukcji statycznych. Dla przypomnienia, wspomniałem o metodach, które dla tych samych parametrów wejściowych zwracają zawsze ten sam wynik oraz o klasach, których jedynie jedna instancja jest niezbędna do działania aplikacji.
Często, gdy w takich przypadkach zwracam uwagę na to, że przecież równie dobrze mogły to być obiekty, słyszę odpowiedź w stylu “Po co? Przecież wystarczy mi klasa ze staticami?”. Oczywiście, że wystarczy. Warto jednak zwrócić uwagę, że wykorzystujemy paradygmat obiektowy i to obiekt powinien być punktem wyjścia. Wydaje mi się jednak, że dla wielu programistów jest zupełnie inaczej, nie myślą oni o obiektach oraz o powiązaniach pomiędzy nimi. Dla nich na początku jest klasa.
Ponadto, warto jeszcze podkreślić to, o czym pisałem wcześniej czyli o różnicy, która wynika z wykorzystania obiektów i klas ze staticami - w tym pierwszym wypadku zależności są jawne, natomiast w tym drugim przypadku, następstwem korzystania z metod statycznych jest to, że bez wglądu we “wnętrzości” nie dowiemy się o nich.
Statycznie w pewnym stanie
Czasami zdarza się zobaczyć konstrukcje, które posiadają statyczne atrybuty, które definiują ich stan oraz metody, które z nich korzystają. Może to być na przykład coś takiego:
class ApplicationConfig {
private static $config = array();
public static function init(array $config) { /** method body **/ }
public static function get($paramName) { /** method body **/ }
}
Spotkaliście się z czymś takim? W tym momencie mogę jedynie powtórzyć to, co napisałem wyżej - po takich klasach widać, że programista myśli “klasowo”, a nie obiektowo, że to właśnie klasa jest punktem wyjścia, a nie obiekty i relacje pomiędzy nimi. Bo jaki byłby problem, aby ta klasa wyglądała tak:
class ApplicationConfig {
private $config = array();
public function _construct(array $config) { /** method body **/ }
public function get($paramName) { /** method body **/ }
}
i w odpowiednim momencie była tworzona?
Ja uważam, że żaden, a powody stworzenia tej pierwszej konstrukcji mogą być moim zdaniem dwa:
- Programista nie widzi różnicy pomiędzy dwoma rozwiązaniami.
- Programista woli umożliwić sobie dostep do tej klasy/instancji wszędzie, niż zastanowić się nad tym, gdzie tak naprawdę będzie potrzebna.
W pierwszym punkcie każdy z nas był i wszystko sprowadza się w tym wypadku do wytłumaczenia różnic i podzielenia się wiedzą.
Powód drugi sprowadza się do tego, o czym już wspomniałem - do wygody. Warto jednak mieć na uwadze, że ta wygoda może przerodzić się w problem, gdy w którymś momencie okaże się, że klasa jest wykorzystywana dosłownie wszędzie i jakakolwiek zmiana pociąga za sobą szereg implikacji. Czy takie ryzyko nie istniałoby, gdybyśmy wykorzystali obiekt. Oczywiście, że nie, ale byłoby nieporównywalnie trudniej do takiej sytuacji doprowadzić.
Decyzje podejmuj świadomie
Czy metody i atrybuty statyczne są nam niezbędne do tego, aby móc tworzyć funkcjonalny i spełniający założenia kod? Nie. Gdyby nagle zniknęły z naszego świata, to wydaje mi się, że dość szybko przyzwyczailibyśmy się do ich braku i po otarciu pierwszych łez dalej tworzylibyśmy oprogramowanie.
Jednak, pomimo tych wszystkich rzeczy, o których pisałem wyżej, pomimo faktu, że sam uważam, że bez static’ów da się żyć, nie mam na celu Was namawiać do ich bezwarunkowego porzucenia. Prawda jest taka, że sam również nie zamierzam z nich rezygnować.
Ważne jest to, abyście w momencie podejmowania decyzji o tym, czy coś ma zostać statyczne, wiedzieli o wszystkich konsekwencjach wybranego rozwiązania, abyście mieli ich świadomość, bo dzięki temu będziecie mieli większą szansę na uniknięcie czekających na Was pułapek.
Pozostałe artykuły z cyklu
- Jak programować obiektowo? cz. 9 – klasy abstrakcyjne
- Jak programować obiektowo? cz. 8 – interfejsy
- Jak programować obiektowo? cz. 7 – final
- Jak programować obiektowo? cz. 6 – wartości stałe
- Jak programować obiektowo? cz. 5 – ach ten static…
- Jak programować obiektowo? cz. 4 – metod ciąg dalszy
- Jak programować obiektowo? cz. 3 – czym jest metoda?
- Jak programować obiektowo? cz. 2 – atrybuty klasy
- Jak programować obiektowo? cz. 1 – wstęp