czwartek, 17 lipca 2008
Łatwość dostępu ponad uporządkowaniem informacji
* trzeba mieć klienta (alternatywą są interfejsy www, ale te z kolei mają IMHO kiepskie usability)
* swoista złożoność podejścia sprawia, że takie rozwiązanie skutecznie odstrasza mnie od regularnego korzystania.
Ostatnio sprawdzam w działaniu Notatnik Google, który stawia przede wszystkim na prostotę pracy z informacjami, szczególnie z takimi, które mają charakter szkiców, wolnych myśli, przemyśleń. No i jest zdalny.
Logika działania aplikacji jest zgodna z innymi narzędziami Google i samej idei Web 2.0. Przede wszystkim prostota i usability. Mamy zatem bardzo proste narzędzie, które umożliwia:
* tworzenia notatek (i nadawanie etykiet)
* notowania stron www (forma zakładek z możliwością cytowania tekstu strony)
* możliwość tworzenia sekcji w celu ewentualnego strukturyzowania notatek.
Dzięki integracji z Firefoxem poprzez odpowiedni plugin dostajemy pod prawym przyciskiem możliwość zanotowania fragmentu czytanej strony.
W zasadzie to wszystko (no dobra prawie wszystko). Najważniejsze jest jednak to, że nie jest potrzebna ogromna ilość opcji, które pozwolą zorganizować pracę z notatkami - prosta idea tagowania (wszechobecna w web 2.0) i możliwość cytowania fragmentów stron internetowych to praktycznie to, czego potrzeba w 85% przypadków (nie pytajcie skąd ta liczba ;-)).
W ten sposób mam zdalne narzędzie, w dodatku rozproszone - dokumenty można współdzielić a nawet współedytować oraz notatki mają charakter luźno ustrukturyzowany. Jak dla mnie świetnie. Jest to ciekawa alternatywa dla innych form przechowywania danych. Oczywiście jak sama nazwa wskazuje - bardziej notatek niż formalnych dokumentów, ale wtedy już warto poświęcić nieco czasu na SVN czy jakieś inne formy przechowywania plików na zdalnym serwerze (hmmm... Google Docs!?).
Przychodzi mi do głowy pewien wniosek, że w przypadku informacji, z którymi często pracujemy, które chcemy mieć pod ręką, ważniejsze staje się łatwość pracy z takimi dokumentami (patrz Google Notatnik) niż ich uporządkowanie (SVN czy serwery plików czy whatever).
Z drugiej strony to kolejne narzędzie Google, od którego można się uzależnić, co zaczyna być coraz bardziej niebezpieczne. Jeszcze przyjdzie taki dzień, kiedy Google zmonopolizuje świat - no dobra już to prawie zrobiło. Z tym że ilość danych o nas przechowywana przez Google staje się coraz bardziej niebezpieczna.
Jestem ciekaw waszych sposobów pracy ze zdalnymi dokumentami/plikami/notatkami, szczególnie takimi dokumentami, które wymagają częstej edycji, zaglądania do nich.
środa, 16 lipca 2008
Strategie doskonałości. Najprostsze możliwe środowisko.
Tutaj możesz pobrać pełną wersję PDF artykułu
Kto nie uwielbia tych wspaniałych chwil, kiedy projekt nabiera kształtu, kolejne funkcjonalności pojawiają się jedna za drugą i nie możemy się nacieszyć wspaniałym programistycznym dziełem. Pielęgnujemy każdy fragment, aby nasze oprogramowanie było jeszcze wspanialsze.
Jest to również moment, kiedy projekt zaczyna stawać się coraz bardziej złożony. Jeśli na przykład pracujemy nad aplikacją internetową, to przychodzi taki moment, kiedy aplikacja obsługuje logowanie, transakcje, współpracę z bazą danych, wielojęzyczność, złożone reguły bezpieczeństwa. Istnieje również pewna logika przetwarzania zdarzeń użytkownika – najczęściej korzystamy z jakiegoś frameworka MVC. System rzeczywiście jest już całkiem złożony. Restart całej aplikacji zajmuje kilkanaście a czasem kilkadziesiąt sekund, a nawet kilka minut.
Każdy dzień pracy w takim środowisku powoduje, że się przyzwyczajamy do tej całej złożoności. Co więcej, orientujemy się w niej coraz lepiej, czujemy się jak ryba w wodzie, korzystając z dobrze już znanych niuansów. Osiadamy w przyzwyczajeniu.
Rozwijamy dalej aplikację - tworzymy nową funkcjonalność. Niech będzie to ekran z wykresami tworzonymi na podstawie danych przechowywanych w systemie. Jeszcze nigdy nie mieliśmy do czynienia z wykresami, a przynajmniej nie z tą biblioteką, z której mamy skorzystać. Zatem ucząc się umiejętności związanych z tworzeniem wykresu, zaczynamy osadzać nowe rozwiązanie w złożonym systemie. W międzyczasie musimy przetestować kilka wariantów i sprawdzić, jak dokładnie działa komponent, którego chcemy użyć po raz pierwszy.
Zatem eksperymentujemy z różnymi parametrami. Zanim dokonamy testów, musimy odpowiednio skonfigurować framework MVC, odpowiednio osadzić komponent wykresu na stronie lub panelu, być może dodać elementy związane z bezpieczeństwem i wielojęzycznością. A wszystko to pojawia się tylko po to, żeby nauczyć się jak wykorzystywać komponent.
A przecież dopiero się uczymy...
Dopiero eksperymentujemy!
Nie jest nam potrzebny cały ten narzut.
Nagle jeden z parametrów nie do końca działa tak jak byśmy chcieli. W zasadzie nie wiemy o co chodzi. Jednocześnie cały system się nie uruchamia, bo na szybko oprogramowaliśmy controller (z MVC), więc przy okazji zmagamy się z jego błędami – problem z dostępem do danych.
W końcu udało się. Wracamy do naszego komponentu, ale jeszcze musimy zrestartować aplikację. Trwa to dość długo. Zbyt długo.
Czy w ogóle to całe środowisko jest potrzebne do przetestowania komponentu? Narzut ogromny – należy stworzyć wszystkie elementy potrzebne do uruchomienia testowego fragmentu, które narzuca środowisko. Czas potrzebny na efektywne badanie kolejnych parametrów jest subiektywnie oceniając wydłużony o 10-50 % z powodu złożoności systemu, w którym proces się odbywa.
A gdyby mieć pod ręką najprostsze możliwe środowisko aplikacji webowej (czasami dostarczane razem z bibliotekami jako blank application) i używać je do testowania nowych elementów lub prostych fragmentów funkcjonalności. Bez narzutu pozostałych elementów środowiska, takich jak bezpieczeństwo, wielojęzyczność, logowanie! Najprościej jak to tylko możliwe. Oczywiście, kiedy już uda się przeprowadzić próby na prostym środowisku, przenosimy rozwiązanie do złożonego systemu, w którym pracujemy.
Być może nie tworzysz aplikacji webowych, ale z pewnością znajdziesz analogie w obszarze, w którym się specjalizujesz. Ogólny schemat postępowania jest taki sam bez względu na technologię.
Być może wydaje się to bardzo oczywiste -bo jest oczywiste. Ale dlatego tak często o tym zapominamy i tak często tracimy niepotrzebnie czas i często szargamy swój system nerwowy analizując wyjątki z warstwy danych, mimo że w tym momencie zajmujemy się eksperymentowaniem z komponentem wykresu. Wiele razy byłem świadkiem (lub uczestnikiem) takiej sytuacji!
Całość powyższych rozważań można sprowadzić do zasadniczej myśli:
Pracuj w możliwie najprostszym środowisku
Bardzo łatwo ulec przekonaniu, które z pewnością w tym momentem przychodzi do głowy: „... ale w moim środowisku nie można nic uprościć! Nie można przenieść tego, co robię, w inne, prostsze środowisko...”. Mogę cię zapewnić Czytelniku, że prawie zawsze można środowisko uprościć. Główne pytanie, na które trzeba sobie odpowiedzieć, to czy rzeczywiście warto, ponieważ bywają sytuacje, w których stworzenie uproszczonego środowiska lub przygotowania środowiska, będzie mało opłacalne. Jednak w dużej części przypadków warto to robić, tym bardziej że wypracowane rozwiązanie często może zostać użyte podczas prac nad następnymi funkcjonalnościami. Zatem poniesiony wysiłek zaprocentuje jeszcze wielokrotnie.
Kilka praktycznych sposobów na upraszczanie środowiska opisałem poniżej. Jestem przekonany, mój Czytelniku, że będą stanowić inspiracje, dzięki którym będziesz pracował jeszcze efektywniej i ten wspaniały zawód programisty, stanie się jeszcze wspanialszy.
Twórz uproszczone konteksty pracy
W zasadzie przedstawiony na wstępie przykład jest ucieleśnieniem tej zasady.
Kiedy chcesz sprawdzić nowy komponent, bibliotekę, zastanów się jak możesz najprościej ją przetestować i poznać jej właściwości. Zazwyczaj złożona aplikacja, nad którą właśnie pracujemy, jest ostatnim miejscem nadającym się do tego celu.
Jeśli nigdy dotąd nie używałeś wyrażeń regularnych, a teraz będą ci potrzebne, zanim zaczniesz z nimi pracę najzwyczajniej świecie stwórz klasę z metodą main (tu mam na myśli Javę, ale każdy w to miejsce może wstawić sposób, w jaki tworzy się słynne już HelloWorld – bez narzutów technologicznych). I tam przetestuj wszystkie interesujące cię przypadki. W miejsce wyrażeń regularnych można wstawić każdą dowolną inną funkcjonalność: obiekty funkcyjne z jakarta-commons, generowanie pdf-ów, kopiowanie danych, łączenie się z serwerem SMTP i wysyłanie maili, bibliotekę do współpracy z bazą danych, funkcje statystyczne. Można wymieniać bez liku.
Kiedy już pracujesz w złożonym środowisku, np. budujesz strony internetowe z użyciem frameworka MVC, i chcesz przetestować nowy komponent graficzny, zazwyczaj lepiej zrobić to w środowisku poza projektem, a nie w aplikacji, którą właśnie tworzysz.
Jeśli chcesz przetestować działanie pola tekstowego, na który jest nałożona maska pewnego wzorca, który określa format danych przyjmowanych przez to pole, zrób to na możliwie najprostszym przykładzie, poza aplikacją. Zyskasz sporo czasu. Jeśli zrobisz to jeden raz – drugi, trzeci i kolejne będą coraz łatwiejsze.
Testuj jednostkowo i hermetyzuj logikę
Jeśli kiedyś zastanawiałeś się, do czego mogą przydać ci się testy jednostkowe, to mogę cię zapewnić, że między innymi mogą posłużyć jako świetne narzędzie do upraszczania środowiska pracy. Po pierwsze są świetnym zamiennikiem dla metody main – dają większe możliwości tworzenia bardziej złożonych przypadków testowania. Dodatkowo dzięki użyciu mock objects jesteśmy w stanie pracować z minimalnym podzbiorem logiki biznesowej.
Na przykład jeśli pracujemy nad metodą lub funkcją, która (1) przygotowuje treść maila i (2) go wysyła, nie musimy go fizycznie wysyłać. Podstawiamy mock object (bardzo uproszczoną implementację do celów testowych) i nie musimy czekać za każdym razem, żeby mail został wysłany. Oczywiście samo wysyłanie wiadomości też można przetestować. Ale można zrobić to osobno, a ewentualnie na końcu, kiedy oba mechanizmy (1) i (2) działają, przetestować je razem. Zgodnie z zasadą
W danej chwili testuj, zmieniaj lub pracuj nad jedną rzeczą
(ta zasada nie ogranicza się do programowania!)
Chciałbym zaznaczyć jedną rzecz. Stosowanie powyższych zasad nie uuchroni cię przed błędami, ale pozwoli ci zmniejszyć ilość sytuacji, w których błędy się będą pojawiać. Wyobraź sobie, że dzięki nawykowi upraszczania środowiska oszczędzisz godzinę dziennie (a możesz z pewnością więcej), w ciągu tygodnia oszczędzasz 5 godzin, w ciągu miesiąca 20 godzin. W ciągu roku ... ?
Powróćmy jednak do testów. Żeby w pełni używać testów jednostkowych musimy nauczyć się pewnej cennej umiejętności, którą nazywam tutaj hermetyzacją logiki. Tworząc oprogramowanie musimy tworzyć je modułowo, tak aby poszczególne części były jak najmniej między sobą zależne (ang. coupling ), tak aby można było ich używać niezależnie. W przypadku systemów obiektowych istnieje cała filozofia projektowania obiektowego, której celem jest osiągnięcie stanu, w którym poszczególne części systemu są od siebie jak najmniej zależne.
Jednak zarówno w przypadku systemów obiektowych jak i nieobiektowych można zastosować ogólniejszą zasadę odpowiedzialności , tzn. tak tworzyć system, aby wszystkie jego części miały jednoznacznie określoną odpowiedzialność, bez względu czy to będzie metoda, klasa, pakiet, procedura, funkcja czy formularz interfejsu użytkownika. Część taka będzie zamykać w sobie dobrze zdefiniowaną funkcjonalność, bez zbędnych powiązań z zewnętrznymi elementami.
Jeśli zatem kodujesz logikę aplikacji w interfejsie użytkownika, jesteś daleki od realizacji tej zasady. W ten sposób wyraźnie wiążesz ze sobą interfejs użytkownika i logikę, nie jesteś w stanie jej w prosty sposób rozdzielić i pracować nad nimi osobno. Jeśli tworzysz interfejs użytkownika, umieszczaj w tej części kod, który dotyczy tylko i wyłącznie interfejsu użytkownika – czyli budowania jego wyglądu, ustawiania pól i ich odczytywania. W interfejsie użytkownika nie powinniśmy tworzyć złożonych algorytmów przetwarzania danych (no chyba, że dotyczą wyświetlania elementów na ekranie), do tego celu używamy niezależnych funkcji, metod, klas.
Z pomocą w tym przypadku przychodzą między innymi wzorzec MVC oraz model architektury wielowarstwowej. Zawsze jednak można się posłużyć zdroworozsądkowym stosowaniem zasady odpowiedzialności, której konsekwencją są wymienione w poprzednim zdaniu koncepcje.
Chciałbym jeszcze raz podkreślić słowo „zdroworozsądkowym” – nie zawsze MVC czy inne koncepcje tego typu są do zastosowania w każdej sytuacji. Istnieje być może 3 % sytuacji, kiedy warto zakodować logikę w interfejsie użytkownika. Ale to jest jest tylko 3%. No może 5%, albo 10%. Ale cały czas jest to odstępstwo od reguły!
W danej chwili testuj, zmieniaj lub pracuj nad jedną rzecz
Powyższa reguła, choć już wspomniana wcześniej, jest tak istotna, że zasługuje na osobny punkt. Być może znacie odpowiedź na pytanie: „Jak zjeść słonia”. „Po kawałku”. Dokładnie ta reguła powinna towarzyszyć nam bezkompromisowo w codziennej pracy. A często nie towarzyszy. Możecie mi wierzyć, że często. Co więcej, również i mnie się zdarza czasami błądzić i jej nie stosować.
Zabierając się do realizacji większego zadania (takiego przynajmniej na 3-4 godziny), warto poświęcić kilka chwil na to, żeby zaplanować swoje działania. Zaplanować po to, by naszym słoniem, jak duży by on nie był, się nie zadławić.
Pamiętam bardzo dobrze moje doświadczenia programistyczne, kiedy podejmując się jakiegoś zadania, tworzyłem kilka godzin pełne rozwiązanie i wtedy uruchamiałem je... po raz pierwszy. No cóż, nigdy nie działało w tym momencie, bo w zasadzie nie miało prawa. Następne kilka godzin spędzałem na debugowaniu i poprawianiu kodu. Myślałem, że uda mi się zjeść całego słonia, od razu.
Zatem co warto zrobić na początku realizacji zadania? Mały plan. Tak aby ustalić przybliżony sposób tworzenia rozwiązania fragment po fragmencie. Załóżmy, że mam napisać sieć neuronową. (Jeśli nie wiesz zbyt dobrze co to takiego, nie przejmuj się, nie ma to dużego znaczenia w dalszych rozważaniach.) Nie tworzę gotowego rozwiązania. Krok po kroku zaczynam implementować najprostsze rzeczy. Na początek być może warto zaimplementować funkcję symulującą działanie neuronu. Być może później stworzyć klasę neuronu. Przetestować jego metody. Następnie być może implementować sieć. Następnie regułę propagacji wstecznej. To tylko przykładowa kolejność. Ważne, żeby w danym
momencie rozwijać, zmieniać jedną rzecz na raz . Kiedy ona zacznie działać i będzie przetestowana, brać się za następną. Oczywiście całość warto przemyśleć już na samym początku (przynajmniej na pewnym poziomie ogólności), ale implementować kawałek po kawałku, krok po kroku. Takie mikroiteracje. Ciekawą formą osiągnięcia opisanego wyżej efektu jest Test-Driven Development (aczkolwiek, nie jest to z pewnością jedyny sposób – na pewno warty uwagi).
Wiele razy widziałem sytuację, kiedy należało na przykład zaimplementować w aplikacji internetowej generowania raportu na podstawie danych przechowywanych w systemie i danych z bieżącego formularza. Programista jednocześnie rozwijał akcję (klasa obsługującej zdarzenie z interfejsu użytkownika), zmieniał interfejs użytkownika i pracował nad generowaniem raportu do pliku pdf (testując przy okazji jak generować pliki pdf). Wyobraźcie sobie, jak wiele szczegółów należy ogarniać umysłem, aby zapanować nad tym wszystkim. W takiej sytuacji zazwyczaj wszystkie elementy interferują ze sobą, a że są nie w pełni skończone, więc trudno znaleźć przyczynę popełnianych błędów.
Szukanie przyczyn błędów
Powyżej opisane sposoby mogą znacząco zmniejszyć ilość popełnianych błędów, ale bądźmy szczerzy – przyjdzie taki moment, kiedy przydarzy się sytuacja, w której nasz system lub jego fragment nie działa proprawnie. Jeśli stosujesz zasadę W danej chwili testuj, zmieniaj lub pracuj nad jedną
rzeczą , to zadanie znalezienia przyczyny błędu masz znacząco ułatwione. Ponieważ dokonujesz niewielkich kroków rozwijając kod i łatwo możesz określić zmiany, które doprowadziły do wystąpienia błędu. Ta reguła działa, kiedy kroki są wystarczająco małe. Jeśli przez ostatnią godzinę dokonałeś mniej więcej kilkunastu zmian w plikach konfiguracyjnych, kodzie i w HTML-u, to znaleźć przyczynę będzie trudniej. Jeśli natomiast najpierw dokonałeś zmiany w pierwszym pliku konfiguracyjnych i całość zadziałała, później zmiany w drugim pliku konfiguracyjnym i całość zadziałała, następnie dokonałeś zmian w kodzie i tu pojawił się błąd – to wszystko jasne.
Oczywiście w życiu nie jest tak różowo i zdarzają się sytuacje, kiedy błędy pojawiają sie po pewnym czasie (na przykład kilka dni po zmianach albo nawet po kilku miesiącach). Otóż w takich sytuacjach proponowana zasada brzmi następująco:
Uprość dany fragment rozwiązania, tak aby zaczął działać, a następnie dodawaj kolejne elementy, aż do momentu wystąpienia błędu.
Wtedy sytuacja będzie już jasna.
Załóżmy, że występuje taka sytuacja:
Aplikacja oparta o MVC – zdarzenie polega na tym, żeby pobrać odpowiednie dane i wygenerować plik pdf. Istnieje komponent, w jakiś sposób zainicjalizowany, który realizuje to zadanie:
1.pobiera dane ze źródła danych
2.na ich podstawie generuje obiekt potrzebny do wygenerowania pliku pdf
3.tworzony jest plik pdf
4.plik pdf wysyłany jest na ekran
Jak upraszczamy? Przykład postępowania (oczywiście szczegóły działania będą zależne od poszczególnego przypadku). Najpierw upewnijmy się, że komponent, którego używamy, jest poprawnie zainicjalizowany. Można na przykład podstawić za niego inny komponent (np. bardzo uproszczoną implementację komponentu) lub zainicjować go ręcznie, jeśli domyślnie jest tworzony na podstawie pliku konfiguracyjnego. Jeśli konfiguracja komponentu nie była przyczyną błędu, możemy zamiast pobierać dane ze źródła danych spreparować wyniki (badamy czy błąd zwiazany jest z wynikami pochodzącymi ze źródła danych). Następnie możemy sami wygenerować sztuczny obiekt do tworzenia pliku pdf (lub go w ogóle nie tworzyć). Zamiast tworzyć pdf (strumień binarny) można pokusić się o proste dane znakowe (strumień znakowy) i w ten sposób wykluczyć ewentualne błędy tworzenia pliku pdf i pokazywania go na ekranie. Jeśli na którymś etapie odkryjemy, że pojawia się błąd, analizujemy dany etap, stosując powyższy algorytm ograniczając go do tego etapu.
W procesie wyszukiwania błędów niezwykle przydatne są pliki logów czy systemy debugujące (choć te przy złożonych systemach, są bardzo czasochłonne). Są to jednak tylko narządzia i mogą co najwyżej wesprzeć powyżej opisany proces. Całość pracy intelektualnej musimy wykonać sami.
Oczywiście przebieg postępowania będzie inny dla innej technologii czy innej sytuacji. Jesteś ekspertem w swojej dziedzinie, więc najlepiej będziesz wiedział jak tę strategie dostosować do środowiska, w którym działasz.
Skoro jesteśmy przy wyszukiwanie błędów, to przy okazji jeszcze jedna wskazówka:
Przyczyny błędów są najczęściej ukryte tam, gdzie dalibyśmy sobie głowę uciąć, że ich tam nie ma. Najprostsza i jedna z częstszych przyczyn błędów to literówka.
Podsumowanie
Powyższe przykłady miały nieco wyraźniej nakreślić regułę upraszczania środowiska, w którym rozwiązuje się dany problem czy zadanie. Ponieważ opisane powyżej aspekty dotyczą sfery naszych nawyków i przekonań, z pewnością Czytelniku kilkakrotnie mogłeś czuć wewnętrzny opór przed zaakceptowaniem ich treści lub stwierdzić, że w twoim przypadku nie ma to zastosowania. Jest to całkiem naturalny proces, ponieważ przedstawiane powyżej tezy przynależą do świata nauk humanistycznych, a szczególniej społecznych i psychologicznych. Nie są to pewniki. Tezy te potwierdzają się w większości przypadków, ale nie muszą we wszystkich. Ale jak to się mówi... wyjątki potwierdzają regułę.
Dokładniejsze przyjrzenie się swoim nawykom i eksperymenty związane z proponowanymi strategiami na pewno mogą stanowić dobrą zabawę, czego ci Czytelniku życzę. I oczywiście zwiększania efektywności oraz wzbogacania przyjemności z tworzenia oprogramowania.
wtorek, 8 lipca 2008
Wzorce implementacyjne cz. 1
Tutaj możesz pobrać pełną wersję PDF artykułu
Tworząc oprogramowanie tworzymy wiele tysięcy wierszy kodu. Część tego kodu jest naszym kreatywnym wysiłkiem w tworzeniu Nowego, a część to powtarzalne elementy, które pojawiają się na każdym kroku. Jako programiści nie lubimy powtarzać wiele razy tych samych czynności, a jednak to robimy, gdyż nie jesteśmy w stanie tego uniknąć. W takiej sytuacji warto nabyć pewnych nawyków, które pozwolą nam jednoznacznie i świadomie podejmować decyzje, doprowadzą do czytelnego i jasnego w przesłankach kodu.
Przytoczę za Kentem Beckiem zestaw praw, który dotyczy ogromnej części pisanego oprogramowania:
kod oprogramowania częściej jest czytany niż pisany (sic!),
w programowaniu nie istnieje stwierdzenie „zrobione” (sic!) - kod ulega bezustannemu rozwojowi,
sercem oprogramowania (niskopoziomowym) jest odpowiednie zarządzanie przepływem instrukcji oraz zarządzanie jej stanem (zmiennymi),
osoba czytająca kod musi być w stanie łatwo zrozumieć ideę, koncept zawarty w kodzie oraz łatwo dotrzeć do szczegółów implementacyjnych (kod powinien umożliwiać płynne przejście od ogólnej wizji do szczegółów i od szczegółów do wizji).
Wspomniane wcześniej nawyki, które możemy inaczej nazwać wzorcami implementacyjnymi, są w dużym stopniu subiektywną decyzją, choć prawdopodobnie większość z nich jest używana i akceptowana przez profesjonalnych programistów. Warto zatem podkreślić trzy podstawowe wartości, które stoją za przedstawianymi w tym i kolejnych artykułach wzorcami:
komunikacja (komunikatywność),
prostota,
elastyczność.
Koszt oprogramowania
Każdy programista, który brał udział w większym projekcie, wie że wytworzenie oprogramowania to tylko wierzchołek góry lodowej. System stworzony w wersji 1.0 to nie koniec podróży:
z czasem rozwija się – ulega zmianom,
należy naprawiać znalezione błędy.
Koszt całkowity jest więc dużo większy niż tylko oprogramowanie zasadniczych funkcjonalności. Co więcej, z przeprowadzonych badań statystycznych wynika, że koszt utrzymania systemu jest dużo większy niż jego wytworzenia (w średniej relacji 30%/70%).
A co to takiego ten magiczny „koszt utrzymania”? Koszt zrozumienia, zmiany, przetestowania i wdrożenia. Ile razy zaglądaliśmy do własnego kodu (nie mówiąc już o czyimś kodzie), który został stworzony pół roku wcześniej i stwierdzaliśmy, że nie wiemy o co chodzi! Stosowanie wzorców – niskopoziomowych wzorców implementacyjnych, dotyczących strategii tworzenia kodu źródłowego, upraszcza nam życie – wiemy czego spodziewać się w kodzie i wiemy, że łatwo będziemy go mogli przeczytać.
Zatem o co chodzi? Co to za wzorce? Chciałbym przedstawić kilka a może kilkanaście strategii, które ułatwią podejmowanie decyzji podczas tworzenia oprogramowania. Dla uwiarygodnienia przedstawianych propozycji, podpierał się będę kodem źródłowym projektów:
Spring Framework,
Hibernate,
Struts2,
kod źródłowy JDK6.
Oczywiście nie sposób zawrzeć tychże strategii w jednym artykule, dlatego niniejszym rozpoczynam szereg artykułów poświęconych wzorcom implementacyjnym. Do dzieła!
Klasa
Klasa jest tak podstawowym elementem programowania obiektowego, że równie trudno określić oczywiste zasady, jakimi należy posługiwać się definiując i nazywając klasy. Istnieje wiele technik, które ułatwiają wyodrębnianie klas podczas analizy, takie jak karty CRC (http://en.wikipedia.org/wiki/Class-Responsibility-Collaboration_card) czy diagram klas analitycznych (Robustness Diagram) (http://www.agilemodeling.com/artifacts/robustnessDiagram.htm). Jednak ciągle to do nas jako projektantów lub programistów należy decyzja, jak stworzyć nową klasę. Jest to największa sztuka związana z programowanie obiektowym.
Pokrótce można powiedzieć, że
każda klasa powinna mieć dobrze określoną odpowiedzialność
Co oznacza dobrze? Klasa powinna być odpowiedzialna zazwyczaj za jedną rzecz w systemie.
Na przykład klasa Converter powinna dokonywać tylko i wyłącznie operacji konwersji i nie zajmować się niczym innym (np. zapisywaniem wyników konwersji do bazy danych). Po czym poznać, że klasa łamie tę zasadę? Główny objaw do duża ilość metod (choć to tylko objaw ilościowy), ostatecznie należy przeanalizować nazwy metod, a w zasadzie to, co takiego one robią. Jeśli metody nie są spójne ze sobą i wychodzą poza przeznaczenie klasy, to odpowiedzialnośc nie została dobrze określona.
Nazwa klasy musi być z jednej strony zwięzła, a z drugiej strony oddająca w pełni przeznaczenie klasy.
Nadanie nazwy klasie to próba pogodzenia dwóch skrajnych sił – z jednej strony nazwa powinna być jak najkrótsza i jak najprosztsza (łatwiej ją zapamiętać, skojarzyć), a z drugiej strony musi precyzyjnie odzwierciedlać przeznaczenie klasy (co czasem wymaga dłuższych nazw). Jedna z najważeniejszych funkcji nazwy klasy to komunikatywność. W praktyce nie warto od razu szukać idealnej nazwy, tylko na początku nadać taką, która nam przychodzi do głowy, a z czasem gdy odnajdziemy dużo lepszą, zmienić ją. W dobie zaawansowanych narzędzi refaktoryzacji, zmiana nazwy klasy nie powinna być zbyt wielkim problemem.
Warto w praktyce dążyć w pierwszej kolejności do nazw klas złożonych z jednego słowa (mam tu na myśli przede wszystkim klasy, które nie są dziedziczone z innych), gdyż łatwiej je zapamiętać. Ponadto nazwa klasy powinna być jak najbliższa świata klienta (słowem z jego dziedziny), jeśli klasa odzwierciedla element świata klienta.
Jest jeszcze jedna wskazówka, którą warto się kierować:
Im mniej klas w projekcie, tym lepiej
Oczywiście kierunek jej oddziaływania jest przeciwny do kierunku oddziaływania reguły, która nakazuje wyodrębnianie jednoznacznej odpowiedzialności, gdyż ta z kolei powoduje większe rozdrobnienie klas. Tutaj jednak musimy działać zgodnie z maksymą wypowiedzianą przez Einsteina: twórzmy rzeczy najprościej jak tak tylko możliwe, ale nie prościej.Te dwie siły trzeba w praktyce zrównoważyć. Z jednej strony tworzyć klasy od jasno określonej odpowiedzialności, z drugiej starać się ograniczać ilość powstających klas. Im więcej klas, tym więcej nazw do zapamiętania, miejsc do analizowania, debugowania, testowania itp. itd.
Podsumowując powyższe rozważania:
klasa musi mieć dobrze określoną odpowiedzialność (jednoznacznie) i wszystkie metody muszą być spójne z tą odpowiedzialnością,
nazwa klasy powinna być zwięzła, ale z drugiej strony jednoznacznie wyrażająca odpowiedzialność klasy,
w pierwszej kolejności należy szukać nazw złożonych z jednego słowa,
im mniej klas w projekcie tym lepiej.
Dobrym case study są projekty open source. Jeśli nie znasz omawianej biblioteki, nie ma to większego znaczenia, gdyż zazwyczaj nie będziemy analizować funkcjonalności tychże, jakże wspaniałych narzędzi, a przyjrzymy się bliżej strukturze ich klas.
Na początek spójrzmy na klasę org.hibernate.cfg.Configuration z projektu Hibernate 3. Nazwa Configuration jest łatwa, jednoznaczna, wydaje się świetnie oddawać odpowiedzialność obiektu – jest to klasa skupiająca w sobie informacje związaną z konfiguracją Hibernate'a.
Jak się bliżej przyjrzymy zawartości klasy, to zobaczymy m. in. takie elementy:
public class Configuration implements Serializable {
private static Logger log = LoggerFactory.getLogger( Configuration.class );
protected Map classes;
protected Map imports;
protected Map collections;
.........
public Configuration addFile(File xmlFile) throws MappingException {
.........
public Configuration addXML(String xml) throws MappingException {
.........
protected void parseMappingElement(Element subelement, String name) {
..........
private void parseSecurity(Element secNode) {
..........
Oprócz metod do przechowywania i zarządzania konfiguracją, pojawiają się m. in. metody do parsowania pliku XML. Tutaj można poddać wątpliwość jednoznaczność odpowiedzialności tejże klasy, gdyż zaczyna być ona odpowiedzialna za parsowanie pliku XML. Choć można by szukać uzasadnienia tej decyzji w zasadzie im mniej klas, tym lepiej.
Inna klasa z tego samego projektu org.hibernate.cfg.Mapping, budzi mniejsze wątpliwości – do tej klasy wydzielono wszelkie elementy związane z mapowaniem ORM (http://en.wikipedia.org/wiki/Object-relational_mapping). Nazwa klasy jest prosta, jednoznaczna i ekspresyjna.
Spójrzmy jeszcze na podwórko jdk6. Klasa java.text.DateFormat ma bardzo ostro określoną odpowiedzialność – formatowanie dat w zależności od ustawień regionalnych (Locale). Jednoznaczna i prosta nazwa (potrzebne były dwa słowa do tego celu).
Takich przykładów znajdziemy tu bardzo dużo np.:
java.util.regex.Matcher – nazwa jasno wyraża odpowiedzialność – zbiór operacji związanych z dopasowywaniem wyrażeń regularnych,
java.net.Socket – nazwa jasno i jednoznacznie określa przeznaczenie klasy – reprezentacja gniazda TCP oraz zbiór operacji na tym gnieździe.
Analizując kod źródłowy projektów Hibernate i jdk6 wyraźnie widać preferencje, którymi zaznaczają się w tych projektach:
w Hibernate często zdarza się łamanie zasad odpowiedzialności na rzecz zmniejszenia ilości klas w projekcie (których i tak jest dużo), co często jednak prowadzi do powstawania trudnych do ogarnięcia klas (kilkanaście a nawet kilkadziesiąt metod),
w jdk6 (i wcześniejszych również) tendencja jest zgoła odwrotna, zdecydowanie nacisk kładzie się na ostrą, jednoznaczną odpowiedzialność, kosztem wytwarzania dużej ilości klas – np. żeby obsłużyć w pełni wyrażenie regularne potrzeba aż trzech klas (!).
Polecam analizę kodów źródłowych tychże projektów oraz pozostałych (np. spring framework), gdyż jest to jeden z najlepszych praktycznych nauczycieli naprawdę dobrego stylu pisania oprogramowania.
Już samo podjęcie decyzji jaką stworzyć klasę (jaka odpowiedzialność, jakie metody, jaka nazwa), jest nielada wyzwaniem. Dlatego warto poświęcić wieczór, aby przeanalizować za i przeciw, tak aby trafnie podejmować tę jedną z częściej podejmowanych decyzji.
Oczywiście to zaledwie początek i wierzchołek góry lodowej. Niebawem nieco rozważań na temat interfejsów.