wtorek, 31 stycznia 2012

Boski obiekt

I'm Object almighty!

Czym jest boski obiekt? Najkrótsza odpowiedź na to pytanie jest zarazem chyba najlepszą: jest wszystkim, odpowiada za wszystko. I choć może wydawać się, że stworzenie takiego bytu jest naprawdę trudne, to rzeczywistość udowodniała mi wiele razy, że aplikacje zawierają pełno tego typu tworów.

Jak one powstają?
Najczęściej poprzez przyrost tzn. na początku mamy klasę, która jest odpowiedzialna za określoną funkcjonalność, ale w miarę analizowania wymagań lub zwiększania ich liczby (czyli rozrastania się aplikacji) dorzucamy do niej dalsze funkcjonalności, które "tam pasują". I jeszcze pół biedy, gdy taka funkcjonalność jest realizowana jako metody publiczne, gorzej jest, gdy rozrasta się "funkcjonalność" pierwszej metody publicznej, a to co dodajemy później jest implementowane jako prywatne metody, "bo przecież nie jest to używane na zewnątrz, więc idąc w myśl idei OOP powinno być prywatne".

Dlaczego powstają?
Wydaje mi się, że najczęściej wynika to z założenia, że "to przecież tylko kilka linijek kodu" i "pasuje tutaj". Po czym okazuje się, że kilka linijek kodu rozrasta się do kilku metod, a stwierdzenie "pasuje tutaj" można byłoby z powodzeniem zamienić na asocjację.
I o ile nie ma problemu, gdy programista zauważa problem i stara się go poprawić. Dzięki temu następnym razem najpierw przemyśli problem, a dopiero później zaimplementuje rozwiązanie. Niestety często zdarza się tak, że programista albo nie dostrzega, że obciążył obiekt zbyt dużą odpowiedzialnością albo (i to chyba gorsze) ignoruje bądź umniejsza znaczenie problemu.

Let's see this creature

No to może jakiś przykład:
Klient zleca nam utworzenie sklepu internetowego. Możliwość wybierania kategorii, podkategorii, filtrów itp. w celu sprecyzowania, co nas dokładnie interesuje. Poza tym podstrony z ogólnymi informacjami o firmie, dane kontaktowe etc. I oczywiście jakiś koszyk ułatwiający kupno większej ilości produktów:)
Proste? Mając takie wymagania zaczynamy tworzenie sklepu:) Naszą główną klasą będzie WebShop. Zakładamy, że będzie miała jedną publiczną metodę execute(), gdzie na podstawie url będziemy podejmowali decyzję o tym, który kontroler ma być załadowany i która akcja wywołana. Parametry wysłane getem i postem przekażemy do danego kontrolera. W razie braku kontrolera lub akcji wyrzucamy wyjątek:
<?php
class WebShop
{
  public function execute()
  {
    /*
    1) Get information from url.
    2) Create specified controller or throw exception if controller doesn't exists.
    3) Set data from request.
    4) Execute action or throw exception if action doesn't exists.
    */
  }


Klientowi jednak nie podoba się informacja o niezłapanych wyjątkach:) Prosi nas o dodanie strony z informacją o tym, że żądana strona nie istnieje. Nic trudniejszego:
  public function execute()
  {
    /*
    1) Get information from url.
    2) Create specified controller or throw exception if controller doesn't exists.
    3) Set data from request.
    4) Execute action or throw exception if action doesn't exists.
    5) If exception occured then redirect to error page
    */
  }


Klient jest zadowolony z efektu. Jednak w pewnym momencie pyta się, czy jest możliwe stworzenie jakiś bardziej opisowych (ludzkich) linków do konkretnych stron, a w szczególności do kategorii i podkategorii, bo ten pytajnik i liczne ampersandy nie podobają mu się za bardzo. Wolałby mieć coś na wzór ścieżki katalogów. Da się? Jasne:)
  public function execute()
  {
    /*
    1) Get url.
    2) Parse url. 
    3) Create specified controller or throw exception if controller doesn't exists.
    4) Set data from request.
    5) Execute action or throw exception if action doesn't exists.
    6) If exception occured then redirect to error page
    */
  }


Oczywiście potrzebujemy jeszcze jakiejś struktury do przechowywania danych np. baza danych:
  public function execute()
  {
    /*
    1) Database initiation.
    2) Get url.
    3) Parse url. 
    4) Create specified controller or throw exception if controller doesn't exists.
    5) Set data from request.
    6) Execute action or throw exception if action doesn't exists.
    7) If exception occured then redirect to error page
    */
  }


