Nieodłączną częścią realizacji projektu programistycznego jest wybór używanych technologii. Dotyczy to zarówno samego początku, jak i późniejszych etapów, gdy czasami trzeba zdecydować się np. na skorzystanie z jakiejś biblioteki X lub jej zamiennika Y. Takie dylematy mają i koderzy-amatorzy (co często uzewnętrzniają na przeróżnych forach ;]), i firmy tworzące oprogramowanie komercyjnie. Decyzję trzeba jednak w końcu podjąć; czy są jakieś uniwersalne kryteria, którymi można się kierować?
Inżynieria oprogramowania każe zwykle zdać się na poprzednie doświadczenia lub wiedzę ekspertów. Ta druga opcja jest często preferowana wobec braku podstaw (tj. wspomnianych doświadczeń) do skorzystania z pierwszej. Wydaje się to rozsądne. W końcu osoba, która bardzo dobrze zna dane rozwiązanie, powinna mieć też odpowiednie pojęcie o tym, czy aplikuje się ono do konkretnego problemu.
A jednak mam co do tego całkiem poważne wątpliwości. Potrafiłbym znaleźć kilka sensownych argumentów na to, że osoba biegła w stosowaniu danej technologii nie musi być wcale dobrym doradcą. W grę mogą bowiem wchodzić silne czynniki zaburzające solidność osądu eksperta. Przychodzą mi na myśl zwłaszcza dwa:
Jak wynika z powyższych argumentów, wypowiedziane przez eksperta zdanie na temat przydatności jakiegoś rozwiązania niekoniecznie musi być obiektywne i wartościowe. Oczywiście nie twierdzę, że największe pole do popisu przy podejmowaniu decyzji powinni mieć wobec tego początkujący, bo to z kolei zakrawa na przegięcie w drugą stronę :) Sądzę jednak, że najbardziej kompetentnymi i obiektywnymi osobami byłyby takie, które wprawdzie stosowały dane rozwiązanie w praktyce, ale nie są w nim całkiem biegłe. A już zupełnie dobrze byłoby wówczas, jeśli mają one też pewne pojęcie o porównywalnych, alternatywnych rozwiązaniach.
Część bywalców kanału #warsztat może wiedzieć, że od jakiegoś czasu w wolnych chwilach rozwijam pewien niewielki projekt. Jest to prosty IRC-owy bot, który potrafi wykonywać różne predefiniowane czynności, dostępne za pomocą komend rozpoczynających się od kropki. Wśród nich mamy między innymi wysyłanie zapytań do wyszukiwarki Google, wyświetlanie skrótów artykułów z Wikipedii, przekazywanie wiadomości między użytkownikami i jeszcze kilka innych. W sumie jest ich już około kilkanaście, więc pomyślałem, że dobrym rozwiązaniem byłby wbudowany system pomocy oraz podpowiedzi opartych na “domyślaniu się”, jakie polecenie użytkownik chciał wydać.
Brzmi to może nieco zagadkowo, ale w gruncie rzeczy chodzi o zwykłe autouzupełnianie (autocompletion), do którego jesteśmy przyzwyczajeni w wielu innych miejscach – takich jak środowiska programistyczne czy choćby wyszukiwarki internetowe. Oczywiście tutaj mówimy o nawet miliardy razy mniejszej skali całego rozwiązania, ale ogólna zasada we wszystkich przypadkach jest – jak sądzę – bardzo podobna.
Cały mechanizm opiera się właściwie o jedną strukturę danych zwaną drzewem prefiksowym (prefix tree). Jest to całkiem pomysłowa konstrukcja, w której każde dopuszczalne słowo (zwane tradycyjnie w takich drzewach kluczem) wyznacza pewną ścieżkę od korzenia drzewa. Kolejne krawędzie etykietowane są tu znakami słowa.
Najważniejszą cechą drzewa prefiksowego jest jednak to, że wspomniane ścieżki mogą się nakładać, jeśli początkowe części danych słów się pokrywają. Stąd zresztą bierze się nazwa struktury, gdyż pozwala ona szybko znaleźć ciągi zaczynające się od podanego tekstu – czyli właśnie prefiksu.
Oto proste zadanie. Proszę sobie przypomnieć dowolną funkcję z dowolnej platformy/biblioteki/frameworka/itp. posiadającą przynajmniej jeden parametr, który nigdy nie został przez nas użyty w żadnym jej wywołaniu. Idealnie by było, gdyby ów argument występował na innej pozycji niż ostatnia – tak, żeby jego pominięcie wymagało podania zera, NULL
-a, null
a, nil
a, Nil
a, None
-a, pustego napisu/tablicy/listy lub dowolnej innej, “neutralnej” wartości.
Jeżeli udało nam się uporać z tą łamigłówką, to mam kolejną w postaci prostego pytania: dlaczego takie funkcje w ogóle istnieją? Zadając to pytanie na głos, często doczekamy się odpowiedzi, które niewiele mają wspólnego z prawdą. Bo istnieją rzadkie sytuacje, gdy ów felerny parametr jednak się przydaje. Bo nie ma innego dobrego miejsca, gdzie można by go umieścić. Bo język programowania jest kiepski i tak już musi być. Bo w sumie co to za kłopot z tym jednym null
em więcej. Bo.. bo… – i tak dalej.
W rzeczywistości prawidłową odpowiedzią jest zazwyczaj brzydkie słowo na ‘k’, czyli kompatybilność :) Pół biedy, jeśli chodzi tutaj o kompatybilność wsteczną – ona jest codziennością co bardziej złożonych aplikacji i systemów oraz wszelkich bibliotek, które mają swoją własną historię. Nie da się jej uniknąć i zwykle nawet nie warto próbować, chociaż oczywiście okresowe jej porzucanie jest w gruncie rzeczy wskazane.
Jest jednak jeszcze kompatybilność wprzód. Dziwna to idea, mająca na celu przewidywanie przyszłych wymagań wobec systemu i ułatwiająca lub wręcz umożliwiająca sprostanie im. Należy więc ona do dziedzin z pogranicza prorokowania i prognoz pogody, zatem z miejsca wydaje się cokolwiek podejrzana. Co więcej, potrafi się ona wkraść do projektu tak podstępnie i niezauważenie, że nie musi być nawet świadomie stosowana, by zostawić w nim swoje ślady. Ślady zupełnie niepożądane, o czym jednak przekonujemy się zwykle dopiero dużo później.
Mniej więcej coś takiego zdarzyło mi się ostatnio podczas tworzenia wersji androidowej swojej gry sprzed paru lat, czyli Taphoo. Sprawa dotyczy formatu niewielkich (<1 kB) plików, w którym obie wersje przechowują poszczególne etapy gry. Nie należy on w żadnym razie do przesadnie skomplikowanych, gdyż jest prostym formatem binarnym, składającym się z trzech części:
Format etapów w Taphoo
Oryginalnie format ten służył jedynie windowsowej wersji gry, ale nie widziałem żadnych powodów, dla których nie mógłbym użyć go także w powstającym porcie na platformę Android. Z pewnością znaczącym czynnikiem było to, że razem z pierwotną wersją gry napisałem też do niej – nie chwaląc się – bardzo dobry edytor :) Rozsądne było więc, aby przepisać kod wczytujący poziomy na Javę i włączyć go do gry w wersji androidowej.
Intuicja każe przypuszczać, że wtedy właśnie pojawiły się kłopoty. Faktycznie tak było. W związku mam jeszcze jedną, ostatnią dzisiaj zagadkę, która powinna być stosunkowo łatwa do rozwiązania na podstawie informacji podanych wcześniej. Co mianowicie jest przykładem niepotrzebnej kompatybilności wprzód w opisanym wyżej formacie i jakie problemy może to stwarzać podczas portowania?…
W ostatnich latach rozproszone systemy kontroli wersji (w skrócie DVCS) stały się popularne, zwłaszcza w środowiskach związanych z open source. Warto je jednak znać nie tylko wtedy, gdy pracujemy nad projektami z otwartym źródłem. Jak bowiem pisałem wcześniej, mogą być one przydatne chociażby do małych jednoosobowych projektów. Ponadto bywają nierzadko używane w większych oraz mniejszych firmach. Ogólnie mamy więc duże szanse, aby prędzej lub później zetknąć się z takimi systemami.
Ten post jest adresowany przede wszystkim do osób, które nie miały wcześniej dłuższej styczności rozproszonymi systemami kontroli wersji. Ma on na celu wyjaśnienie wielu spośród tych tajemniczo brzmiących słówek, na jakie nieustannie można się natknąć, gdy pracujemy z DVCS-ami. Istnieje oczywiście szansa, że bardziej zaawansowani użytkownicy również wyciągną nowe wiadomości z lektury (albo chociaż przejrzenia) niniejszej notki. A jeśli nawet nie, to powtórzenie znanej sobie wiedzy na pewno im nie zaszkodzi ;)
Wiadomo powszechnie, że Java listenerami stoi i typowe jest używanie jest różnego rodzaju klas wewnętrznych, które są następnie podawane jako interfejsy do wywołań zwrotnych. W ten sposób obsługuje się różnego rodzaju zdarzenia, począwszy od interakcji użytkownika z programem aż po ważne notyfikacje pochodzące z systemu operacyjnego.
Gdy klasa zawierające takie handlery dostatecznie się rozrośnie, pojawia się oczywiście potrzeba jej zrefaktorowania i podzielenia na dwie lub większą liczbę mniejszych. W trakcie tego procesu czasami chciałoby się też wydzielić owe klasy wewnętrzne, obsługujące zdarzenia – i wtedy możemy napotkać pewien kłopot. Kłopocik właściwie ;)
Dotyczy on zależności między klasami wewnętrznymi a klasą je otaczającą. Ponieważ mówimy o niestatycznych niestatycznych klasach wewnętrznych, typowe jest odwoływanie się do składników klasy otaczającej z klasy zewnętrznej. Mówiąc bardziej po ludzku, implementacja np. zdarzenia kliknięcia przycisku może sięgać do obiektu okna/dialogu/itp., zawierającego tenże przycisk:
Przeniesienie całej powyższej klasy w inne miejsce nastręcza wówczas problem, gdyż odwołuje się ona do metody klasy ją otaczającej (czyli finish()
). Należałoby więc posiadać obiekt tej klasy, zapewnić aby rzeczona metoda była dostępna (co nie zawsze jest wskazane), aby wywoływać ją z właściwymi parametrami – które też trzeba jakoś przekazać – i tak dalej… Krótko mówiąc na naszej drodze do ładnie zorganizowanego kodu naraz pojawia się sporo przeszkód.
Czy nie dałoby się po prostu jakoś przekazać tego “kawałka kodu”, tych kilku czy kilkunastu instrukcji opakowanych w coś, co można podać w inne miejsce programu?… Okazuje się, że jak najbardziej, a rozwiązaniem na problem refaktorowanych klas wewnętrznych jest… więcej klas wewnętrznych ;-) Nic nie stoi bowiem na przeszkodzie, aby rzeczony fragment procedury zdarzeniowej zapakować w metodę klasy implementującej interfejs Runnable
. Tak, dokładnie ten interfejs który zazwyczaj kojarzy się z wątkami i klasą Thread
. W rzeczywistości reprezentuje on “cokolwiek co da się uruchomić”, więc jak ulał pasuje w charakterze rozwiązania problemu.
Aplikując je do powyższego przykładu, możemy wydzielić powyższy listener (potencjalnie wraz z kilkunastoma podobnymi) i kazać mu jedynie wywoływać metodę run
jakiegoś obiektu Runnable
. W niej zaś umieszczamy nasz pierwotny kod i przekazujemy do nowo wydzielonej klasy:
Znawcy tematu powiedzieliby, że w ten sposób utworzyliśmy domknięcie (closure), bo w inne miejsce programu przekazaliśmy fragment kodu wraz z jego kontekstem; w tym przypadku jest to chociażby referencja this
na rzecz której wywoływany jest finish
. Fani programowania funkcyjnego stwierdzą z kolei, że ta technika jest kiepską imitacją wyrażeń lambda i najprawdopodobniej też będą mieli rację.
Nie zmienia to jednak faktu, że jeśli programujemy w starej (nie)dobrej Javie, to technika ta może być po prostu użyteczna – niezależnie od tego, jaki paradygmat za nią stoi. Dlatego też chciałem dzisiaj się nią podzielić.
Dzisiejszy okrągły, 512-ty post to doskonała okazja, żeby pochwalić się nową produkcją… No, może nie do końca nową, ale z pewnością wartą zauważenia :) Chodzi o Taphoo – czyli moją całkiem udaną grę sprzed kilku lat – w wersji na platformę mobilną Android. Styl grafiki został przy okazji trochę zmieniony, a sterowanie jest oczywiście dostosowane do (znacząco innego) interfejsu urządzeń. Z opinii wczesnych testerów wynika na szczęście, że po tym przeniesieniu na mniejsze (ale za to dotykowe i przenośne) ekrany gra wciąż prezentuje się przynajmniej tak samo dobrze ;)
Jeśli więc mamy szczęście posiadać telefon, tablet, netbook, lodówkę lub suszarkę z Androidem, to zachęcam do przyjrzenia się tej produkcji (*). Zwłaszcza że jest ona – podobnie jak wersja pod Windows – całkowicie freeware :)
(*) W przypadku suszarek i lodówek mogą wystąpić problemy z wyświetlaniem grafiki i sterowaniem.
Za sprawą najnowszego komiksu z xkcd – a właściwie tekstu, który pojawia się po najechaniu myszką na obrazek – w Internecie robi karierę mem na temat rzekomej “zbieżności” artykułów Wikipedii do hasła ‘filozofia’. Zbieżność ta była zdefiniowana przez kolejne kliknięcia w pierwszy (z paroma wyjątkami) link w każdym artykule i kolejne powtarzanie tego procesu. W końcu powinniśmy trafić na wspomniany tekst o filozofii.
Zabawną cechą tego memu jest fakt, iż stosuje się do niego swego rodzaju zasada nieoznaczoności: samym swoim istnieniem (i popularnością) oddziałuje on na zjawisko, które zdaje się opisywać. W końcu mówimy tutaj o serwisie, którego zawartość edytują tysiące użytkowników, a dzięki całej tej plotce ich uwaga jest tym bardziej skupiona na tekstach leżących na ścieżce do tej nieszczęsnej ‘filozofii’… jeśli takowa faktycznie istnieje.
A czy istnieje? No właśnie :) Traktując to jako swego rodzaju pouczającą zabawę, postanowiłem zbadać sprawę i wysmażyłem odpowiedni program, który potrafi automatycznie wykonywać procedurę opisaną na początku, tj. kolejno przechodzić przez teksty w Wikipedii i “klikać” w pierwsze linki z ich treści. Szybko zorientowałem się wtedy, że twierdzenie o zbieżności do Philosophy jest tylko wycinkiem większej, bardziej interesującej całości.
Okazało się bowiem, że Wikipedia ma coś w rodzaju uniwersalnego atraktora, którego ‘filozofia’ jest częścią. Ma on postać cyklu zmieniającego się w czasie (z powodu edycji, oczywiście) i obejmuje interesujący zestaw pojęć, które semantycznie są połączone zależnością rekurencyjną. Obserwując kształt owego cyklu w dłuższym przedziale czasowym, można by zapewne wyodrębnić terminy zupełnie podstawowe, które są w nim obecne stale lub wypadają tylko na krótką chwilę. Kto wie, może w ten sposób udało by się dokonać jakiegoś fundamentalnego odkrycia z dziedziny filozofii, na przykład odpowiedzieć na pytanie o życie, Wszechświat i całą resztę? ;-)
Nieco mniej ambitnym wyzwaniem jest zbadanie, jak daleko od atraktora znajdują się pewne hasła. Innymi słowy, ile kliknięć dzieli nas od wpadnięcia w cykl zapętlających się pojęć, jeśli zaczynamy od danego artykułu? Jeśli zastanowimy się nad tym chwilę, to można dojść do wniosku, że odpowiedź byłaby miarą odwrotności “stopnia abstrakcji” danego pojęcia. Skoro bowiem w atraktorze znajdują się terminy bardzo ogólne, oddalanie się od niego powinno nas przybliżać do konkretów. Czy tak jest w rzeczywistości?…
Wygląda na to, że faktycznie coś w tym jest. Poniżej przedstawiam tendencyjne wyniki sprawdzenia pewnej liczby bynajmniej wcale nie losowo wybranych haseł. i obliczenia odległości każdej z nich od Czarnej Dziury Abstrakcji:
blog | 14 | Internet | 7 | programming | 8 |
compiler | 6 | Turing machine | 5 | Angry Birds | 18 |
Sun | 4 | velociraptor | 2 | Boeing 747 | 22 |
South Park | 28 | Darth Vader | 8 | Julius Caesar | 34 |
Nie jestem wprawdzie do końca pewien, co oznacza to, że Angry Birds są niemal dwa razy bardziej abstrakcyjne od Juliusza Cezara, ale na pewno musi to coś znaczyć ;]