Banałem będzie stwierdzenie, że szukanie błędów w kodzie bywa wkurzające. Odpowiedź na proste pytanie – dlaczego coś nie działa? – to czasami długie godziny dłubania w programie, mało przyjemnych rendez-vous z debuggerem czy nawet stosowania bardziej drastycznych metod. Ale ponieważ w programowaniu nic nie dzieje się bez powodu (a przynajmniej chcemy w to wierzyć ;D) i przy założeniu, że dysponujemy odpowiednio dużymi zasobami: czasu, samozaparcia, a przede wszystkim umiejętności, w końcu uda się znaleźć tę upragnioną przyczynę błędu…
A przyczyny mogą być trywialne. Pomyłka o jedynkę, nieopacznie wstawione
x
zamiast y
, pomylenie plusa z minusem , i tak dalej. Takie błahostki zgodnie z prawem Murphy’ego mają wybitną skłonność do pojawiania się akurat w tych newralgicznych miejscach, które mają największy wpływ na działanie całego kodu – i oczywiście całkowicie je rozstrajają. A co gorsza, z bliżej niewiadomych przyczyn programistę uważnie oglądającego fragmenty z takimi “literówkami” ogarnia najczęściej chwilowa ślepota tudzież niewytłumaczalny zanik spostrzegawczości. Dopiero za n-tą inspekcją, przeprowadzoną najlepiej po długim odpoczynku od kodu, da się cokolwiek znaleźć.
Z drugiej strony poważne i trudne do znalezienia błędy mogą mieć poważne i trudne do usunięcia przyczyny. Inżynieria oprogramowania mówi, że im wcześniej w cyklu tworzenia programu błąd się pojawił, tym trudniej jest go potem wyeliminować. Jeżeli więc kruczek tkwił w założeniach projektowych, w modelu programu lub, co gorsza, w określeniu funkcjonalności, to nie będzie łatwo poradzić sobie z nim na etapie implementacji. Wyburzenie kilku ścian nie pomoże, jeśli fundamenty położono na niestabilnym gruncie.
Którego z tych dwóch rodzajów błędów oczekiwalibyśmy, jeżeli spędziliśmy już na debugowaniu dużo, naprawdę dużo czasu? Błąd drugiego typu to sprawa ciężkiego kalibru. Można wówczas powiedzieć, to oto odkryliśmy problem, który rzeczywiście jest adekwatny do tych godzin czy dni spędzonych na polowaniu na niego. Inaczej mówiąc, możemy uznać, że faktycznie zrobiliśmy wszystko, co w naszej mocy, i że cały ten wysiłek był naprawdę potrzebny, gdyż nie było innej drogi.
Z kolei pierwszy typ błędu to właściwie przeciwny biegun. Teoretycznie można było go załatwić w najwyżej kilkanaście minut, po prostu przeglądając jeszcze raz kod i dopisując ten zapomniany przecinek czy cokolwiek innego. Jak mogliśmy w ogóle spędzić nad tym tyle czasu, który przecież powinno się wykorzystać bardziej produktywnie?
Jeżeli kiedykolwiek ktoś odkryje przyczynę owej chwilowej ślepoty programistów na drobne omyłki, to osobiście uważam, że zasługuje przynajmniej na Ig Nobla :) Sądzę też jednak, że o wiele lepiej jednak paść jej ofiarą niż stwierdzić, że znaleziony błąd jest ową “ciężką sprawą”. Może i obniży to nieco naszą samoocenę, ale przynajmniej oszczędzi mnóstwa roboty.
Dane liczbowe przedstawiać można na wiele sposobów. Najbardziej kompletnym jest zwykle tabelka, ale o wiele ładniejszą jest odpowiedni wykres. Czasem sztuką jest dobrać jego odpowiedni typ, gdyż rodzajów wykresów jest wbrew pozorom bardzo dużo. Jeżeli ktoś nie wierzy, niech sprawdzi w dowolnym arkuszu kalkulacyjnym :)
Jednym z ciekawszych jest wykres radarowy. W nim ze środka wykresu na zewnątrz wypuszczone są osie (różna może być ich liczba), na których z kolei zaznaczone są wartości. W najprostszej wersji wygląda to jak pajęczyna z napaćkanymi punktami i w sumie nie jest specjalnie sugestywne.
Ciekawiej zaczyna się robić, jeżeli punkty na poszczególnych osiach połączymy ze sobą i zamalujemy wnętrze tak powstałego wielokąta. Jeżeli bowiem wartości na osiach przedstawiają pewne właściwości jednego obiektu lub zjawiska, to powstała figura niejako opisuje go w sposób całościowy. Weźmy na przykład taki wykres, w którym w subiektywny sposób ocenimy sobie różne charakterystyki jakiegoś hipotetycznego kawałka kodu źródłowego:
Figura taka ma jeszcze jedną istotną cechę: powierzchnię. Na pierwszy rzut oka dość ciężko określić, w jaki sposób zależy od wartości na poszczególnych osiach. Czy na przykład gwałtowny wzrost jednej zmiennej przy identycznych wartościach pozostałych da ostatecznie większe pole wielokąta niż równomierny przyrost na wszystkich osiach?
Nie jest to oczywiste i dlatego wydaje się całkiem interesujące :) Naturalnie znając wszystkie wartości, rzeczone pole policzyć jest bardzo łatwo.
Dlaczego jednak wspominam o tym wszystkim? Otóż uważam, że gry w których przedstawia się graczowi bardzo dużo danych – a więc na przykład ekonomiczne, strategiczne czy RPG – są zwykle dość ubogie pod względem sposobów prezentacji tych danych. Prawie zawsze królują w nich nieśmiertelne tabelki i czasami tylko jakieś wykresy liniowe czy słupkowe.
A przecież można by nieco się wysilić i zafundować graczowi jakąś bardziej atrakcyjną formę. W końcu jeśli ktoś nie lubi odmóżdżających strzelanek to jeszcze nie znaczy, że uśmiecha mu się wpatrywanie się w rzędy numerków ;P
Dzisiaj w programowaniu aplikacji obowiązują dwie proste i fundamentalne zasady. Po pierwsze, programujemy obiektowo i kod zamykamy w klasy z polami i metodami. Po drugie, tworzymy programy działające w środowisku graficznym, z okienkami, przyciskami, polami tekstowymi i innymi klasycznymi elementami interfejsu.
Jednak jeden plus dwa równa się kłopoty, przynajmniej w C++. Już raz narzekałem zresztą na to, że w tym języku obiektowość i GUI to nie jest najlepsze połączenie. Zgrzyta tu mechanizm obsługi zdarzeń generowanych przez interfejs graficzny.
Wcześniej napisałem, że możliwym rozwiązaniem problemu jest zasymulowanie w jakiś sposób delegatów, czyli – z grubsza – wskaźników na metody obiektów. To jedna z dróg radzenia sobie z kwestią obsługi zdarzeń. Ale też inna, wykorzystująca mechanizm metod wirtualnych i polimorfizmu. Polega to na zdefiniowaniu jednolitej klasy bazowej dla obiektów, które mają odbierać zdarzenia. Zwie się je zwykle event handlers, co jak zwykle nie ma dobrego tłumaczenia. Taki handler wyglądać może na przykład tak:
Mając jakiś element interfejsu użytkownika, np. przycisk, przekazujemy mu nasz własny obiekt implementujący powyższy interfejs. Metody tego obiekty są następnie (polimorficznie) wywoływane w reakcji na odpowiednie zdarzenia.
Tak oczywiście można robić w C++, ale nie jest to zbyt wygodne. Tym czego znów brakuje, to niestatyczne klasy wewnętrzne, obecne choćby w Javie, których brak w C++ nie da się do końca zastąpić wielokrotnym dziedziczeniem.
Albo delegaty, albo sposób opisany przed chwilą – zapewne nie ma żadnej innej drogi obiektowej obsługi zdarzeń. Niestety, żaden z tych sposobów nie jest obecnie wspierany w C++ i nie zanosi się, by miało się to wkrótce zmienić.
Kiedyś nazywało się ich mecenasami, a dzisiaj częściej sponsorami. Składali oni u artystów zamówienia na określone dzieła, a w zamian potrafili porządnie sypnąć groszem. W ten sposób powstała duża część podziwianych do dzisiaj wytworów sztuki, z których większość nosi widoczne ślady dostosowywania się do wymagań dawnych sponsorów.
Programowanie może i nie jest sztuką (choć tutaj można by długo dyskutować), ale podobne relacje chyba i tutaj obowiązują. Przede wszystkim chodzi tutaj o komercyjne wytwarzanie oprogramowania, kiedy to klient określa swoje oczekiwania i płaci później za gotowy produkt, jeśli ten je spełnia. Ale nie tylko; otóż przypadkowo odkryłem też zupełnie inną formę “sponsorowania”, gdzie pieniądze w ogóle nie wchodzą w grę…
Sprawa dotyczy programu ColorShop, który to w przypływie natchnienia popełniłem ponad cztery lata temu. Kilka miesięcy wpadłem na pomysł jego poważnego udoskonalenia i sporządziłem nawet kawałek czegoś, co można nazwać wstępną dokumentacją… Ale, jak wiadomo, koder ma dziesięć pomysłów na sekundę, zatem także i ten został odłożony na półkę i szansa na jego realizację nie była zbyt duża.
Aż tu nagle dowiaduję, że w ramach jednego z przedmiotów w bieżącym semestrze (o wiele mówiącej nazwie Programowanie w technologii .NET) jest napisanie średniej wielkości aplikacji, wykorzystującej takie-a-takie elementy .NET Framework. Zamiast więc pisać kolejną pseudobazę jakichś niby-danych, pomyślałem więc, że mógłbym wziąć ten właśnie projekt. I tak zrobiłem.
Nie obyło się jednak bez większych i mniejszych modyfikacji i usprawnień, koniecznych aby program spełniał wymagania co do użytych technologii. Trudno powiedzieć, jak wpłynęły one na jego ostateczną postać, ale jedno jest prawie pewne: bez tego program miałby spore szanse nigdy nie powstać. Mogę więc spokojnie powiedzieć, że uczelnia w tym momencie zasponsorowała go – nie pieniędzmi, ale właśnie motywacją potrzebną do jego ukończenia. W końcu nic tak nie zachęca do pracy jak perspektywa zaliczenia jakiegoś przedmiotu ;)
A co do samego programu, to właściwie pozostało już tylko kilka mało programistycznych czynności końcowych, potrzebnych aby ColorShop 2.2 mógł zostać opublikowany w sieci – co powinno się niebawem stać.
W każdym języku programowania potrzebny jest system wejścia/wyjścia. To zresztą bardzo często wykorzystywana jego część, więc powinna charakteryzować się wysoką jakością. Chcielibyśmy mianowicie nie tylko tego, aby I/O było szybkie. Powinno być też elastyczne i proste w obsłudze. Czasami udaje się te wymagania pogodzić w całkiem zadowalający rezultat, a czasem nie.
Weźmy na przykład Javę. Posiada ona bardzo rozwinięty system wejścia/wyjścia, umożliwiający odczyt i zapis z wielu różnych źródeł: ekranu, plików, gniazdek sieciowych, potoków łączacych wątki, itp. Ponadto komunikacja może odbywać się na wiele sposobów: mamy na przykład dość “surowe” strumienie, nieco bardziej użyteczne czytacze (readers) i zapisywacze (writers), a także kanały (channel) i bufory (buffers).
Cały system wydaje się zatem bardzo zaawansowany. Niestety, w praktyce jest on zdecydowanie przerośnięty, a poza tym charakteryzuje się pewną ‘ciekawą’ cechą – nazwijmy to – projektową. Osobiście uważam, że twórcy Javy w tym momencie przedobrzyli i chcąc zastosować bardzo elastyczny w założeniu wzorzec Dekorator, stworzyli interfejsowy koszmarek. Wspomniany wzorzec polega na kolejnym “opakowywaniu” obiektów tak, aby rozszerzać ich możliwości; obiekt ‘zewnętrzny’ nie musi przy tym wiedzieć dokładnie, czym jest obiekt ‘wewnętrzny’. I tutaj rzeczywiście tak jest, lecz na nieszczęscie sami musimy zawinąć obiekt w te wszystkie warstwy.
Przykład? System.in
, czyli strumień standardowego wejścia, w swej pierwotnej postaci jest niemal zupełnie bezużyteczny. Żeby zrobić z nim cokolwiek sensownego (np. odczytać linię tekstu), musimy najpierw opakować go do postaci odpowiedniego czytacza:
Podobnie jest chociażby z plikami. Za każdym razem musimy ubrać nasz obiekt na cebulkę, aby był on przydatny, przechodząc przy okazji przez cały arsenał bardzo różnych klas, od których jest wręcz gęsto w JDK.
Trzeba aczkolwiek przyznać uczciwie, że System.IO
z .NET też zawiera całe mnóstwo różnych klas. Tam konieczność podobnego opakowywania zachodzi jednak o wiele rzadziej, gdyż interfejsy tych klas są trochę inteligentniejsze.
A co ze “staroświeckimi” językami, jak C++ czy Delphi? No cóż, w nich operuje się głównie pojęciem uniwersalnych strumieni i w zasadzie niczego więcej. Nie trzeba ich jednak niczym otaczać, bo fabrycznie potrafią już chociażby operować na podstawowych typach danych, a nie tylko ciągach bitów. Niby to mniej elastyczne i nie tak “obiektowo czyste”, ale o ile przyjemniejsze w użyciu.
Uczcimy dzisiaj chwilą ciszy pamięć naszych rozpoczętych projektów, które nigdy nie doczekały się ukończenia. Sporo ich było, prawda?… Potrzeba by naprawdę dłuższej chwili żałoby, żeby wspomnieć wszystkie. Tak się bowiem nieprzyjemnie składa, że wiele znaków na niebie i ziemi wskazuje, że prawdziwe jest stwierdzenie, iż:
O ile w grę nie wchodzą silne czynniki zewnętrzne, każdy rozpoczęty projekt ma bardzo duże szanse na to, by nigdy nie doczekać się realizacji.
Takim silnym czynnikiem nie jest jednak presja wynikająca z pracy w zespole. Jest to czynnik jak najbardziej wewnętrzny, a projekty grupowe mają chyba takie same szanse popadania w stagnację i wreszcie w kompletny zastój. Zapewne jedynie sankcje natury edukacyjnej (gdy chodzi np. o tak zwane projekty ‘na zaliczenie’) czy finansowej (kiedy mówimy o komercyjnym wytwarzaniu oprogramowania) są w większości przypadków wystarczającą “zachętą”. A pewnie i nie zawsze…
Zaś w przypadku projektów totalnie amatorskich i całkowicie dobrowolnych powyższe przygnębiające prawo ma, jak sądzę, wielkie prawdopodobieństwo okazać się trafne. Prawie zawsze znajdzie się jakiś powód – czasem istotny, a czasem zupełnie błahy, a często nawet trudny do określenia – żeby wszystko stanęło i przestało się rozwijać. A im dłuższa jest przerwa, tym trudniej kontynuować potem przerwaną pracę. I wreszcie przekraczamy ten punkt krytyczny, czasową granicę, po przejściu której do danego projektu po prostu się nie wraca.
Okropnie to pesymistyczne, więc może teraz spróbujmy się choć trochę pocieszyć. Otóż nawet jeśli większość naszych przedsięwzięć koderskich nie doczekała się przybrania ostatecznego kształtu, to przecież czas włożony w ich rozwój nie był do końca stracony. Prawdopodobnie tylko w taki sposób można poszerzać swoje umiejętności i zyskiwać niezbędną wiedzę – nie tylko o tym, że kończenie projektów jest bardzo, bardzo trudne :)
A ponadto te niedokończone pomysły nie są przecież zakopywane pod ziemią – chyba że znikają w płomieniach formatowanych dysków czy zostają pożarte przez komputerowe robaki. Rozsądni programiści robią jednak kopie zapasowe i pieczołowicie gromadzą takie “odpady”. A nuż bowiem coś się jeszcze przyda? Jakiś fragment, ciekawe rozwiązanie czy nawet kilka linijek, które kiedyś wyprodukowaliśmy i które akurat teraz są nam potrzebne. To prawda, że po dłuższym czasie możemy być już w zupełnie innym miejscu, pisać w zupełnie innym języku programowania i korzystać z całkowicie innych bibliotek czy innych narzędzi. W kodowaniu pewne rzeczy są jednak uniwersalne i czasem pogrzebanie w zarzuconych projektach może okazać się pożyteczne.
Kto wie, może dzięki takiemu recyklingowi uda się powstrzymać wzrost tej góry śmieci…
Myślę, że większość programistów miała przynajmniej przelotny kontakt z wyrażeniami regularnymi. Zwykle używa się do bardziej zaawansowanego wyszukiwania podciągów w tekście – nie określonych sekwencji liter, ale fragmentów określonych raczej pewnymi warunkami. Innym ich zastosowaniem jest też sprawdzanie, czy podany łańcuch pasuje do pewnego określonego formatu. Przy pomocy poniższego wyrażenia:
można na przykład sprawdzić poprawność adresu e-mail (czy zawiera on znaczek @, czy nazwa domeny jest przynajmniej formalnie poprawna, itd.). Nietrudno zauważyć jednak, że sama postać wyrażenia jest dość odstręczająca, a poza tym sporo elementów się w nim powtarza – choćby sekwencja dopasowująca pojedynczy znak alfanumeryczny. Poza tym dokładna składnia wyrażeń w danej bibliotece może być niekiedy specyficzna, chociaż podstawy (czyli np. elementy pokazane powyżej) powinny być w zasadzie wszędzie takie same.
Mimo to czasem warto użyć tego narzędzia również w bardziej zaawansowanym celu: parsowania ciągów znaków w celu wydzielenia i odczytania z nich określonych fragmentów. Zapewne nie da się w ten sposób przetworzyć na przykład dokumentu XML czy innego skomplikowanego formatu, lecz w prostszych przypadkach może się to okazać szybsze i wygodniejsze. Alternatywą jest oczywiście samodzielne napisanie parsera, tokenizera czy innego tego typu obleśnego automatu stanów :)