Do tego dochodzi jeszcze trzymanie sesji użytkownika, ponieważ musimy zapamiętywać stan tego koszyka:)
  public function execute()
  {
    /*
    1) Start session.
    2) Database initiation.
    3) Get url.
    4) Parse url. 
    5) Create specified controller or throw exception if controller doesn't exists.
    6) Set data from request.
    7) Execute action or throw exception if action doesn't exists.
    8) If exception occured then redirect to error page
    */
  }


Oczywiście z czasem klient może sobie zażyczyć kolejnych funkcjonalności np. dodanie możliwości logowania do systemu w celu identyfikacji stałych klientów i udostępniania im jakichś zniżek itp., co spowoduje, że będziemy musieli jeszcze dodatkowo sprawdzać, czy użytkownik może uzyskać dany zasób.
Poza tym, w miarę rozrastania się aplikacji sami możemy potrzebować jakiś dodatkowych funkcjonalności np. jakiegoś loggera do zapisywania wyjątków.

how could this happen?

Czytając powyższe niektórzy pewnie uśmiechną się pod nosem, bo przecież podjęcie takich decyzji (umieszczenia całej funkcjonalności w jednym obiekcie), jest na pierwszy rzut oka czymś błędnym. I może większość programistów z jakimkolwiek doświadczeniem, posiadając pełną listę wymagań, niestworzyłaby tak złożonych klas.

Z tym, że zazwyczaj wymagania początkowe nie są takie same jak końcowe. Funkcjonalność systemu, już w trakcie jego tworzenia zmienia się, ewoluuje, klient po kolejnych prezentacjach aplikacji zdaje sobie sprawę z coraz to nowszych braków czy też wad. Oczywiście zadaniem programisty (zespołu programistów) jest uwzględnienie wszelkich uwag klienta i wkomponowanie ich do aktualnie tworzonego systemu. I często właśnie w tym momencie zapada decyzja na dołożenie 'kilku linijek kodu' do istniejącej już klasy.

Nie zawsze nowa funkcjonalność jest w tak widoczny sposób rozdzielna, jak w prezentowanym przykładzie. Czasami wydaje się, że rzeczywiście 'jej miejsce jest tutaj'. Najczęściej jest tak w przypadkach, gdy funkcjonalność już istniejąca rozwija się, dokładamy do niej kolejne elementy. W przypadku goniących terminów, naciskającego klienta i menadżera pomijana zostaje (nie)zbędna analiza, dzięki której możemy szybko przekonać się, że warto zastanowić się nad rozbudową istniejącej struktury klas i zależności pomiędzy obiektami. To wszystko sprawia, że decyzje są podejmowane w oparciu o przeczucie, które niestety czasami nas zawodzi. Gdy jesteśmy w trakcie implementacji ciężko już cofnąć się do początku i przeanalizować wszystko należycie, ponieważ generuje to straty czasu, a co za tym idzie - straty finansowe. Tak więc, nawet jeżeli zauważymy, że to co piszemy nie jest idealne, jest już za późno, żeby rozpocząć wszystko na nowo bądź naprawiać to, co zostało stworzone.

jak tworzyć kod ateistyczny?

W swoim rozważaniu pomijam tworzenie tego typu obiektów przez programistów rozpoczynających swoją przygodę z programowaniem, ponieważ jest to jeden z tych nieodłącznych kroków, które trzeba wykonać. Tworzenie takich klas, a właściwie późniejsze zarządzanie nimi, pozwala dostrzec wady takich rozwiązań i dzięki temu, wystrzegać się ich w przyszłości.

