środa, 5 listopada 2008
Weekendowe warsztaty Wzorce projektowe
Warsztaty umożliwiają nabycie praktycznych umiejętności tworzenia aplikacji z użyciem wzorców projektowych w języku Java.
Zobacz program
http://www.bnsit.pl/files/Wzorce_projektowe_java_i_refaktoring.pdf
Czego potrzebujesz?
• Notebook z kartą wi-fi
• Zapału, chęci i otwartego umysłu
Twoja inwestycja to jedyne: 800 zł.
Nigdzie nie znajdziesz szkolenia ani warsztatów w takiej cenie i jakości.
Powiedz znajomym.
Gdzie?
Warszawa, 13-14.12.2008
Wrocław, 13-14.12.2008
Kraków, 13-14.12.2008
Kontakt:
m.sieraczkiewicz [[[[[ MAUPA ]]]]]] bnsit.pl
+48 500 189 752
http://www.bnsit.pl
niedziela, 10 sierpnia 2008
2. Spotkanie ŁJUG - podsumowanie
Wczoraj, tj. 09.08.2008 odbyło się kolejne, drugie już spotkanie Łódzkiego JUGa. Mimo wakacyjnego okresu, znalazło się wielu śmiałków, którzy odważyli się poświęcić część sobotniego popołudnia, aby przyjść na spotkanie, które dotyczyło tak wspaniałego języka jak Java.
Temat był dość nietypowy, ponieważ Wzorce implementacyjne, nie są szerzej znanym terminem, a jak się okazuje, wielu z nas ma na co dzień do czynienia z tematami przez nie podejmowanymi. Zagadnienia te często zahaczają o zagadnienia podstawowe takie jak sposoby nazywania, wydzielania klas, tworzenia wyjątków itp.
To taka magia, którą prędzej czy później włada doświadczony programista. Lecz czasami można do tej magii zbliżyć się wcześniej.
Poniżej zamieszczam prezentację z tego spotkania. Nie zastąpi ona oczywiście osobistej obecności, ale może pozwolić przybliżyć się do tematu i zachęcić do dalszej eksploracji.
Tutaj możesz wersję PDF prezentacji
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.
czwartek, 19 czerwca 2008
Porządki w kodzie czyli nie tylko o refaktoringu
Poniżej zamieszczam cały artykuł poświęcony porządkom w kodzie - poprawnie sformatowany.
Tutaj możesz pobrać pełną wersję PDF artykułu
Porządki w kodzie czyli nie tylko o refaktoringu cz. 3
Tutaj możesz pobrać wersję PDF artykułu
Ze względu na problemy z formatowaniem kodu w blogspot artykuł ten warto przeczytać w formie pliku PDF.
Przyjrzyjmy się teraz metodzie boolMultipy oraz zmiennej lokalnej o nazwie sum. Zmienna ta jest deklarowana na samym początku metody, uzywana jest dużo później. Jest to nawyk, który pozostał jeszcze po językach proceduralnych (takich jak wczesne C przy PL/SQL), gdzie wymaga się zadeklarowania wszystkich zmiennych używanych w metodzie na samym początku. Na szczęście większość współczesnych języków programowania (w szczególności języków obiektów) nie ma tego ograniczenia. Zamiast tego podejścia sugeruję realizację zasady leniwej deklaracji zmiennych, którą można wyrazić za pomocą zdania
Deklaruj zmienne najpóźniej jak to tylko możliwe
Warto to robić z bardzo prostego powodu – łatwiej będzie czytać kod, jeśli w zasięgu wzroku będziemy mieli operacje wykonywane na danej zmiennej. Przy bardziej złożonych metodach umieszczenie deklaracji na samym początku, może spowodować, że analizując dalszą część metody nie będziemy w stanie zorientować się czy zmienna była do tej pory przetwarzana czy nie i co wpłynęło na jej wartość.
Załóżmy, że przewinęliśmy ekran tak, że widzimy następujący kod:
for( int i = 0; i < len; i++ ) {
if ( this.get(i) && extendedBitSet.get(i) ) {
sum++;
}
}
return sum % 2 ;
Rodzi się we mnie natychmiast pytanie – a co to za suma? Czy działo się z nią coś wcześniej? Czy może jej wartość została pobrana z zewnątrz?
W przypadku jednej zmiennej jeszcze to nie jest duży problem, ale wyobraźmy sobie sytuację, kiedy takich zmiennych jest pięć. Śledzenie logiki takiej metody będzie bardzo trudne.
Bardzo prosta operacja przeniesienia deklaracji nieco niżej spowoduje, że kod będzie dużo bardziej czytelny:
public int boolMultiply( ExtendedBitSet extendedBitSet ) {
int len = 0;
if( this.fixedLength < extendedBitSet.fixedLength ) {
len = this.fixedLength;
} else {
len = extendedBitSet.fixedLength;
}
int sum = 0;
for( int i = 0; i < len; i++ ) {
if ( this.get( i ) && extendedBitSet.get( i ) ) {
sum++;
}
}
return sum % 2 ;
}
Nieco bardziej złożona sytuacja jest w metodzie toByteArray. Jest ona niesamowicie trudna w analizie. Mi zajęło około 20 minut zrozumienie zasady jej działania. Teraz sobie wyobraźmy projekt złożony z tysiąca klas, z których każda zawiera tego typu metody. Zapanowanie nad takim kodem będzie graniczyło z cudem. Spójrzmy na kod tej metody:
public byte[] toByteArray() {
int bytesNumber = 0;
if ( fixedLength % 8 == 0 ) {
bytesNumber = fixedLength / 8;
} else {
bytesNumber = fixedLength / 8 + 1;
}
byte [] arr = new byte[ bytesNumber ];
for( int j = bytesNumber - 1, k = 0; j >= 0 ; j--, k++ ) {
for( int i = j * 8 ; i < ( j + 1 ) * 8; i++ ) {
if ( i == fixedLength ) {
break;
}
if ( get( i ) ) {
arr[ k ] += (byte) Math.pow( 2, i % 8 );
}
}
}
return arr;
}
Metoda toByteArray zwraca bajtową reprezentacje ciągów bitowych – czyli ciąg bitowy o długości 14 bitów można zaprezentować za pomocą 2 bajtów, zaś ciąg bitowy o długości 23 bity można zaprezentować za pomocą 3 bajtów.
Algrotym można opisać w następujących krokach:
określ liczbę bajtów,
dla każdej ósemki bitów (lub kilku bitów w przypadku niepełnych bajtów) wykonaj następującą operację:
sprawdź wartość każdego bitu
jeśli bit ma wartość 1, dodaj odpowiednią potęgę dwójki (wynikającą z pozycji bitu w bajcie) do wyniku danego bajtu.
Dlaczegóżby nie wyrazić tego algorytmu w formie programistycznej? Często o tym się zapomina. Jako programiści próbujemy w zwięzły sposób wyrazić nasze pomysły, co zazwyczaj prowadzi do bardzo nieczytelnych rozwiązań, których nie tylko inni ale i my sami nie jesteśmy w stanie zrozumieć w krótkim czasie. Ideałem jest dążenie do tego, aby jedno spojrzenie wystarczyło do odszyfrowania intencji twórcy. Nie potrzebujemy zagłębiać się w szczegóły realizacji algortmu, ważne byśmy wychwycili jego główną myśl. Spójrzmy na inną implementację metody toByteArray:
public byte[] toByteArray() {
int bytesCount = computeBytesCount();
byte [] byteArray = new byte[ bytesCount ];
for ( int i = 0; i < bytesCount; ++i ) {
int byteNumber = bytesCount - i - 1;
byteArray[ byteNumber ] = computeByteValue( i );
}
return byteArray;
}
Najważniejsza zmiana, polega na bezpośrednim wyrażeniu algorytmu na ogólnym poziomie. Dzięki czemu jesteśmy w stanie w krótkim czasie zrozumieć intencję twórcy. Pomocnicza metoda computeBytesCount znajduje ilość bajtów nowej reprezentacji. W pętli dla każdego bajtu wykonywana jest operacja obliczania wartości bajtu i zapamiętywania wyniku. Czyż taki zapis nie jest dużo prostszy? Co z tego, że nie widać wszystkich elementów realizacji algorytmu. Bardziej dociekliwi zawsze mogą zajrzeć do metod computeBytesCount i computeByteValue.
Mogą one wyglądać następująco:
private byte computeByteValue( int byteNumber ) {
int firstBitPosition = byteNumber * 8;
int lastBitPosition = ( byteNumber + 1 ) * 8 - 1;
byte byteValue = 0;
for ( int i = this.nextSetBit( firstBitPosition );
i >= firstBitPosition && i <= lastBitPosition;
i = this.nextSetBit( i + 1 ) ) {
int currentBitPosition = i - firstBitPosition;
if ( get( i ) == true ) {
byteValue += (byte) Math.pow( 2, currentBitPosition % 8 );
}
}
return byteValue;
}
private int computeBytesCount() {
int bytesCount = 0;
if ( fixedLength % 8 == 0 ) {
bytesCount = fixedLength / 8;
} else {
bytesCount = fixedLength / 8 + 1;
}
return bytesCount;
}
Warto zauważyć, że kod realizujący zadanie zbytnio się nie uprościł, ale dużo łatwiej jest go teraz przeanalizować i zrozumieć. Teoria refaktoringu nazywa tego typu działania wyłuskiwaniem metody (ang. extract method). Prawdę mówiąc tutaj poszliśmy nieco dalej, gdyż zmodyfikowaliśmy nieco implementację algorytmu, tak aby stała się bardziej czytelna. Zwróćmy uwagę na wiersze:
int firstBitPosition = byteNumber * 8;
int lastBitPosition = ( byteNumber + 1 ) * 8 - 1;
Tak naprawdę są one wyodrębnieniem bardzo enigmatycznych wyrażeń z wiersza:
for( int i = j * 8 ; i < ( j + 1 ) * 8; i++ ) {
Czyż intencja w takim przypadku nie staje się oczywista? Moje wieloletnie doświadczenia doprowadziły mnie do wniosku: Jawnie nazywaj złożone elementy kodu
zamiast
j * 8
napisz int firstBitPosition = byteNumber * 8;
Zasadę te rozszerzam do instrukcji warunkowych. Często w kodzie możemy spotkać wyrażenia, za którymi kryje się pewna logika. Warto bezpośrednio wyrazić naszą intencję. Czytelność niesamowicie wzrasta. Zamiast
if ( index >= 0 )
napisz boolean isIndexInRange = ( index >= 0 );
if ( isIndexInRange )
Kod zaczyna się czytać jak książkę! Przecież programowanie jest dla ludzi. Ułatwiajmy zatem sobie życie.
A oto najważniejsza zasada, będąca kwintesencją powyższych rozważań:
Pisz kod w taki sposób, aby czytało się go jak powieść. Używaj jednoznacznych i jednocześnie prostych nazw. Realizowane operacje dziel na logiczne części i każdą implementuj w osobnych metodach.
Myślę, że jak na jeden raz, wystarczy. Wystarczy, żeby zaostrzyć apetyty i wzbudzić ochotę na więcej. Żeby oprowadzić nieco po ogrodzie refaktoringu i jego przyległościach. Zapewniam, że to niesamowite miejsce i daje niesamowicie wiele radości i satysfakcji.
Poniżej zamieszczam ostateczną wersję kodu, który przechodzi załączone testy (oczywiście ze zmianą uwzględniającą niestatyczność metod merge i boolMultiply).
Końcowa postać przykładowej klasy (warto ją porównać z postacią początkową):
public class ExtendedBitSet extends BitSet {
private int fixedLength = 0;
public ExtendedBitSet( int size, String str ) {
super( size );
fixedLength = size;
initializeBitSet( str );
}
public ExtendedBitSet( String str ) {
this( str.length(), str );
initializeBitSet( str );
}
private void initializeBitSet( String str ) {
int strLength = str.length();
for( int i = 0; i < strLength; ++i ) {
if ( str.charAt( strLength - 1 - i ) == '1' ) {
set( i );
}
}
}
public void merge( ExtendedBitSet extendedBitSet ) {
for ( int i = extendedBitSet.nextSetBit( 0 ); i >= 0;
i = extendedBitSet.nextSetBit( i + 1 ) ) {
this.set( this.fixedLength + i );
}
this.fixedLength = this.fixedLength + extendedBitSet.fixedLength;
}
public int boolMultiply( ExtendedBitSet extendedBitSet ) {
int len = 0;
if( this.fixedLength < extendedBitSet.fixedLength ) {
len = this.fixedLength;
} else {
len = extendedBitSet.fixedLength;
}
int sum = 0;
for( int i = 0; i < len; i++ ) {
if ( this.get( i ) && extendedBitSet.get( i ) ) {
sum++;
}
}
return sum % 2 ;
}
public byte[] toByteArray() {
int bytesCount = computeBytesCount();
byte [] byteArray = new byte[ bytesCount ];
for ( int i = 0; i < bytesCount; ++i ) {
int byteNumber = bytesCount - i - 1;
byteArray[ byteNumber ] = computeByteValue( i );
}
return byteArray;
}
private byte computeByteValue( int byteNumber ) {
int firstBitPosition = byteNumber * 8;
int lastBitPosition = ( byteNumber + 1 ) * 8 - 1;
byte byteValue = 0;
for ( int i = this.nextSetBit( firstBitPosition );
i >= firstBitPosition && i <= lastBitPosition;
i = this.nextSetBit( i + 1 ) ) {
int currentBitPosition = i – firstBitPosition;
if ( get( i ) == true ) {
byteValue += (byte) Math.pow( 2, currentBitPosition % 8 );
}
}
return byteValue;
}
private int computeBytesCount() {
int bytesCount = 0;
if ( fixedLength % 8 == 0 ) {
bytesCount = fixedLength / 8;
} else {
bytesCount = fixedLength / 8 + 1;
}
return bytesCount;
}
public String convertToBitString( int size ) {
char [] resultArray = new char[ size ];
for ( int i = 0; i < size; ++i ) {
resultArray[ i ] = '0';
}
for ( int i = this.nextSetBit( 0 ); i >= 0; i = this.nextSetBit( i + 1 ) ) {
resultArray[ size - 1 - i ] = '1';
}
return new String( resultArray );
}
public String convertToBitString() {
return convertToBitString( this.fixedLength );
}
}
Dodatek. Test przykładowej klasy.public class ExtendedBitSetTest extends TestCase {
public void testConstructorSizeAndString() throws Exception {
assertEquals( "{0, 2}", new ExtendedBitSet( 10, "101" ).toString() );
assertEquals( "0000000101", new ExtendedBitSet( 10, "101" ).convertToBitString() );
assertEquals( "0000001011", new ExtendedBitSet( 10, "1011" ).convertToBitString() );
assertEquals( "0010001011", new ExtendedBitSet( 10, "10001011" ).convertToBitString() );
assertEquals( "{0, 1, 3, 7}", new ExtendedBitSet( 10, "10001011" ).toString() );
assertEquals( "{0}", new ExtendedBitSet( 10, "001" ).toString() );
assertEquals( "0000000001", new ExtendedBitSet( 10, "001" ).convertToBitString() );
assertEquals( "00000000001", new ExtendedBitSet( 10, "001" ).convertToBitString( 11 ) );
}
public void testMerge() throws Exception {
ExtendedBitSet extendedBitSet1 = new ExtendedBitSet( 10, "1" );
ExtendedBitSet extendedBitSet2 = new ExtendedBitSet( 10, "1" );
assertEquals( "0000000001", extendedBitSet1.convertToBitString() );
assertEquals( "0000000001", extendedBitSet2.convertToBitString() );
ExtendedBitSet mergedBitSet = ExtendedBitSet.merge( extendedBitSet1, extendedBitSet2 );
String mergedString = mergedBitSet.convertToBitString();
assertEquals( "00000000010000000001", mergedString );
assertEquals( "{0, 10}", mergedBitSet.toString() );
assertTrue( mergedBitSet.get( 0 ) == true );
}
public void testBoolMultiple() throws Exception {
ExtendedBitSet extendedBitSet1 = new ExtendedBitSet( 3, "1" );
ExtendedBitSet extendedBitSet2 = new ExtendedBitSet( 10, "1" );
assertEquals( 1, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
extendedBitSet1 = new ExtendedBitSet( 1000, "1" );
extendedBitSet2 = new ExtendedBitSet( 2, "1" );
assertEquals( 1, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
extendedBitSet1 = new ExtendedBitSet( 10, "1" );
extendedBitSet2 = new ExtendedBitSet( 10, "1" );
assertEquals( 1, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
extendedBitSet1 = new ExtendedBitSet( 10, "1" );
extendedBitSet2 = new ExtendedBitSet( 10, "10" );
assertEquals( 0, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
extendedBitSet1 = new ExtendedBitSet( 10, "10" );
extendedBitSet2 = new ExtendedBitSet( 10, "10" );
assertEquals( 1, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
extendedBitSet1 = new ExtendedBitSet( 10, "110" );
extendedBitSet2 = new ExtendedBitSet( 10, "110" );
assertEquals( 0, ExtendedBitSet.boolMultiply( extendedBitSet1, extendedBitSet2 ) );
}
public void testToByteArray() throws Exception {
ExtendedBitSet extendedBitSet = new ExtendedBitSet( "100000110" );
byte[] toByteArray = extendedBitSet.toByteArray();
assertEquals( 1, toByteArray[ 0 ] );
assertEquals( 6, toByteArray[ 1 ] );
extendedBitSet = new ExtendedBitSet( "10111111111" );
toByteArray = extendedBitSet.toByteArray();
assertEquals( 5, toByteArray[ 0 ] );
assertEquals( -1, toByteArray[ 1 ] );
}
}
środa, 18 czerwca 2008
Porządki w kodzie czyli nie tylko o refaktoringu cz. 2
Tutaj możesz pobrać wersję PDF artykułu
Ze względu na problemy z formatowaniem kodu w blogspot artykuł ten warto przeczytać w formie pliku PDF.
Zatem zrobiliśmy pierwszy krok w kierunku uporządkowania pierwotnego kodu.
Zróbmy zatem kolejny. Przyjrzyjmy się bliżej następującemu fragmentowi:
public class ExtendedBitSet extends BitSet {
int length ;
Pole klasy length ma widoczność pakietową. Prawdopodobnie nie to było zamierzeniem autora. Być może zapomniał w sposób jawny napisać odpowiedni modyfikator dostępu. W każdym razie warto znać jedną z podstawowych konsekwencji tego rozwiązania: pole to będzie dostępne dla wszystkich klas w pakiecie, co ogranicza jego enkapsulacje informacji. Przypadek ten można uogólnić do stwierdzenia: Nadawaj najbardziej restrykcyjny z możliwych modyfikatorów dostępu
W tym przypadku byłby to oczywiście modyfikator prywatny:
private int length;
W przypadku gdy tworzona klasa będzie klasą bazową innych klas oraz będzie potrzeba udostępnienia tego pola, należy użyć modyfikatora dostępu protected:
protected int length;
Dobrą praktyką jest jednak wybieranie modyfikatora private, aż do momentu, gdy nie zaistnieje potrzeba użycia tego pola w klasie podrzędnej (czyli do momentu wyprowadzania nowych klas). Jest to analogia do typowej dla administratorów sieciowych strategii bezpieczeństwa: domyślnie blokuj.
Poza powyższymi uwagami, chciałbym zaproponować jeszcze jedno udoskonalenie. Jestem zwolennikiem jawnego inicjalizowania zmiennych, gdyż jest to bezpośrednie ujawnienie intencji autora. Jeśli tworzę pole obiektowe, to jawnie przypisuję mu wartość początkową na null, kiedy tworzę liczbę całkowitą inicjuję ją zerem. W ten sposób oszczędzam potencjalnemu czytelnikowi mojego kodu domyślania się, czy rzeczywiście chodziło mi o wartość domyślną, czy być może nie dokońca znając szczegóły języka, przyjąłem błędne założenie. W przypadku wyszukiwania przyczyn błędów może to mieć duże znaczenie.
Zatem podsumowując:
Jawnie inicjuj pola i zmienne
W efekcie uzyskamy takie rozwiązanie:
private int length = 0;
W powyżej przytoczonym wierszu ujawnił się jeszcze jeden nawyk związany ze sposobem programowania
Jak najwięcej przestrzeni dla Twoich oczu
Wzrok lubi przestrzeń. Nie lubi zbitych zdań, ogromnych ilości wyrażeń na niewielkiej przestrzeni. Zróbcie prosty eksperyment. Weźcie niewielki fragment swojego kodu i dodajcie kilka spacji (pomiędzy operatorami, pomiędzy wierszami) i porównajcie, który zapis jest czytelniejszy.
Oto przykład:
public byte[] toByteArray() {
int bytesNumber ;
if(length % 8 == 0) bytesNumber = length / 8 ;
else bytesNumber = length / 8 + 1 ;
byte[] arr = new byte[bytesNumber] ;
for(int j = bytesNumber - 1, k = 0; j >= 0 ; j--, k++) {
for(int i = j * 8 ; i < (j + 1) * 8; i++){
if(i == length) break ;
if(get(i)) arr[k] += (byte)Math.pow(2, i % 8) ;
}
}
return arr ;
}
oraz wersja przestrzenna: public byte[] toByteArray() {
int bytesNumber = 0;
if ( length % 8 == 0 ) {
bytesNumber = length / 8;
} else {
bytesNumber = length / 8 + 1;
}
byte [] arr = new byte[ bytesNumber ];
for( int j = bytesNumber - 1, k = 0; j >= 0 ; j--, k++ ) {
for( int i = j * 8 ; i < ( j + 1 ) * 8; i++ ) {
if ( i == length ) {
break;
}
if ( get( i ) ) {
arr[ k ] += (byte) Math.pow( 2, i % 8 );
}
}
}
return arr ;
}
Modyfikacja ta zajęła mi około dwóch minut. Jednak umiejętność tworzenia kodu zgodnego ze stylem kodowania, która jest moim nawykiem, umożliwia mi pisanie takiego kodu bez żadnego dodatkowego nakładu czasu. Za to jaka przyjemność z czytania! A to dopiero początek. Kiedy mówimy o stylu kodowania przychodzi mi do głowy jeszcze jedna reguła: Używaj konsekwentnie przyjętego standardu kodowania
Obecnie nie wyobrażam sobie tworzenia kodu, który nie podlega z góry ustalonym zasadom. Jeśli chodzi o pracę w zespole, jest to wręcz warunek konieczny pracy grupowej. A mamy ogromne wsparcie, gdyż istnieje wiele gotowych do wykorzystania standardów, np. Code Conventions for the Java Programming Language (http://java.sun.com/docs/codeconv/), oraz narzędzi, które pomogą go, szczególnie w początkowym okresie, sumiennie przestrzegać (Checkstyle http://checkstyle.sourceforge.net/).
Poniżej znajduje się przykład omawianej klasy sformatowany wg standardu opartego na standardzie zaproponowanym przez Suna.
public class ExtendedBitSet extends BitSet {
private int length = 0;
public ExtendedBitSet( int size, String str ) {
super( size );
length = size;
int strLength = str.length();
for( int i = 0; i < strLength; ++i ) {
if ( str.charAt( strLength - 1 - i ) == '1' ) {
set( i );
}
}
}
public ExtendedBitSet( String str ) {
super( str.length() );
int strLength = str.length();
length = strLength;
for( int i = 0; i < strLength; ++i ) {
if( str.charAt( strLength - 1 - i ) == '1' ) {
set( i );
}
}
}
public void merge( ExtendedBitSet extendedBitSet ) {
for ( int i = extendedBitSet.nextSetBit( 0 ); i >= 0;
i = extendedBitSet.nextSetBit( i + 1 ) ) {
this.set( this.length + i );
}
this.length = this.length + extendedBitSet.length;
}
public int boolMultiply( ExtendedBitSet extendedBitSet ) {
int sum = 0;
int len = 0;
if( this.length < extendedBitSet.length ) {
len = this.length;
} else {
len = extendedBitSet.length;
}
for( int i = 0; i < len; i++ ) {
if ( this.get(i) && extendedBitSet.get(i) ) {
sum++;
}
}
return sum % 2 ;
}
public byte[] toByteArray() {
int bytesNumber = 0;
if ( length % 8 == 0 ) {
bytesNumber = length / 8;
} else {
bytesNumber = length / 8 + 1;
}
byte [] arr = new byte[ bytesNumber ];
for( int j = bytesNumber - 1, k = 0; j >= 0 ; j--, k++ ) {
for( int i = j * 8 ; i < ( j + 1 ) * 8; i++ ) {
if ( i == length ) {
break;
}
if ( get( i ) ) {
arr[ k ] += (byte) Math.pow( 2, i % 8 );
}
}
}
return arr ;
}
public String convertToBitString( int size ) {
char [] resultArray = new char[ size ];
for ( int i = 0; i < size; ++i ) {
resultArray[ i ] = '0';
}
for ( int i = this.nextSetBit( 0 ); i >= 0; i = this.nextSetBit( i + 1 ) ) {
resultArray[ size - 1 - i ] = '1';
}
return new String( resultArray );
}
public String convertToBitString() {
return convertToBitString( this.length );
}
}
Wróćmy do naszego pola length. Tak naprawdę posiada ono jeszcze jeden mankament – jego nazwa jest dokładnie taka sama jak nazwa metody z klasy bazowej. Jest to sytuacja niekorzystna z dwóch powodów: dwa różne byty nie powinny mieć tej samej nazwy (metoda i pole), gdyż może to prowadzić do pomyłek,
nazwa zmiennej nie odzwierciedla sensu właściwości. Pole to przechowuje wartość, która określa ustaloną długość wektora bitowego. Dużo większy sens miałaby na przykład nazwa fixedLength.
Zmieńmy zatem nazwe pola length na fixedLength.
Podsumowując powyższe rozważania:
Nie używaj jednej nazwy do różnych celów
oraz
Nadawaj polom, metodom i klasom nazwy, które jednoznacznie odzwierciedlają ich znaczenie
Analizując dalej przykład, spójrzmy na oba konstruktory, wyraźnie zauważymy pewną właściwość - jest tam mnóstwo powtarzającego się kodu. W ten sposób docieramy do zasady będącej esencją refaktoringu:Eliminuj wszelkie powtórzenia
Powtórzenie to zło, które towarzyszy programistom na każdym kroku. Kuszące kopiuj-wklej, zazwyczaj ostatecznie prowadzi do kilkunastominutowych lub co gorsza wielogodzinnych poszukiwań błędów, wynikających z rozsynchronizowania się podobnych fragmentów kodu. Powtórzenia na dłuższą metę są nie do utrzymania, stąd ich eliminowanie jest podstawowym celem wszelkich refaktoringów. Przykładowy kod możemy zmienić do następującej postaci:
public ExtendedBitSet( int size, String str ) {
super( size );
fixedLength = size;
initializeBitSet( str );
}
public ExtendedBitSet( String str ) {
this( str.length(), str );
initializeBitSet( str );
}
private void initializeBitSet( String str ) {
int strLength = str.length();
for( int i = 0; i < strLength; ++i ) {
if ( str.charAt( strLength - 1 - i ) == '1' ) {
set( i );
}
}
}
Kod nam się powoli porządkuje i wygląda coraz lepiej. Wprowadziliśmy zmiany związane z wyglądem (standard kodowania i przestrzeń), wyeliminowaliśmy kilka powtórzeń i niejednoznaczności. Oczywiście zawsze należy wyważyć stopień refaktorowania lub upiększania kodu, tak aby nie stać się ofiarą perfekcjonizmu. Warto wesprzeć się pomocą innych programistów, najlepiej takich, którzy sami posługują się pewnymi zasadami oraz posiadają duże doświadczenie, i poprosić o opinię. Z pewnością wiele można się będzie dowiedzieć na temat swojego programowania.
niedziela, 8 czerwca 2008
Porządki w kodzie czyli nie tylko o refaktoringu cz. 1
Tutaj możesz pobrać wersję PDF artykułu
Ze względu na problemy z formatowaniem kodu w blogspot artykuł ten warto przeczytać w formie pliku PDF.
Początkowo moim zamysłem było stworzenie artykułu o refaktoringu. Jednak im bardziej zastanawiałem się nad tematem, tym bardziej utwierdzałem się w przekonaniu, iż nie będę pisał tylko i wyłącznie o refaktoringu. Chodzi o coś znacznie istotniejszego, o przelanie bardzo rozległej wiedzy, a w zasadzie doświadczenia związanego z tworzeniem kodu. Kodu, który nie tylko działa, nie tylko jest dobrze zaprojektowany, ale przede wszystkim doskonale się czyta. Kiedy osiągamy tę umiejętność, stajemu u progu profesjonalizmu. Programistycznego profesjonalizmu. Zatem będzie to artykuł między innymi o refaktoringu, ale wzbogacony o zbiór przemyśleń, sugestii, czasami również wątpliwości, którą mają pobudzić Cię, Czytelniku, do refleksji, zweryfikowania swoich programistycznych poczynań. Wierzę, że spowodują cały proces zmian – wprowadzenia nowych, dobrych nawyków. Programowanie bardzo szybko ewoluuje. Pamiętam jeszcze dość dobrze czasy, kiedy rozpocząłem swoją przygodę z kodowaniem jakieś dziesięć lat temu. Programy pisało się wtedy całkiem inaczej. Ceniono pomysłowość, zwięzłość i enigmatyczność. Im kod był bardziej niezrozumiały, tym programista był lepszy. Jednak z czasem systemy informatyczne stawały się coraz bardziej skomplikowane, wymagały coraz większej wiedzy i co najważniejsze, stały się produktem pracy zespołowej. Obecnie pojedynczy programista nie jest w stanie zdziałać zbyt wiele. Być może stworzy rozbudowany program desktopowy, natomiast nie będzie w stanie w wystarczająco skończonym czasie stworzyć rozproszonego systemu, opartego o architekturę trójwarstwową, zapewniającego odpowiedni poziom bezpieczeństwa, umożliwiającego zarządzanie prawami dostępu do wybranych części aplikacji, realizującym wielojęzyczność itp. itd. Takie systemy tworzy obecnie kilkunastu lub kilkudziesięciu programistów, w zależności od wielkości projektu, przez kilka lub kilkanaście miesięcy. Programista przestał być nierozumianym przez nikogo indywidualistą, a stał się graczem zespołowym, nastawionym na współpracę. Co za tym idzie, sposób kodowania też musiał się zmienić. Wyłonił się podstawowy postulat dotyczący kodowania: Przede wszystkim czytelność Istnieją przynajmniej trzy podstawowe powody, które potwierdzają ważność tego stwierdzenia: wymagania się zmieniają, programowanie to umiejętność zespołowa, projekty są zbyt duże, aby pojedyncza osoba była w stanie ogarnąć całość. Z tych właśnie powodu w ciągu ostatnich kilku lat bardzo mocno rozwijają się takie techniki jak refaktoring, pisanie testów oraz zwraca się ogromną uwagę na standard kodowania. To właśnie Czytelność będzie głównym bohaterem tego artykułu. Będzie on zawierać sugestie i przemyślenia, które ułatwią realizację powyższego postulatu. Niektóre wskazówki będą stanowić moją subiektywną opinię, inne będą wyrażać mądrość doświadczeń społeczności programistycznej. Oczywiście należy pamiętać o pewnej zasadzie: “Jedyną niezmienną zasadą jest to, że nie ma niezmienych zasad”. Uogólniając, należy stwierdzić, iż przedstawiane wnioski sprawdziły się w wielu sytuacjach, co nie znaczy, że są zasadne w 100% przypadkach. Dlatego należy uważnie się przyglądać pojawiającym się na co dzień problemom i odważnie stosować przytoczone wskazówki. Warto krytycznie spojrzeć na swoje nawyki lub ich brak i rozpocząć zmiany. Zatem do dzieła! Analiza kodu mniej doświadczonych programistów, często doprowadzała mnie do zaskakujących spostrzeżeń, umożliwiających znalezienie źródła problemów młodych (ale również i tych doświadczonych) adeptów sztuki programowania. Dlatego artykuł ten oparty będzie o przykład nie najlepiej napisanej klasy, która będzie analizowana i stopniowo udoskonalana. Celem, postawionym przed autorami poniższego kodu, było zaimplementowanie klasy pochodnej klasy java.util.BitSet (wektora bitowego) wzbogaconej o: możliwość konkatenacji, właściwość narzuconej długości wektora (pole length), specyficznego mnożenia dwóch wektorów bitowych polegającego na zwróceniu wartości 0, jeśli jedynki w obu wektorach bitowych powtarzają się na parzystej ilości miejsc, oraz wartości 1, jeśli jedynki pokrywają się na nieparzystej ilości miejsc, operację zamiany wektora na ciąg znakowy (w określonym z góry formacie), operację zamiany wektora w ciąg bajtów. Pragnę zaznaczyć, iż treść przykładu nie ma tu większego znaczenia. Przytoczony kod służy tylko jako ilustracja często występujących niedoskonałości programistycznych. Ponadto, ponieważ nieodłączną częścią refaktoringu są testy, sprawdzające testowany kod, jako dodatek do artykułu została zamieszczona klasa testowa do analizowanej klasy. Oto zaproponowana implementacja nowej wersji wektora bitowego: Twórz spójne interfejsy i klasy Jeśli przyjrzymy się klasie bazowej BitSet, łatwo zauważymy, iż żadna publiczna metoda nie jest statyczna. Dostępne są m. in. niestatyczne metody or( Bitset ), xor( Bitset ), których celem jest modyfikacja obiektu na rzecz którego są one wywolywane (operacja na this), a nie udostępnienie metody zewnętrznej (statycznej), która tworzy nowy obiekt, będący efektem implementowanej operacji. Zatem obydwie metody (merge i boolMultiply) swoją postacią wprowadzają rozdźwięk w strukturze nowej klasy, prowadząc do niespójnego interfejsu klasy ExtendedBitSet. W tym przypadku utrzymanie spójności poprzez zamianę metod statycznych na metody niestatyczne, uprości używanie klasy ExtendedBitSet, gdyż będzie się z niej korzystać tak samo jak z klasy BitSet. Istnieje jeszcze jedna zasada, którą warto przytoczyć analizując metody merge i boolMultiply: Unikaj statycznych elementów w programowaniu Elementy statyczne to pozostałość po programowaniu proceduralnym, gdyż statyczność oznacza globalność. A przecież jedną z konsekwencji programowania obiektowego jest zamykanie implementowanych funkcjonalności w autonomicznych i możliwie jak najbardziej niezależnych obiektach. Dlatego elementów statyczne używaj tylko wtedy, kiedy nie ma innego wyjścia lub kiedy informacja lub operacja ma rzeczywiście charakter globalny. Zatem używaj pól statycznych jako stałych, szczególnie stałych globalnych, zaś metod statycznych używaj dla operacji globalnych. Przykładem użycia metod i pól statycznych jest wzorzec Singletonu, jednak „wzorcowość” tego wzorca bywa kwestionowana (http://c2.com/cgi/wiki?SingletonsAreEvil). Ponadto należy pamiętać, że metody statyczne nie są polimorficzne, co oznacza, że nie możemy dostarczyć ich alternatywnych implementacji oraz że nie możemy ich zastępować za pomocą mocków. Zatem ich użycie powoduje usztywnienie kodu oraz utrudnia testowanie. Zmieńmy zatem nieco przytoczony kod, zgodnie z pierwszymi dwoma regułami:Przede wszystkim czytelność
Poprzez przykład do celu
W pierwszej kolejności spójrzmy na klasę całościowo. Jedna z pierwszych rzeczy, która rzuca się w oczy to fakt, że metody konkatenacji i mnożenia wektorów są statyczne. Jest to sprzeczne z bardzo ważną zasadą: import java.util.* ;
public class ExtendedBitSet extends BitSet {
int length ;
public ExtendedBitSet(int size, String str) {
super(size) ;
length = size ;
int strLength = str.length();
for(int i = 0; i < strLength; ++i) {
if(str.charAt( strLength - 1 - i) == '1') set(i) ;
}
}
public ExtendedBitSet(String str) {
this( str.length(), str );
int strLength = str.length();
for(int i = 0; i < strLength; ++i) {
if(str.charAt( strLength - 1 - i) == '1') set(i) ;
}
}
public static ExtendedBitSet merge(ExtendedBitSet a, ExtendedBitSet b) {
StringBuffer str = new StringBuffer(a.convertToBitString() + b.convertToBitString()) ;
return new ExtendedBitSet(a.length + b.length, str.toString()) ;
}
public static int boolMultiply(ExtendedBitSet a, ExtendedBitSet b) {
int sum = 0 ;
int len ;
if(a.length < b.length) len = a.length ;
else len = b.length ;
for(int i = 0; i < len; i++) {
if (a.get(i) && b.get(i)) sum++ ;
}
return sum % 2 ;
}
public byte[] toByteArray() {
int bytesNumber ;
if(length % 8 == 0) bytesNumber = length / 8 ;
else bytesNumber = length / 8 + 1 ;
byte[] arr = new byte[bytesNumber] ;
for(int j = bytesNumber - 1, k = 0; j >= 0 ; j--, k++) {
for(int i = j * 8 ; i < (j + 1) * 8; i++){
if(i == length) break ;
if(get(i)) arr[k] += (byte)Math.pow(2, i % 8) ;
}
}
return arr ;
}
public String convertToBitString( int size ) {
char [] resultArray = new char[ size ];
for ( int i = 0; i < size; ++i ) {
resultArray[ i ] = '0';
}
for ( int i = this.nextSetBit(0); i >= 0; i = this.nextSetBit(i + 1) ) {
resultArray[ size - 1 - i ] = '1';
}
return new String( resultArray );
}
public String convertToBitString() {
return convertToBitString( this.length );
}
}
To dopiero pierwsze wprawki, więcej już niebawem. public void merge( ExtendedBitSet extendedBitSet ) {
for ( int i = extendedBitSet.nextSetBit(0); i >= 0;
i = extendedBitSet.nextSetBit(i + 1) ) {
this.set( this.length + i );
}
this.length = this.length + extendedBitSet.length;
}
public int boolMultiply( ExtendedBitSet extendedBitSet ) {
int sum = 0 ;
int len ;
if(this.length < extendedBitSet.length) len = this.length ;
else len = extendedBitSet.length ;
for(int i = 0; i < len; i++) {
if (this.get(i) && extendedBitSet.get(i)) sum++ ;
}
return sum % 2 ;
}
c. d. n.
sobota, 7 czerwca 2008
JUG Łódź
Najważniejsze założenia na najbliższy czas, to trzy miesiące testów wewnętrznych - spotkań, które mają rozkręcić zainteresowanych po to, by później rozpocząć już działania na szerszą skalę.