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:

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:

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ę SOLIDSingle 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:

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:

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

Jestem fanatykiem obiektowego programowania i nieustannie pogłębiam swoją wiedzę we wszelkich tematach z nim związanych. Wszystko czego się dowiem konfrontuję z rzeczywistością, ponieważ teoria, która nie ma odzwierciedlenia w praktyce, traci swój sens tam, gdzie zaczyna się praca programisty :)

  • mheki

    Nie wspomniałeś o tzw. nazwanych konstruktorach, czyli statycznych metodach powszechnie używanych w DDD. Jestem ciekaw jakie jest Twoje zdanie na ten temat.
    Co do TDD to PhpSpec wspiera tworzenie obiektów z wykorzystaniem nazwanych konstruktorów przez $this->beConstructedThrough(..., [])
    Pozdrawiam

    • Sebastian Malaca

      Co do nazwanych konstruktorów, to są niekiedy bardzo przydatne. Ułatwiają enkapsulację w obrębie pakietu. Prawda jest jednak taka, że te metody wcale nie muszą być statyczne, bo jaki problem zrobić fabrykę z publicznymi metodami?

      O to właśnie chodzi ze statikami – one nie są niezbędne, nie są obiektowe (z definicji! a przecież to programowanie obiektowe :), da się bez nich żyć i kod, który by powstał byłby naprawdę dobry. A spróbuj stworzyć taki kod bez interfesjów.

  • mheki

    nie zgodzę się z Tobą Sebastian w kwestii, że zaprezentowany powyżej kod łamie SRP („Wszystko w jednym miejscu”). SRP mówi, że „an object/module should have only one reason to change”. Tutaj użycie metody statycznej w zasadzie nie różni się od tworzenia obiektu za pomocą new i nie wprowadza dodatkowej przyczyny jego modyfikacji.

    • Sebastian Malaca

      Tak, nie różni się (zakładam, że mówimy o klasie User, tak?). Prawie, ponieważ tutaj w pewnym stopniu jesteśmy w stanie kontrolować to w jaki sposób tworzymy ten obiekt.

      Jednak powody do zmian są dwa:
      1) inny sposób tworzenia obiektu (zwróć uwagę, że konstrukcja obiektu może być dużo bardziej złożona)
      2) logika obiektu domenowego

      Tak czy inaczej pozwolę sobie na zacytowanie siebie: „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”.

      Bo problem zacznie się jak konstrukcja instancji będzie bardziej rozbudowana.

      • mheki

        OK, czyli kod którego jeszcze nie ma w klasie User mógłby pogwałcić SRP 😉 Tymczasem jeśli konstruktor jest prywatny, a wszystkie obiekty tej klasy tworzymy w taki sam sposób używając nazwanego konstruktora to tej zasady nie łamiemy 🙂
        BTW sam nie jestem fanem statycznych metod i unikam ich tam gdzie są niepotrzebne.

        • Sebastian Malaca

          Moim zdaniem fakctory method bez względu na stopień skomplikowania kodu jest łamaniem SRP. Jednak czasami decydujemy się na takie rozwiązanie. Rzecz w tym, aby być świadomym tego, co się robi i jakie konsekwencje za tym idą.

  • anonym

    Sebastian – mógłbyś zaprezentować implementację singletonu bez static w PHP?

    • Sebastian Malaca

      A da się?

      • Tuminure

        Da się, np. globalami.

        PS Nie wiem czy się cieszyć czy płakać ze swojej pomysłowości…

        • Sebastian Malaca

          Ja ze swojej strony gratuluję poczucia humoru 😛

    • Potfur

      A po co ci singleton? Toż to zło!

      • Rafał Łużyński

        singletonizm to zło

  • Rafał Łużyński

    1. „A primary use case for static methods is making regular functions findable by putting them in class where people look for them.” i to nie łamie zasady SRP, ponieważ – „Static methods are nothing more than namespaced global functions” Mathias Verraes. W innych językach wyróżnia się też class methods (w PHP to też static), które służą głównie jako factory methods.

    2. Funkcji statycznej nie można zamockować, jeżeli się z niej korzysta jak z globala, natomiast jeśli wstrzykujemy obiekt, który ją posiada, to nie ma problemu, wystarczy w teście wstrzyknąć inny obiekt. Ten sam problem tyczy się singletonów, są złe jeżeli traktujemy je jako wartości globalne.

Send this to friend

webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL
webmastah.weekly
Cotygodniowa porcja linków ze świata WEBDEV BEZ spamu, TYLKO samo mięcho!
Zobacz poprzednie wydania. Dołącz do 2 tysięcy webdeveloperów!
HTML5, CSS3, JS (React, Angular, Ember, Vue), PHP, SQL