Jednak występowanie tego typu obiektów w aplikacjach, nad którymi pracują zawodowi programiści, to już całkiem inna kwestia.
Idealnym rozwiązaniem byłoby przeprowadzenie gruntownej analizy każdej nowej funkcjonalności oraz wszelkich zmian w istniejących wymaganiach. Niestety, jak już pisałem wyżej, czas i koszty zazwyczaj nie są naszymi sprzymierzeńcami w tym przedsięwzięciu. Wraz ze stopniem zaawansowania prac nad projektem ich wpływ (negatywny) na tempo (słuszność?) podejmowanych decyzji jest tym bardziej odczuwalny. Co w takim wypadku?
Warto rozpisać sobie w postaci kolejnych kroków czynności, które muszą być wykonane, aby osiągnąć zamierzony cel. Gdy już mamy tą listę warto zastanowić się, czy którychś nie da się zrealizować za pomocą już istniejącej funkcjonalności. Następnie należy zastanowić się, czy pozostałe nie kolidują w żaden sposób z istniejącym kodem. Możliwe, że niektóre z nich mogą, w sposób widoczny, aspirować do miana dodatkowej funkcjonalności już istniejących klas, które nie są bezpośrednio (obecnie) powiązane z analizowanym wymaganiem. Na podstawie takiego 'dokumentu', którego stworzenie nie powinno zająć dłużej niż 0,5 - 1 h, jesteśmy w stanie zaplanować swoją pracę w bardziej wydajny sposób oraz zmniejszyć szanse nałożenia nieodpowiedniej/zbyt wielkiej funkcjonalności na istniejące bądź nowe klasy.

Odradzam pisania na żywca "bo coś wydaje się proste". Jeżeli jest proste, to i taka pseudo-analiza przedstawiona wyżej nie zabierze więcej niż 10 minut, a dodatkowo utwierdzi w słuszności podjętych decyzji. I może pomóc uniknąć tych błędnych:)

