Kto koduje trochę dłużej z użyciem technik programowania obiektowego, ten zapewne zna ideę wzorców projektowych. Są one czymś w rodzaju przepisów na określone klasy i związki między nimi, które rozwiązują konkretne problemy. Przykładowo znany wzorzec Iterator stwarza uniwersalny sposób na dostęp do elementów kolekcji – niezależnie od tego, czy jest to lista, drzewo, zwykła tablica, itp.
Ale oprócz tych pożytecznych wzorców istnieją też… antywzorce. Zdecydowanie nie są one instrukcją, jak pewne fragmenty programów pisać. O nie, wręcz przeciwnie – mówią one, jak ich nie pisać i pokazują typowe oraz mniej typowe błędy popełnione we wszystkich fazach tworzenia oprogramowania.
Więcej o nich można poczytać choćby w Wikipedii. Zamieszczona tam lista jest niezwykle obszerna, ale trudno się dziwić – mówimy przecież o sposobach, jak zrobić coś źle, a w tej dziedzinie ludzkość ma wybitne osiągnięcia :)
Do ciekawszych (i pouczających) przykładów takich antywzorców należą chociażby:
switch
– niezwykle “pomysłowy” sposób na zakodowanie kilku następujących po sobie czynności przy pomocy pętli i instrukcji wyboru, która zależnie od tego czy jest to 1., 2., czy N-ta iteracja wykonuje jakieś z góry ustalone czynności. Przyznam, że gdy to pierwszy raz zobaczyłem, nie mogłem wprost uwierzyć, że ktoś może wpaść na coś tak niedorzecznegoJak widać ten antywzorce dotyczą wielu różnych aspektów programowania i projektowania, i czasami ich niepoprawność wcale nie jest taka oczywista. (Chociaż na pewno ostatnia pozycja z listy powyżej jest aż nazbyt oczywista). Warto zatem zapoznać się z nimi; dzięki temu możemy znaleźć inspirację zarówno do unikania błędów, jak i ich popełniania ;]
Wyobraźmy sobie, że kończymy właśnie dużą partię kodu – kilkadziesiąt albo kilkaset linijek. Zamykamy ostatnią klamerkę (ewentualnie piszemy end
lub coś w tym guście), stawiamy ostatni średnik. Potem jeszcze tylko kilkukrotne przeciąganie suwaka paska przewijania, kilka pobieżnych rzutów oka na powstały listing i… kompilujemy.
Co się wtedy dzieje? Ano zwykle w tym momencie zatrzymuje nas jakiś trywialny błąd składniowy :) W sumie nic wielkiego, jakiś brak średnika albo powtórzony nawias; poprawiamy i próbujemy dalej. Po kilku razach w końcu zaakceptuje nasz program w całości – i zwykle dopiero wtedy zaczyna się prawdziwe debugowanie :]
Nie da się ukryć, ze czas tuż przed i w czasie kompilacji może być dość stresujący. Niby należałoby się już cieszyć, że oto wykonaliśmy jakiś (zwykle niewielki) kroczek na drodze do działającej całości. Zwłaszcza jeśli przez długi czas kod był rozgrzebany i nie dało się kompilować, domknięcie go z powrotem do kompilowalnej całości – uzupełnionej zapewne o nowe funkcje i możliwości – jest bardzo satysfakcjonujące.
Jednocześnie jednak to chwila próby. Pisanie kodu to w pewnym stopniu akt twórczy, ale jego kompilacja to proces najzupełniej prozaiczny, który sprawdza, jak nasze pomysły wypadają pod względem poprawności językowej. I jakby tego było mało, kod skompilowany na początku jest zwykle bardzo odległy od kodu poprawnego. Kompilacja jest więc zapowiedzią kolejnej, jeszcze “przyjemniejszej” fazy programowania – testów.
Jak zatem załagodzić kompilacyjny stres? Myślę, że należy wypracować sobie odpowiadającą nam częstotliwość przeprowadzanych kompilacji. Jeżeli bowiem będziemy przeprowadzali je zbyt rzadko, wówczas każda próba będzie trwała dłużej (więcej kodu = więcej błędów składniowych i tym podobnych), a w wynikowym kodzie ukrytych będzie więcej błędów do usunięcia podczas debugowania. Z drugiej strony trudno jednak kompilować i testować każdą drobnostkę, bo można całkowicie utonąć w szczegółach.
Czyli mówiąc wprost: ciężkie jest życie programisty :) W miarę nabywania doświadczenia okazuje się aczkolwiek, że potrafimy bez większych problemów doprowadzić do kompilacji coraz dłuższe partie kodu, w których w rezultacie nie ukrywa się już tak wiele błędów jak wcześniej. W programowaniu ważne jest bowiem, aby nie zrażać się pierwszymi (ani następnymi) niepowodzeniami.
Przychodzi czasem ochota, aby zapewnić dostęp do tych samych danych na różne sposoby. Ten pomysł można zrealizować w sposób poprawny albo i nie :) Dzisiaj właśnie przekonałem się ostatecznie, dlaczego stosowana przeze mnie metoda należała do tej drugiej kategorii.
Otóż dość powszechnie wykorzystuję pewną strukturę o nazwie SMouseEventArgs
, która docelowego zawiera informacje o jakimś zdarzeniu związanym z myszą (klik, przesunięcie, itd.). Wśród tych informacji jest między innymi pozycja kursora w momencie zajścia zdarzenia, która początkowo była opisana po prostu przez dwa pola X
i Y
.
W pewnym momencie zauważyłem jednak, że dość często stosuję konstrukcję podobną do tej:
gdzie POINT2F
jest strukturą opisującą punkt na płaszczyźnie. Aby zaoszczędzić sobie pisania (i konstrukcji tymczasowego obiektu) postanowiłem w przypływie kreatywności dodać do SMouseEventArgs
drugi sposób dostępu do tych współrzędnych – pole Position
. Jak? Otóż… przy pomocy unii:
Sęk w tym, że POINT2F
jest porządną strukturą i zawiera między innymi kilka konstruktorów. A jest niedozwolone, by obiekt klasy posiadającej zdefiniowany przez programistę konstruktor albo nietrywialny destruktor mógł być elementem unii.
Kompilator raczył mnie więc błędem, ale wówczas w kolejnym przypływie kreatywności stosowałem “objeście” w postaci anonimowej struktury:
I byłem święcie przekonany, że przecież skoro POINT2F
nie alokuje własnej pamięci, nie otwiera plików ani nie robi żadnych innych tego typu czynności, po których trzeba by sprzątać, to przecież nic złego nie może się stać…
Naturalnie byłem w błędzie :) Dalszym elementem układanki jest konstruktor SMouseEventArgs
, przyjmujący kilka parametrów i inicjalizujący nimi strukturę:
Na oko niby wszystko jest w porządku. Tylko dlaczego cokolwiek byśmy przekazali jako x
i y
, w wynikowej strukturze zawsze i tak współrzędne będą zerami?!
Ot, kolejna nierozwiązania zagadka Wszechświata. Przynajmniej do chwili, gdy uświadomimy sobie dwie rzeczy:
Aplikując te zasady do powyższego przypadku, mamy bardzo ciekawy scenariusz. Mianowicie pola X
i Y
są poprawnie wypełniane parametrami konstruktora SMouseEventArgs
, lecz w chwilę potem to samo miejsce pamięci jest… nadpisywane przez konstruktor POINT2F
. Dlaczego? Ano dlatego, że pole Position
też musi zostać zainicjowane, a domyślny konstruktor POINT2F
wstępnie ustawia je na punkt (0,0).
Mniej więcej takie są skutki prób bycia mądrzejszym od twórców języka i kompilatora :) Można by oczywiście brnąć dalej w to rozwiązanie, zmieniając kolejność deklaracji pól albo jawnie inicjalizować pole Position
zamiast X
i Y
. Cały czas jednak jest to stąpanie po cienkim lodzie.
Dlatego chyba najwyższy czas ograniczyć swoją kreatywność i następnym razem zastosować może mało efektowne, ale za to stuprocentowo bezpieczne metody dostępowe :)
Wśród standardowych kontrolek spotykanych w każdym systemie graficznego interfejsu jest jedna, która zdecydowanie wyróżnia się stopniem skomplikowania. Z wierzchu to tylko mały, podłużny prostokąt z wpisanym ciągiem znaków, który można modyfikować. Jednak mechanizmy sprawiające, że jest to możliwe, nie są wcale takie proste.
Mówię tu oczywiście polu tekstowym, znanym też jako textbox, editbox lub po prostu edit.
Powodów, dla których właśnie ta kontrolka wyróżnia się złożonością, jest przynajmniej kilka. Są to chociażby:
Tak naprawdę choćby w systemie Windows pola tekstowe umożliwiają nieco więcej, jak na przykład możliwość cofnięcia ostatniej operacji, obsługa Schowka czy nawet menu kontekstowe. Takie cuda nie są chyba jednak potrzebne do szczęścia :)
Zauważmy też, że kontrolka umożliwiająca edycję dłuższego tekstu dzielonego wiersza to coś zupełnie innego niż zwykły textbox i jej możliwości są nieporównywalnie większe. Dochodzą tam bowiem paski przewijania, zaznaczanie rozciągnięte na kilka wierszy, i tak dalej.
Implementując proste pole tekstowe zauważyłem, że bardzo pomaga przy tym odpowiednia konstrukcja pewnych podstawowych mechanizmów GUI – jak mouse capture czy fokus klawiatury, modelu propagacji zdarzeń oraz modułu odpowiedzialnego za wypisywanie tekstu. Łącznie jednak wychodzi z tego i tak nadspodziewanie duża ilość kodu ze sporą liczbą zagmatwanych ifów :]
Trochę poprzednio ponarzekałem na technikę RAII stosowaną w C++, a raczej na towarzyszący jej brak wygodnej instrukcji finally
. Było to być może odrobinę niesprawiedliwe, gdyż mechanizm ma dość duże możliwości – przynajmniej potencjalnie :)
Natrafiłem jakiś czas temu na Usenecie na ciekawy pomysł związany z tą właśnie techniką. Chodzi tu o wykrywanie, z jakiego powodu obiekt lokalny ma zostać zniszczony. Może się to bowiem odbyć w normalny sposób (gdy kończy się wykonanie odpowiedniego bloku kodu i program przechodzi dalej) lub w wyniku odwijania stosu podczas obsługi wyjątku. W obu przypadkach w C++ jest jednak wywoływany jeden i ten sam destruktor.
Jest to w porządku, jeżeli jego zadaniem jest tylko zwolnienie zasobu (czyli np. zamknięcie otwartego deskryptora pliku). Możemy sobie aczkolwiek wyobrazić zastosowanie RAII do tzw. transakcji:
Transakcja to termin znany głównie programistom zajmującym się bazami danych lub innymi pokrewnymi dziedzinami “sortowania ogórków” ;) W skrócie, jest to taki ciąg operacji, który musi być zaaplikowany albo w całości, albo wcale. Jeżeli po drodze zdarzy się coś nieoczekiwanego i transakcję trzeba przerwać, wszystkie wykonane do tej chwili operacji powinny zostać odwrócone (rollback).
Można by to zrobić automatycznie w reakcji na rzucenie wyjątku, gdyby C++ pozwalał na wykrycie wspomnianych dwóch sposobów niszczenia obiektu. Ciekawym pomysłem na to jest dopuszczenie więcej niż jednego destruktora:
Zwykły, bezparametrowy, byłby wywoływany w przypadku zwyczajnego opuszczenia bloku kodu. Natomiast destruktor przyjmujący parametr włączałby się wówczas, gdy niszczenie obiektu zdarzy się z powodu wyjątku. Taki destruktor “łapałby” więc na chwilę taki wyjątek – lecz nie po to, by go obsłużyć, ale wyłącznie w celu odpowiedniego zakończenia życia obiektu w sytuacji kryzysowej. Parametr takiego destruktora odpowiadałby typowi wyjątku, który ten destruktor miałby “łapać”.
Obecnie nie jest naturalnie możliwe stosowanie w C++ takiej konstrukcji. Istnieje jednak sposób na sprawdzenie, czy jesteśmy właśnie w trakcie obsługi jakiegoś wyjątku. Służy do tego mało znana funkcja uncaught_exception
z przestrzeni std
(nagłówek exception):
Wprawdzie nie zapewnia ona dostępu do samego obiektu wyjątku (ani poznania jego typu), ale pozwala na zorientowanie się, czy taki wyjątek w ogóle wystąpił. A to, jak widać, najczęściej wystarczy. Tak więc chociaż przeciążanie destruktorów na pierwszy rzut oka brzmi interesująco (i intrygująco), nie jest, jak sądzę, zbytnio potrzebne.
Właśnie się dowiedziałem, że dzisiaj właśnie przypada Dzień Programisty. A to dlatego, że dziś jest dokładnie 256-ty dzień roku, a znaczenia tej liczby chyba nie trzeba wyjaśniać. Wprawdzie taka okazja mogłaby pasować chyba do każdej informatycznej profesji, ale i tak uważam ją za o niebo lepszą chociażby od rocznicy urodzin naszego tzw. patrona św. Izydora czy innych tego typu pomysłów.
Mamy zatem święto koderów i należałoby je uczcić w jakiś nietypowy sposób. W pierwszej chwili pomyślałem, że tego odmiany mógłbym dla odmiany… nie napisać ani jednej linijki kodu. Niestety, ta sposobność przepadła kilka godzin wcześniej :) A może wobec tego wcielić w życie sentencję głoszącą, iż święto trzeba uczcić pracą? :)
Niezależnie od ich własnych pomysłów na obchodzenie tego dnia, życzę wszystkim programistom oraz tym, którzy dopiero do tego miana aspirują, przede wszystkim powodzenia w realizacji wszelkich koderskich zamierzeń. Wiadomo, że do wszystko dochodzi się głównie ciężką pracą i odpowiednią motywacją, ale czasami przydaje się ta odrobina szczęścia, na którą nie mamy wpływu. Niech dzisiaj (i nie tylko dzisiaj) to szczęście sprzyja wszystkim programistom, aby ta ilustracja po prawej jak najmniej do nas pasowała ;]
Wyjątki są sposobem na zasygnalizowanie nietypowych i niespodziewanych błędów, które poważnie zaburzają działanie programu. I właśnie to, że potencjalnie wyjątek może wystąpić w bardzo wielu miejscach w kodzie, rodzi pewne kłopoty. Problemami są chociażby zasoby: coś, co się pozyskuje, wykorzystuje, a następnie zwalnia, gdyż w przeciwnym razie doszłoby do wycieku. Typowym zasobem jest chociażby dynamicznie alokowana pamięć – jeżeli jej nie zwolnimy, nastąpi klasyczny wyciek, jako że C++ nie posiada garbage collectora, który mógłby się tym zająć za nas.
W C++ zalecanym rozwiązaniem tego problemu jest technika znana jako RAII (Resource Acquision Is Initialization – pozyskanie zasobu jest inicjalizacją). Korzysta ona z faktu, że w naszym ulubionym języku programowania możemy tworzyć obiekty lokalne z konstruktorami i destruktorami. Te drugie wywołają się zawsze przy opuszczaniu danego bloku kodu – niezależnie od tego, czy stało się z powodu wyjątku czy tez normalnego przebiegu programu. Pomysł polega więc na tym, by tworzyć obiekt specjalnie przygotowanej klasy w momencie pozyskania zasobu, zaś destruktor tego obiektu zajmie się już jego zwolnieniem, niezależnie od powodu.
Dopóki korzystamy z pamięci albo z plików, wszystko jest w porządku; odpowiednie klasy (jak auto_ptr
) posiada bowiem Biblioteka Standardowa. Gorzej jeśli chcemy skorzystać z innego rodzaju zasobów. Jeśli odpowiednia klasa realizująca technikę RAII nie istnieje, nie pozostaje nam nic innego, jak samemu ją sobie zapewnić (czytaj: napisać). I tak dla każdego rodzaju niestandardowych zasobów, które używamy. Po niedługim czasie można by z tych klas ułożyć własną “bibliotekę standardową” ;)
Alternatywą dla RAII jest dodanie trzeciego bloku (po try
i catch
) do konstrukcji łapiącej wyjątki. Jest on zwykle nazywany finally
. Instrukcje zawarte w tym bloku są wykonywane zawsze po tych z bloku try
– niezależnie od tego czy wyjątek wystąpił czy nie. Jest to więc bardzo dobre miejsce na wszelki kod zwalniający pozyskane wcześniej zasoby, np.:
Co ciekawe, posiadają go języki, które jeden z najważniejszych zasobów – pamięć – mają zarządzaną przez odśmiecacz, który praktycznie wyklucza możliwość powstania wycieków. Rzecz jednak w tym, że niektóre zasoby, jak chociażby otwarte pliku, nie mogą sobie czekać na to, aż odśmiecacz przypomni sobie o nich, gdyż wtedy byłyby blokowane stanowczo zbyt długo.
Czy C++ też potrzebuje instrukcji finally
? Na pewno nie jest to bardzo paląca potrzeba, jako że technika RAII zapewnia komplet potrzebnej tutaj funkcjonalności. To drugie, alternatywne rozwiązanie ma jednak szereg zalet:
finally
można też pożytecznie wykorzystać nawet wówczas, gdy w grę nie wchodzi możliwość pojawienia się wyjątku. Jeżeli na przykład w jakiejś skomplikowanej funkcji mamy wiele miejsc, w których może nastąpić jej zakończenie, a przy każdej okazji może być potrzeba wykonania jeszcze jakichś czynności końcowych. Wtedy moglibyśmy zamknąć całą treść funkcji w try
, a owe czynności umieścić w sekcji finally
. Trzeba by się było jednak liczyć z tym, że blok try
nie jest darmowy i jego użyciu nakłada pewien narzut.I przede wszystkim: RAII i finally
nie wykluczają się nawzajem. Dlatego obecność tego drugiego mechanizmu w C++ na pewno by nam nie zaszkodziła :)