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:)