10 komentarzy:

  1. Przypomina mi się jeden obrazek. Stoi sobie architekt (programista). Widzi pusty plac budowy i mówi:

    "Tym razem zbuduję to jak należy".

    Niestety ciągłe zmiany, kombinowanie i napięte terminy sprawiają że prościej i szybciej coś po prostu dopisać jak rozwiązać to elegancko.

    Nie muszę mówić że po wszystkim powstał jakiś dziwny twór na drabinkach a programista patrzy i mówi:

    "Ku***, znowu to zrobiłem".

    Ja takich "znowu to zrobiłem" miałem kilka w życiu. Jak klient zmienia założenia połowy projektu pod koniec jego realizacji bo mu się "odwidziało" to stajemy przed dylematem - zrobić to porządnie czy szybko ? A czasami po prostu mówi się "szybko" bo inaczej nie można. I powstają takie kwiatki.
    Czy da się tego uniknąć ? Nie. Tylko w idealnym świecie gdzie klient zmieniając solidnie założenia jest gotów poczekać aż owe zmiany wprowadzimy zamiast nastawiać się że 'tam dodadzą, tam usuną, zrobię na rano'.

    Tego "znowu" nie miałem nigdy jeżeli projekt był starannie przemyślany, dokumentacja była profesjonalna i kompletna (mało kiedy niestety - tylko duże poważne firmy które zamówiły wiele projektów coś takiego robią)a ja od początku do końca mogłem projekt zaplanować i zrealizować bez niespodzianek albo bez dużych niespodzianek.

    OdpowiedzUsuń
  2. Znam ten rysunek:) A z tą sytuacją chyba spotkał się każdy programista, który ma już trochę kodu za sobą.

    Gdzieś słyszałem, że są dwa typy projektów: idealne i te zrealizowane. Smutne, ale prawdziwe. W naszej pracy wielokrotnie musimy iść (niekiedy na liczne) ustępstwa. A im bliżej deadline'ów, tym ustępstw jest więcej.

    Tyle się teraz czyta, pisze, mówi etc. o wzorcach projektowych, a prawda jest tak, że w sporej ilości aplikacji, to anty wzorce są częściej implementowane. Powodem tego jest brak zasobów (czas, pieniądze, ludzie itp.) i nawet przy dobrze udokumentowanych projektach tego nie da się uniknąć, bo trzeba byłoby wyeliminować jakiekolwiek zmiany w wymaganiach, a to jest niewykonalne.

    OdpowiedzUsuń
  3. Boski obiekt jest zaprzeczeniem obiektowości jako takiej - ktokolwiek kto ma jakiekolwiek pojęcie o programowaniu powinien unikać tego jak ognia.
    Nawet jeśli nie znamy dokładnej specyfikacji, jesteśmy w w stanie rozdzielać poszczególne funkcjonalności na niezależne klasy - logowanie osobno, zarządzanie URLami osobno, dostęp do bazy osobno.
    I nie ma wyjątków - używanie takiego monstrum w jakimkolwiek sensownym projekcie powinno być karane.

    OdpowiedzUsuń
  4. Powinno. Z tym, że życie często weryfikuje takie założenia.

    Przykład z wpisu jest tak dobrany, że na pierwszy rzut oka widać, że za całą funkcjonalnością stać powinna większa ilość klas.
    Jednak w prawdziwych projektach takie byty powstają (częściej niż by się chciało przyznać).
    Czasami przyrostowo tzn. mamy prostą klasę i dodajemy kilka linijek, później znowu i znowu, ... aż pewnego dnia orientujemy się, że nasza niewielka klasa rozrosła się do niesamowitych rozmiarów.

    Drugim przypadkiem jest świadoma decyzja o implementacji czegoś takiego spowodowana ograniczonymi zasobami.

    Jeżeli jest to duży projekt, to przepisanie kodu, aby odpowiednio rozdzielić zależności, nie jest czasami opłacalnym pomysłem. W takim wypadku łatwiej dorzucić te kilka linii (1-2 dni) niż robić to dobrze (1-2 tygodnie), bo klient mógłby 'nie przełknąć' tych terminów.

    Bardzo łatwo jest podejmować decyzję nt. tego, czego używać, a czego nie, kiedy dyskusja toczy się w oderwaniu od rzeczywistości. W prawdziwych projektach niestety rzadko jest miejsce na tak krytyczne spojrzenie.

    OdpowiedzUsuń
  5. Wierz mi, właśnie ja i moi ludzie od dwóch tygodni refaktoryzujemy potężny system, bo jak zobaczyłem jak jest zaprojektowany, to mnie krew zalała.
    Jeśli jakikolwiek programista pozwala sobie (nawet tłumacząc się deadline'ami) na pisanie gniotów, które za kilak tygodnie będą nierozszerzalne, jest przedszkolakiem a nie programistą.
    Programista popełnia błędy, ale nie takie.
    A ja akurat zawsze rozmawiam na temat praktyki - teoria jest nudna :)

    OdpowiedzUsuń
  6. Ty to nazywasz błędami, a ja decyzjami.

    Ja nie piszę o kodzie, który napisał początkujący programista nie mający pojęcia o programowaniu i jego całą aplikacją jest instancja jednej klasy.

    Mam na myśli kod, na którego wygląd wpłynęły takie, a nie inne decyzje, podejmowane na podstawie różnych zmiennych. Deadline nie jest usprawiedliwieniem, ale podejmując niektóre decyzje trzeba wziąć pod uwagę nie tylko 'piękno programowania obiektowego', ponieważ mając na celu tylko to, może się okazać, że na naszą 'idealną' aplikację nie ma już chętnego.

    Bo tak naprawdę boskie obiekty (te które ja oglądałem), to nie są implementacje, nad którymi należy się pochylić i płakać. Jest to zazwyczaj całość, którą należy rozbić na kawałki i odpowiednio przeprojektować.

    OdpowiedzUsuń
    Odpowiedzi
    1. Jeżeli piszemy aplikację typu "napisz, oddaj, zapomnij" to człowiek się nie będzie specjalnie przejmował.
      Jeżeli mówimy tu o czymś z czym będziemy pracować przez dłuższy okres czasu to takie zachowanie przypomina sranie do własnego łóżka. Przepraszam za wyrażenie.

      Usuń
  7. Prawidłowe zaprojektowanie jak mają wyglądać obiekty jest nie lada wyzwaniem dla początkujących programistów w OO. Wiadomo można podglądać wypociny innych programistów, którzy udostępniają swoje prace w sieci, chociaż niedoświadczony nie zauważy czy jest to napisane dobrze czy też źle. Znacie jakąś dobrą publikację/książkę pokazującą dobre praktyki podczas programowania OO ?

    OdpowiedzUsuń
    Odpowiedzi
    1. Podobno to jest dobre, ale tylko słyszałem takie oceny:
      http://helion.pl/ksiazki/programowanie-obiektowe-w-php-5-hasin-hayder,probph.htm

      Bardzo dobra książka o wzorcach:
      http://www.amazon.com/Design-Patterns-Elements-Reusable-Object-Oriented/dp/0201633612

      http://helion.pl/ksiazki/wzorce-projektowe-rusz-glowa-elisabeth-freeman-eric-freeman-bert-bates-kathy-sierra,wzorrg.htm

      http://helion.pl/ksiazki/inzynieria-oprogramowania-w-ujeciu-obiektowym-uml-wzorce-projektowe-i-java-bernd-bruegge-allen-h-dutoit,iowuje.htm

      Usuń