Postawiłbym tezę, że każdy szerzej używany język programowania ma ok. jedną wyróżniającą cechę, definiującą w znacznym stopniu sposób, w jaki się go używa. Innymi słowy, każdy język z czymś się kojarzy. I tak myśląc o C, przypominamy sobie od razu wskaźniki; w Pythonie przychodzi nam na myśl składnia oparta na wcięciach; Pascal kojarzy nam się natychmiast z begin
/end
; w LISP-ie mamy miliardy nawiasów; w C++ przeciążanie operatorów; w PHP kod przeplatany wstawkami HTML – i tak dalej. Chyba tylko C# jest – przynajmniej dla mnie – swego rodzaju wyjątkiem w tej kwestii (co niekoniecznie musi być złe, bo oznacza również brak wyraźnie irytujących feature‘ów).
Dzisiaj jednak chciałem napisać o Javie z tego względu, że przyszło mi ostatnio kodować nieco w tym języku. Wrażenie, jakie w związku z tym odnoszę, jest takie, iż Java to obecnie chyba najbardziej “klasycznie obiektowy” język ze wszystkich, które mają w branży jakieś znaczenie. Tu nie ma żadnych udziwnień, które łamałyby paradygmat OOP-u, na który składają się m.in. obiekty dostępne przez referencje, komunikacja za pomocą interfejsów i metod, wyraźny podział na proste typy wbudowane i złożone klasy, jawne tworzenie kopii obiektów, i tak dalej. Z jakichś powodów “wynalazki” w rodzaju typów generycznych czy wyrażeń lambda przebijają się do Javy bardzo, bardzo powoli.
Do tej listy trzeba też dopisać jakąkolwiek formę wskaźników na funkcje, przydatną w implementacji callbacków, czy też delegatów przeznaczonych do obsługi zdarzeń. Zamiast tego javowe API muszą uciekać się do (znów wybitnie OOP-owego) implementowania ustalonych interfejsów i polimorficznych wywołań metod. Do tego jednak język posiada unikalny feature, który – przynajmniej w założeniu – ma ten proces wydatnie ułatwiać. To właśnie tytułowe niestatyczne klasy wewnętrzne (inner classes).
Idea jest prosta. Jeśli klasę B umieścimy wewnątrz klasy A, to zabieg ten w Javie będzie miał skutek nie tylko dla widoczności tej pierwszej. Klasa B będzie wtedy klasą wewnętrzną A także w tym sensie, że każdy jej obiekt będzie zawsze związany z jakimś obiektem klasy A. Tą związanie odbywa się automatycznie i objawia dostępem do składników klasy “otaczającej”:
To w sumie nie jest aż tak imponujące. Wprawnym okiem można łatwo zauważyć, że to w gruncie rzeczy tylko cukierek składniowy dla przekazywania this
do konstruktora klasy wewnętrznej i późniejszego odwoływania się do niego. Ciekawiej jest wtedy, gdy mała, wewnętrzna, pomocnicza klasa jest na tyle mała i pomocnicza, że nie warto nawet nadawać jej nazwy, bo potrzeba nam tylko jednego jej obiektu:
Wówczas wszystko co o tej klasie wiemy to to, że implementuje ona pewien interfejs i właśnie za jego pośrednictwem możemy się do jej jedynego obiektu odwoływać.
Tak w skrócie wygląda teoria. Z praktycznego punktu widzenia mogę natomiast powiedzieć, że dwojga złego lepiej już te wątpliwej świeżości cukierki składniowe mieć, skoro innego wyjścia nie ma… Mam tu na myśli oczywiście fakt, że wobec braku delegatów/zdarzeń/domknięć/itp. jedynym sposobem na komunikację zwrotną w aplikacjach javowych jest wywoływanie metod z ustalonych interfejsów, implementowanych przez obiekty, które następnie podaje się jako “słuchacze” (listeners). Niestatyczne klasy wewnętrzne zapewniają przynajmniej łatwe połączenie między tymi sztucznie wprowadzonymi obiektami i resztą programu.
Trzeba jednak uważać, by nie zamienić swojego kodu w spaghetti, co może się łatwo zdarzyć, jeśli radośnie wpleciemy definicje klas w treść funkcji. Jest to szczególnie niepożądane w kodzie inicjalizującym np. elementy UI, w którym należy po kolei poustawiać wszystkie listenery dla wszystkich kontrolek. Zdefiniujmy je wszystkie w locie i będziemy mieli całą obsługę zdarzeń w jednej metodzie. Fuj!
Dlatego lepiej już definiować takie pomocnicze obiekty w podobny sposób, jak wyżej – tj. jako pola, którym przypisujemy klasy stworzone ad hoc, zawierające metody z reakcjami na zdarzenia. To oczywiście tylko nędzna imitacja składni prawdziwych procedur zdarzeniowych, ale przynajmniej jest to podróbka akceptowalna.
Wydaje mi się, że przynajmniej pod kilkoma względami programowanie przypomina prowadzenie samochodu. Obu tych umiejętności względnie łatwo się nauczyć i niemal niemożliwe jest zapomnieć. Po nabyciu pewnej wprawy mamy też wystarczającą biegłość, by nie musieć koncentrować całej uwagi na którejś z tych czynności. Wyjątkiem są jedynie te miejsca, w których zalecane jest zachowanie szczególnej ostrożności.
Dla mnie (i pewnie nie tylko dla mnie) takimi miejscami w kodzie są styki programu z otoczeniem, przez które musi przebiegać jakaś komunikacja polegająca na wymianie danych i/lub informacji o zdarzeniach. Fragmenty te są ważne i nierzadko problematyczne, gdyż często pociągają za sobą konieczność dopasowania sposobu wykonywania programu do zewnętrznych wymagań. Jak na skrzyżowaniu, trzeba czasem chwilę poczekać i przynajmniej parę razy obejrzeć się dookoła.
W tym kontekście używa się słowa ‘synchronizacja’, jednak kojarzy mi się ono nieodparcie z programowaniem współbieżnym. To trochę złe skojarzenie, bowiem nie trzeba wcale tworzyć kilku wątków czy procesów, by kwestia zaczynała mieć znaczenie. Wystarczą operacje wejścia-wyjścia (zwłaszcza względem mediów wolniejszych niż lokalny system plików) lub obsługa jakiegoś rodzaju zdarzeń zewnętrznych, albo chociażby RPC (Remote Procedure Call – zdalne wywoływanie procedur) – ale oczywiście ta lista nie wyczerpuje wszystkich możliwości. Ogólnie chodzi o wszelkie odstępstwa od możliwości wykonywania programu krok po kroku – a właściwie kroczek za kroczkiem, gdzie każda sekwencja stałej liczby instrukcji zajmuje umowną, niezauważalną chwilę.
Jeśli by się nad tym zastanowić przez moment, to takich sytuacji jest sporo. Ba, właściwie to wspomniane scenariusze sekwencyjne są raczej wyjątkiem, a nie regułą. Dzisiejsze aplikacje działają w wielozadaniowych środowiskach, na ograniczonych pulach zasobów, wymieniają dane przez różne (niekoniecznie szybkie) kanały informacji, i jeszcze dodatkowo są pod ciągłą presją wymagań co do cechy określanej angielskim słówkiem responsiveness – czyli “komunikatywności z użytkownikiem”. Nic dziwnego, że wspomniane przeze mnie wcześniej ‘punkty szczególnej ostrożności’ stają się na tyle istotne, że zazwyczaj to wokół nich buduje się całą architekturę programu.
W jaki sposób są one realizowane? Zależy to od wielu czynników, w tym od rodzaju aplikacji oraz możliwości i struktury systemowego API, które ona wykorzystuje. Tym niemniej można wyróżnić kilka schematów, aplikowalnych w wielu sytuacjach – chociaż nie zawsze z równie dobrym skutkiem. Są nimi:
i jest przestępstwem ściganym z urzędu w każdym rozsądnym zespole projektowym :) Istnieją aczkolwiek okoliczności łagodzące, zezwalające na jego popełnienie pod ściśle określonymi warunkami. Musimy jedynie być pewni, że zużywanie do 100% czasu procesora przez nasz program jest akceptowalne i że między kolejnymi zapytaniami możemy też zrobić coś produktywnego. Tak się składa, że istnieje typ aplikacji, w którym oba te warunki mogą być spełnione: gry. Ich pętla główna to nic innego jak aktywne czekanie na informacje o zdarzeniach z renderowaniem kolejnych klatek w tak zwanym międzyczasie.
Takie krótkie drzemki zdecydowanie zmniejszają zużycie procesora przez aplikację, ale nie dają jej dużego pola manewru. Ta metoda jest więc stosowalna główne dla usług działających w tle. A raczej byłaby, gdyby nie istniały znacznie lepsze :)
BackgroundWorker
z Windows Forms jest tak prosty w użyciu). Gorzej jest wtedy, gdy na potrzeby asynchronicznego callbacku musimy rozbić na kilka części (i stanów) program, który bez tego działałby niemal sekwencyjnie.Między powyższymi sposobami możliwe są “konwersje”, oczywiście do pewnego stopnia. Wymagać to może uruchomienia dodatkowego wątku, w którym wykonujemy polling operację asynchroniczną lub wręcz blokującą, i którego stan możemy odpytywać lub otrzymać jako sygnał na obiekcie synchronizacyjnym po wejściu w stan przerywalnego czekania.
Nieczęsto jednak taka zabawa ma uzasadnienie. W najlepszym razie otrzymamy rozwiązanie równoważne, a najgorszym stracimy na wydajności operacji dostosowanej pod konkretny typ powiadamiania. Lepiej jest jednak trzymać się tego, co dana platforma i API nam proponuje.
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ć.
Graficzny interfejs może (i powinien) ładnie wyglądać, ale jego najważniejszą cechą jest oczywiście interaktywność. GUI musi przede wszystkim umożliwiać reakcję na poczynania użytkownika, a to jest możliwe przez odpowiednią obsługę zdarzeń.
Informację o tym, że kliknięto gdzieś myszką lub wciśnięto klawisz, pozyskać jest oczywiście bardzo łatwo. W Windows wystarczy w tym celu obsługiwać komunikaty systemowe przychodzące do okna. O wiele większym wyzwaniem jest przetłumaczenie informacji “kliknięto w punkt (458,89) obszaru klienta” na “kliknięto w obrębie kontrolki TextBox o nazwie Text1
“. Komunikat należy bowiem przekazać do odpowiedniej kontrolki (-ek) – tej, której on dotyczy.
Jest pewnie wiele sposób na zrealizowanie takiego przekazywania, jednak najbardziej sensowne wydają mi się dwa. Pierwszym z nich jest propagacja zdarzenia na kolejne poziomy drzewa systemu GUI. Kliknięcie jest więc przekazywane do okna i od tej pory cała odpowiedzialność za jego obsłużenie spada na to właśnie okno. Ono sprawdza, czy myszka trafiła w którąś z kontrolek potomnych; jeśli tak, to znów zdarzenie jest przekazywane właśnie do tej kontrolki i okno się już nim nie zajmuje. Proces ten przebiega aż dojdziemy do najniższego możliwego poziomu, czyli kontrolki nie zawierającej już żadnych innych. Tam następuje właściwa obsługa zdarzenia.
Drugi sposób zakłada, że zdarzenie nie będzie “rozłazić” się po całym drzewie, tylko od razu trafiać do tej właściwej, docelowej kontrolki. Naturalnie nie zawsze da się tak zrobić. W przypadku wciśnięć klawiszy możemy pamiętać, która kontrolka ma tzw. fokus i do niej kierować komunikaty. Natomiast obsługa myszki wymaga wysłania swego rodzaju sondy wgłąb drzewa kontrolek w celu znalezienie tej najmniejszej, w którą trafił kursor (operacja ta jest znana jako hit test).
Generalnie zdarzenia z punktu widzenia ich obsługi można podzielić na dwie grupy: na zdarzenia myszki i na… wszystkie inne :) To właśnie te pierwsze przysparzają najwięcej kłopotów. Nie tylko wymagają rekurencyjnego przeszukiwania drzewa kontrolek, ale też mogą mieć globalne konsekwencje, jak np. zmiana fokusu. Zajmowanie się tymi konsekwencjami jest łatwiejsze, gdy wszystko odbywa się “na górze” – na poziomie głównych klas systemu GUI, a nie pojedynczych kontrolek. Jest to jeden z powodów przemawiających za wyborem drugiego sposobu przekazywania informacji o zdarzeniach.