Programista 1

You might also like

Download as pdf or txt
Download as pdf or txt
You are on page 1of 82

www • programistamag • pl

Magazyn programistów i liderów zespołów IT

1 / 2012 (0 1) Cena 19.90 zł (w tym VAT 8%)

Programowanie gier pod iOS


Biblioteka Cocos2D

Wybrane Erlang - język C++11 - część I. Wykorzystanie


elementy języka inny niż C++ czy Nowy standard Kinect w sys­te­
Objective-C Java języka C++ mie Windows
Bardzo praktyczne porady Fascynująca podróż do kra- Kilka słów o tym, jakie (miłe!) Pierwszy krok do stworze-
dotyczące używania języka iny programowania w logice niespodzianki niesie ze sobą nia interfejsu użytkownika
Objective-C przy tworzeniu oraz programowania funk- nowa odsłona standardu rodem z filmu Minority
aplikacji pod iOS cyjnego języka C++ Report
REDAKCJA/EDYTORIAL

Szanowni twórcy oprogramowania i specjaliści branży IT

Przed Wami pierwsze wydanie magazynu „Pro- pozwalające znacznie usprawnić proces tworze-
gramista”, które ukazało się również w postaci dru- nia aplikacji na urządzenia mobilne ze stajni Apple.
kowanej. Od niniejszego numeru magazyn staje się Znajdziecie u nas również ciekawy artykuł traktują-
miesięcznikiem. Przychy­liliśmy się też do Waszych cy o popularnym ostatnio podejściu Domain Driven
próśb i przygotowaliśmy magazyn w wersji elektro- Design. Ponadto kontynuujemy tematykę poruszo-
nicznej w plikach ePub i .mobi oraz .pdf. ną w premierowym wydaniu naszego miesięcznika:
Rośnie ilość i złożoność otaczających nas syste- omawiamy możliwości nowego standardu języka
mów informatycznych. Rynek tabletów i smartfo- C++ oraz prezentujemy ciekawe tematy z zakresu
nów ciągle odnotowuje wzrosty i naturalną koleją inżynierii oprogramowania.
rzeczy powstają tysiące nowych aplikacji na te urzą- Na koniec pragniemy serdecznie podziękować
dzenia: od gier po programy użytkowe. Rozwijają za konstruktywne opinie dotyczące pierwszej, elek-
się języki programowania, powstają kolejne wersje tronicznej edycji magazynu. Wydanie to udostępni-
„mobilnych” systemów operacyjnych. W tej sytuacji liśmy bezpłatnie, by każdy mógł wyrobić sobie opi-
nie ma wyjścia: chcąc pozostać konkurencyjnym nię o projekcie. Olbrzymie zaintereso­wanie, jakim
w zawodzie Programisty, trzeba się na bieżąco roz- cieszył się premierowy egzemplarz, utwierdza nas
wijać, dokształcać. Naszym celem jest Wam to zada- w przekonaniu, że tego typu wy­dawnictwa brakowa-
nie ułatwić. ła na polskim rynku i dopinguje do jeszcze bardziej
W bieżącym numerze przedstawiamy bibliotekę wytężonej pracy nad magazy­nem.
Cocos2D: jeden z najpopularniejszych silników do Cieszy nas, że zdecydowana większość z Was po-
tworzenia gier na platformę iOS. Ponadto poleca- zytywnie oceni­ła inicjatywę! Dziękujemy za wsparcie
my praktyczny artykuł na temat języka Objective-C, i życzymy przyjemnej lektury!
w którym autor omawia przydatne jego właściwości,

Z wyrazami szacunku,
Redakcja

Magazyn Programista wydawany jest


przez firmę Anna Adamczyk

Wydawca: Kierownik produkcji: Współpraca:


Anna Adamczyk Krzysztof Kopciowski Michał Bartyzel
annaadamczyk@programistamag.pl bok@keylight.com.pl Mariusz Sieraczkiewicz
Sławomir Sobótka
DTP: Artur Machura
Redaktor naczelny: Marek Sawerwain
Łukasz Łopuszański Krzysztof Kopciowski
Łukasz Mazur
lukaszlopuszanski@programistamag.pl Rafał Kułaga
Dział reklamy:
reklama@programistamag.pl
Redaktor prowadzący: tel. +48 663 220 102 Adres wydawcy:
Rafał Kocisz Dereniowa 4/47
tel. +48 604 312 716
rafal.kocisz@gmail.com 02-776 Warszawa
Prenumerata: Druk:
Korekta: prenumerata@ Zamów wydanie w wersji papierowej
Tomasz Łopuszański programistamag.pl przez www.programistamag.pl
4 / 1 . 2012 . (1)  /
SPIS TREŚCI

BIBLIOTEKI I NARZĘDZIA
Biblioteka Cocos2D: wprowadzenie............................................................................................. 6
Rafał Kocisz

JĘZYKI PROGRAMOWANIA
C++11 część I...................................................................................................................................... 22
Bartosz Szurgot, Mariusz Uchroński, Wojciech Waga

Wybrane elementy języka Objective-C i ich wykorzystanie................................................... 30


Łukasz Mazur

Erlang - język inny niż C++ czy Java.............................................................................................. 40


Marek Sawerwain

PROGRAMOWANIE GRAFIKI
Direct3D – podstawy........................................................................................................................ 50
Wojciech Sura

PROGRAMOWANIE URZĄDZEŃ
Wykorzystanie sensora Kinect w systemie Windows .............................................................. 56
Łukasz Górski

INŻYNIERIA OPROGRAMOWANIA
Domain Driven Design krok po kroku część II: Zaawansowane modelowanie DDD
– techniki strategiczne: konteksty i architektura zdarzeniowa............................................. 64
Sławomir Sobótka

KLUB LIDERA IT
Dokumentowanie architektury. Jak zorganizować proces rozwoju architektury?........... 72
Michał Bartyzel, Mariusz Sieraczkiewicz

WDROŻENIA
Projekt, oprogramowanie i wdrożenie pla­tformy inwestycyjnej highsky.com.................. 76
Wojciech Holisz

KOMIKS........................................................................................................................................................ 49
Maciej Mazurek

O ile nie zaznaczono inaczej, wszelkie prawa do wszystkich materiałów zamieszczanych na łamach magazynu
Programista są zastrzeżone. Kopiowanie i rozpowszechnianie ich bez zezwolenia jest wzbronione. Naruszenie
praw autorskich może skutkować odpowiedzialnością prawną, określoną w szczególności w przepisach ustawy
o prawie autorskim i prawach pokrewnych, ustawy o zwalczaniu nieuczciwej konkurencji i przepisach kodeksu
cywilnego oraz przepisach prawa prasowego.

Redakcja magazynu Programista nie ponosi odpowiedzialności za szkody bezpośrednie i pośrednie, jak również
za inne straty i wydatki poniesione w związku z wykorzystaniem informacji prezentowanych na łamach magazy­
nu Programista. Wszelkie nazwy i znaki towarowe lub firmowe występujące na łamach magazynu są zastrzeżone
przez odpowiednie firmy.

/ www.programistamag.pl / 5
BIBLIOTEKI I NARZĘDZIA
Rafał Kocisz

Biblioteka Cocos2D: wprowadzenie


iOS to platforma, która rozbudza wyobraźnię wielu programistów. Któż z nas nie
marzy o karierze niezależnego twórcy gier i setkach tysięcy dolarów zarobionych
dzięki sprzedaży aplikacji na AppStore? W niniejszym artykule przedstawiona jest
biblioteka, która może być kluczem do spełnienia tych marzeń.

G
łównym celem niniejszego artykułu jest zapozna- wiście wspomagane akceleracją sprzętową). Jeśli jesteś
nie Czytelnika z podstawowymi blokami budulco- początkującym programistą gier, to zabawa z dwuwymia-
wymi, które oferuje biblioteka Cocos2D. Po jego rem jest wręcz zalecana (chociażby dlatego, że algorytmy
przeczytaniu będziesz wiedział, czego możesz spodziewać stosowane w tego typu grach są o wiele łatwiejsze w im-
się po tym silniku i na ile przydatny będzie on w Twoich plementacji w stosunku do ich odpowiedników stosowa-
projektach. Cocos2D to potężna biblioteka i szczegółowe nych w grach 3D).
jej opisanie to temat, który kwalifikuje się bardziej na Dodatkowo, za Cocosem stoi bardzo liczna, prężna
książkę niż na artykuł. Z tego względu niniejszy tytuł kła- i otwarta społeczność złożona w dużej mierze z niezależ-
dzie nacisk na ogólne zrozumienie koncepcji, na których nych programistów gier, co daje możliwość stosunkowo
opiera się biblioteka Cocos2D, oraz przedstawienie relacji łatwego uzyskania wsparcia. Ze względu na swoją popu-
między nimi. W jednym z ostatnich sekcji artykułu wska- larność Cocos2D może poszczycić się posiadaniem dużej
zane są materiały, z których zainteresowani Czytelnicy ilości wysokiej jakości materiałów edukacyjnych (samo-
będą mogli skorzystać w celu poszerzenia swojej wiedzy uczków, książek, forów dyskusyjnych) oraz narzędzi, któ-
na temat prezentowanej tu biblioteki. re wspierają i ułatwiają pracę z tą biblioteką.
Podsumowując, Cocos2D jest niewątpliwie biblioteką,
DLACZEGO COCOS2D? z którą warto się zapoznać. Jeśli masz ochotę zanurkować
w jego barwny świat, zapraszam do lektury dalszej części
Zanim przejdziemy do omówienia możliwości biblioteki niniejszego artykułu.
Cocos2D, spróbujmy odpowiedzieć sobie na podstawowe
pytanie: co sprawia, że warto zainteresować się właśnie GRAF SCENY
tym konkretnym rozwiązaniem?
Pierwszy ważny powód, dla którego warto rozważyć Najważniejsza, centralna koncepcja, wokół której zbudo-
używanie Cocos2D, to fakt, iż biblioteka ta jest całkowicie wana jest biblioteka Cocos2D to tzw. graf sceny (zwany
darmowa, zaś jej licencja pozwala tworzyć zarówno apli- czasami drzewem sceny bądź hierarchią sceny). Zrozu-
kacje komercyjne, jak i niekomercyjne. Licencja Cocos2D mienie tej koncepcji jest kluczowe w przypadku gdy ktoś
jest otwarta, silnik rozpowszechniany jest razem z kodem chce efektywnie korzystać z prezentowanego tu silnika.
źródłowym. Oznacza to, że nic nie stoi na przeszkodzie, Wyobraź sobie, że masz zaimplementować fragment
aby w razie potrzeby zajrzeć do kodu źródłowego biblio- graficznego interfejsu użytkownika w grze, coś podob-
teki czy wręcz wprowadzić do niej własne modyfikacje. nego do menu przedstawionego na Rysunku 1. Spróbuj
Cocos2D napisany jest w języku Objective-C. Biorąc spojrzeć na ten obraz okiem programisty i zastanów
pod uwagę, że Cocos2D obsługuje urządzenia z rodziny się, jak można by zorganizować model takiego interfej-
iOS oraz OS X, wybór ten wydaje się bardzo trafny: Ob- su użytkownika na poziomie kodu źródłowego. Pierwsza
jective-C to natywny język programowania w wymienio- myśl, która zapewne przyjdzie Ci do głowy, to przechowy-
nych systemach. Dla osób, które znają inny język obiekto- wanie płaskiej listy obiektów reprezentujących wszystkie
wy (np. C++, Java, C#), nauka Objective-C nie powinna widoczne elementy na ekranie (elementy tła, przyciski
sprawić większych trudności. Osobom rozpoczynającym i elementy tekstowe). Obiekty takie przechowywałyby in-
przygodę z programowaniem sugerowałbym poświęce- formacje o stanie poszczególnych elementów menu (np.
nie nieco czasu na solidne zapoznanie się z Objective-C, ich pozycja, stan, widoczność). Informacji tych można by
zanim na poważnie zaczną zajmować się programowanie użyć do rysowania całej sceny, obsługi zdarzeń użytkow-
gier pod iOS przy użyciu biblioteki Cocos2D. nika itp.
Jak sugeruje nazwa biblioteki, Cocos2D wspomaga pro- Podejście takie można by oczywiście z powodzeniem
gramowanie gier 2D. Warto podkreślić jednak, że mowa zastosować, ale... zanim zaczniesz kodować, zastanów
tutaj o nowoczesnych grach 2D, w których na porządku się, czy nie można by było zorganizować tego lepiej?
dziennym są wykonywane w czasie rzeczywistym trans- Powiedzmy, że chciałbyś zaimplementować prosty efekt
formacje obrazów (np. rotacja czy skalowanie), obsługa polegający na tym, że druga warstwa tła z umieszczony-
przeźroczystości czy post-processing (wszystko to oczy- mi na niej kontrolkami płynnie wsuwa się przy rozpoczę-

6 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

Rysunek 1. Proste menu gry (źródło: opracowanie własne) Rysunek 2. Efekt wsuwania się menu (źródło: opracowanie
własne)

ciu gry, zaś przy jej zakończeniu wysuwa się poza ekran
(patrz: Rysunek 2).
Sprawa niby prosta, jednakże przy zastosowaniu pła-
skiej listy jako struktury danych reprezentującej kolekcję
kontrolek, implementacja takiego efektu staje się nieco
uciążliwa: trzeba ręcznie wybrać wszystkie obiekty, które
chcemy wsuwać\wysuwać, i dla każdego z nich odpowied-
nio modyfikować ich pozycje. W tej sytuacji aż się prosi,
aby potraktować drugą warstwę tła jako płaszczyznę, na
której leżą wszystkie pozostałe kontrolki, i przesunąć ją,
razem ze wszystkim elementami, które są na niej umiesz-
czone. W tym celu możemy zastosować właśnie graf sceny.
Graf sceny to drzewiasta struktura danych pozwalająca
reprezentować obrazy (zarówno 2D i 3D) tak, aby zacho-
wać informację o hierarchii obiektów na nich występują-
cych. Kluczowym elementem grafu sceny jest węzeł (ang. Rysunek 3. Hierarchia sceny dla prostego menu w grze (źródło:
node), który spełnia dwojaką rolę: po pierwsze, repre- opracowanie własne)
zentuje wybrany element sceny; po drugie, jest kontene-
rem, który może przechowywać inne węzły, stanowiące Spróbujmy odnieść przedstawione wyżej rozważania
jego dzieci (ang. children). Węzeł posiadający dzieci na- do naszej przykładowej sceny. Na Rysunku 3 przedsta-
zywany jest rodzicem (ang. parent). W grafie sceny wy- wiona jest wspomniana scena z oznaczeniem elementów
stępuje jeden węzeł, który nie posiada rodzica; nazywa- hierarchii w grafie. Korzeniem grafu jest pierwsza war-
my go korzeniem (ang. root). Znamienne dla grafu sceny stwa tła (czerwony prostokąt otaczający). Korzeń ma
jest to, że każdy rodzic definiuje dla swoich dzieci swoisty jednego potomka: drugą warstwę tła (niebieski prostokąt
lokalny układ współrzędnych. Oznacza to, iż współrzęd- otaczający). Ten z kolei posiada czwórkę dzieci: przyci-
ne dzieci (a także inne ich właściwości) rozpatrywane są ski (żółte prostokąty otaczające). Każdy przycisk posia-
w odniesieniu do układu rodzica. Np. jeśli węzeł rodzic da jednego potomka, którym jest umieszczony pod nim
(korzeń) ma pozycję (50, 50), zaś jego potomek znaj- napis. Do reprezentacji takiej sceny potrzebowalibyśmy
duje się w punkcie (10, 15), to rzeczywista (ekranowa) dwóch konkretnych klas-węzłów reprezentujących sta-
pozycja tego drugiego wyniesie (60, 75). W takim ujęciu, tyczny obrazek (elementy tła i przyciski) oraz tekst (napi-
gdy zmienimy pozycję danego węzła, to automatycznie sy pod przyciskami).
przemieszczone zostaną jego dzieci. Mając do dyspozycji tak zorganizowaną scenę, za-
Każdy węzeł w grafie sceny posiada identyczny inter- programowanie efektu wsuwania się drugiej warstwy tła
fejs, opisany zazwyczaj przez abstrakcyjną klasę bazową. z umieszczonymi na niej kontrolkami staje się bardzo pro-
Po tej klasie dziedziczą inne klasy, reprezentujące kon- ste; wystarczy jedynie odpowiednio zmodyfikować pozy-
kretne węzły. Czytelnicy, którzy mieli styczność z tzw. cję węzła reprezentującego tę warstwę, a o właściwe po-
wzorcami projektowymi (ang. design patterns), słusznie zycjonowanie pozostałych elementów zadba graf sceny.
skojarzą przedstawiony tutaj opis ze wzorcem kompozyt Graf sceny to nieocenione narzędzie przy tworzeniu
(ang. composite); de facto graf sceny jest niemalże wzor- aplikacji wyświetlających złożone obrazy zbudowane
cowym przykładem takiego podejścia projektowego. z grup powiązanych ze sobą obiektów (pod tę kategorię

/ www.programistamag.pl / 7
BIBLIOTEKI I NARZĘDZIA

Rysunek 4. Hierarchia klas wywodzących się z CCNode (źródło:


http://www.cocos2d-iphone.org/ )

można z powodzeniem podciągnąć większość nowocze- wyglądać nieco inaczej od tej, która jest tutaj przedsta-
snych gier 2D oraz 3D). Jest on również bardzo przydat- wiona (dotyczy ona Cocos2D w wersji 1.0.1), jednak-
ny w innych zastosowaniach, np. określanie widoczności że szereg koncepcji reprezentowanych przez wybrane
obiektów czy detekcja kolizji. Na tym etapie ważne jest, klasy niewątpliwie pozostaną niezmienne. Klasy te będą
abyś zrozumiał podstawową ideę tego wzorca projekto- szczegółowo omówione w kolejnych podpunktach niniej-
wego, gdyż stanowi on serce biblioteki Cocos2D. szego artykułu.
Zanim jednak do tego przejdziemy, przyjrzyjmy się
KLASA BAZOWA CCNODE bardziej szczegółowo interfejsowi klasy CCNode. Jak już
wcześniej wspominałem, jest to klasa abstrakcyjna i nie
Tak jak wspominałem w poprzednim punkcie, kluczo- ma bezpośredniej reprezentacji wizualnej, jednakże pełni
wymi elementami w grafie sceny są węzły. Każdy wę- bardzo istotną rolę, gdyż zawiera pola i metody wspólne
zeł dziedziczy po klasie bazowej, która definiuje spój- dla wszystkich węzłów przechowywanych w grafie sceny.
ny interfejs dla wszystkich obiektów umieszczanych Oto lista najważniejszych z nich:
w hierarchii. W przypadku biblioteki Cocos2D klasa
ta nazywa się CCNode. Po tej klasie dziedziczą kolejne ■■ tworzenie nowego węzła:
klasy, reprezentujące konkretne obiekty, które można
umieszczać w grafie sceny. Na Rysunku 4 przedstawio- CCNode* childNode = [CCNode node];

na jest hierarchia klas wywodzących się z klasy bazo-


wej CCNode. ■■ dodawanie węzła-dziecka:
Jak widać, klas tych jest całkiem sporo. Cocos2D to
bardzo aktywnie rozwijana biblioteka, więc w momen- [node addChild:child z:0 tag:73];

cie kiedy czytasz niniejszy artykuł, hierarchia ta może

8 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

■■ wyłuskiwanie węzła-dziecka: żesz zrealizować przypisując do węzła reprezentującego


wspomniany element odpowiednie akcje. O tym, jakie są
CCNode* child = [node getChildByTag:73]; kategorie akcji, porozmawiamy za moment; teraz kilka
słów na temat tego, jak można obsługiwać je z poziomu
■■ usuwanie węzła-dziecka określonego za pomocą iden- interfejsu klasy CCNode. Na początek stwórzmy prostą
tyfikatora: (z opcjonalnym czyszczeniem, polegającym akcję reprezentującą proces migania węzła i przypiszmy
na zastopowaniu wszystkich akcji przypisanych do jej identyfikator:
usuwanego węzła):
CCAction* action =
[node removeChildByTag:73 cleanup:YES]; [CCBlink actionWithDuration:5
blinks:10];

■■ usuwanie węzła-dziecka za pośrednictwem wskaźnika: action.tag = 37;

[node removeChild:child]; Akcja ta przypisana do określonego węzła sprawi, że za-


miga on 10 razy w przeciągu 5 sekund. Klasa CCNode po-
■■ usuwanie wszystkich dzieci z węzła: zwala kontrolować akcje w następujący sposób:

[node removeAllChildrenWithCleanup:YES]; ■■ uruchomienie akcji:

■■ usuwanie siebie samego z węzła-rodzica: [node runAction:action];

[node removeFromParentAndCleanup:YES]; ■■ wyłuskanie akcji określonej za pomocą identyfikatora:

Jak łatwo się można domyśleć, parametr z przekazywany CCAction* retrievedAction = [node
getActionByTag:37];
w metodzie addChild określa głębokość (ang. depth) wę-
zła w scenie i pozwala kontrolować kolejność rysowania
elementów przechowywanych w grafie. Zasada jest pro- ■■ zastopowanie akcji określonej za pomocą identyfikatora:
sta: elementy z małymi wartościami z rysowane są jako
pierwsze, zaś te, które mają największą głębokość – jako [node stopActionByTag:37];

ostatnie. Jeśli część węzłów posiada taką samą wartość


parametru z, to porządek ich rysowania określony jest ■■ zastopowanie akcji określonej za pomocą wskaźnika:
kolejnością ich dodawania do węzła-rodzica.
Z kolei parametr tag to rodzaj identyfikatora, za po- [node stopAction:action];

mocą którego możemy szybko wyłuskiwać czy usuwać


węzły za pomocą takich metod jak getChildByTag czy ■■ zastopowanie wszystkich akcji przypisanych do danego
removeChildByTag. Korzystając z tagów, należy pa- węzła:
miętać o tym, że Cocos2D nie rozwiązuje problemu ich
unikalności, tj. jeśli umieścisz w scenie dwa, lub wię- [node stopAllActions];

cej węzłów o identycznych identyfikatorach, to będziesz


w stanie dostać się tylko do pierwszego z nich; pozostałe Ostatnią właściwością klasy CCNode, o której chciałbym
będą niedostępne. Cocos2D wykorzystuje tagi również do opowiedzieć, to możliwość tzw. harmonogramowania
identyfikacji tzw. akcji (co to są akcje, dowiesz się już wiadomości (ang. message scheduling). Brzmi to nieco
za moment); w tym miejscu chciałbym tylko wspomnieć, tajemniczo, jednakże w praktyce rzecz jest bardzo pro-
że identyfikatory węzłów i akcji nie kolidują ze sobą, tak sta. Przede wszystkim należy wyjaśnić, że w nomenkla-
więc dopuszczalna jest sytuacja, gdy zarówno węzeł, jak turze języka Objective-C wysłanie wiadomości do obiektu
i przypisana do niego akcja mają ten sam tag. oznacza po prostu wywołanie metody na tym obiekcie.
Jak już kilka razy wspomniałem, węzły mogą mieć W takim ujęciu harmonogramowanie wiadomości ozna-
przypisane akcje (ang. actions). Koncepcję akcji opi- cza cykliczne wywoływanie określonej metody. Wyobraź
szę szczegółowo w oddzielnym podpunkcie niniejszego sobie, że chciałbyś mieć możliwość oprogramowania ja-
artykułu; tutaj przedstawię je tylko pokrótce, tak abyś kiejś dedykowanej logiki dla danego typu węzłów (np.
mógł zrozumieć relację pomiędzy nimi a węzłami. Pisząc detekcja kolizji bądź sprawdzanie warunku zakończenia
w największym skrócie, akcje to obiekty reprezentujące gry). Tego typu logikę umieszcza się zazwyczaj w meto-
działania wykonywane na obiektach sceny. Wyobraź so- dzie update bądź process, która wywoływana jest po
bie chcesz zaimplementować graficzny element na ekra- narysowaniu kolejnej ramki gry, przyjmującej jako pa-
nie (np. przycisk), który miarowo pulsuje (na przemian rametr przyrost czasu od poprzedniego jej wywołania
powiększa się i zmniejsza) albo miarowo miga (na prze- (ang. time delta). Jeśli potrzebujesz takiej funkcjonalno-
mian pojawia się i znika) bądź po prostu przemieszcza się ści w węźle, to musisz odpowiednio przeładować metodę
pomiędzy dwoma punktami. Wszystkie te operacje mo- scheduleUpdateMethod (patrz: Listing 1).

/ www.programistamag.pl / 9
BIBLIOTEKI I NARZĘDZIA

Listing 1. Harmonogramowanie prostej funkcji obsługującej procesu uaktualniania stanu gry i w rezultacie mają bar-
logikę węzła dzo istotny wpływ na wydajność aplikacji:

-(void) scheduleUpdateMethod
{ ■■ kCCDirectorTypeDisplayLink: gwarantuje najlepszą
[self scheduleUpdate]; wydajność oraz płynność renderowania, dzięki zasto-
} sowaniu mechanizmu synchronizacji procesu uak-
tualniania ekranu z jego sprzętowym odświeżeniem;
-(void) update:(ccTime)delta
{ niestety – mechanizm ten dostępny jest od wersji 3.1
// Ta metoda będzie wywoływana po narysowaniu systemu iOS wzwyż,
// kolejnej ramki gry. ■■ kCCDirectorTypeNSTimer: gwarantuje największą
}
prze­nośność (będzie działać w każdej wersji systemu
iOS), ale jest za to najwolniejszy,
REŻYSER, SCENA, WARSTWA ■■ kCCDirectorTypeThreadMainLoop: szybki, ale spra-
wia problemy w sytuacji, kiedy chcemy używać z po-
Chciałbym przedstawić teraz trzy klasy, które pełnią w bi- ziomu aplikacji Cocos2D widoków UIKit,
bliotece Cocos2D bardzo ważne role. Mowa tu o klasach ■■ KCCDirectorTypeMainLoop: jak wyżej.
CCDirector, CCScene oraz CCLayer. Dwie ostatnie z wy-
mienionych dziedziczą z CCNode i podobnie jak ona, nie mają
wizualnej reprezentacji. CCScene to kontener dla wszyst- Domyślnie biblioteka Cocos2D korzysta z reżysera typu
kich obiektów znajdujących się na scenie. Obiekty tej klasy kCCDirectorTypeDisplayLink, zaś w sytuacji gdy nie jest
reprezentują zazwyczaj ekrany gry: menu główne, tabelę on dostępny (na dzień dzisiejszy bardzo mało prawdopodob-
wyników czy wreszcie – właściwą rozgrywkę. Z kolei klasa na sytuacja), przełącza się na typ kCCDirectorTypeNSTimer.
CCLayer (warstwa) służy do grupowania obiektów, głównie Jeśli chciałbyś sam określić typ reżysera, to musisz zmo-
w celu zapewnienia właściwej kolejności ich renderowania. dyfikować następujący fragment kodu źródłowego w klasie
Można sobie np. wyobrazić, że ekran rozgrywki składa się reprezentującej delegata aplikacji:
z trzech warstw: statycznego tła, części dynamicznej (ru-
chome obiekty gry) oraz paska statusu przedstawiające- if ( ! [CCDirector
setDirectorType:
go liczbę zdobytych punktów oraz żyć. Ważną cechą klasy kCCDirectorTypeDisplayLink] )
CCLayer jest to, że potrafi ona przechwytywać zdarzenia [CCDirector
generowane przez użytkownika za pośrednictwem takich setDirectorType:
kCCDirectorTypeDefault];
kontrolerów jak ekran dotykowy czy akcelerometr. Szcze-
gólną rolę w tej układance pełni klasa CCDirector; pełni
ona rolę reżysera – decyduje o tym, która scena (CCScene) Kolejną klasą z inwentarza biblioteki Cocos2D, z którą
będzie w danym momencie aktywna. warto się szczegółowo zapoznać, jest CCScene. Obiekty
Na początek przyjrzyjmy się, co oferuje klasa CCDirec- tej klasy są zawsze korzeniami w grafie sceny. Rozwią-
tor. Po pierwsze, klasa ta jest tzw. singletonem (singleton zanie to wydaje się momentami odrobinę sztuczne (klasa
jest to wzorzec projektowy, który zapewnia, że dana klasa CCScene w zasadzie nie rozszerza w żaden sposób funk-
posiada tylko jedną instancję). Fakt ten wydaje się być cjonalności CCNode), jednakże metody runWithScene,
dość oczywisty (czy widziałeś kiedyś film lub przedsta- replaceScene czy pushScene z klasy CCDirector potra-
wienie, za które odpowiadało dwóch reżyserów?). Głów- fią współpracować tylko z obiektami tego typu. Poza tym,
ne zadanie reżysera w bibliotece Cocos2D to zarządzanie sceny można opakować obiektami klas dziedziczących po
scenami oraz przechowywanie danych konfiguracyjnych. klasie bazowej CCSceneTransition, co pozwala uzyskać
W szczególności klasa CCDirector odpowiada za: miłe dla oka efekty przejść między scenami gry.
Pracując z biblioteką Cocos2D, zazwyczaj przyjmuje
■■ startowanie sceny, się konwencję, że dziećmi sceny są jedynie warstwy (tj.
■■ zamianę bieżącej sceny, obiekty wywodzące się z klasy CCLayer), zaś one dopiero
■■ wkładanie (ang. pushing) nowej sceny na bieżącą, zawierają w sobie węzły reprezentujące konkretne obiek-
■■ zdejmowanie (ang. popping) bieżącej sceny, ty występujące w grze.
■■ gwarantowanie dostępu do bieżącej sceny, Bardzo przydatnym mechanizmem jest wkładanie (ang.
■■ pauzowanie, kontynuowanie oraz kończenie gry, pushing) oraz zdejmowanie (ang. popping) bieżącej sceny
■■ przechowywanie i gwarantowanie dostępu do global- z poziomu reżysera. Operacje te niewątpliwie kojarzą się
nych danych konfiguracyjnych biblioteki Cocos2D, ze strukturą danych zwaną stosem. I rzeczywiście, myśląc
■■ gwarantowanie dostępu do okna oraz widoku OpenGL, o tym, w jaki sposób klasa CCDirector zarządza scenami,
■■ konwertowanie współrzędnych UIKit i OpenGL, można posłużyć się modelem stosu. Oczywiście, wkładając
■■ kontrolowanie procesu uaktualniania stanu gry. nową scenę na stos, bieżąca nadal pozostaje widoczna.
Mechanizm ten jest bardzo przydatny np. w sytuacji kie-
Pracując z biblioteką Cocos2D, mamy do wyboru cztery dy w trakcie gry chcemy wyświetlić okno dialogowe, bądź
typy reżyserów. Typy te implikują sposób kontrolowania podręczne menu. W tej sytuacji wkładamy nową scenę

10 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

Rysunek 5. Hierarchia klas wywodzących się z CCSceneTransition


(źródło: http://www.cocos2d-iphone.org/ )

na bieżącą, czekamy na działanie użytkownika, a w koń- transitionWithDuration:1


cu zdejmujemy bieżącą scenę ze stosu i automatycznie scene:[MyScene scene]
withColor:ccBLACK];
wracamy do przerwanej rozgrywki. Manipulując scenami,
trzeba jednak uważać, aby nie przesadzić, gdyż za każdym // Zamiana scen z użyciem efektu przejścia.
razem kiedy Cocos2D podmienia sceny, nowa scena jest [[CCDirector sharedDirector]
tworzona w pamięci, zaś stara zostaje usuwana. replaceScene:transition];
Jak przed momentem wspominałem, używanie scen
pozwala uzyskiwać bardzo ciekawe efekty przejść (ang. Wspomniany efekt polega na tym, że aktualna scena
transition) pomiędzy ekranami gry reprezentowanymi powoli blaknie, aż do momentu kiedy cały ekran stanie
przez różne obiekty klasy CCScene. Służą ku temu kla- się czarny (lub inny; docelowy kolor można sobie do-
sy wywodzące się z klasy bazowej CCSceneTransition. wolnie zdefiniować), po czym dzieje się efekt odwrot-
Hierarchia tych klas przedstawiona jest na Rysunku 5. Na ny, tyle że pojawia się nowa scena. Zaimplementowanie
Listingu 2 pokazany jest przykład zastosowania jednego takiego efektu jest bardzo proste. W pierwszym kroku
z przejść, tzw. zanikania (ang. fade). należy stworzyć obiekt reprezentujący efekt przejścia;
w naszym przypadku będzie on typu CCTransitionFade.
Listing 2. Przykład zastosowania efektu CCTransitionFade W konstruktorze możemy określić długość efektu (w se-
kundach), kolor oraz docelową scenę. Potem wystarczy
// Inicjalizacja obiektu reprezentującego efekt tylko przekazać obiekt przejścia (na Listingu reprezento-
// przejścia.
CCTransitionFade* transition = wany przez zmienną transition) do odpowiedniej me-
[CCTransitionFade tody w klasie CCDirector. W tym przypadku należy pa-

/ www.programistamag.pl / 11
BIBLIOTEKI I NARZĘDZIA

miętać, iż przejścia będą działać ze wszystkimi metodami ■■ -(void) ccTouchesMoved:(NSSet *)touches


reżysera przewidzianymi do manipulacji scenami z jed- withEvent:(UIEvent *)event – ta funkcja jest wy-
nym wyjątkiem, który stanowi funkcja popScene. Dlacze- woływana, kiedy palec przesuwa się po ekranie,
go? Powód jest prosty: popScene nie przyjmuje żadnych ■■ -(void) ccTouchesEnded:(NSSet *)touches
argumentów, po prostu zdejmuje ze stosu bieżącą scenę withEvent:(UIEvent *)event – ta funkcja jest wy-
i nie ma możliwości opakowania tej sceny obiektem re- woływana, kiedy palec odrywa się od ekranu,
prezentującym efekt przejścia. ■■ -(void) ccTouchesCancelled:(NSSet *)touches
Cocos2D oferuje cały szereg efektów przechodzenia withEvent:(UIEvent *)event – ta funkcja jest wy-
między scenami, informacje na ich temat można znaleźć woływana, kiedy proces przesuwania palca na ekranie
w dokumentacji biblioteki. zostaje przerwany (zdarzenie to występuje bardzo
Korzystając z efektów przejść, warto pamiętać o jed- rzadko, powinno się je obsługiwać podobnie jak zda-
nej zasadzie, która brzmi: lepsze jest wrogiem dobrego. rzenie ccTouchesEnded).
Przejścia między scenami wyglądają bardzo efektownie,
jednakże jeśli przesadzimy z ich stosowaniem, to użyt-
kownicy naszej gry dostaną białej gorączki (szczególną DUSZKI
uwagę należy zwrócić na to, by czasy przejść między sce-
nami nie były zbyt długie). Jak dotąd omawialiśmy niemalże same abstrakcyjne kon-
Często zdarza się sytuacja kiedy warto podzielić scenę cepcje: reżyser, graf sceny, warstwy, węzły... Wszystkie
na warstwy. Wtedy można skorzystać z klasy CCLayer. Na to jest oczywiście potrzebne, jednakże trudno byłoby
Listingu 3 pokazane jest, jak można dodać kilka warstw zrealizować ciekawą gry składającą się jedynie z obiek-
do sceny. tów nie posiadających reprezentacji graficznej. Jak więc
za pomocą Cocos2D wyświetlić zwykły obrazek? Odpo-
Listing 3. Dodawanie warstw do sceny wiedzią na to jest klasa CCSprite. Klasa ta reprezentuje
tzw. duszka (ang. sprite). Duszek w nomenklaturze na-
CCScene* scene = [CCScene node]; zewnictwa używanego przez adeptów dziedziny wiedzy
CCLayer* backgroundLayer = [Background node]; określanej jako grafika komputerowa oznacza obraz (tek-
[scene addChild: backgroundLayer]; sturę) renderowaną bezpośrednio na ekran, w określonej
pozycji. Termin ten pojawił się już w latach 70 zeszłego
CCLayer* spritesLayer = [Sprites node];
stulecia. Wiele systemów komputerowych z tamtych oraz
[scene addChild:spritesLayer];
późniejszych lat używało duszków (których rysowanie
CCLayer* hudLayer = [Hud node]; często bywało wspierane sprzętowo) do tworzenia dwu-
[scene addChild: hudLayer]; wymiarowych gier. Nowoczesne duszki (chociażby takie,
jakie oferuje Cocos2D) oprócz pozycji na ekranie pozwa-
Tak zainicjowana scena posiada trzy warstwy, które będą lają określać skalę (ang. scale) ich wielkości, kąt obrotu
(razem ze swoimi dziećmi) renderowane w kolejności ich (ang. rotation angle), punkt zaczepienia (ang. anchor po-
dodawania do sceny. Zaraz, zaraz! Ale czy podobnego int), poziom przeźroczystości (ang. opacity level), efekt
efektu nie można by uzyskać za pomocą zwykłych wę- obrotu wokół osi (ang. flipping effect) czy ton koloru
złów? Odpowiedź brzmi: owszem, można by. Co w takim (ang. color tint). Wszystkie te właściwości duszków sta-
razie odróżnia warstwy (CCLayer) od zwykłych węzłów nowią bazę dla uzyskiwania szeregów efektów, które na
(CCNode)? Zasadnicza różnica polega na tym, że warstwy co dzień oglądamy w nowoczesnych grach 2D.
potrafią przechwytywać zdarzenia generowane przez Stworzenie duszka za pomocą biblioteki Cocos2D jest
ekran dotykowy. Ponieważ takie przechwytywanie wiąże bardzo proste. Najłatwiej można to zrobić przez załado-
się ze sporym narzutem wydajnościowym, domyślnie me- wanie obrazka do tekstury (CCTexture2D), która z kolei
chanizm ten jest wyłączony. Aby go włączyć, wystarczy będzie przypisana do obiektu typu CCSprite. Z poziomu
odpowiednio ustawić właściwość isTouchEnabled do- kodu źródłowego wygląda to następująco:
stępną w klasie CCLayer:
CCSprite* heroSprite =
self.isTouchEnabled = YES; [CCSprite spriteWithFile:@”hero.png”];

Kiedy tylko do właściwości isTouchEnabled zostanie W tym momencie warto napisać kilka
przypisana wartość YES, Cocos2D zacznie wywoływać na słów o tym, w jaki sposób Cocos2D po-
obiekcie reprezentującym warstwę cały szereg funkcji zycjonuje duszki. Tutaj mała zagadka:
zwrotnych służących do obsługi zdarzeń generowanych jak według Ciebie będzie wyglądał ekran
przez ekran dotykowy: po narysowaniu duszka załadowanego
za pomocą przedstawionego wyżej frag-
■■ -(void) ccTouchesBegan:(NSSet *)touches
withEvent:(UIEvent *)event – ta funkcja jest wy- Rysunek 6. Przykładowa tekstura używana do wyświetlenia
woływana, kiedy palec dotknie ekranu dotykowego, duszka (źródło: http://www.lostgarden.com)

12 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

1.0 oznacza odpowiednio szerokość bądź


wysokość tekstury. Punkt zaczepienia w bi-
bliotece Cocos2D ustawiony jest domyślnie
na wartość (0.5, 0.5), czyli środek tekstury.
Gdybyśmy ustawili go na wartość (0.0, 0.0),
to wskazywałby on na lewy-górny róg tek-
stury itd. Wracając do problemu przycięcia,
cały haczyk polega na tym, że pozycja dusz-
ka na ekranie zawsze odnosi się do tego pun-
tu zaczepiania. Innymi słowy, gdy ustawimy
ekranową pozycję duszka na punkt (0, 0),
zaś jego punkt zaczepienia określony będzie
jako (0.5, 0.5), to zostanie on narysowany
w taki sposób, iż punkt centralny tekstury re-
prezentującej duszka pojawi się na układzie
współrzędnych ekranu w punkcie (0, 0), czyli
– jak wyżej pisałem, w lewym-dolnym rogu.
Jeśli chcielibyśmy zmodyfikować pozycję na-
szego duszka na ekranie w taki sposób, aby
był w całości wyświetlony w lewym-dolnym
rogu, można ustawić jego punkt zaczepie-
nia na wartość (0.0, 0.0), bądź zmodyfiko-
wać jego pozycję, przesuwając go w prawo
i w górę o wartości odpowiadające połowie
szerokości i wysokości reprezentującej go
tekstury.
Z efektywnym korzystaniem z duszków
wiąże się szereg technicznych ograniczeń,
wynikających przede wszystkim z charak-
terystyki nowoczesnych układów wspoma-
gających sprzętowo renderowanie grafiki.
Cocos2D w miarę swoich możliwości ukrywa
przed programistą techniczne detale, jed-
nakże znajomość i świadomość istnienia ww.
ograniczeń jest kluczem do tworzenia efek-
tywnych i płynnie działających gier pod iOS.
Niestety, od pewnych kwestii nie da się uciec.
Rysunek 7. Duszek wyświetlony na ekranie (pozycja domyślna) Pierwszy problem, którego należy być świadomym, to
wymiarowość tekstur. Układy graficzne, z których korzy-
mentu kodu (obrazek hero.png przedstawiony jest na Ry- sta system iOS, potrafią współpracować jedynie z tekstu-
sunku 6)? W ramach podpowiedzi dodam, że obiekty kla- rami o wielkościach będących potęgami dwójki z zakresu
sy CCSprite mają domyślnie ustawioną pozycję (0, 0). [2, 2048]. Ograniczenie to, połączone z niefrasobliwym
Jeśli chcesz poznać odpowiedź na to pytanie, spójrz podejściem do kwestii korzystania z tekstur może prowa-
proszę na Rysunek 7. Jak widać, duszek został wyświe- dzić do poważnych problemów związanych ze zużyciem
tlony tylko fragmentarycznie i na dodatek w lewym dol- pamięci. Dla przykładu, jeśli stworzymy za pomocą bi-
nym rogu ekranu. Czy aby wszystko jest w porządku? blioteki Cocos2D duszka reprezentowanego przez obraz
Jak najbardziej! Kwestia wyświetlenia duszka w lewym o rozmiarach 257 na 257 pikseli, to w celu jego załado-
dolnym rogu wiąże się z tym, jak Cocos2D postrzega wania silnik wygeneruje teksturę o rozmiarach 512 na
układ współrzędnych. Według konwencji przyjętej w tej 512 pikseli. Taka tekstura, przy założeniu, że korzystamy
bibliotece, punkt (0, 0) tego układu znajduje się właśnie z 32-bitowego formatu piksela, może zająć około 1MB
w lewym dolnym rogu ekranu. Problem przycięcia dusz- w pamięci karty graficznej. Dla nieświadomego tego pro-
ka jest troszkę bardziej skomplikowany. Wiąże się on blemu programisty może stanowić to niemały szok...
z tzw. punktem zaczepienia, o którym wspominałem kilka Inny ważny problem związany z korzystaniem z duszków
akapitów wyżej. Punkt zaczepienia jest punktem zdefi- wiąże się z przełączaniem tekstur. Rzecz polega na tym, że
niowanym w układzie współrzędnych tekstury. Układ ten nowoczesne układy graficzne zaprojektowane zostały w taki
jest zorganizowany w dość specyficzny sposób. Punkty sposób, iż nie potrafią współdziałać z dużą ilością tekstur
umieszczane w tym układzie mogą przyjmować warto- w tym samym czasie. Typowy schemat współpracy ukła-
ści ze zmiennoprzecinkowego zakresu [0.0, 1.0]. Wartość du graficznego z teksturami wygląda następująco: załaduj

/ www.programistamag.pl / 13
BIBLIOTEKI I NARZĘDZIA

Rysunek 8. Przykładowy atlas tekstur


(źródło: http://pocketgod.wikia.com)

teksturę; rysuj korzystając z danych zawartych w aktualnie Lekarstwem na opisane wyżej problemy jest stosowa-
załadowanej teksturze; załaduj kolejną teksturę; itd. Ha- nie tzw. atlasów tekstur (ang. texture atlases). Są to po
czyk w całej tej zabawie polega na tym, że operacja przeła- prostu obrazki o wymiarach kompatybilnych z wymia-
dowania tekstury jest bardzo czasochłonna. Tak bardzo, że rami tekstur, do których upakowane są wszystkie ele-
niefrasobliwe jej używanie może literalnie zabić wydajność menty graficzne wykorzystywane w grze. Oprócz tego,
gry. Wyobraź sobie, że tworzysz dwuwymiarową grę typu w oddzielnym pliku umieszcza się informacje o położeniu
shoot'em up, czyli klasyczną strzelaninę, w której Twój bo- oryginalnych obrazków na atlasie, dzięki czemu można
hater pędząc w uzbrojonym pojeździe kosmicznym niszczy się do nich odwoływać w trakcie działania gry. Na Ry-
hordy Obcych próbujących podbić Ziemię. Gra oczywiście sunku 8 pokazany jest przykładowy atlas tekstur. Pro-
składa się z całego szeregu animacji opartych na duszkach. ces tworzenia takich atlasów jest zazwyczaj zautoma-
Pół biedy, jeśli wpadniesz na pomysł, aby klatki animacji tyzowany. Istnieją dedykowane narzędzia, korzystające
pojazdu gracza i poszczególnych przeciwników umieścić w zaawansowanych algorytmów optymalizacyjnych słu-
w oddzielnych plikach. Gorzej, jeśli każdą klatkę zechcesz żące po to aby składać dużą liczbę małych plików gra-
trzymać w osobnym obrazku. Jeśli tak zrobisz, gwarantuję ficznych w jeden duży. Część z tych narzędzi przysto-
Ci, że Twoja gra będzie (może) działać w granicach 2-3 sowana jest do współpracy z biblioteką Cocos2D. Jedną
FPS'ów, nawet na urządzeniach nowej generacji. z ciekawszych opcji w tym zakresie jest TexturePacker

14 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

(http://www.texturepacker.com/), który w podstawowej rować „w locie”, bez posiłkowania się oddzielną teksturą.
wersji dostępny jest za darmo. Z tej racji, obiektów klasy CCLabelTTF w praktyce używa
Cocos2D oczywiście oferuje wsparcie dla atla- się głównie do wyświetlania informacji użytecznych przy
sów tekstur, które obsługiwane są za pomocą klas debugowaniu aplikacji czy do szybkiego prototypownia.
CCSpriteFrameCache oraz CCSpriteFrame. Przedstawie- Cocos2D oczywiście wspiera czcionki bitmapowe. Do
nie procesu tworzenia atlasu i obsługiwania go z poziomu ich obsługi wykorzystuje się klasę CCLabelBMFont. Ko-
silnika wykracza poza ramy niniejszego artykułu. Ważne rzystanie z tej klasy jest równie proste jak korzystanie
jest, abyś był świadom istnienia tych klas, aczkolwiek jeśli z CCLabelTTF (patrz Listing 5), ale... Analizując ten Li-
planujesz pracować z Cocosem na poważnie, to prędzej czy sting, można zauważyć dwie istotne kwestie. Po pierwsze,
później będziesz musiał zapoznać się z tym zagadnieniem. aby wczytać pożądany font, musimy wskazać konkretny
W razie czego ciekawy samouczek opisujący ten temat mo- plik .fnt. Od razu pojawia się pytanie: skąd go wziąć? Po
żesz znaleźć tutaj: http://www.raywenderlich.com/1271/ drugie, przy wczytywaniu fonta nie podajemy jego wiel-
how-to-use-animations-and-sprite-sheets-in-cocos2d. kości, co wydaje się zrozumiałe: przecież wymiar fonta
bitmapowego zależy od tego, jak go sobie narysujemy. Te
RENDEROWANIE TEKSTU dwa spostrzeżenia wiążą się z podstawowymi problemami
dotyczącymi stosowania fontów bitmapowych. Pierwsza
Renderowanie napisów jest jednym z nieodzownych ele- kwestia w zasadzie nie jest problemem sensu stricto. To
mentów każdej niemalże gry. W tej sytuacji nie do pomy- raczej konsekwencja decyzji używania takiego a nie inne-
ślenia jest, aby Cocos2D nie oferował żadnych usprawnień go formatu fonta. Po prostu w celu stworzenia pliku .fnt
w tym zakresie. Jak się zapewne domyślasz, uspraw- (a także towarzyszącego mu pliku .png) musimy skorzy-
nienia takie istnieją i zamierzam je w tym podpunkcie stać z narzędzia. Opcji jest kilka:
zaprezentować.
Najprostszym sposobem wyświetlenia tekstu przy ■■ Hiero (http://slick.cokeandcode.com/demos/hiero.jnlp):
pomocy biblioteki Cocos2D jest użycie obiektu klasy aplikacja webowa napisana w Javie; jej główną zaletą
CCLabelTTF. Klasa ta, jak się łatwo domyśleć po jej na- jest fakt, iż jest ona darmowa; niestety, poza tym stwa-
zwie, służy do wyświetlania napisów w oparciu o czcionki rza sporo problemów związanych z błędami istniejącymi
TrueType. Podstawowe użycie tej klasy pokazane jest na w tym narzędziu,
Listingu 4. ■■ Glyph Designer (http://glyphdesigner.71squared.com/):
natywna aplikacja przeznaczona dla systemów z ro-
Listing 4. Proste użycie klasy CCLabelTTF dziny OS X; wysoce stabilna i oferująca szereg przy-
datnych funkcji, aczkolwiek płatna (aktualna cena:
CCLabelTTF* label = 29.99$); jeśli planujesz na poważnie zająć się progra-
[CCLabelTTF labelWithString:@"Hello, Cocos2D!"
fontName:@"Arial" mowaniem gier pod iOS z wykorzystaniem Cocos'a, to
fontSize:32]; bezwzględnie polecam zakup tego narzędzia,
■■ BMFont (www.angelcode.com/products/bmfont/): na-
Jak widać, rzecz jest bardzo prosta. Inna sprawa, że pro- tywna aplikacja przeznaczona pod systemy z rodziny...
sto nie zawsze oznacza wydajnie. Zanim zaczniesz uży- Windows. Darmowa i stabilna, jednakże – jeśli Twoim
wać klasy CCLabelTTF, musisz być świadom, że pod ma- podstawowym środowiskiem pracy jest OS X – to jej
ską działa ona w ten sposób, iż w locie generuje teksturę używanie może być nieco kłoptliwe.
i renderuje do niej żądany tekst, korzystając z odpowied-
niej definicji czcionki TrueType. Takie renderowanie do Przedstawienie mechanizmu generowania fonta bitmapo-
pośredniej tekstury jest operacją dość zasobożerną i jeśli wego przekracza ramy niniejszego artykułu; zaintereso-
planujesz na bieżąco modyfikować zawartość tekstu re- wanych odsyłam do dokumentacji wymienionych wyżej
prezentowanego przez CCLabelTTF to licz się z potencjal- narzędzi.
nym spadkiem wydajności Twojej aplikacji. Drugi problem związanym z używaniem fontów bitma-
Drugi problem związany z używaniem czcionek powych to stały rozmiar czcionki. Oczywiście węzły typu
TrueType jest tego rodzaju, że czasami trudno jest dobrać CCLabelBMFont można normalnie skalować (jak każdy
krój liter odpowiedni do Twojej gry. W praktyce często inny węzeł w grafie sceny), jednakże w takim przypadku
robi się tak, że grafik przygotowuje dedykowany zestaw traci się jakość (co akurat w przypadku fontów jest dość
znaków dla danej gry w postaci tzw. czcionki bitmapowej istotne). Rozwiązaniem jest przygotowanie fontów bitma-
(ang. bitmap font). Czcionka taka zazwyczaj dostarczana powych w różnych wielkościach.
jest w postaci dwóch plików: tekstury, na której umiesz-
czone są poszczególne znaki, oraz metadanych (np. pliku Listing 5. Proste użycie klasy CCLabelBMFont
w formacie XML, JSON czy plist), które definiują pozycje
i atrybuty poszczególnych znaków na teksturze. Prakty- CCLabelBMFont* label =
[CCLabelBMFont
ka pokazuje, że czcionki bitmapowe są o wiele częściej labelWithString:@"Hello, Cocos2D!"
używane w grach niż czcionki TrueType. Napis oparty na fntFile:@"bitmapfont.fnt"];
czcionce bitmapowej można też łatwiej i wydajniej rende-

/ www.programistamag.pl / 15
BIBLIOTEKI I NARZĘDZIA

SYSTEMY CZĄSTECZEK nic a nic nie obchodzi, czy działa na prostym węźle typu
CCSprite, czy na złożonej scenie. Dzięki kompozytowej
Nieodłącznym elementem nowoczesnych gier 2D są strukturze węzłów obracanie pojedynczego duszka czy
efekty oparte na systemach cząsteczek. Pisząc w dużym warstwy reprezentującej złożoną grupę obiektów odbywa
uproszczeniu, system taki złożony jest ze zbioru wielu się w ten sam sposób. Akcje są kluczowym elementem
cząstek emitowanych przez źródło. Każda cząstka ma biblioteki Cocos2D; można wręcz pokusić się o stwierdze-
określone parametry, między innymi: czas życia, rozmiar, nie, że obok koncepcji grafu sceny stanowią jeden z trzo-
kolor, położenie czy prędkość. Kluczowym elementem nów filozofii na której opiera się ten silnik. W niniejszym
systemu jest tzw. emiter, którego głównym zadaniem jest podpunkcie zapoznamy się z nimi bardziej szczegółowo.
tworzenie cząstek, sterowanie ich ruchem i modyfikacja Klasą bazową dla wszystkich akcji jest CCAction. Jest
ich parametrów. Po upływie czasu życia cząstki są usu- to klasa abstrakcyjna, z której dziedziczą cztery inne
wane. Za pomocą systemów cząsteczek można wizualizo- klasy:
wać szereg ciekawych efektów, np.: deszcz, śnieg, dym,
ogień, wybuchy, opary itp. ■■ CCFiniteTimeAction: klasa abstrakcyjna reprezen-
Cocos2D oferuje bazową klasę abstrakcyjną CCParti- tująca kategorię akcji wykonywalnych w skończonym
cleSystem oraz dwie klasy konkretne: CCParticleSys- czasie; z niej z kolei wywodzą się dwie kolejne kate-
temPoint oraz CCParticleSystemQuad. Klasy te repre- gorie akcji: CCActionInstant oraz CCActionInterval
zentują uniwersalny system cząsteczkowy zdefiniowany (więcej szczegółów na ich temat podam za chwilę),
przez szereg właściwości definiujące różnorakie aspekty ■■ CCFollow: akcja, która pozwala, aby określony węzeł
jego działania. W celu stworzenia własnego systemu czą- podążał za innym węzłem,
steczek musimy stworzyć klasę dziedziczącą CCParticle- ■■ CCRepeatForever: akcja, która powtarza inną akcję
SystemPoint bądź CCParticleSystemQuad, a następnie w nieskończoność,
odpowiednio skonfigurować odziedziczone właściwości ■■ CCSpeed: akcja, która zmienia częstotliwość wywoły-
(różnice pomiędzy tymi klasami mają charakter czysto im- wania metody uaktualniającej logikę innej akcji.
plementacyjny; pierwsza z nich jest zoptymalizowana pod
starsze urządzenia z rodziny iOS, druga – pod nowsze). Na Listingu 6 przedstawiony jest fragment kodu, który
Jak można się domyśleć, takie „ręczne” dostrajanie syste- tworzy akcję obracającą w nieskończoność węzeł, do któ-
mu z poziomu kodu źródłowego nie jest łatwym zadaniem. rego będzie przypisana.
Na szczęście istnieje druga, łatwiejsza droga, polegają-
ca na skorzystaniu z narzędzia Particle Designer (http:// Listing 6. Definicja akcji obracającej w nieskończoność wę-
particledesigner.71squared.com/). Narzędzie to pozwala zeł, do którego będzie przypisana
za pomocą szeregu kontrolek graficznego interfejsu użyt-
// Stwórz akcję, która wykonuje pełny obrót węzła
kownika dostroić system cząsteczek, przy czym w okienku // (360 stopni).
podglądu możemy w czasie rzeczywistym oglądać rezulta- CCRotateBy* rotateBy360 =
ty wprowadzanych przez nas zmian. Kiedy uzyskamy już [CCRotateBy actionWithDuration:2 angle:360];
zadowalający nas efekt, wystarczy wyeksportować konfi-
// Stwórz akcję, która powtarza w nieskończoność
gurację systemu w formacie Cocosa (plik .plist) i stworzyć // akcję rotateBy360.
obiekt CCParticleSystemQuad w następujący sposób: CCRepeatForever* rotateForever =
[CCRepeatForever actionWithAction:rotateBy360];
CCParticleSystem* system =
// Przypisz akcję do węzła.
[CCParticleSystemQuad
particleWithFile:@"ps.plist"]; [spriteNode runAction:rotateForever];

po czy możemy cieszyć się pięknym efektem cząsteczko- Większość akcji, z którymi mamy do czynienia, dzie-
wym w naszej grze. je się w określonym przedziale czasu. Wszystkie ta-
Particle Designer jest narzędziem płatnym (aktualna kie akcje reprezentowane są przez abstrakcyjną klasę
cena: 7.99$), jednakże warto się w niego zaopatrzyć, CCActionInterval. Najbardziej podstawową z tych akcji
szczególnie jeśli planujesz używać w swojej grze dużo jest niewątpliwie CCMoveTo. Akcja ta odpowiada za prze-
efektów opartych na cząsteczkach. sunięcie węzła do określonej pozycji w zadanym interwale
czasu. Na przykład tak skonfigurowana akcja:
AKCJE
CCMoveTo* moveTo =
[CCMoveTo actionWithDuration:1
Obiekty klas wywodzących się z CCAction, o których position:CGPointMake(100, 100)];
opowiadałem bardzo ogólnie na początku niniejszego ar-
tykułu, reprezentują akcje, które można wykonywać na
węzłach. Za ich pomocą można przesuwać, skalować, ob- spowoduje, że węzeł, do którego będzie ona przypisana
racać i wykonywać wiele innych ciekawych operacji na przesunie się do punktu (100, 100) w przeciągu jednej
elementach grafu sceny. Obiektu reprezentującego akcję sekundy. Zamienne dla akcji wywodzących się z klasy ba-

16 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

zowej CCActionInterval jest to, że po ich zakończeniu CCMoveTo* moveToP1 =


nie ma potrzeby odłączania ich od węzła. Kiedy taka akcja [CCMoveTo actionWithDuration:1
się zakończy, to sama usunie się z węzła i zwolni zaj- position:CGPointMake(50, 50)];
mowaną pamięć. Biblioteka Cocos2D oferuje cały zestaw
CCSequence* sequence =
użytecznych akcji interwałowych. Za ich pomocą można [CCSequence actions:moveToP2,
niemalże w dowolny sposób manipulować węzłami. Peł- moveToP3,
ną listę tych akcji można znaleźć w dokumentacji silni- moveToP4,
ka, np. tutaj: http://www.cocos2d-iphone.org/api-ref/ moveToP1,
nil];
latest-stable/interface_c_c_action_interval.html.
Domyślnie akcje przypisane do węzła działają w spo- [sprite runAction:sequence];
sób równoległy. To znaczy, gdybyś przypisał do węzła
dwie akcje z identycznym interwałem: skalowanie i obrót,
to obydwie operacje wykonałyby się równocześnie. Cza- Proste, nieprawdaż? Sekwencję taką możemy łatwo za-
sami jednak pożądane jest, aby pewne akcje wykonywały pętlić, korzystając z akcji CCRepeatForever:
się w trybie sekwencyjnym. W takiej sytuacji można sko-
rzystać z akcji CCSequence. Wyobraź sobie, że chciałbyś CCRepeatForever* repeatSequence =
[CCRepeatForever actionWithAction:sequence];
zrealizować ruch duszka po kwadracie (punkty (50, 50),
(100, 50), (100, 100), (50, 100)), przy założeniu, że każ-
dy bok kwadratu będzie pokonany w przeciągu sekundy. Równie łatwo możemy kontrolować szybkość całej se-
Zadanie to można zrealizować za pomocą akcji CCMoveTo kwencji. W tym celu wykorzystamy akcję CCSpeed:
oraz CCSequence (patrz: Listing 7).
CCSpeed* speed =
[CCSpeed
Listing 7. Ruch węzła po kwadracie zrealizowany za pomocą actionWithAction: repeatSequence
akcji CCSequence speed:0.5f];

CCMoveTo* moveToP2 =
Mam nadzieję, że w tym momencie zaczynasz już zdawać
[CCMoveTo actionWithDuration:1
position:CGPointMake(100, 50)]; sobie sprawę, jak potężnym, użytecznym, a jednocześnie
łatwym w obsłudze mechanizmem są akcje. Ale to wcale
CCMoveTo* moveToP3 = nie koniec ich możliwości!
[CCMoveTo actionWithDuration:1
Inna, ciekawa kategoria akcji to akcje wywodzące się
position:CGPointMake(100, 100)];
z klasy bazowej CCActionEase. Są to tzw. akcje łagodzące
CCMoveTo* moveToP4 = (ang. easing actions). Mówiąc ogólnikowo, ich zadaniem
[CCMoveTo actionWithDuration:1 jest modyfikacja danego efektu w czasie. Jak to wygląda
position:CGPointMake(50, 100)];
w praktyce? Wyobraź sobie, że przesuwasz duszka za po-

Rysunek 9. Hierarchia klas wywodzących się z CCEaseAction


(źródło: http://www.cocos2d-iphone.org/ )

/ www.programistamag.pl / 17
BIBLIOTEKI I NARZĘDZIA

Rysunek 10. Hierarchia klas wywodzących się z CCGridAction


(źródło: http://www.cocos2d-iphone.org/ )

Rysunek 10. Hierarchia klas wywodzących się z CCGridAction


(źródło: http://www.cocos2d-iphone.org/ )

mocą akcji CCMoveTo. Zauważ, że ruch duszka jest rów- Listing 8. Modyfikacja charakterystyki ruchu węzła za pomo-
nomierny. Korzystając z akcji łagodzących, możesz wpły- cą akcji łagodzącej
wać na charakterystykę tego ruchu, np. możesz sprawić,
CCMoveTo* moveTo =
że na początku i na końcu ruchu duszek będzie poruszał [CCMoveTo actionWithDuration:5
się wolniej, tak aby sprawić wrażenie, że powoli nabiera position:CGPointMake(200, 200)];
prędkości ruszając z miejsca, a później wytraca ją, ha-
CCEaseInOut* ease =
mując. Korzystając z tego narzędzia, możesz sprawić, że
[CCEaseInOut
Twoje animacje będą bardziej dynamiczne i wygładzone. actionWithAction:move
Na Listingu 8 pokazane jest, w jaki sposób można użyć rate:4];
akcji łagodzącej w celu zmodyfikowania charakterystyki
[sprite runAction:ease];
ruchu węzła.

18 / 1 . 2012 . (1)  /


BIBLIOTEKA COCOS2D: WPROWADZENIE

Zastosowanie akcji typu CCEaseInOut sprawi, że węzeł Listing 9. Ruch węzła po kwadracie zrealizowany za pomocą
będzie na początku ruchu powoli przyśpieszał, zaś przy akcji CCSequence, z wywoływaniem funkcji zwrotnej
jego końcu – łagodnie zwalniał. Oczywiście Cocos2D po-
CCMoveTo* moveToP2 =
siada w swoim arsenale cały szereg klas reprezentujących [CCMoveTo actionWithDuration:1
inne funkcje łagodzące. Hierarchia tych klas przedstawio- position:CGPointMake(100, 50)];
na jest na Rysunku 9. Szczegółowe informacje na temat
CCMoveTo* moveToP3 =
ich działania znajdują się w dokumentacji biblioteki:
[CCMoveTo actionWithDuration:1
http://www.cocos2d-iphone.org/wiki/doku.php/ position:CGPointMake(100, 100)];
prog_guide:actions_ease.
CCMoveTo* moveToP4 =
Kolejną kategorią akcji oferowaną przez prezentowany
[CCMoveTo actionWithDuration:1
silnik są tzw. akcje bazujące na siatce (ang. grid actions). position:CGPointMake(50, 100)];
Głównym zadaniem tych akcji jest dostarczenie miłych
dla oka efektów wizualnych o quasi-trójwymiarowym CCMoveTo* moveToP1 =
[CCMoveTo actionWithDuration:1
charakterze. Wszystkie akcje tego typu dziedziczą po kla- position:CGPointMake(50, 50)];
sie bazowej CCGridAction. Hierarchia tych klas pokaza-
na jest na Rysunku 10. Efekty te mają charakter pseudo CCCallFunc* callback =
[CCCallFunc actionWithTarget:self
3D, w ramach przykładu można tu wymienić efekt prze-
selector:@selector(onSequenceFinished)];
wrócenia kratki (CCPageTurn3D), efekt fali (CCWaves) czy
efekt płynu (CCLiquid). To, na co warto zwrócić uwagę, CCSequence* sequence =
[CCSequence actions:moveToP2,
to fakt, że efekty oparte na siatce będą działać jedynie
moveToP3,
wtedy, gdy nasza aplikacja będzie miała dostęp do bufora moveToP4,
głębokości (ang. depth buffer). W celu tego zapewnienia moveToP1,
należy ręcznie zmodyfikować kod odpowiadający za ini- nil];

cjację widoku OpenGL i zamienić w nim wartość parame- [sprite runAction:sequence];


tru depthFormat z 0 na GL_DEPTH_COMPONENT16_OES (bu-
for 16-bitowy) lub GL_DEPTH_COMPONENT24_OES (bufor // ...
24-bitowy). Ceną za te efekty wizualne reprezentowane
-(void) onSequenceFinished
przez akcje z rodziny CCGridAction jest oczywiście wy- {
dajność (a co za tym idzie, również przenośność, bo gry CCLOG(@"Sequence finished!");
}
wykorzystujące intensywnie quasi-trójwymiarowe efekty
mogą nie działać poprawnie na starszych urządzeniach
z rodziny iOS). Oprócz klasy CCCallFunc Cocos2D oferuje kilka dodatko-
Ostatnią kategorią akcji, którą chciałbym zaprezento- wych wariantów akcji służącej do wywoływania funkcji:
wać, są tzw. akcje natychmiastowe (ang. instant actions). CCCallFuncN, CCCallFunc0 oraz CCCallFuncND. Klasy
Dziedziczą one po klasie bazowej CCActionInstant (na te mają to do siebie, iż za ich pomocą do funkcji zwrotnej
Rysunku 11 przedstawiona jest hierarchia tych klas). można przekazać kontekst w postaci różnego rodzaju pa-
Akcje te pozwalają wykonywać na węzłach działania na- rametrów. Szczegóły działania tych klas opisane są w do-
tychmiastowe, np. ustawić pozycję, ukryć bądź odwrócić kumentacji biblioteki.
według wybranej osi. Uważni Czytelnicy zadają sobie za-
pewne w tym momencie pytanie: po co nam tego typu CO NAS OMINĘŁO?
akcje? Przecież ten sam efekt można by uzyskać poprzez
zwyczajne zmodyfikowanie właściwości danego węzła. To Na akcjach zakończę opisywanie właściwości biblioteki.
prawda, jednakże akcje natychmiastowe bywają bardzo Nie myśl jednak, że temat został wyczerpany. Cocos2D
przydatne w sytuacji, kiedy używamy sekwencji. Łatwo oferuje dużo więcej możliwości, po prostu opisanie ich
wyobrazić sobie sytuację, gdy chcielibyśmy użyć tego wszystkich jest niemożliwe ze względu na ograniczoną
typu akacji, jako część łańcuchu zdarzeń reprezentowane- ilość miejsca przeznaczoną na niniejszy artykuł. Poniżej
go przez obiekty typu CCSequence. W tej sytuacji ręczne przedstawiam listę wybranych elementów funkcjonal-
ustawianie właściwości węzła na nic się nam nie przyda. ności oferowanej przez Cocos2D, których nie udało mi
Rozważając akcje natychmiastowe, warto wspomnieć się tu opisać, a na które zdecydowanie warto zwrócić
o akcji typu CCCallFunc. Akcja ta pozwala wywołać do- uwagę:
wolną funkcję, przekazaną do niej za pomocą selektora.
Bywa to bardzo przydatne przy pracy z sekwencjami, cho- ■■ odgrywanie muzyki i efektów dźwiękowych: podsumo-
ciażby po to, aby wywoływać funkcje zwrotne w kluczo- wując w dwóch słowach – jest i działa,
wych momentach ich działania. Na Listingu 9 przedsta- ■■ obsługa map kafelków (zarówno ortogonalnych, jak
wiony jest zmodyfikowany przykład z Listingu 7. Różnica i izometrycznych) oraz integracja z narzędziem Ti-
polega na tym, że za każdym razem, kiedy nasz węzeł led: bardzo interesujący, aczkolwiek obszerny temat;
„zatoczy” pełen kwadrat, to wywołana będzie określona sprawna obsługa map kafelków to w opinii wielu pro-
funkcja zwrotna. gramistów gier jedna z mocniejszych zalet Cocosa,

/ www.programistamag.pl / 19
BIBLIOTEKI I NARZĘDZIA

■■ integracja z sinikami symulacji fizycznych (Box2D rzenia pełnej gry; zawiera również spory zastrzyk wiedzy
i Chipmunk), na temat bibliotek fizycznych Box2D oraz Chipmunk,
■■ kompleksowe narzędzia wspomagające pracę z Co- ■■ Learn Cocos2D Game Development With iOS 5, Apress,
cos2D, np. CocosBuilder (http://cocosbuilder.com/), 2011; książka autorstwa Steffena Itterheima oraz An-
LevelHelper (http://www.levelhelper.org/) czy Sprite- dreasa Löwa; kolejne świetne wprowadzenie w temat
Helper (http://www.spritehelper.org/). Cocosa; dużo informacji na temat narzędzi wspoma-
gających (między innymi wymienionych w niniejszym
Informacje na temat wymienionych tu (a także wielu in- artykule),
nych tematów) związanych z biblioteką Cocos2D znaleźć ■■ Cocos2d for iPhone 1 Game Development Cookbook,
możesz między innymi w materiałach, które przedstawi- Packt Publishing, 2011; książka autorstwa Nathana
łem w przedostatnim podpunkcie tego tekstu. Burby zorganizowana jako zestaw praktycznych po-
rad i rozwiązań zadań związanych z programowaniem
CO DALEJ? gier w oparciu o silnik Cocos2D; mocną stroną tej po-
zycji jest fakt, iż zahacza ona również o zagadnienia
Nasza wspólna podróż po świecie Cocosa powoli się koń- zaawansowane,
czy. Mam nadzieję, że koniec niniejszego artykułu bę- ■■ http://cocoadevcentral.com/d/learn_objectivec/: bar-
dzie dopiero początkiem Twojej przygody z biblioteką dzo przystępny (a zarazem krótki) samouczek pre-
Cocos2D! Pamiętasz, jak we wstępnie pisałem, że jed- zentujący najważniejsze aspekty języka Objective-C;
ną z silnych stron tego rozwiązania jest dostęp do dużej jeśli nie miałeś wcześniej do czynienia z tym językiem,
ilości wysokiej jakości materiałów edukacyjnych? Jeśli a masz już doświadczenia z innymi językami obiekto-
po przestudiowaniu niniejszego tekstu czujesz niedosyt wymi, zacznij naukę od przeczytania tego dokumentu,
wiedzy (efekt jak najbardziej pożądany!), to na koniec ■■ Programming in Objective-C, 4th Edition, Addison-We-
chciałbym wskazać Ci kilka ciekawych miejsc w sieci oraz sley, 2011: książka autorstwa Stephena G. Kochana
książek, które pomogą Ci lepiej zapoznać się z Cocosem: traktująca o języku Objective-C; jeśli zajdzie potrzeba,
abyś poznał tajniki natywnego języka systemu iOS, ta
■■ http://www.cocos2d-iphone.org: strona domowa bi- książka będzie świetnym przewodnikiem.
blioteki; tutaj warto na bieżąco sprawdzać, co się dzieje
w świecie Cocosa; zajdziesz tu najważniejsze nowinki Miej świadomość, że powyższa lista przedstawia moje ulu-
na temat biblioteki oraz szereg innych ciekawych infor- bione materiały edukacyjne związane z Cocosem i Objec-
macji (szczególnie budujące są wiadomości o kolejnych tive-C. Nie poprzestawaj na niej. Cocos2D to biblioteka,
zbudowanych na bazie Cocosa tytułch, które odniosły która żyje. Nowe samouczki powstają z dnia na dzień;
spektakularny sukces na AppStore); tutaj znajdziesz nowe książki się piszą. Nie bój się zatem korzystać z wy-
również obszerną dokumetację techniczną biblioteki, szukiwarki; jest szansa, że jeśli nie dziś, to jutro znajdziesz
■■ http://www.raywenderlich.com/: blog techniczny, na interesujące Cię informacje. Zachęcam również do ak-
którym publikowane są na bieżąco samouczki adre- tywnego korzystania z forum dyskusyjnego biblioteki do-
sowane do programistów aplikacji na platformę iOS; stępnego pod adresem http://www.cocos2d-iphone.org/
duża część z tych samouczków poświęcona jest wła- forum/. Na pewno też możesz się spodziewać kolejnych
śnie Cocosowi; zarówno ilość, jak i jakość prezentowa- artykułów traktujących o Cocosie na łamach Programisty.
nych tam materiałów robi wrażenie,
■■ http://www.learn-cocos2d.com/: strona prowadzona PODSUMOWANIE
przez Steffena Itterheima, współautora książki Learn
Cocos2D; można tu znaleźć szereg interesujących ar- Czytając niniejszy artykuł, zaznajomiłeś się z podstawo-
tykułów na temat Cocosa; Steffen jest również auto- wymi koncepcjami, na których opiera się Cocos2D, po-
rem bardzo ciekawego silnika Kobold2D, stanowiącego znałeś również zakres jego możliwości, mam nadzieję,
rozszerzenie Cocosa, że co najmniej na tyle, aby odpowiedzieć sobie na py-
■■ Learning Cocos2D, Addison Wesley, 2011: książka au- tanie o przydatność tej biblioteki przy realizacji Twoich
torstwa Roda Strougo oraz Ray'a Wenderlicha; świetne własnych przedsięwzięć. Realizacji tych ostatnich życzę
wprowadzenie do Cocosa, pokazane na przykładzie stwo- Ci z całego serca!

Rafał Kocisz rafal.kocisz@gmail.com

Rafał od dziesięciu lat pracuje w branży związanej z produkcją oprogramowania. Jego za-
wodowe zainteresowania skupiają się przede wszystkim na nowoczesnych technologiach
mobilnych oraz na programowaniu gier. Rafał pracuje aktualnie jako Techniczny Koordy-
nator Projektu w firmie BLStream.

20 / 1 . 2012 . (1)  /


ZONE

NOWY WYMIAR E-EDUKACJI


TEMATY SZKOLEŃ DLA PROGRAMISTÓW!
STACJONARNYCH

Ł A C A J
TRENERZY SZKOLEŃ
STACJONARNYCH NIE PRZEP N I A
Z K O L E
ZA S Z N E!
A M IS T Y C
PR O G R

95% CENY SZKOLEŃ


STACJONARNYCH

OFERTA SZKOLEŃ

Język Objective-C 2.0 i tworzenie aplikacji dla iPhone'a


Refaktoryzacja kodu w języku PHP
Tworzenie aplikacji internetowych z użyciem platformy .NET
Tworzenie aplikacji internetowych z wykorzystaniem Ruby on Rails
Tworzenie aplikacji z użyciem biblioteki Hibernate
Tworzenie aplikacji z użyciem EJB3
Wprowadzenie do C# i platformy .NET
Wprowadzenie do technologii Flex
Zaawansowane wykorzystanie typów generycznych w języku Java

CO MIESIĄC DWA NOWE SZKOLENIA!

w w w. d e v c a s t z o n e . c o m
JĘZYKI PROGRAMOWANIA
Bartosz Szurgot, Mariusz Uchroński, Wojciech Waga

C++11 - część I
C++11 to nowy standard języka C++, zatwierdzony przez komitet ISO 12 sierpnia
2011 roku. Ponieważ zawiera on dużo zmian i nowości, zarówno po stronie samego
języka, jak i biblioteki standardowej, warto się z nimi zapoznać już teraz.

J
ęzyk C++ od lat zajmuje czołowe pozycje na listach Dodatkowo fakt jego dostępności, zarówno dla platformy
popularności języków programowania. Trudno się Linux, jak i Windows oraz możliwość darmowego stoso-
temu dziwić, gdyż łączy on możliwości tworzenia roz- wania, czyni z niego niemal idealnego kandydata do two-
budowanych abstrakcji, charakterystycznych dla języków rzenia przykładów.
wysokiego poziomu, z wydajnością języków niskopozio- Po ostatniej konferencji C++ and Beyond, podczas wy-
mowych. Ponad pół roku temu w końcu miało miejsce wiadu Herb Sutter został zapytany, kiedy, jego zdaniem,
wydarzenie, na które programiści C++ czekali od wielu większość kompilatorów będzie wspierać nowy standard.
lat - został ogłoszony nowy standard języka, nazywany Odpowiedział, że prawdopodobnie za około rok. Dla mniej
potocznie C++11. Nowy standard niesie ze sobą dużo popularnych platform - około dwóch lat. By nie czekać tak
zmian. Wprowadzono wiele nowych mechanizmów, takich długo, zapraszamy do poznawania elementów C++11,
jak na przykład wątki czy wyrażenia lambda. Poszerza oferowanych przez GCC, już dziś. Zatem do dzieła...
także zakres stosowalności i ogólności znanych już me-
chanizmów, takich jak na przykład szablony czy tworze- GENERACJA LICZB
nie skróconych nazw dla typów. PSEUDOLOSOWYCH
Duża część mechanizmów C++11 została przedstawio-
na w cyklu artykułów C++0x (w chwili pisania części z nich Na początku należy uświadomić czytelnikowi, że progra-
standard nie był jeszcze zatwierdzony), opublikowanych mowe generatory liczb losowych tak naprawdę nie gene-
na łamach czasopisma Software Developer`s Journal, w rują prawdziwych liczb losowych, dlatego nazywane są
drugiej połowie 2011 r. oraz na początku 2012 r. Niniejszy generatorami liczb pseudolosowych (ang. PRNG - Pseu-
cykl jest kontynuacją prezentacji Nowej Twarzy C++. Au- do Random Number Generator). Generatory programo-
torzy zalecają zapoznanie się z materiałami dotyczącymi we oparte są na formułach matematycznych, które dają
C++11, w szczególności referencjami do r-wartości (ang. wrażenie losowości. Oprócz generatorów programowych
rvalue reference), odwołania do których pojawiają się istnieją także sprzętowe generatory liczb losowych (ang.
w niniejszym tekście. Ułatwi to zrozumienie tekstu i od- TRNG - True Random Number Generator), których dzia-
krycie nowych możliwości zastosowań prezentowanych łanie jest oparte na zasadzie obrazowania właściwości
mechanizmów. i parametrów fizycznego procesu stochastycznego, naj-
Z założenia cykl ten ma prezentować praktyczne zasto- częściej szumu elektrycznego. Programowe generowanie
sowania, nie zaś suchą teorię. Dlatego, spośród licznych liczb pseudolosowych, o wysokich parametrach, jest więc
rozszerzeń, zarówno samego języka, jak i jego biblioteki dość skomplikowanym problemem.
standardowej, wybór autorów pada na elementy dostęp- Przed pojawieniem się standardu C++11 istniał tylko
ne i wspierane przez istniejące kompilatory. W chwili pi- jeden sposób na generację liczb pseudolosowych w C++.
sania tych słów zdecydowanie najlepsze wsparcie, wśród Mowa tutaj o użyciu funkcji srand() oraz rand(), z bi-
kompilatorów, z jakimi autorzy mieli styczność, ma GCC. blioteki C. W celu inicjalizacji generatora liczb pseudolo-
sowych należało jednarazowo wywołać funkcję srand().
Jako parametr do tej funkcji należy przekazać liczbę typu
Kompilowanie przykładów unsigned int tzw. seed. Należy zwrócić szczególną uwa-
gę na wartość seeda, ponieważ w przypadku, gdy wartość
Do kompilowania przykładów należy użyć kompilato­ ta będzie stałą, to przy każdym uruchomieniu programu
ra GCC w wersji 4.6, ustawiając standard języka na C++0x: ciąg wygenerowanych liczb pseudolosowych będzie taki
g++-4.6 -Wall -std=c++0x src.cpp sam. Z tego powodu jako wartość seed podawany jest
Większość prezentowanych fragmentów kodu NIE będzie bieżący czas systemowy. Po zainicjalizowaniu generatora
działać na starszych wersjach, które miały minimalne lub liczby pseudolosowe można generować przy użyciu funk-
żadne wsparcie dla C++0x. W przypadku nowszych wersji cji rand(). Na Listingu 1a zostało przedstawione użycie
GCC niektóre przykłady mogą wymagać drobnych modyfi­ funkcji srand() do inicjalizacji generatora liczb pseudo-
kacji. Pamiętajmy, że w chwili wydania GCC 4.6 C++0x nie losowych wartością bieżącego czasu systemowego oraz
był zatwierdzonym standardem. Oprócz tego GCC nie jest użycie funkcji rand() do generacji liczb pseudolosowych.
jeszcze w pełni zgodny z propozycją standardu języka. Wywołanie funkcji time(nullptr) pozwala na uzyskanie
bieżącego czasu systemowego.

22 / 1 . 2012 . (1)  /


C++11 CZĘŚĆ I

Listing 1a. Przykład inicjalizacji i użycia generatora liczb Szablon linear_congruential_engine wymaga mini-
losowych w starym stylu malnej ilości pamięci do przechowywania swojego stanu.
Stan generatora opartego o ten szablon przechowywany
int randBetween(const int &min, const int &max)
{ jest w postaci pojedynczej liczby całkowitej, zawierają-
return (rand() % (static_cast<int>(max - min + cej poprzednio wygenerowaną liczbę pseudolosową, lub
1)) + min); początkowy seed, jeżeli żadna liczba pseudolosowa nie
}
została jeszcze wygenerowana. Okres takiego generatora
const int min = 1;
const int max = 10; zależy od użytych parametrów algorytmu i może wynosić
srand(static_cast<unsigned int>(time(nullptr))); do 264, ale najczęściej jest mniejszy. Z tego powodu ge-
cout << rand() << endl; neratory oparte o szablon linear_congruential_engine
cout << randBetween(min, max) << end
nie generują liczb pseudolosowych o dużej losowości.
Funkcja rand() pozwala na generację liczb pseudoloso- Szablon mersenne_twister_engine pozwala na ge-
wych z zakresu od 0 do RAND_MAX. Standard określa war- nerację liczb pseudolosowych o największej losowości,
tość RAND_MAX na wartość co najmniej 32767. Niestety a okres generowanych liczb jest dużo większy niż dla sza-
często wartości najmłodszych bitów dla liczb generowa- blonu linear_congruential_engine. Rozmiar pamięci
nych przez funkcję rand() są mało losowe. Oznacza to, potrzebny do przechowywania stanu generatora opartego
że użycie funkcji randBetween(...) do generacji liczb o szablon mersenne_twister_engine zależy od wartości
z małego zakresu (np. [1,5]) skutkuje niezbyt wysoką użytych parametrów i jest dużo większy niż w przypad-
losowością wygenerowanych liczb. Warto zwrócić uwagę ku szablonu linear_congruential_engine. Na przykład
na fakt, że funkcje srand() oraz rand() nie pozwalają predefiniowany generator mt19937, oparty o szablon
na zmianę rozkładu prawdopodobieństwa generowanych mersenne_twister_engine, ma okres generowanych
liczb pseudolosowych. liczb pseudolosowych równy 219937-1, podczas gdy jego
Dlatego standard C++11 został wzbogacony o rozbu- stan przechowywany jest na 624 liczbach całkowitych,
dowaną bibliotekę do generacji liczb pseudolosowych, o czyli w około 2.5kB pamięci.
różnych rozkładach prawdopodobieństwa, z wykorzysta- Szablon subtract_with_carry_engine przechowu-
niem różnych algorytmów. Biblioteka ta została zdefinio- je stan generatora na 25 liczbach całkowitych (na oko-
wana w pliku nagłówkowym random. ło 100B pamięci). Jakość liczb pseudolosowych genero-
Standard C++11 definiuje następujące klasy i szablo- wanych przez generator, oparty na tym szablonie, jest
ny generatorów liczb pseudolosowych: jednak gorsza, niż dla generatora opartego o szablon
mersenne_twister_engine.
■■ random_device, Klasa random_device jest prosta w użyciu i nie wy-
■■ linear_congruential_engine, maga żadnych parametrów. Natomiast utworzenie ge-
■■ mersenne_twister_engine, neratora liczb pseudolosowych w oparciu o pozosta-
■■ subtract_with_carry_engine. łe szablony wymaga podania wielu parametrów, które
mogą być skomplikowane. Dobór parametrów ma
Klasa random_device nie jest programowym generato- wpływ na jakość generowanych liczb pseudolosowych.
rem liczb pseudolosowych, wymaga on specjalnego sprzę- Na przykład szablon mersenne_twister_engine wy-
tu pozwalającego wygenerować niedeterministyczne licz- maga zdefiniowania 14 parametrów. Podobnie szablony
by losowe. Zgodnie z definicją szablonu random_device, linear_congruential_engine oraz subtract_with_
jeżeli komputer nie jest wyposażony w sprzętowy ge- carry_engine wymagają podania dużej liczby parame-
nerator liczb losowych, to biblioteka wykorzysta jeden trów w celu zdefiniowania konkretnego generatora. Dla-
z generatorów programowych (wykorzystanie konkret- tego standard zawiera predefiniowane generatory liczb
nego generatora zależy od implementacji biblioteki). Ja- pseudolosowych. Są to:
kość liczb pseudolosowych wygenerowanych z użyciem
szablonu random_device związana jest z jego entropią, ■■ minstd_rand0,
której wartość można uzyskać poprzez wywołanie funkcji ■■ minstd_rand,
entropy(). Jeśli funkcja entropy() zwróci wartość 0, to ■■ mt19937,
oznacza to, że do generacji liczb pseudolosowych został ■■ mt19937_64,
wygenerowany generator programowy. Użycie szablonu ■■ ranlux24_base,
random_device został przedstawiony na Listingu 1b. ■■ ranlux48_base.

Listing 1b. Genaracja liczb pseudolosowych przy użyciu Aby wygenerować liczbę pseudolosową w C++11, należy
srand( )/rand( ) utworzyć instancję generatora liczb pseudolosowych oraz
dla generatorów programowych określić rozkład prawdo-
random_device rnd;
cout << "Entropia: " << rnd.entropy() << endl; podobieństwa. Najprostszą metodą utworzenia generatora
cout << "Wartość minimalna: " << rnd.min() liczb pseudolosowych jest wykorzystanie jednego z pre-
<< " wartość maksymalna: " << rnd.max() << endl; definiowanych generatorów. W Listingu 1c został wyko-
cout << "Liczba pseudolosowa: " << rnd() << endl;
rzystany predefiniowany generator liczb pseudolosowych

/ www.programistamag.pl / 23
JĘZYKI PROGRAMOWANIA

mt19937, oparty o szablon mersenne_twister_engine. Lista zawiera najczęściej spotykane w statystyce rozkła-
Podobnie, jak w przypadku generacji liczb pseudolosowych dy. Programista otrzymuje więc zestaw gotowych narzę-
z wykorzystaniem funkcji srand() i rand(), należy za- dzi do pracy z liczbami losowymi!
inicjalizować generator. W przykładzie generator mt19937 Jako przykład niech posłuży kod z Listingu 1e, służący
został zainicjalizowany wartością bieżącego czasu syste- do generacji miliona liczb losowych o rozkładzie normal-
mowego. Następnie został zdefiniowany rozkład prawdopo- nym, wartości oczekiwanej równej 50 oraz wariancji rów-
dobieństwa, w tym przypadku jest to rozkład jednostajny o nej 6. Jako generator liczb pseudolosowych użyty został
zakresie od 1 do 99. W celu wygenerowania liczby pseudo- typ mt19937. Wektor mn przechowuje liczbę wystąpień
losowej należy wywołać obiekt funkcyjny, zdefiniowanego liczb z zakresu od 1 do 100. Następnie na ekran wypisy-
wcześniej rozkładu prawdopodobieństwa, jako argument wana jest zawartość owego wektora.
podając instancję generatora liczb pseudolosowych.
Listing 1e. Przykład generowania liczb pseudolosowych o
Listing 1c. Przykład generowania liczby pseudolosowej, o rozkładzie normalnym
rozkładzie jednostajnym
#include <random>
mt19937 eng(static_cast<unsigned #include <functional>
long>(time(nullptr))); #include <iostream>
uniform_int_distribution<int> dist(1, 99); using namespace std;
cout << dist(eng) << endl; int main(void)
{
const int max = 100;
Istnieje możliwość wyeliminowania konieczności określa- // liczba losowań
const int it = 1000000;
nia generatora liczb pseudolosowych i rozkładu prawdopo-
// określenie rozkładu prawdopodobieństwa
dobieństwa w momencie generacji liczby pseudolosowej. normal_distribution<double> normal(50.0, 6.0);
Efekt taki można uzyskać poprzez użycie szablonu bind(), // utworzenie instancji generatora liczb
zdefiniowanego w pliku nagłówkowym functional. Przy- // pseudolosowych
mt19937 enginei(static_cast<unsigned
kład użycia funkcji bind(), w kontekście generacji liczb long>(time(nullptr)));
pseudolosowych, został przedstawiony na Listingu 1d. // 'zbindowanie' generatora z rozkładem
// prawdopodobieństwa
auto rndi = bind(normal, enginei);
Listing 1d. Użycie funkcji bind(...) w kontekście generowania
// wektor przechowujący informację o częstości
liczb pseudolosowych
// występowania losowanych liczb
vector<int> mn(max+1);
mt19937 eng(static_cast<unsigned // generacja liczb pseudolosowych
long>(time(nullptr))); for (unsigned int i = 0; i<it; ++i)
uniform_int_distribution<int> dist(1, 99); ++mn.at(rndi());
auto gen = bind(dist, eng); //
cout << gen << endl; for (int i = 1; i<mn.size(); ++i)
cout << i << " " << mn[i] << endl;
return 0;
W standarcie C++11 zostały zdefiniowane różne rozkłady }
prawdopodobieństwa:
Aby lepiej unaocznić rozkłady prawdopodobieństwa, war-
■■ uniform_int_distribution, to się przyjrzeć jego graficznej reprezentacji, dla różnych
■■ uniform_real_distribution, zestawów parametrów. Na Rysunku 1a przedstawiony zo-
■■ bernoulli_distribution, stał przykład rozkładu jednostajnego. Rysunek 1b przed-
■■ binomial_distribution, stawia rozkład normalny, znany także pod nazwą rozkła-
■■ geometric_distribution, du Gaussa. Wykładniczy rozkład prawdopodobieństwa
■■ negative_binomial_distribution, ilustruje Rysunek 1c.
■■ poisson_distribution, Wartym uwagi jest fakt, iż C++11, wymuszając jawne
■■ exponential_distribution, tworzenie generatorów liczb pseudolosowych, rozwiązu-
■■ gamma_distribution, je jednocześnie dwa problemy, spotykane w wielu ge-
■■ weibull_distribution, neratorach (także niektórych z biblioteki C). Pierwszym,
■■ extreme_value_distribution, dość oczywistym, jest rozdzielenie samego generatora
■■ normal_distribution, (rozumianego jako ,,źródło pseudolosowości'') od kodu
■■ lognormal_distribution, generującego pożądany rozkład generowanych liczb. Ta-
■■ chi_squared_distribution, kie podejście jest znaczącą poprawą architektury, zwięk-
■■ cauchy_distribution, szającym elastyczność i umożliwiającym proste tworze-
■■ fisher_f_distribution, nie własnych rozkładów, w miarę potrzeb. Drugą istotną
■■ student_t_distribution, korzyścią jest możliwość tworzenia osobnych (tj. nieza-
■■ discrete_distributio, leżnych) generatorów w różnych wątkach. Programiści
■■ piecewise_constant_distribution, tworzący aplikacje obliczeniowe, korzystające z PRNG
■■ piecewise_linear_distribution. i wątkowania, często spotykali się z problemem niejawnej

24 / 1 . 2012 . (1)  /


C++11 CZĘŚĆ I

Rysunek 1a. Jednostajny rozkład prawdopodobieństwa Rysunek 1b. Normalny rozkład prawdopodobieństwa

serializacji wykonania, na funkcjach zwracających liczby


pseudolosowe. Działo się tak, ponieważ niektóre z gene-
ratorów posiadały wewnętrzne blokady, zabezpieczające
przed wyścigiem podczas odczytu/zapisu stanu generato-
ra. Podczas częstego dostępu z wielu wątków, okazywało
się to być wąskim gardłem dla czasu wykonania, zabija-
jąc tym samym równoległość. W C++11 problem ten nie
istnieje - każdy wątek może posiadać własny generator,
nie blokujący pozostałych.
Kończąc wywód o generacji liczb pseudolosowych, nie
sposób pominąć kwestii doboru początkowej wartości
seed, dla generatorów. W powyższych przykładach, dla
uproszczenia, użyty był bieżący czas systemowy. Jest to
jak najbardziej poprawne, jednak trzeba mieć świado-
mość konsekwencji takiego postępowania. Weźmy przy-
kład z dziedziny bezpieczeństwa systemów IT. Załóżmy,
że mamy potrzebę wygenerowania jednorazowego, loso- Rysunek 1c. Wykładniczy rozkład prawdopodobieństwa
wego numeru sesji, przypisanego do konkretnego użyt-
kownika WWW, zaraz po zalogowaniu. Tworzymy więc się powtarzać, a ludzie są tylko ludźmi, podobne błędy
program, używamy najlepszego generatora liczb pseudo- nadal nie należą do rzadkości. Warto więc być świado-
losowych, inicjujemy go czasem systemowym, wybieramy mym, jak ogromny wpływ na jakość ciągu wyjściowego
rozkład jednostajny... Program jest gotowy, a my cieszy- ma wartość początkowa. Robert R. Coveyou (matematyk)
my się poziomem bezpieczeństwa... Ale czy na pewno? powiedział kiedyś: generowanie liczb losowych jest zbyt
Pamiętajmy, że ciągi pseudolosowe są w 100% determi- ważne, by pozostawić je przypadkowi. Dobrze o tym pa-
nistyczne. Jeśli więc ktoś będzie miał taki sam generator, miętać, kiedy i nam przyjdzie taka potrzeba.
zainicjowany taką samą wartością początkową, uzyska
identyczny ciąg wartości z generatora. Teraz wystarczy, ALTERNATYWNA SKŁADNIA FUNKCJI
że wiemy, około której godziny użytkownik zainicjował
sesję, by odgadnąć jego numer sesji! Jak? Jeśli wiemy, Standard C++11 definiuje nową, dodatkową składnię
że zalogowanie miało miejsce w przeciągu ostatniej go- funkcji. Przykładowa funkcja zapisana w tym formacie
dziny, mamy raptem 3600 możliwych kluczy sesyjnych znajduje się na Listingu 2a.
do sprawdzenia. Co więcej, jeśli 2 użytkowników zaloguje
się w tej samej sekundzie, dostaną identyczny numer! Listing 2a. Alternatywna składnia funkcji
Powyższy przykład nie jest bynajmniej akademicki.
Pod koniec lat '90 dokładnie taką podatność miał silnik auto sum(int x, int y) -> int
{
kryptograficzny, używany do generowania kluczy do szy-
return x+y;
frowania transmisji, w przeglądarce Mozilla. Nie pomógł
}
nawet fakt, że czas był pobierany z dokładnością milise-
kundową. Odnosząc się do naszego przypadku, zamiast
3600 prób potrzeba 3600000, co nie stanowi najmniej- Warto zwrócić uwagę, że słowo kluczowe auto nie ma
szego problemu dla komputerów. Ponieważ historia lubi tu nic wspólnego z automatycznym określaniem typu

/ www.programistamag.pl / 25
JĘZYKI PROGRAMOWANIA

zwracanego. Typ ten jest jawnie podany jako tak zwa- kretny już typ argumentów ustalony w czasie kompilacji.
ny trailing-return-type. Listing 2b pokazuje bardziej Rozwiązanie to jest bardzo elastyczne, gdyż pozwala na
użyteczne zastosowanie nowej składni, którego nie dało- stworzenie pomocniczego szablonu, który „wyekstrahuje“
by się zapisać inaczej. np. typ argumentu podanej funkcji. Wadą tego rozwią-
zania jest to, że jest mało czytelne, nie ma możliwości
Listing 2b. Alternatywna składnia funkcji przekazania ustalonego typu poza pomocniczą funkcję
szablonową, a do tego wywołanie wymusza ewaluację
template<class A, class B> wyrażenia, co nie zawsze jest pożądane. Rozwiązanie nr
auto sum(const A &a, const B &b) -> decltype(a+b)
{ 2 wydaje się być pozbawione tych problemów, jest jednak
return x+y; uzależnione od konwencji kodowania. I jeżeli autor kolek-
} cji nie zadeklaruje value_type jako typu elementu swo-
jej kolekcji, metoda ta nie zadziała. Rozwiązanie nr 3 jest
Tutaj wartość zwracana ustalana jest za pomocą opera- mniej elastyczne niż pierwsze, bo nie pozwoli już określić
tora decltype opisanego w kolejnym punkcie. Operator typu argumentu funkcji. Jest ono również nieprzenośne,
ten nie może stać przed nazwą funkcji, gdzie znajduje się ponieważ korzysta z rozszerzenia kompilatora. Wymaga
typ w klasycznej deklaracji, gdyż tam zmienne a i b nie także napisania wyrażenia dwa razy, co z kolei utrudnia
są jeszcze znane. utrzymywanie kodu.
Mimo swojej zwięzłości, przykład z Listingu 2b nie ob-
razuje wcale trywialnej sytuacji. Typy szablonowe A oraz Listing 3b. Operacje na kolekcji elementów dowolnego typu
B mogą być różne, co wyklucza podanie jednego z nich
jako wartości zwracanej. Dodatkowo może być zdefinio- template<class T>
void real_swap(T &x, T &y)
wany operator dodawania dla tychże dwóch typów, zwra-
{
cający wartość zupełnie innego typu niż A lub B. T temp=x;
x=y;
DECLTYPE y=temp;
}

Wraz z upowszechnieniem się programowania generycz- template <class T>


nego w latach dziewięćdziesiątych pojawił się problem void swap1(T& v)
określania typu podanych wyrażeń. Typ z kolei jest nie- {
real_swap(*v.begin(),*v.rbegin());
zbędny do deklarowania zmiennych oraz funkcji. Jest to
}
dość intuicyjne wymaganie, jeżeli popatrzymy z perspek-
tywy kompilatora, który chce obliczyć, ile pamięci potrze- template <class T>
ba na obiekt danego typu. Liczba typu int, double czy void swap2(T& v)
obiekt klasy reprezentującej produkt magazynowany wy- {
typename T::value_type temp=*v.begin();
magają różnych ilości pamięci. *v.begin()=*v.rbegin();
Przykładowy kod prezentujący powyższy problem jest *v.rbegin()=temp;
zlokalizowany w Listingu 3a. Funkcja swap ma za zadanie }
zamienić pierwszy element podanej kolekcji z ostatnim.
template <class T>
Nie jest to jak widać szczególnie wyszukany problem, ale
void swap3(T& v)
jego rozwiązanie jest już na pewno kłopotliwe, ponie- {
waż musimy zadeklarować typ zmiennej temp, który jest typeof(*v.begin()) temp=*v.begin();
oczywiście taki sam, jak typ elementu danej kolekcji, ale *v.begin()=*v.rbegin();
*v.rbegin()=temp;
tego nie znamy.
}

Listing 3a. Zamiana dwóch zmiennych dowolnego typu


C++11 przyniósł nowy mechanizm pozwalający na unik-
template <class T> nięcie tego typu problemów - operator decltype (w po-
void swap(const T& v)
{ wyższym przykładzie można również wykorzystać nowe
TYPE temp=*v.begin(); słowo kluczowe auto). Powodem, dla którego operator
*v.begin()=*v.rbegin(); został nazwany decltype, a nie typeof, było uniknięcie
*v.rbegin()=temp;
pewnych nieścisłości w przypadku referencji. Decltype
}
znaczy tyle co "declared type", czyli typ zadeklarowany.
Problem ten można jednak obejść, nie wykorzystując Listing 3c przedstawia dwie funkcje foo() i bar() zwra-
mechanizmów C++11 na kilka różnych sposobów, każdy cające odpowiednio int i referencję do int. Typ wyraże-
jednak jest daleki od ideału. Kilka rozwiązań Czytelnik nia foo() to oczywiście int, ale typ wyrażenia bar() to
znajdzie na Listingu 3b. Funkcja swap1 wykorzystuje po- też int, który przypisywany jest do zmiennej w dokład-
mocniczy szablon real_swap. Podczas konkretyzowania nie taki sam sposób. Decltype ma za zadanie rozróżnić
tego szablonu tworzona jest funkcja, która bierze kon- typ tych wyrażeń.

26 / 1 . 2012 . (1)  /


C++11 CZĘŚĆ I

Listing 3c. Wyrażenia różnych typów RANGE-BASED FOR


int foo(); Nie ma chyba programisty, który w swoim życiu nie na-
int &bar();
pisałby pętli iterującej po tablicy. Jeżeli zamiast tablicy
int x=foo(); użyjemy vectora z biblioteki standardowej, zwykłe wypi-
int y=bar(); sanie jego zawartości robi się mało czytelne; prezentuje
to Listing 4a.
Reguły określania decltype są następujące:
Listing 4a. Pętla po wektorze stringów, w stylu C++03
1. Jeżeli e jest zmienną lub nazwą pola klasy, decltype(e)
zwraca typ, z którym e zostało zadeklarowane. Jeżeli std::vector<std::string> v;
v.push_back("ala");
e jest referencją do typu, decltype(e) jest również v.push_back("ma");
referencją do typu. v.push_back("kota");
2. Jeżeli e jest złożonym wyrażeniem, decltype(e) zwróci
for(std::vector<std::string>::const_iterator it=v.
typ, który zwróciłoby wyliczenie tego wyrażenia. Dodat-
begin(); it!=v.end(); ++it)
kowo jeżeli wyrażenie zwróciłoby lvalue decltype(e) std::cout << *it << std::endl;
zwraca referencję do typu.
To, że potrzeba uproszczenia tej składni istniała, widać po
Listing 3d pokazuje kilka przykładów. tym, że w bibliotece standardowej wprowadzono funkcję
szablonową for_each mającą naśladować obecną w wie-
Listing 3d. Przykłady decltype lu językach pętlę foreach. Odpowiednik tej pętli został
wprowadzony w C++11 i jest pokazany na Listingu 4b.
const int&& foo();
int i;
double x; Listing 4b. Pętla po tablicy, w stylu C++11

decltype(foo()) x1; // const int&& int my_array[5] = {1, 2, 3, 4, 5};


decltype(i) x2; // int
decltype(x) x3; // double for (int &x: my_array)
decltype((x)) x4; // const double& x *= 2;

for (int x: my_array)


To, co może wydawać się mało intuicyjne, to różnica w ty-
std::cout << x << ' ';
pie zmiennych x3 i x4. Wynika ona bezpośrednio z reguł
decltype: x jest nazwą zmiennej, zatem zastosowanie Pętla taka pozwala iterować nie tylko po tablicy, ale rów-
ma pkt. 1, lecz (x) jest wyrażeniem złożonym zwracają- nież po liście inicjalizacyjnej (ang. initializer list) oraz
cym lvalue, zatem zastosowanie ma pkt. 2. po wszystkich kontenerach udostępniających funkcje
Czyżby to było jedyne miejsce w C++, gdzie podwójne begin() oraz end(), zwracające sekwencyjne iteratory
nawiasy działają inaczej niż pojedyncze? Nie. Dołożenie na początek i koniec kolekcji.
dodatkowych nawiasów przy wywołaniu funkcji wieloar- Dalsze uproszczenie zapisu jest możliwe dzięki wyko-
gumentowej robi jeszcze więcej zamieszania, bo zmienia rzystaniu nowego wcielenia słowa kluczowego auto. Zo-
znaczenie przecinka rozdzielającego argumenty od siebie. stało ono pokazane na Listingu 4c.
Śledząc kolejne propozycje definicji decltype'a, roz-
patrywane przez członków komitetu standaryzacyjne- Listing 4c. Pętla po wektorze stringów w stylu "C++11"
go, możemy zauważyć, że autorzy rozszerzenia też
mieli wątpliwości. Na przykład dokument N1607 nie std::vector<std::string> v;
v.push_back("ala");
określa jasno tego przypadku, z kolei N1705 podaje, v.push_back("ma");
że decltype((e))==decltype(e) co, jak wiemy, nie v.push_back("kota");
jest już prawdą.
for(auto &s: v)
By pokazać elastyczność decltype, Listing 3e przedsta-
std::cout << s << std::endl;
wia zmienioną wersję kodu, z Listingu 3a, wywołującą ope-
rator + dla dwóch argumentów, która wykorzystuje mecha-
nizm idealnego przekazywania (ang. perfect forwarding). Zapis pętli z Listingu 4c jest wyraźnie krótszy niż jego
odpowiednik z Listingu 4a. Pytanie tylko, czy znaczy do-
Listing 3e. Przekazywanie parametrów wywołania w połą- kładnie to samo? Okazuje się, że nie do końca, gdyż w
czeniu z decltype przypadku auto nie mamy możliwości jawnego określe-
nia, czy chcemy dostać iterator czy też const_itera-
template<typename F1, typename F2>
auto add(F1&& f1, F2&& f2) -> decltype tor. W tym wypadku zależy to od kontekstu. Jeżeli vector
(forward<F1>(f1) + forward<F2>(f2)) jest stały, to s będzie referencją const. Jeżeli chcemy
{
return forward<F1>(f1) + forward<F2>(f2); wymusić takie zachowanie, dla modyfikowalnego vecto-
} ra, musimy to zapisać jawnie. Można to zrobić na dwa

/ www.programistamag.pl / 27
JĘZYKI PROGRAMOWANIA

sposoby: stosując jawne podanie typu (Listing 4b) lub Ciekawym rozszerzeniem składni C++11 jest też do-
używając nowych metod kontenerów standardowych: danie pętli for, automatycznie iterującej po elementach
cbegin() oraz cend(). tablic i kontenerów. Taki zwięzły zapis nie tylko pozwoli
na skrócenie zapisu standardowych konstrukcji języka,
PODSUMOWANIE ale także fenomenalnie uprości tworzenie uogólnionego
kodu. Od tej pory szablony nie będą musiały już spraw-
W niniejszym artykule przedstawione zostały nowe elemen- dzać, czy dany element jest tablicą czy też kontenerem.
ty języka C++11. Bardzo dużą i ważną zmianą jest doda- Do iterowania wystarczy nazwa samego obiektu!
nie generatorów liczb pseudolosowych do biblioteki stan-
dardowej. Uczynienie tak ważnego elementu elastycznym CIĄG DALSZY
i standardowym z pewnością przyczyni się do powstawania
wyższej jakości kodu oraz, na co autorzy mają nadzieję, W następnym odcinku niniejszego cyklu opisany zosta-
mniejszej liczby luk bezpieczeństwa z nimi związanych. nie mechanizm cech typów. Jest to kolejny ukłon w stro-
Niemałym plusem, dla osób tworzących metaprogramy, nę tworzących metaprogramy, mający ułatwić spraw-
jest pojawienie się konstrukcji decltype, pozwalającej dzanie własności parametrów. Omówione zostaną także
na ,,sprawdzanie'' typu wyrażenia w czasie kompilacji. W rozszerzenia składni dotyczące typów wyliczeniowych
połączeniu z alternatywną składnią funkcji, programista oraz reprezentacji i mierzenia czasu. Na koniec pojawi
otrzymuje potężne narzędzie, umożliwiające pisanie bar- się także kilka drobnych, lecz bardzo przydatnych roz-
dziej generycznego kodu, który stanie się jeszcze prost- szerzeń: jawne operatory konwersji oraz nowe modyfi-
szy w utrzymaniu. katory metod.

bartek.szurgot@baszerr.eu
Bartosz Szurgot http://www.baszerr.eu

Absolwent Informatyki wydziału Informatyki i Zarządzania Politechniki Wrocławskiej.


Główne zainteresowania techniczne to: programowanie, Linux, urządzenia wbudowane oraz
elektronika. W wolnym czasie tworzy oprogramowanie open-source oraz układy elektroni­
czne. W C++ programuje od 10 lat.

Mariusz Uchroński mariusz.uchronski@gmail.com

Absolwent Elektroniki i Telekomunikacji wydziału Elektroniki Politechniki Wrocławskiej


oraz pracownik Wrocławskiego Centrum Sieciowo-Superkomputerowego. Główne nurty
zainteresowań technicznych to: programowanie oraz obliczenia HPC, a w szczególności pro-
gramowanie GPU w CUDA i OpenCL. W C++ programuje od 6 lat.

Wojciech Waga wojciech.waga@gmail.com

Absolwent Informatyki na wydziale Matematyki i Informatyki Uniwersytetu Wrocławskiego,


obecnie słuchacz studiów doktoranckich biologii molekularnej UWr oraz pracownik
Wrocławskiego Centrum Sieciowo-Superkomputerowego. Programuje w C++ od 12 lat.

28 / 1 . 2012 . (1)  /


JĘZYKI PROGRAMOWANIA
Łukasz Mazur

Wybrane elementy języka


Objective-C i ich wykorzystanie
Obecnie język Objective-C bardzo szybko zyskuje na popularności. Rośnie jego wy-
korzystanie za sprawą dużej popularności aplikacji na iPhone'a. Warto więc znać kil-
ka przydatnych mechanizmów, które usprawnią nasze rozwiązania implementacyjne
w wykonywanych projektach.

W
obecnym okresie język Objective-C bardzo szyb- różnic i niuansów w porównaniu do Javy czy C (więcej
ko zyskuje na popularności. Spowodowane jest szczegółowych informacji można znaleźć w serwisie:
to głównie ekspansją na rynku produktów firmy https://developer.apple.com).
Apple, jak iPhone, iPad czy iTouch. Po wykupieniu progra-
mu developerskiego Apple'a, możemy dystrybuować wła- BLOKI
sną aplikację i zarabiać na tym niemałe pieniądze. Jako
programista natywnych aplikacji warto znać kilka waż- Artykuł rozpoczniemy od bloków. Blok jest obiektem, któ-
nych i przydatnych elementów języka, które mogą bardzo ry zamyka w sobie jakąś jednostkę (część) kodu. Jest
ułatwić nam przyszłą pracę przy rozwijaniu naszych sys- tzw. segmentem kodu, który może być w dowolnym mo-
temów. Elementy te mogą także zaciekawić osoby dopie- mencie uruchomiony ( i wykonany w kontekście imple-
ro zaczynające przygodę z platformą iOS. W niniejszym mentowanej metody) lub przekazany jako argument do
artykule postaram się zaprezentować elementy języka, funkcji. Tak przekazywany argument można dalej trakto-
które są bardzo istotne podczas rozwijania oraz utrzymy- wać jak każdy inny obiekt. Jest to wygodne i przydatne
wania projektów na mobilne platformy. W szczególności rozwiązanie.
będą to: Bloki są cechą języka C, które zostały dodane do
iPhone i iPad SDK w wersji iOS 4. W języku Objective-C
■■ programowanie z użyciem bloków, „bloki” to inaczej domknięcia leksykalne (ang. closures)
■■ kategorie i ich wykorzystanie, lub funkcjonujące w innych językach wyrażenia lambda.
■■ protokoły oraz selektory w Objective-C, Każde takie wyrażenie posiada zasięg leksykalny zakre-
■■ zarządzanie pamięcią w Objective-C, su, w którym zostało stworzone (tak więc jeśli w bloku
■■ użyteczne makra w Objective-C. domknięcia użyjemy zmiennej lokalnej, będzie ona ist-
nieć do chwili destrukcji samego wyrażenia lambda).
Na początku warto krótko wspomnieć, czym jest język Zamiast używać funkcji zwrotnych wymagających odpo-
Objective-C. Jest to rozszerzenie języka C o możliwo- wiedniej struktury danych (reprezentujących wszystkie
ści obiektowe, wzorowane na Smalltalku. Język obrał informacje kontekstowe potrzebne do wykonania opera-
odmienną drogę od C++. Jest używany głównie w fra- cji), wystarczy mieć możliwość bezpośredniego dostępu
mework'u Cocoa w systemie Mac OS X oraz w iOS. Ob- do zmiennych lokalnych. Bloki umożliwiają taki dostęp,
jective-C jest głównym językiem programowania na więc stwarza nam to wielkie możliwości na wykorzystanie
iPhone'a. Jest on rozszerzeniem języka ANSI C o moż- bloków.
liwości programowania obiektowego. Zatem dostajemy Na Listingu 1 został przedstawiony przykład deklaracji
dziedziczenie, enkapsulacje czy polimorfizm. Elemen- i użycia bloku.
ty składniowe, o jakie rozszerzono w tym celu język C,
używają dwóch symboli: [] oraz @ (rozszerzeń składni Listing 1. Przykład deklaracji oraz użycia bloku kodu
jest dużo więcej, ale tylko te wchodzą w jakiekolwiek
interakcje ze składnią języka C). Nawiasy kwadratowe // plik implementacji (*.m)
int (^sum) (int, int) = ^(int arg1, int arg2){
są używane do wywoływania metod, natomiast @ do de- return arg1 + arg2;
finicji specyficznych dla języka Objective-C. Istnieją też };
specjalnie dla Objective-C wprowadzone typy, istnieją- int result = sum( 1, 2);
NSLog(@"suma wynosi: %d", result);
ce już według reguł języka C, z których najważniejszym
jest id. Typ ten jest uniwersalną "referencją do obiektu"
(dokładnie to wskaźnikiem, z punktu widzenia języka W przykładzie z Listingu 1 deklarujemy (i wykorzystuje-
C). Ponieważ nie jest to artykuł traktujący o zmianach my) blok przyjmujący dwa argumenty typu int i zwraca-
(i o samym języku), które wprowadza język, lecz o przy- jący wartość typu int. Znak ^ jest wymagany do zdefi-
datnych elementach, nie będę więc zgłębiał wszystkich niowania bloku. Przykład jest generalnie poglądowy i nie

30 / 1 . 2012 . (1)  /


WYBRANE ELEMENTY JĘZYKA OBJECTIVE-C I ICH WYKORZYSTANIE

ma większego sensu czy znaczenia z punktu widzenia in- Listing 4. Pobieranie bloków
żynierii programowania.
Prawdziwą siłę bloków można doświadczyć, używając // pobranie bloku bez argumentów
- (void)someMethod:(void(^)())block;
ich jako zwrotnych (callback'owych), kiedy inna operacja
została zakończona, przekazywanych do funkcji jako ar- // pobranie bloku z argumentami
gument. Mogą wiec być alternatywą dla delegacji w przy- - (void)someMethod:(void(^)(NSString *test, id
anyObject))block;
padku, gdy zaistnieje potrzeba, kiedy obiekt delegata ma
implementować tylko jedną metodę (Listing 2). // przypadki typu zwracanego
- (void)someMethod:(BOOL(^)())block;
- (void)someMethod:(NSRange *(^)())block;
Listing 2. Przykład wykorzystania bloku
// wywołanie bloku
// istnieje możliwość zdefiniowania typu bloku za - (void)someMethod:(void(^)(NSString *test))block {
pomocą słówka typedef block(@"Przykładowy tekst w bloku");
typedef int (^Multiply)(int, int); }

// w takiej sytuacji możemy zdefiniować metodę Listing 5. Używanie bloków zwróconych z metod
// przyjmującą blok jako argument, w ten sposób
- (void) multiplyMethod: (Multiply) mult;
// przypisanie do tablicy - bloku
- (void)registerCallback:(BOOL(^)())callback {
// implementacja mogłaby wyglądać następująco
[_listeners addObject:[[callback copy]
- (void) multiplyMethod: (Multiply) mult{
autorelease]];
int a, b;
}
a = 4;
b = 6;
- (void)runCallbacks {
NSLog(@"Wynik operacji %d * %d wynosi: %d", a,
int i = [_listeners count];
b, mult(a,b));
while (i--) {
}
BOOL (^block)() = [_listeners
objectAtIndex:i];
// następnie w kodzie możemy opisać blok i użyć
BOOL state = block();
naszej metody
// tutaj dalsza część kodu ...
Multiply myMult = ^(int arg1, int arg2){
}
return arg1*arg2;
}
};

[self multiplyMethod:myMult];
Wszystko, co możemy zrobić, wykorzystując bloki, moż-
na również zrobić w inny sposób. Stwierdzenie to jest
Definicja bloku następuje po słowie kluczowym typedef, prawdą, bloki jednak dostarczają bardzo użytecznej
po czym specyfikujemy strukturę bloku: możliwości na uproszczenie kodu i uczynienia rzeczy
bardziej przejrzystymi. Warto więc poeksperymentować
typedef int (^Multiply)(int, int); na kilku przykładach, a po pewnym czasie ciekawe roz-
wiązania z pewnością się pojawią. Na przykład, załóżmy,
Gdybyśmy nie zdefiniowali typu bloku za pomocą typedef, że mamy połączenie URL i chcemy poczekać na wynik.
deklaracja funkcji musiałaby wyglądać następująco (czyli Dwa popularne podejścia to: zapewnienie callback'u de-
zawierać całą specyfikację dla bloku): legata lub użyć bloku. Wykorzystamy do zaprezentowa-
nia tego fikcyjną klasę URLConnection jako przykład
- (void) multiplyMethod:(int (^)( int a, int b)) mult; (Listing 6).

Bloki są cechą języka C, które zostały dodane do iPhone Listing 6. Przykład wykorzystania delegata
i iPad SDK w wersji iOS 4. Pozwalają na stworzenie „blo-
ku” kodu, który można dalej przekazać, jak każdy obiekt URLConnection* someConnection=[[[URLConnection
alloc] initWithURL:someURL] autorelease];
(jest to wygodne i przydatne rozwiązanie).
someConnection.delegate=self;
[someConnection start];
Listing 3. Przekazywanie bloku // następnie w danej klasie
// obsługujemy metodę delegata
// bardzo prosty przypadek - (void)connection:(URLConnection)connection
[foo someMethod:^{ didFinishWithData:(NSData*)
// pożądany kod w tym miejscu {
}]; // wykonaj działania na danych
}
// blok pobierający argumenty
[foo someMethod:^(NSString *var1, BOOL var2) {
// var1 oraz var2 mogą być użyte Natomiast w przypadku korzystania z bloku, można osa-
// jak argumenty w funkcji
dzić kod, który jest wywoływany dokładnie tam, gdzie
}];
tworzymy połączenie (Listing 7).

/ www.programistamag.pl / 31
JĘZYKI PROGRAMOWANIA

Listing 7. Przykład wykorzystania bloku Listing 9. Przykład kategorii MathUtilities na klasie NSNum-
ber
URLConnection* someConnection = [[[URLConnection
alloc] initWithURL:someURL] autorelease]; // plik nagłówkowy (*.h)
someConnection.successBlock = ^(NSData*)data {
// wykonaj działania na danych #import <Foundation/Foundation.h>
}; @interface NSNumber (MathUtilities)
[someConnection start]; - (float) toRadians;
- (float) toDegrees;
@end
Dodatkowo załóżmy, że mamy wiele połączeń w swojej
// NSNumber+MathUtils.m
klasie i wszystkie potrzebujemy obsłużyć przy użyciu tego
#import "NSNumber+MathUtils.h"
samego delegata. Teraz więc musimy rozróżnić poszcze- @implementation NSNumber (MathUtilities)
gólne połączenia w naszej metodzie delegata. To może - (float) toRadians
się komplikować, im więcej połączeń jest dostępnych (Li- {
return [self floatValue] * (M_PI/180);
sting 8). }
- (float) toDegrees
Listing 8. Obsługa metody w podejściu z delegatem {
return [self floatValue] * (180/M_PI);
}
- (void)connection:(URLConnection)connection
@end
didFinishWithData:(NSData*)
{
// przykład użycia nowej metody w kodzie
if(connection == self.connection1)
...
{
NSNumber *myDegrees = [NSNumber
// wykonaj działania na danych z connection1
numberWithFloat:180];
}
NSLog(@"Radiany: %.2f", [myDegrees toRadians]);
if(connection == self.connection2)
...
{
// wykonaj działania na danych z connection2
}
if(connection == self.connection3) Kategorie mogą być również wykorzystywane do za-
{ stąpienia / nadpisania metod, które dziedziczą z klasy
// wykonaj działania na danych z connection3 bazowej (co stanowi alternatywę do tworzenia podkla-
}
...
sy). Zawsze można uzyskać dostęp do nadpisanej me-
} tody przy użyciu specyfikatora super. Przypuszczalnie
z perspektywy kompilatora (code generation) może
Wykorzystując podejście korzystające z bloku, można użyć prowadzić do mniejszego narzutu kodu w stosunku do
unikalnego bloku na dane połączenie URL, co jest bardziej opcji tworzenia podklasy (wyłącznie po to, aby zastąpić
przejrzyste w trakcie późniejszego utrzymania kodu. metodę).
Powyżej przedstawiłem krótki opis oraz przykładowe Metody w kategoriach są nierozróżnialne z metodami
wykorzystanie bloków, o których oczywiście można pisać klasy - w szczególności mają dostęp do zmiennych pry-
o wiele więcej, myślę jednak, że taka dawka informacji watnych. Kategorie mogą również przedefiniować istnie-
powinna zaciekawić Czytelnika do dalszego zgłębienia jące już metody (np. mogą poprawić błędną implementa-
tematu. Szczegółowych informacji można wyszukać na: cję lub rozszerzyć funkcjonalność). Należy zaznaczyć, iż
https://developer.apple.com/resources/ w dziale: Blocks w sytuacji, gdy mamy dwie kategorie, które przedefinio-
Programming Topics. wują tę samą metodę - specyfikacja języka nie określa
dokładnie, która z metod będzie użyta.
KATEGORIE Kategorie stanowią ciekawą możliwość rozproszenia
realizacji klasy na jednym lub kilka plików. Objective-C
Jednym z problemów, przed którym stanęli projektanci Programming Guide wymienia kilka zalet tego podejścia,
języka Objective-C, było zadbanie o ułatwienie kontroli w tym:
nad dużymi fragmentami kodu, gdyż był to powszechny
problem w przypadku programowania strukturalnego. ■■ możliwość grupowania metod, które wykonują podob-
Kategorie to fragmenty klasy odpowiedzialne za okre- ne zadania,
śloną funkcjonalność, którą realizuje klasa (przykładowo ■■ konfiguracji klasy dla różnych zastosowań, zachowując
klasa NSString może mieć część odpowiedzialną za tłu- jednak jeden zestaw kodu.
maczenie tekstu). Co więcej metody kategorii dodawane
są w czasie wykonania. W związku z tym metody mogą Tworzenie kategorii jest sposobem na rozdzielenie imple-
być dodane do istniejących klas bez rekompilacji i bez mentacji metod na kilka części. Pozwala to na:
dostępu do źródła.
Przykładem jest kategoria MathUtilities, która dodaje do ■■ oddzielną kompilację,
klasy NSNumber metody (toRadians, toDegrees) przydatne ■■ grupowanie metod, które dzięki temu są jasno
podczas zamiany stopni na radiany i odwrotnie (Listing 9). rozdzielone,

32 / 1 . 2012 . (1)  /


WYBRANE ELEMENTY JĘZYKA OBJECTIVE-C I ICH WYKORZYSTANIE

■■ klasy mogą być rozszerzane odmiennie w różnych apli- Listing 11. Przykład implementacji kategorii
kacjach bez konieczności duplikowania kodu,
■■ każda klasa może zostać rozszerzona - nawet klasy // plik implementacji (*.m)
@implementation NSString (reverse)
z framework'u Cocoa. -(NSString *) reverseString
{
Jako alternatywę dla tworzenia podklas, kategorie w Ob- NSMutableString *reversedStr;
int len = [self length];
jective-C zapewniają możliwość dodawania nowych me-
// automatycznie zwalniany obiekt NSString
tod w danej klasie. Interesujące jest to, iż wszelkie me- reversedStr = [NSMutableString
tody, które są dodawane w danej kategorii, stają się stringWithCapacity:len];
// mało efektywne
częścią bazowej klasy (którą kategoria rozszerza). Inny-
while (len > 0)
mi słowy, jeśli dodać metodę do klasy NSString, to każ- [reversedStr appendString:
da instancja lub podklasa utworzona z NSString będzie [NSString stringWithFormat:@"%C", [self
mieć dostęp do tej metody. Definiowanie kategorii jest characterAtIndex:--len]]];
return reversedStr;
identyczne do zdefiniowania interfejsu dla klasy. Niewiel- }
kim wyjątkiem jest tutaj to, iż nazwę kategorii potrzeba @end
wyspecyfikować wewnątrz nawiasów: (nazwa) tuż po
deklaracji interfejsu. Przykładowyf format został przed- Listing 12. Przykład użycia zaimplementowanej kategorii
stawiony na Listingu 10. (reverse)

#import <Foundation/Foundation.h>
Listing 10. Przykład definiowania kategorii #import "NSString+Reverse.h"
int main (int argc, const char * argv[])
// plik nagłówkowy (*.h) {
// ogólny schemat definiowania kategorii NSAutoreleasePool *pool = [[NSAutoreleasePool
@interface ClassToAddMethodsTo (category) alloc] init];
// ... lista metod NSString *str = [NSString
@end stringWithString:@"Tekst demonstracyjny."];
NSString *rev;
// definicja kategorii reverse dla klasy NSString NSLog(@"String: %@", str);
@interface NSString (reverse) rev = [str reverseString];
-(NSString*) reverseString; NSLog(@"Po odwróceniu mamy: %@",rev);
@end [pool drain];
return 0;
}
Dla przykładu, na Listingu 10 mamy zdefiniowaną kate-
gorię, która dodaje metodę do klasy NSString. Metoda
reverseString dodaje możliwość odwrócenia znaków PROTOKOŁY
w łańcuchu tekstowym dla wszystkich obiektów utworzo-
nych z klasy NSString. Protokół to lista metod, które klasa musi zadeklarować
Podobnie jak w przypadku zmiany w deklaracji kate- i zaimplementować. Od wersji Objective-C 2.0 mogą one
gorii (w sekcji @interface), sekcja @implementation posiadać metody opcjonalne. Protokoły realizują idee
jest w podobny sposób zmodyfikowana. Tuż po nazwie wielokrotnego dziedziczenia specyfikacji, ale nie imple-
klasy dodajemy nazwę kategorii zawartą w nawiasach. mentacji. W C++ ten wzorzec osiąga się poprzez wie-
Na Listingu 11 znajduje się implementacja interfejsu zde- lokrotne dziedziczenie klas bazowych (abstrakcyjnych),
finiowanego wcześniej na Listingu 10. Należy zauważyć, a w C# i w Javie poprzez interfejsy. Przykł. pewna klasa
iż w obu przypadkach została dodana nazwa kategorii GDButton może mieć delegata implementującego proto-
(reverse). kół z opcjonalną metodą showShining. Obiekt GDButton
Zalecana konwencja nazewnicza dla plików kategorii jest może sprawdzić, czy delegat implementuje showShining
następująca: poprzez refleksje, i jeśli tak, to wysyłać odpowiedni ko-
munikat do delegata, by skorzystać z tej funkcjonalności.
"ClassToAddMethodsTo+CatgoryName" Warto zwrócić uwagę na różnice pomiędzy interfejsami
z Javy a protokołami z Objective-C, polegające na tym,
Przykładowo, dla pliku interfejsu i pliku implementacji że w Objective-C klasa może implementować protokół,
kodu kategorii przedstawianego na listingach, zostały wy- nawet jeśli nie zostało to zadeklarowane (w szczególno-
korzystane następujące nazwy: ści, wykorzystując kategorie, możemy nawet poprawić
istniejącą już klasę tak, by zaczęła implementować ja-
NSString+Reverse.h kiś protokół). Można sprawdzić, czy obiekt jest zgodny
NSString+Reverse.m
z określonym protokołem, jak pokazano poniżej:

Warto na koniec zaznaczyć, iż nie można dodać zmien- -(BOOL) conformsToProtocol:(Protocol*)protocol

nych do klasy w danej kategorii. Również nazwy kategorii if ([myObject conformsToProtocol:@


muszą być unikalne w całej aplikacji.

/ www.programistamag.pl / 33
JĘZYKI PROGRAMOWANIA

protocol(MyProtocol)]) { Listing 15a. Przykład użycia klasy implementującej protokół


// jeżeli tak to wykonaj zadane czynności - plik nagłówkowy (*.h)
}
... // aby utrzymać przykład zwięzłym,
// klasa adoptująca protokół
Klasa może implementować wszystkie metody dla dane- // to klasą appDelegata
go protokołu bez deklarowania zgodności z nim. W takim #import <UIKit/UIKit.h>
#import "ClassWithProtocol.h"
przypadku conformsToProtocol: zwróci wartość NO. @interface TestAppDelegate : NSObject
Podsumowując, należy zapamiętać to, iż protokoły są <UIApplicationDelegate, ProcessDataDelegate>
odpowiednikami interfejsów w C# i Javie. Definiują one {
UIWindow *window;
zbiór metod, które muszą być zaimplementowane przez ClassWithProtocol *protocolTest;
klasę wykorzystującą dany protokół oraz pewna klasa }
może być zgodna z dowolną liczbą protokołów (Listing @property (nonatomic, retain) UIWindow *window;
@end
13).
Listing 15b. Przykład użycia klasy implementującej protokół
Listing 13. Przykład deklaracji protokołu - plik implementacji (*.m)

// deklaracja protokołu #import "TestAppDelegate.h"


#import "ClassWithProtocol.h"
@protocol MouseListener
-(BOOL) mousePressed;
@implementation TestAppDelegate
-(BOOL) mouseClicked;
@synthesize window;
@end - (void)processSuccessful:(BOOL)success;
{
// deklaracja protokołu NSLog(@"Proces zakończony");
@protocol KeyboardListener }
-(BOOL) keyPressed; - (void)
@end applicationDidFinishLaunching:(UIApplication*)
// deklaracja (interfejsu) klasy application
wykorzystującej 2 protokoły {
@interface InputReader: NSObject <MouseListener,
KeyboardListener> // utworzenie i inicjalizacja okna (window)
@end window = [[UIWindow alloc]
initWithFrame:[[UIScreen mainScreen] bounds]];
Listing 14. Przykład definicji i implementacji protokołu protocolTest = [[ClassWithProtocol alloc] init];
[protocolTest setDelegate:self];
[protocolTest startSomeProcess];
// plik nagłówkowy (*.h)
#import <Foundation/Foundation.h> [window makeKeyAndVisible];
// definicja protokołu }
@protocol ProcessDataDelegate <NSObject> - (void)dealloc
@required {
- (void) processSuccessful: (BOOL)success; [window release];
@end [super dealloc];
}
@interface ClassWithProtocol : NSObject @end
{
id <ProcessDataDelegate> delegate;
} SELEKTORY
@property (retain) id delegate;
-(void)startSomeProcess; Selektor może być zarówno nazwą metody (lub komuni-
@end
katu do obiektu), która jest zaimplementowana w kodzie
// plik implementacji (*.m) źródłowym klasy, z której chcemy wywołać metodę (lub do
// implementacja klasy wykorzystującej protokół której chcemy wysłać komunikat). Selektora można tak-
#import "ClassWithProtocol.h"
że użyć, aby wywołać metodę na danym obiekcie. Należy
@implementation ClassWithProtocol
@synthesize delegate; również pamiętać, iż wszystkie metody o tej samej nazwie
- (void)processComplete mają ten sam selektor. W tym prostym przykładzie będzie
{ zamieniony dowolny ciąg znaków na małe litery (metoda
[[self delegate] processSuccessful:YES];
} lowercaseString) przy użyciu selektora (Listing 16).
-(void)startSomeProcess
{ Listing 16. Przykład wykorzystania selektora
[NSTimer scheduledTimerWithTimeInterval:5.0
target:self
// plik nagłówkowy (*.h)
selector:@selector(processComplete) userInfo:nil
// selectorAppDelegate.h
repeats:YES];
#import <UIKit/UIKit.h>
}
@interface SelectorAppDelegate : NSObject
@end
<UIApplicationDelegate> {
UIWindow *window;

34 / 1 . 2012 . (1)  /


WYBRANE ELEMENTY JĘZYKA OBJECTIVE-C I ICH WYKORZYSTANIE

NSString *string; Jest znanym faktem, iż GC pobiera dodatkowe zaso-


} by systemu w trakcie swojego działania, a na mobilnych
@property (nonatomic, retain) IBOutlet UIWindow
*window; platformach (w porównaniu do maszyn desktopowych)
@property (nonatomic, retain) NSString *string; są one szczególnie ograniczone. Prawdopodobnie dlatego
@end decyzja o obsłudze zarządzania pamięcią poprzez manu-
// plik implementacji (*.m)
alne liczenie referencji i zwalnianie obiektów ma sens.
// SelectorAppDelegate.m Chociaż GC jest możliwy do zaimplementowania na
#import "SelectorAppDelegate.h" mobilnych platformach (Android ma go zaimplementowa-
@implementation SelectorAppDelegate
nego!), zużywa to baterię o wiele bardziej. Co gorsze, zu-
@synthesize window, string;
- (void) applicationDidFinishLaunching:(UIApplication życie pamięci jest także większe i aplikacje mogą czasami
*) application { tracić zdolność reakcji, podczas gdy GC wykonuje swoją
[window makeKeyAndVisible]; pracę. Nie zamierzam zagłębiać się w szczegóły niskich
NSString *str = @"WITAM, TO JEST PRZYKŁAD TEKSTU";
SEL sel = @selector(lowercaseString); poziomów zarządzania pamięcią, ponieważ można zna-
NSString *lower = (([str leźć wspaniałą dokumentację na stronie Apple'a do tego
respondsToSelector:sel]) ? @"YES" : @"NO"); tematu, ale zamiast tego chcę pokazać kilka najbardziej
NSLog(@"Odpowiada na selektor: %@", lower);
// sprawdzenie czy można wysłać komunikat
typowych przypadków oraz ogólne zasady zarządzania
if([str respondsToSelector:sel]) //selektor pamięcią na iOS.
(lower == @"YES") Najważniejszą zasadą jest to, iż trzeba zwolnić tylko te
NSLog(@"lowercaseString: %@" , [str
obiekty, które „twój kod posiada”, to znaczy te, które są
lowercaseString]);
} alokowane przy zastosowaniu alloc. Każda alokacja po-
(void)dealloc { winna mieć odpowiednie zwolnienie (release), inaczej pa-
[window release]; mięć będzie wyciekać (leak). Aby być bardziej pewnym,
[super dealloc];
} można obserwować właściwość retainCount w zwalnia-
@end nym obiekcie.

Jak można zauważyć w kodzie, specyfikator SEL jest Listing 17a. Przykład alokacji i zwolnienia obiektu
krótszą formą instrukcji @selector(). Selektor można
zapisać jako: NSString *text = [[NSString alloc] init];
// wykonaj działania na zmiennej text
[text release];
SEL sel = @selector(lowercaseString);

Jeśli nie tworzymy obiektu poprzez alokowanie pamięci


Metoda wywołana za pomocą komunikatu lowercase- za pomocą alloc, oznacza to, iż ten obiekt jest automa-
String zmieni dany ciąg znaków na małe litery. Po uru- tycznie zwalniany w następnym etapie (na niższym po-
chomieniu aplikacji, na wyjściu z konsoli będzie to wyglą- ziomie). Musi to być zapewnione w kodzie obiektu, który
dać następująco: pobieramy. Nie powinno się go zwalniać manualnie (bę-
dzie to zwolnienie podwójne i wyrzuci błąd), co zostało
Odpowiada na selektor: YES pokazane na Listingu 17b.
lowercaseString: witam, to jest przykład tekstu

Listing 17b. Przykład niepoprawnego zwolnienia pamięci


Sprawdzenie poprzez respondsToSelector jest bardzo
istotne, gdy nasza aplikacja ma pracować jednocześnie NSString *text = [[NSString
stringWithString:@"Sample text"];
na różnych wersjach systemów iOS 3.x, 4.x, 5.x. Poszcze- // wykonaj działania na zmiennej text
gólne wersje nie są kompatybilne, tzn. obiekty w wersji // obiekt będzie automatycznie zwolniony później
wyższej mogą zawierać metody, których nie było na wer- ...
// instrukcja spowoduje wyrzucenie błędu!
sji niższej. W takim przypadku wysłanie komunikatu bez
[text release];
sprawdzenia, czy istnieje dany selektor, spowoduje crash
aplikacji.
Potrzeba przechować (retain) obiekty, których nie posia-
ZARZĄDZANIE PAMIĘCIĄ damy poprzez alokację, inaczej mogą być one zwalniane
zewnętrznie (w kodzie, z którego pochodzą), gdy jeszcze
Jeżeli Czytelnik nie miał okazji bliżej przyjrzeć się języ- potrzebujemy wykonywać na nich pewne operacje. Naj-
kowi Objective-C (pod kątem programowania na iOS), łatwiej zrobić to przez specyfikacje retain w deklaracji
a jego doświadczenie pochodzi z innych środowisk pro- właściwości klasy (Listing 17c).
gramistycznych, takich jak Java czy .NET, to w tym mo-
mencie należy oswoić się ze złą wiadomością. Pomimo Listing 17c. Przykład właściwości klasy zachowującej przypi-
tego, iż GC (garbage collector) jest dostępny na MacOS sany obiekt
(od wersji 10.5), nie jest on obsługiwany i wspierany na
// plik nagłówkowy (*.h)
systemie iOS.

/ www.programistamag.pl / 35
JĘZYKI PROGRAMOWANIA

@property (retain) NSString *text; rie (crash) aplikacji, ze względu na dostęp do zdealo-
// plik implementacji (*.m) kowanych obiektów. Sytuacja taka będzie zauważalna
@synthesize text;
// przypisanie automatycznie zwalnianego obiektu podczas testowania aplikacji na urządzeniu (symulator
// do właściwości spowoduje jego zachowanie nie jest 100% kompatybilny z urządzeniem). Możemy
... również wspomóc się narzędziami instruments (jest to
self.text = [NSString
zestaw narzędzi dostarczanych wraz ze środowiskiem
stringWithString:@"Przykładowy napis"];
... Xcode) do wykrycia wycieków (leaks) lub zdealokowa-
nych obiektów.

Bardzo ważne jest, iż trzeba wykonać manualne zwolnie- MAKRA


nie pamięci dla wszystkich właściwości klasy, które prze-
trzymują wartości, oraz dla wszystkich zmiennych, w któ- Na koniec artykułu chciałem wspomnieć o mechanizmie
rych przechowywane są referencje do innych obiektów. instrukcji preprocesora do definiowania makr. Instrukcje
Operację tę wykonujemy w metodzie dealloc, przypisu- preprocesora są ważnym składnikiem języka Objective-C.
jąc najpierw obiektowi wartość nil, a następnie urucha- Przykładowe zastosowanie na Listingu 18 pokazuje, jak za
miamy metodę release (Listing 17d). Można do tej pro- ich pomocą można zdefiniować wspólne wymiary dla po-
cedury napisać stosowne makro, które zaoszczędzi nam szczególnych kontrolek w projekcie. Jest to pewnego ro-
sporo czasu. dzaju definiowanie stylu dla pewnego zestawu obiektów,
które powinny mieć spójny wygląd (analogia do CSS).
Listing 17d. Przykład prostej metody dealloc Bardzo istotnym elementem instrukcji preprocesora jest
możliwość definiowania za ich pomocą makr. Makra ta-
// plik implementacji (*.m) kie są pewnego rodzaju zestawem poleceń, które moż-
- (void)dealloc {
text = nil;
na wywołać w kodzie, używając nazwy makra. Następnie
[text release]; kompilator zamienia je na instrukcje podane w definicji,
[super dealloc]; włączając je do kodu. Są one bardzo przydatne w sytu-
}
acjach, gdy kod jest powtarzalny i (jak zazwyczaj) jest
go zbyt dużo. Wtedy taka zamiana na makro ma sens.
Pewnym przypadkiem użycia jest dodanie obiektu do Wyróżniamy makra podobne do postaci obiektu:
kolekcji. Kolekcja wtenczas przejmuje prawo własno-
ści nad tym obiektem, więc nie potrzeba martwić się #define <identyfikator> <instrukcje>
przykład: #define PI 3.14159
o przechowywanie go samemu. Kolekcja traci prawo
własności do tego obiektu, jeżeli zostanie zwolniona
z pamięci. Na Listingu17e zostało to przedstawione za oraz podobne do funkcji:
pomocą kodu.
#define <identyfikator>(<lista parametrów>)
<instrukcje>
Listing 17e. Przykład przypisania obiektu do kolekcji i jej przykład: #define RADTODEG(x) ((x)*57.29578)
zwolnienia

// plik nagłówkowy (*.h)


@property (retain) NSString *text;
Definicja makra może być usunięta poleceniem:
// plik implementacji (*.m)
@synthesize text; #undef <identyfikator>
// przypisanie automatycznie zwalnianego obiektu
// do właściwości spowoduje jego zachowanie
... Oto kilka przydatnych makr, które można wykorzystać
self.text = [NSString w projektach. Wielu z nich używam regularnie, a warto
stringWithString:@"Przykładowy napis"]; wiedzieć o makrach i ich wykorzystaniu, ponieważ jest to
...
bardzo przydatne.

Powyżej zostało scharakteryzowanych kilka ważniejszych Listing 18. Instrukcje preprocesora definiujące wspólne
elementów dotyczących zarządzania pamięcią. Zdaję so- wymiary
bie sprawę, iż zabiera to trochę czasu, aby przyzwyczaić
// przydatne marginesy
się do opisanego podejścia i wykorzystywać go. Nie jest #define MARGIN 10
to tak proste, jak automatyczne zarządzanie pamięcią #define BIG_MARGIN 20
w środowiskach, w których działa GC. W iOS 5.0 możemy ...
użyć automatycznego liczenia referencji (ARC), które jest
// deklaracje wspólnych miar
podobne do GC. Ma to również swoje wady, jak i zalety #define CONTROL_WIDTH 200
jak każdy niuans. #define CONTROL_HEIGHT 36
...
Trzeba być bardzo ostrożnym, ponieważ niepoprawna
#define BIG_CONTROL_WIDTH 300
obsługa pamięci powoduje jej wycieki (leaks) lub awa-

36 / 1 . 2012 . (1)  /


WYBRANE ELEMENTY JĘZYKA OBJECTIVE-C I ICH WYKORZYSTANIE

#define BIG_CONTROL_HEIGHT 44 //wykorzystanie makra w kodzie


... ...
UIColor* color = HEXCOLOR(0xff00ff00);
// deklaracje wspólnych ramek w projekcie, ...
// tutaj zamiast liczb można wykorzystać
// wcześniej zdefiniowane rozmiary
#define TITLE_FRAME CGRectMake(0,0,300,30) Makro szybkiej enumeracji po kolekcji. Następujące ma-
#define LABEL_FRAME CGRectMake(0,0,300,44) kro pomoże w szybkim wyliczeniu / przejściu nad kolek-
cjami (Listing 20). Wyszczególniono tam dwa makra, dla
Listing 19. Makro zamieniające wartość heksadecymalną na przykładu pierwsze z nich wykorzystać można do enume-
RGB racji obiektów z kolekcjami typu NSArray, kolejne zaś do
enumeracji kluczy w kolekcjach typu NSDictionary.
#define HEXCOLOR(c)[UIColor colorWithRed:((c>>24)&0
xFF)/255.0 \ Podczas pisania aplikacji na iPhone'a, prawdopodobnie
green:((c>>16)&0xFF)/255.0 \ wiele czasu poświęcamy na pracę z ramkami (frame) w kla-
blue:((c>>8)&0xFF)/255.0 \ sie UIView (oraz pochodnych). Zmieniamy je, przesuwamy
alpha:((c)&0xFF)/255.0]
po ekranie lub transformujemy. Często potrzeba zmienić

Listing 20. Makra definiujące enumeracje po kolekcji

#define WOEnumerate(collection, object) \


for (id enumerator = [collection objectEnumerator], \
selector = (id)@selector(nextObject), \
method = (id)[enumerator methodForSelector:(SEL)selector], \
object = enumerator ? ((IMP)method)(enumerator, (SEL)selector) : nil; \
object != nil; \
object = ((IMP)method)(enumerator, (SEL)selector))

#define WOKeyEnumerate(collection, object) \


for (id enumerator = [collection keyEnumerator], \
selector = (id)@selector(nextObject), \
method = (id)[enumerator methodForSelector:(SEL)selector], \
object = enumerator ? ((IMP)method)(enumerator, (SEL)selector) : nil; \
object != nil; \
object = ((IMP)method)(enumerator, (SEL)selector))
// użycie w kodzie
WOEnumerate(fileArray, fileName)
NSLog(@"Nazwa pliku: %@", filename);

Listing 21. Makra ułatwiające prace z ramkami

// pobranie parametrów ramki


#define width(a) a.frame.size.width
#define height(a) a.frame.size.height
#define top(a) a.frame.origin.y
#define left(a) a.frame.origin.x
// zmiana pozycji ramki
#define FrameReposition(a,x,y) a.frame=CGRectMake(x,y,width(a),height(a))
// zmiana rozmiaru ramki
#define FrameResize(a,w,h) a.frame=CGRectMake(left(a),top(a),w,h)

Listing 22. Makra do szybkiego tworzenia przydatnych kolekcji

#define $array(objs...) [NSArray arrayWithObjects: objs, nil]


#define $set(objs...) [NSSet setWithObjects: objs, nil]
#define $range(loc, len) NSMakeRange(loc, len)
#define $format(format, objs...) [NSString stringWithFormat: format, objs]

Listing 23. Makra wywołujące alert systemowy

// jak wiadomo wywołanie alertu nie jest jedną linią kodu


#define ALERT(X) {UIAlertView *alert = [[UIAlertView alloc] \
initWithTitle:@"Info" message:X delegate:self cancelButtonTitle:@"OK" \
otherButtonTitles: nil];[alert show];[alert release];}

// dzięki tym makrom potrzeba tylko jednej linii aby uruchomić alert
#define ALERT_TITLE(X,Y) {UIAlertView *alert = [[UIAlertView alloc] \
initWithTitle:X message:Y delegate:self cancelButtonTitle:@"OK" \
otherButtonTitles: nil];[alert show];[alert release];}

// wykorzystanie makra w kodzie


...
ALERT(@”Komunikat z informacją”);
...

/ www.programistamag.pl / 37
JĘZYKI PROGRAMOWANIA

Listing 24. Skrócone operacje dla klasy NSString

#define FORMAT_SI(s,i) [@"" stringByAppendingFormat:@" %@ %i ",s,i]


#define FORMAT_SF(s,f) [@"" stringByAppendingFormat:@" %@ %f ",s,f]
#define FORMAT_AMOUNT(amount)[@""stringByAppendingFormat:@"$%.2f", amount]
// itp ...
// zabezpieczenie przed wartością nil dla NSString
#define NOTNULL(value) ((value != nil) ? value : @"")
...

Listing 25. Wybrane sposoby definiowania kolorów

// dostęp do tak zdefiniowanych kolorów jest dużo prostszy


#define YELLOW_A(aplha) [[UIColor yellowColor] colorWithAlphaComponent:aplha]
#define RED_A(aplha) [[UIColor redColor] colorWithAlphaComponent:aplha]
...
#define ORANGE RGBCOLOR(255,81,20)
#define LIGHT_GRAY RGBCOLOR(247,247,247)
...
#define WHITE [UIColor whiteColor]
#define CLEAR [UIColor clearColor]
#define BLUE [UIColor blueColor]
...

rozmiar UIView, przesunąć go w inne położenie czy do- kacji w środowisku docelowym. Prawdopodobnie dlate-
pasować do pewnej zawartości. W takich przypadkach za go Apple udostępnił bardzo wierny symulator zachowa-
każdym razem wystarczy wykorzystanie metody setFra- nia iPhone'a. Korzystajmy więc ze wszystkich możliwości
me ustawiającej nową ramkę ze struktury CGRectMake. środowiska i języka, jakie dostarczył nam producent, by
Przedstawione makra na Listingu 20 pomogły mi zaoszczę- tworzyć jak najlepsze oprogramowanie.
dzić wiele cennego czasu. W artykule zostały zaprezentowane elementy języka,
Nie jest to regułą, lecz bardzo dobrym rozwiązaniem które są bardzo przydatne dla każdego developera pra-
jest utworzenie w swoim projekcie dodatkowego pliku za- cującego przy tworzeniu oprogramowania na platformę
wierającego tylko makra. Plik może przyjąć nazwę przy- iOS. W szczególności programowanie z wykorzystaniem
kładowo: NPCommonMacros.h (gdzie: NP=pierwsze litery bloków, implementowanie kategorii, protokoły, selektory,
od nazwy projektu). Możemy wtedy po dołączeniu go do zarządzanie pamięcią oraz użyteczne makra. Wiadomo,
wybranych plików źródłowych wykorzystywać wszystkie iż nie są to wysoce skomplikowane elementy języka, ale
makra, które tylko potrzebujemy w danej sytuacji. warto o nich pamiętać i ich używać w swoich projektach.
Objective-C doczekał się w 2007 roku nowej wersji 2.0
PODSUMOWANIE i ciągle zyskuje na popularności dzięki sukcesom firmy Ap-
ple. Warto więc interesować się tym językiem, a szczegól-
Pisanie oprogramowania na platformy mobilne to wie- nie pewnego jego swoistymi elementami, które mogą nam
le szczegółów i niuansów. To również obszerny temat. pomóc i ułatwić pracę przy rozwijaniu naszych systemów.
Szczegóły te są zauważalne dopiero jak używa się apli-

W SIECI
PP https://developer.apple.com/devcenter/ios/index.action – serwis Apple'a dla developerów iOS, główne trści: iOS Application
Programming Guide, iOS Development Guide – dokumentacje i przewodniki po architekturze iOS. iOS Human Interface Guide-
lines – przewodnik opisujący wytyczne oraz zasady, przydatne podczas projektowania graficznego interfejsu użytkownika dla
aplikacji iOS
PP https://developer.apple.com/technologies/tools/ – opis narzędzi developerskich i zastosowanych w nich rozwiązaniach
PP http://iosdevelopertips.com/ – przydatne porady i fragmenty kodu dla developerów iOS
PP http://stackoverflow.com/ – serwis posiadający ogromne zbiory odpowiedzi (dla większości języków) na problemy powstające
podczas pisania kodu, w szczególności dla Objective-C (http://stackoverflow.com/questions/tagged/objective-c)

Łukasz Mazur lukash.mazur@gmail.com

Autor jest obecnie programistą / architektem aplikacji biznesowych dedykowanych na plat-


formy mobilne. Pracuje dla irlandzkiej firmy Mobile Travel Technologies Ltd. Jest jednym ze
współtwórców / developerów aplikacji mobile easyJet na system iOS, aktualnie uznawanej za
jedną z najlepiej opracowanych mobilnych aplikacji w swoim segmencie.

38 / 1 . 2012 . (1)  /


JĘZYKI PROGRAMOWANIA
Marek Sawerwain

Erlang
- język inny niż C++ czy Java
Popularność języków C/C++ oraz Java jak na razie jest niezagrożona, jednakże warto
wiedzieć, że istnieją też inne równie dobre języki programowania. Jednym z nich jest
Erlang, naturalnie daleko mniej popularny niż np: Java. Choć jest to język od Javy
starszy, to jednak pod wieloma względami daleko przewyższa wiele starszych oraz
młodszych języków programowania. W tym krótkim artykule nie sposób opisać ca-
łości języka, jednak mamy nadzieję, że zaprezentowane przykłady zachęcą Czytelni-
ków do dalszych poszukiwań.

B
ez wątpienia Erlang to język inny niż C++ czy Java, aplikacjami Erlanga. Naturalnie Erlang to rozwiązanie
choćby dlatego, iż jego korzenie tego języka sięgają Open Source, ale istnieje możliwość pełnego komercyj-
języka Prolog (programowanie w logice) oraz tech- nego wykorzystania Erlanga, a także uzyskania komer-
nik programowania funkcjonalnego. Paradygmaty progra- cyjnego wsparcia podczas realizacji własnych projektów.
mowania w logice oraz funkcyjnego mocno odróżniają się Możliwe jest to także w Polsce, bowiem istnieje centrum
od programowania sekwencyjnego czy metodologii pro- Erlanga w Krakowie (http://www.erlang-solutions.com/
gramowania obiektowego tak szeroko stosowanych we etc/poland).
współczesnych rozwiązaniach. Jednakże sam Erlang ofe-
ruje także możliwość tworzenia aplikacji rozproszonych INSTALACJA PAKIETU ERLANG
i współbieżnych odpornych na uszkodzenia, co dziś, kiedy
większość systemów działa w środowisku równoległym, Choć język Erlang nie jest tak popularny jak C++ czy
znakomicie się sprawdza i pozwala na tworzenie efektyw- Java, to jednak wszystkie duże dystrybucje Linuxa oferu-
nych rozwiązań. ją gotowe pakiety, więc nie trzeba przeprowadzać własnej
Język Erlang powstał w latach osiemdziesiątych minio-
nego wieku, jego pierwsza wersja została wydana w 1986 Rysunek 1. Strona domowa języka Erlang
roku. Jednak większą popularność zdo-
bywa ostatnio, jako język do budowy
rozproszonych i wydajnych aplikacji.
Warto się zapytać o przykładowe aplika-
cje opracowane w Erlangu, jedną z nich
jest program Wings3D, czyli modeler 3D
cieszący się dużą popularnością. W cało-
ści został opracowany w Erlangu. Dobrym
przykładem jest także serwer Yaws, czy-
li serwer WWW także oparty o Erlanga.
Stosowany jest także na Facebooku.
Generalnie należy powiedzieć, że Er-
lang to język do tworzenia aplikacji cza-
su rzeczywistego, może być stosowany
w bankowości, telefonii, a także jako
język programowania ogólnego. Jedną
z ciekawszych możliwości jest możli-
wość aktywnej podmiany kodu w trakcie
działania aplikacji bez konieczności jej
wyłączania.
Biblioteka języka Erlang oferuje także
zestaw funkcji ułatwiających tworzenie
aplikacji o nazwie OTP. Pakiet ten ułatwia
tworzenie i zarządzanie rozproszonymi

40 / 1 . 2012 . (1)  /


ERLANG - JĘZYK INNY NIŻ C++ CZY JAVA

kompilacji. W przypadku dystrybucji opar-


tych o Debiana, czyli także Ubuntu, wy-
starczy z konsoli, będąc użytkownikiem
root, wydać polecenie:

apt-get install erlang

w przypadku Fedory polecenie jest


następujące:

yum install erlang

Naturalnie możemy korzystać z instalato-


rów graficznych, bez konieczności odwoły-
wania się do trybu konsoli.
Jeśli posiadamy MacOS, to korzystamy
ze zbioru repozytoriów np: MacPorts albo
Homebrew, instalacja, podobnie jak w Li-
nuxach, sprowadza się do polecenia:
Rysunek 2. Wings3D, edytor modeli 3D napisany za pomocą
brew install erlang języka Erlang

albo
ponownym uruchomieniu musimy skonfigurować środo-
port install erlang wisko startowe Erlanga, co zrobimy, wybierając z Win-
dow, Preferences, Erlang opcję Installed runtimes. Po
W przypadku systemu Windows należy instalator Erlan- tej czynności warto raz jeszcze zamknąć i ponownie uru-
ga ściągnąć ze strony domowej. Najnowsza wersja to chomić Eclipse i można zakładać nasz pierwszy projekt.
R15B01, więc będzie to plik o nazwie otp_win32_R15B01. Trzeba jeszcze dodać, iż Erlide pozwala na konfigurację
exe. Po jego ściągnięciu i instalacji otrzymujemy kom- procesu uruchamiania projektu, możemy więc wskazać
pletne środowisko do pracy z językiem Erlang. Obecny moduł główny i predykat startowy, co pozwoli na wygod-
jest także pakiet wxErlang, czyli biblioteka do obsługi ne testowanie aplikacji.
GUI. W przypadku instalacji Erlang, w systemach Linux
czy MacOS, warto upewnić się, czy pakiet wxErlang został PODSTAWY ERLANGA – TYPY
zainstalowany, np. dla MacPorts należy wydać polecenie DANYCH
o postaci:
Programy w języku Erlang mają zupełnie inną składnię
port install erlang +wxwidgets niż programy tworzone w typowych językach programo-
wania jak Java czy C++. Wynika to naturalnie z faktu, iż
aby zainstalować obsługę dla GUI. Może też się okazać, Erlang to język oparty o język Prolog, gdzie dodatkowo
że trzeba będzie samodzielnie przeprowadzić kompilację szeroko są wykorzystywane funkcyjne techniki progra-
Erlanga. Dokładne informacje jak to zrobić można odszu- mowania. Oznacza to, iż będzie trzeba zmienić sposób
kać w dokumentacji. myślenia o programach, jakie się tworzy w Erlangu, a to
Ponieważ praca z poziomu konsoli z pewnością nie jest nie jest wbrew pozorom zadanie łatwe, szczególnie jeśli
przyjazna dla użytkownika początkującego, to warto za- długo pisaliśmy programy w tradycyjnych językach struk-
tem postarać się o instalację środowiska Eclipse oraz od- turalnych i obiektowych.
powiedniej wtyczki (o nazwie Erlide), która ułatwi naukę Erlang jednak posiada rozbudowany aparat do tworze-
i tworzenie pierwszych programów w Erlangu. nia tzw. programów sekwencyjnych, a więc podobnych co
Jeśli dokonaliśmy instalacji Erlang'a np. w środowisku do idei do programów pisanych w Javie czy C++, co tylko
Windows (należy zwrócić uwagę, aby poszczególne na- ułatwia tworzenie własnych programów.
zwy katalogów nie zawierały spacji, z tego też powodu Do podstawowych pojęć występujących w Erlangu na-
lepiej nie instalować Erlanga do katalogu Program Files), leżą oczywiście liczby całkowite, liczby zmiennoprzecin-
to instalacja jest bardzo prosta, sprowadza się do urucho- kowe, przykładowo:
mienia środowiska Eclipse, następnie z menu Help wybie-
ramy opcję Install new software..., w oknie dialogowym 12, -234, 16#AB5E, 2#1001010, $A, 3.14, 3.34E-5
wpisujemy adres http://erlide.org/update, a następnie
wybieramy moduły Erlanga do instalacji, a po kilku chwi- Należy dodać, że np. 2#1001010 to zapis liczby w postaci
lach zgadzamy się na restart środowiska Eclipse. Po dwójkowej, ogólnie można bowiem pisać B#VAL, gdzie B

/ www.programistamag.pl / 41
JĘZYKI PROGRAMOWANIA

to baza, a VAL to wyrażenie liczbowe w odpowiedniej ba- Co znajdziemy w pudełku z Erlangiem?


zie. Zapis $A oznacza w tym konkretnym przypadku kod
Ascii litery A. Ważnym pojęciem są także tzw. atomy, czyli Po instalacji środowiska Erlang nie otrzymujemy tylko i wy­
ciągi znaków, np.: łącznie samego kompilatora oraz biblioteki standardowej.
Pierwszą grupą bibliotek, na jaką trzeba zwrócić uwagę
abcd, ciag_oddzielony_podkreśleniami, 'ciąg jest pakiet OTP, czyli framework do budowy rozproszonych
w apostrofach' aplikacji.
Dostępna jest także baza danych czasu rzeczywistego Mne­
Atomy mogą reprezentować różnego rodzaju przydat- sia, oraz dostęp do baz danych w standardzie ODBC. Obecny
ne stałe, np. oznaczające typy komunikatów bądź typy jest także pakiet Orber, czyli implementacja CORBY w Erlangu.
danych. Mamy także pakiet dialyser do statycznej analizy kodu. Warto
Liczby i napisy możemy organizować w tzw. krotki. Jest zajrzeć do katalogu lib po instalacji Erlanga, bowiem w tym
to rodzaj struktury, w której liczba pól w danym przypad- katalogu zainstalowane zostały dodatkowe biblioteki, w tym
ku jest ściśle określona, np.: także pakiet wxErlang używany przez nas do budowy GUI.

{osoba, 'Imię', 'Nazwisko', 12} W naszym pierwszym przykładzie mamy definicję


funkcji silnia w dwóch odsłonach. W pierwszej odsłonie
Pierwszy element wymienionej krotki może być wykorzy- mamy typową rekurencyjną definicję funkcji silnia, gdzie
stywany do sprawdzania, czy badana krotka istotnie re- w pierwszej kolejności określamy wartość funkcji silnia
prezentuje dane o osobie. dla zera (definicja kończy się średnikiem):
Kolejny element to listy, które w odróżnieniu od krotek
mogą zmieniać dynamicznie zawartość. Pewną wadą Er- silnia(0) -> 1;
langa jest fakt, iż ciąg znaków objęty cudzysłowami także
jest listą. Przykłady list są następujące: Natomiast druga linia naszej funkcji to krok rekurencyjny,
zgodny z matematyczną definicją funkcji silnia:
[12, 34, 34, xyz, lla]
[abc, {osoba, 'Imię','Nazwisko', 12}, 23]
silnia(N) -> N * silnia(N-1).
”napis który jest listą”

Jak widać w tym przypadku definicja kończy się kropką,


Lista to podstawowa struktura danych w Erlangu, więc co oznacza koniec procedury silnia. Poprzednio kończyli-
nieraz będziemy do niej wracali. Połączenie krotek i list śmy średnikiem, bowiem wymieniony został tylko jeden
tworzy tzw. typy złożone, co pozwala na tworzenie drzew, z przypadków. Jest to nieco inne podejście niż w Prologu,
szczególnych odmian list, a także typu zbiorowego o za- gdzie poszczególne przypadki definicji danej procedury
chowaniu zgodnym z matematycznym pojęciem zbioru. były kończone kropką.
Ostatnim elementem dotyczącym typów danych są Funkcję silnia możemy zdefiniować też w nieco inny
zmienne. Wzorem języka Prolog, nazwa zmiennej rozpo- sposób, wykorzystując strażnika wartości, w przypadku
czyna się od dużej litery. Natomiast, wartość zmiennej rekurencyjnym powinniśmy napisać, iż wartość N zawsze
można nadać tylko raz, gdyż raz przypisanej wartości nie jest większa niż zero:
można zmienić do końca obowiązującego zasięgu zmien-
nej. Jest to typowe rozwiązanie dla języka Prolog czy też silnia2(N) when N > 0 -> N * silnia2(N-1).
języków funkcyjnych i zawsze sprawia dużo kłopotów, je-
śli dotychczas programowaliśmy tylko i wyłącznie w ję- Uruchomienie naszej funkcji silnia wymaga od nas uru-
zykach obiektowych bądź strukturalnych. Podobnie jak chomienia interpretera Erlanga. Interpreter uruchamia-
w Prologu mamy zmienną anonimową oznaczoną sym- my w tym katalogu, w którym znajduje się plik źródłowy
bolem _ (podkreślenie). Stosowana jest wtedy, gdy dana przyklad1.erl. Następnie należy dokonać kompilacji w na-
wartość nie jest dla nas istotna, jednakże chcemy zazna- stępujący sposób (na końcu należy dopisać kropkę koń-
czyć jej obecność. czącą wyrażenie Erlanga):

CZAS NA PIERWSZY PROGRAM c(przyklad1).

Listing 1 zawiera nasz pierwszy program w Erlangu, choć Po kompilacji zobaczymy komunikat ok, i możemy spraw-
tak naprawdę nie jest to pełny program, a jedynie zbiór dzić działanie np. funkcji silnia:
predykatów (nazywanych także procedurami), czyli tzw.
moduł. Pierwsza linia to nazwa modułu; powinna być taka przyklad1:silnia(10).
sama jak nazwa pliku. Następnie słowem export określamy
listę symboli publicznych, widocznych dla innych użytkowni- Listing 1 zawiera także predykat pole do obliczenia pola
ków naszego modułu. Zapis silnia/1 oznacza, że funkcja sil- powierzchni kwadratu oraz koła. Obliczenie pola kwadratu
nia z jednym argumentem będzie udostępniona publicznie. jest realizowane w następujący sposób:

42 / 1 . 2012 . (1)  /


ERLANG - JĘZYK INNY NIŻ C++ CZY JAVA

Rysunek 3. Ilustracja rekurencyjnego obliczania długości listy i odwracania


kolejności elementów na liście

Długość listy Zmiana kolejności elementów na liście


len([1,2,3,4]) = len([1 | [2,3,4]) reverse([1|2,3], []) =>
= 1 + len([2 | [3,4]]) reverse([2,3], [1|[]])
= 1 + 1 + len([3 | [4]]) reverse([2|3], [1]) =>
= 1 + 1 + 1 + len([4 | []]) reverse([3], [2|[1])
= 1 + 1 + 1 + 1 + len([]) reverse([3|[]], [2,1]) =>
= 1 + 1 + 1 + 1 + 0 reverse([], [3|[2,1]])
= 1 + 1 + 1 + 1 reverse([], [3,2,1]) =>
= 1 + 1 + 2 [3,2,1]
= 1 + 3
= 4

przyklad1:pole({kwadrat,7.67}). Powtórzenie zmiennej na samym końcu oznacza, że war-


tość tej zmiennej stanie się wartością całego wyrażenia.
Warto w tym miejscu spróbować zdefiniować kilka dodat- Tak prosta operacja pozwala na implementację dowolnej
kowych przykładów dla innych figur, pomoże to poznać innej operacji na liście, np. obliczenie długości listy jest
składnię języka Erlang, która jest inna niż w tradycyjnych realizowane w następujący sposób:
językach typu C++ czy Java.
len([]) -> 0;
len([_|T]) -> 1 + len(T).
Listing 1. Pierwszy program, moduł w Erlangu

-module(przyklad1). Pierwsza linia to przypadek dotyczący długości listy pu-


-export([silnia/1, silnia2/1, pole/1]).
stej, czyli wielkość jest równa naturalnie zero, natomiast
silnia(0) -> 1; wartością dłuższej listy jest jedność plus długość listy bez
silnia(N) -> N * silnia(N-1). głowy. Za pomocą rekurencji możemy obliczyć długość
listy, poszczególne etapy pokazuje także Rysunek 3.
silnia2(0) -> 1;
silnia2(N) when N > 0 -> N*silnia2(N-1). Innym przykładem jest odwrócenie kolejności elemen-
tów na liście, co realizujemy w następujący sposób:
pole({kwadrat, Bok}) ->
Bok * Bok; reverse(L) -> reverse(L, []).
reverse([H | R], RL) -> reverse(R, [H | RL]);
pole({kolo, Promien}) ->
3.14159265 * Promien * Promien; reverse([], RL) -> RL.

pole(Other) ->
{invalid_object, Other}. Poszczególne etapy tego procesu można zaobserwować
ponownie na Rysunku 3. Istnieje też możliwość odczyta-
nia dwóch elementów z początku, np. pisząc:
GŁOWA I OGON, CZYLI LISTA
[A,B|C]=[1, 2, 3, 4, 5].

Listy stanowią najważniejszą strukturę danych w języku


Erlang. Z listą związane jest pojęcie głowy, czyli pierw- zmienna A zostanie powiązana z wartością 1, zmienna
szego elementu listy, oraz ogona, stanowiącego pozostałe B z wartością 2, natomiast zmienna C zgodnie z oczeki-
elementy. Jeśli lista ma tylko jeden element, to można waniami z resztą listy [3,4,5]. Przytoczone przypadki to
powiedzieć, iż ma tylko głowę, a ogon jest pusty. Ope- tzw. technika dopasowanie wzorca, jest ona szeroko sto-
racja rozdziału na głowę i ogon to podstawowa operacja, sowana w programach pisanych w Erlangu, jednakże ze
jaka jest wykonywana na liście. Operacja ta została ozna- względu na brak miejsca nie będziemy szerzej omawiać
czona pionową kreską, np. odczytanie głowy jest realizo- tego zagadnienia.
wane jako: O listach warto by jeszcze napisać bardzo dużo, ale
wymienimy jeszcze tylko dwie dodatkowe funkcje z bo-
[ G | _ ] = [1, 2, 3, 4, 5], G. gatych zasobów Erlanga: lists:keymember oraz lists:key-
delete. Zadaniem pierwszej funkcji jest sprawdzenie na
Znak podkreślenia oznacza, że wartość ogona nas nie in- liście krotek, obecności takiej krotki, która na wskazanej
teresuje, i podobnie w tym przypadku, ale tym razem to pozycji zawiera podany element. Natomiast keydelete
głowa jest pomijana: usuwa z listy krotki zawierające podany element na wska-
zanej pozycji. Te dwie funkcje będziemy wykorzystywać
[ _ | O ] = [1, 2, 3, 4, 5], O. do zarządzania listą użytkowników w jednym z następ-
nych przykładów.

/ www.programistamag.pl / 43
JĘZYKI PROGRAMOWANIA

Krótko i szybko, czyli sortowanie

Algorytm sortowania szybkiego zawsze jest podawany jako przykład zawartości opisu algorytmu w językach funkcyjnych.
W przypadku Erlanga jest podobnie, co potwierdza poniższy kod:

-module(przyklad2).
-export([qsort/1]).

qsort([]) -> [];

qsort([Pivot|Rest]) ->
{Smaller, Larger} = partition(Pivot, Rest,[],[]),
qsort(Smaller) ++ [Pivot] ++ qsort(Larger).

partition(_,[], Smaller, Larger) -> {Smaller, Larger};

partition(Pivot, [H|T], Smaller, Larger) ->


if H =< Pivot -> partition(Pivot, T, [H|Smaller], Larger);
H > Pivot -> partition(Pivot, T, Smaller, [H|Larger])
end.

Idea programu jest identyczna do opisu sortowania szybkiego, wybiera się pewien element (tzw. pivot), a następnie przerzuca
się elementy większe i mniejsze przed lub za tym wybranym elementem. W Erlangu wykorzystujemy naturalnie listy i operację
podziału na głowę i ogon. Przemieszczenie elementów jest realizowane przez procedurę partition, natomiast qsort dzieli, a na­
stępnie łączy posortowane fragmenty listy.

ODWROTNA NOTACJA POLSKA Jest ona odpowiedzialna za konwersję ciągu znaków do


wartości liczbowej, a wykonuje to predykat read, zdefinio-
Nasza pobieżna wiedza o listach wystarczy, aby zre- wany w dalszej części Listingu 2. Wykorzystujemy funkcję
alizować bardziej ciekawy przykład, a będzie to kalku- string:to_float odpowiedzialną za właściwą konwersję.
lator w tzw. odwrotnej notacji polskiej. W tym zapisie Jak widać, analizę tego programu można rozpocząć od
argumenty zawsze poprzedzają operatory, czyli zamiast środka, choć naturalnie analiza listy z argumentami roz-
2+5 napiszemy 2 5 + albo zamiast 1+2-5 napiszemy poczyna się od następującej linii:
np. 1 2 + 5 -. W obliczeniach wykorzystuje się stos,
bowiem możemy umieszczać na stosie wielkości liczbo- calc(L) when is_list(L) ->
[R] = lists:foldl(fun calc/2, [],
we, i w momencie otrzymania odpowiedniego operatora string:tokens(L, " ")), R.
odczytać odpowiednią ilość argumentów i wykonać ope-
rację, a następnie umieścić obliczoną wielkość na stosie.
Krótko mówiąc, opracowanie tego typu kalkulatora w Er- w której to predykat calc przyjmuje tylko jeden argument
langu to łatwe zadanie, bowiem jak widać Listing 2 jest i jest to lista z poleceniami do wykonania. Pierwsze za-
dość krótki. danie w tym predykacie to sprawdzenie funkcją is_list,
Mamy tylko jeden predykat o nazwie calc, którego ar- czy argument jest listą, jeśli tak jest, to odczytywane są
gumentem jest ciąg znaków do analizy. Jednakże zde- z listy poszczególne argumenty i realizowane obliczenia.
finiowano także inne przypadki calc odpowiedzialne za To zadanie jest dwuetapowe. W pierwszej kolejności wy-
realizację obliczeń, np. linia: korzystywana jest funkcja string:tokens, której zadaniem
jest zmiana ciągu znaków na listę składającą się z sym-
calc("+", [L1,L2|R]) -> [L2+L1|R]; boli, w naszym przypadku będzie to polegać na tym, iż
ciąg znaków np. 10 20 + zostanie zamieniony na listę
zgodnie z oczekiwaniami realizuje sumę dwóch pierw- [10, 20, ”+”]. W drugim etapie elementy z listy są prze-
szych elementów na liście, a wynik jest wstawiany po- kazywane za pomocą funkcji foldl do wskazanego predy-
nownie na listę. Podobnie pozostałe operatory czy funk- katu, czyli omówionego wcześniej przypadku predykatu
cje, np. w linii: calc z dwoma argumentami. Realizacja obliczeń to efekt
działania lists:foldl. Funkcja ta działa w taki sposób, iż
calc("sin", [L1|R]) -> [math:sin(L1)|R]; poszczególne elementy listy są kolejno wstawiane do po-
danej funkcji, jednakże brane są także pod uwagę wyni-
pobieramy jeden argument i obliczamy wartość funkcji ki, co pozwala np. obliczyć wartość funkcji silnia z piątki
sin i wstawiamy obliczoną wielkość do listy. Ważną rolę w taki sposób:
pełni też linia:
lists:foldl(fun(X, P) -> X * P end, 1,
calc(L, R) -> [read(L)|R]. [1,2,3,4,5]).

44 / 1 . 2012 . (1)  /


ERLANG - JĘZYK INNY NIŻ C++ CZY JAVA

W przypadku naszego kalkulatora poszczególne wywoła- wysłaniu komunikatu za pomocą receive czekamy na ko-
nia calc modyfikują listę z obliczeniami. munikat zwrotny w tym samym formacie {From, Msg},
a po jego otrzymaniu za pomocą io:format wyświetlamy
Listing 2. Kalkulator dla odwrotnej notacji polskiej na konsoli prosty komunikat. Jednak nasz proces z pro-
cedurą loop nadal jest aktywny, możemy go wyłączyć,
-module(rpn). przekazując komunikat stop.
-export([calc/1]).
Jak widać, jest to bardzo prosty przykład, jednakże jest
calc(L) when is_list(L) -> to już aplikacja rozproszona. Możemy, do czego zachęcam,
[R] = lists:foldl(fun calc/2, [],
moduł loop umieścić w innym module, a procedura start
string:tokens(L, " ")), R.
nadal będzie posiadać podobną postać, przy czym spawn
calc("+", [L1,L2|R]) -> [L2+L1|R]; będzie naturalnie odnosił się do innego modułu. W nieco
calc("-", [L1,L2|R]) -> [L2-L1|R];
calc("*", [L1,L2|R]) -> [L2*L1|R]; inny sposób będzie trzeba utworzyć nowy proces.
calc("/", [L1,L2|R]) -> [L2/L1|R];
calc("^", [L1,L2|R]) -> [math:pow(L2,L1)|R]; Listing 3. Przekazywanie komunikatów
calc("sin", [L1|R]) -> [math:sin(L1)|R];
calc(L, R) -> [read(L)|R].
-module(echo).
read(L) -> -export([start/0, loop/0]).
case string:to_float(L) of
{error,no_float} -> list_to_integer(L); start() ->
{F,_} -> F Pid = spawn(echo, loop, []),
end. Pid ! {self(), hello},
receive
{Pid, Msg} -> io:format("echo: ~w~n",[Msg])
NADAWCY I ODBIORCY WIADOMOŚCI end,
Pid ! stop.

Przekazywanie komunikatów to jeden z elementów, za loop() ->


który można polubić programowanie w Erlangu. W na- receive
szym następnym przykładzie zbudujemy bardziej skom- {From, Msg} ->
From ! {self(), Msg},
plikowany przykład, gdzie zaprogramujemy prosty komu- loop();
nikator podobny w swej idei choćby do Gadu-Gadu, ale stop -> true
w pierwszej kolejności zrealizujemy bardziej elementarny end.
przykład, a jego treść zawiera Listing 3.
Początek naszego programu rozpoczyna się od proce- wxERLANG, OKNA I PRZYCISKI
dury start. W pierwszej linii tej procedury powołujemy
nowy proces, który realizuje kod procedury loop. Zada- Zanim krótko opiszemy przykład udający Gadu-Gadu, po-
niem loop jest odbieranie dwóch typów komunikatów. trzebne będzie GUI do naszej aplikacji. Listing 4 przedsta-
Pierwszy komunikat składa się z dwóch elementów: wia przykładowe okno zrealizowane w pakiecie wxErlang,
który jest portem biblioteki wxWidgets do środowiska
{From, Msg} Erlang. W przypadku Windows zawsze mamy dostęp do
tego pakietu, inne systemy mogą od nas wymagać insta-
Zmienna From odpowiada za nadawcę, natomiast Msg lacji dodatkowych pakietów.
stanowi treść wiadomości. Po odebraniu takiej wiadomo- Początek Listingu 4 jest podobny jak w naszych po-
ści jest ona przesyłana z powrotem do nadawcy: przednich przykładach, choć polecenie:

From ! {self(), Msg}, -compile(export_all).

a procedura loop jest ponownie, dzięki rekurencji, uru- zgodnie z oczekiwaniami udostępnia wszystkie procedury
chamiana. Drugi komunikat stop, zgodnie ze swoją na- znajdujące się w module, natomiast:
zwą, zatrzyma działanie loop, ponieważ nie pojawia się
kolejne rekurencyjne wywołanie tej pętli. -include_lib("wx/include/wx.hrl").
Nic więcej o procedurze loop nie da się powiedzieć,
toteż warto wrócić do start. Po uruchomieniu procesu za dołącza definicje różnego rodzaju stałych przydatnych
pomocą spawn, otrzymaliśmy jego identyfikator. Możemy podczas tworzenia programów korzystających z GUI.
teraz, wykorzystując operator !, przesłać komunikat np: Mamy jeszcze definicje stałych:

Pid ! {self(), hello} -define(ABOUT, ?wxID_ABOUT).

Procedura loop odczyta ten komunikat jako {From, Msg}, albo:


bowiem mamy nadawcę oraz treść wiadomości, w na-
szym przypadku będzie to tzw. atom o postaci hello. Po -define(ID_BUTTON, 2005).

/ www.programistamag.pl / 45
JĘZYKI PROGRAMOWANIA

Stałe te będą przez nas wykorzystywane podczas wykry- Str ="Prosty przykład w wxErlang.",
wania zdarzeń i tworzenia widgetów. Program z Listingu 4 MD = wxMessageDialog:new(Frame, Str,
[{style, ?wxOK bor ?wxICON_INFORMATION},
ma trzy procedury. Pierwsza start uruchamia naszą apli- {caption, "O Programie"}]),
kację, setup tworzy interfejs, natomiast loop jest odpo- wxDialog:showModal(MD),
wiedzialna za przetwarzanie komunikatów. wxDialog:destroy(MD),
W całym procesie tworzenia menu nie wykorzystujemy
żadnych specjalnych technik programowania, utworzenie Powyższe linie to typowy kod sekwencyjny, co pozwala
menu Plik, które zawiera jedną opcję Plik, to następujące na łatwe wdrożenie się do pisania programów w Erlangu,
polecenia: gdyż możemy tworzyć oprogramowanie przynajmniej na
początku w sposób dobrze znany, bez konieczności stoso-
MenuBar = wxMenuBar:new(), wania rozwiązań wyłącznie funkcyjnych bądź też progra-
File = wxMenu:new(),
wxMenuBar:append(MenuBar, File, "&Plik"),
mowania w logice.
Nie trzeba też okien mozolnie projektować bezpo-
Poszczególne polecenia rozdzielamy przecinkiem, trzeba średnio poprzez tworzenie kodu, możemy utworzyć po-
zwrócić uwagę na nakazanie obsługi komunikatów, co ro- trzebne elementy interfejsu w postaci plików w formacie
bimy w następujący sposób: XRC. Pakiet wxErlang obsługuje pliki XRC, więc warto
skorzystać z tej możliwości, oszczędność czasu będzie
wxFrame:connect(Frame, command_menu_selected), znacząca.
wxFrame:connect(Frame, close_window).

Listing 4. Okno i menu w pakiecie wxErlang


Dopiero teraz pętla loop będzie otrzymywać odpowied-
nie komunikaty. Obsługa komunikatów jest zgodna -module(wxex2).
-compile(export_all).
z językiem Erlang, czyli mamy blok receive, gdzie defi-
niujemy poszczególne typy komunikatów. Obsługa wyj- -include_lib("wx/include/wx.hrl").
ścia po wyborze z menu opcji Wyjście przedstawia się
-define(ABOUT, ?wxID_ABOUT).
następująco: -define(EXIT, ?wxID_EXIT).

#wx{id=?EXIT, event=#wxCommand{type=command_menu_ start() ->


selected}} -> S=wx:new(),
wxWindow:close(Frame, []) Frame = wxFrame:new(S, ?wxID_ANY, "Okno"),
setup(Frame),
wxFrame:show(Frame),
loop(Frame),
Listing 4 zawiera jeszcze obsługę drugiego komunikatu, wx:destroy().
gdzie wyświetlane jest okno dialogowe „O Programie”.
setup(Frame) ->
Utworzenie takiego okna, jego wyświetlenie, a następnie MenuBar = wxMenuBar:new(),
usunięcie jest realizowane w następujący sposób: File = wxMenu:new(),

Rysunek 4. Projekt głównego okna programu do przekazywania komunikatów opracowany w DialogBlocks

46 / 1 . 2012 . (1)  /


ERLANG - JĘZYK INNY NIŻ C++ CZY JAVA

Help = wxMenu:new(),
Jednakże niezbędne jest wprowadzenie jednej zmiany
wxMenu:append(Help, ?ABOUT, "O programie"), w pliku talker.erl. Należy go dopasować do używanej
wxMenu:append(File, ?EXIT, "Koniec"),
przez nas sieci, a dokładniej zmienić nazwę hosta na od-
wxMenuBar:append(MenuBar, File, "&Plik"), powiedni. Jeśli nasz host nosi nazwę pc0, a węzeł Erlanga,
wxMenuBar:append(MenuBar, Help, "P&omoc"), na którym zostanie uruchomiony serwer, to w0, to pro-
wxFrame:setMenuBar(Frame, MenuBar),
cedura server_node_name posiada następującą postać:

wxFrame:createStatusBar(Frame), server_node_name() ->


wxFrame:setStatusText(Frame, "Witamy w naszym w0@pc0.
programie!"),

wxFrame:connect(Frame, command_menu_selected), Po tej poprawce możemy skompilować i uruchomić ser-


wxFrame:connect(Frame, close_window).
wer oraz program do GUI:
loop(Frame) ->
receive c(talker).
#wx{id=?ABOUT, event=#wxCommand{}} -> c(talkergui).
Str ="Prosty przykład w wxErlang.", talker:start_server().
MD = wxMessageDialog:new(Frame, Str,
[{style, ?wxOK bor ?wxICON_INFORMATION},
{caption, "O Programie"}]),
wxDialog:showModal(MD), Pozostali użytkownicy uruchamiają interpreter Erlanga na
wxDialog:destroy(MD), węzłach o innych nazwach - muszą mieć dostęp do skom-
loop(Frame);
pilowanych plików talker.beam oraz talkergui.beam. W ich
#wx{id=?EXIT, event=#wxCommand{type=command_ przypadku wystarczy, jeśli uruchomią tylko sam program
menu_selected}} ->
z GUI: talkergui:start()., gdyż talker.erl jest już skompilo-
wxWindow:close(Frame, [])
end. wany oraz posiada poprawne odniesienie do serwera.
Użytkownik modułu talker, a w naszym przypadku bę-
dzie to moduł talkergui, (choć można moduł talker uży-
SERWER I KLIENT DO POGADUSZEK wać bezpośrednio z poziomu konsoli) korzysta również
z procedur logon do zalogowania się na serwer, message
Nasz ostatni przykład, czyli aplikacja do wymiany komu- do przekazania wiadomości do innego użytkownika,
ników pomiędzy osobami, ma niestety zbyt duży kod źró- getuserlist do pobrania listy wiadomości oraz logoff do
dłowy, aby w pełni go opisać i zaprezentować. Jednakże wylogowania się z serwera. W procedurach logon i log­
pierwsze zadanie polega na zaprojektowaniu okna dia- off wykorzystujemy m.in. metodę lists:keymember do
logowego. Rysunek 4 przedstawia projekt naszego okna sprawdzania, czy dany użytkownik jest zalogowany.
dialogowego wykonany w programie DialogBlocks. Ponie- Pozostaje tylko jeden problem. Jeśli obsługa komuni-
waż Erlang jak na razie nie ma żadnego dodatkowego katów jest realizowana przez inny moduł, a środowisko
programu wspomagającego projektowanie okien dialogo- GUI również jest zdefiniowane w innym module, to w jaki
wych, to trzeba skorzystać z dodatkowego oprogramowa- sposób przekazać odebraną wiadomość? Naturalnie, trze-
nia. W kodzie źródłowym okno zostało zaprojektowane ba wykorzystać komunikaty Erlanga. W obsłudze klienta,
ręcznie, ale zawsze można skorzystać z plików XRC. po odebraniu komunikatu, wykonujemy dwie czynności,
Idea wykorzystania naszego programu jest następują- na konsole kierujemy odpowiedni komunikat, oraz wy-
ca: należy uruchomić serwer, a następnie na pozostałych syłamy komunikat do ParentModule, a zmienna ta repre-
węzłach uruchamiane jest oprogramowanie klienta łączą- zentuje masz moduł GUI (talkergui):
ce się z naszym serwerem.
Nasza aplikacja składa się z dwóch modułów: talkergui {message_from, FromName, Message} ->
io:format("Wiadomosc od ~p: ~p~n", [FromName,
odpowiedzialnego za GUI oraz talker. Ten drugi moduł to Message]),
modyfikacja aplikacji do przekazywania komunikatów, ParentModule ! {message_from, FromName, Message}
jaką można odszukać w dokumentacji do Erlanga. Naj-
ważniejsze procedury są następujące: W talkergui procedura loop odbiera komunikat z wiado-
mością w następujący sposób:
start_server/0, server/1, logon/2, getuserlist/0,
logoff/0, message/2 {message_from, FromName, Message} ->
CtrlWin = wxWindow:findWindowById(?ID_TEXT_BOX,
[{parent, Frame}]),
Pierwsza start_server jest uruchamiana tylko raz, na wxTextCtrl:appendText(wx:typeCast(CtrlWin,
węźle serwera. Aby opisywany program działał, trzeba wxTextCtrl), FromName ++ ": " ++ Message ++ "\n"),
loop(Frame);
uruchomić interpreter z nadaną nazwą dla uruchomione-
go węzła np. w taki sposób:
W ten sposób udało się nam dość elegancko skomuni-
erl -sname w0 kować program pracujący w trybie konsoli ze środowi-
skiem GUI, a trzeba powiedzieć, iż ten sposób komunika-

/ www.programistamag.pl / 47
JĘZYKI PROGRAMOWANIA

Rysunek 5. Testowanie aplikacji do wymiany komunikacji

cji może odbywać się także w rzeczywistym środowisku mnieć, jest rozwiązaniem, które łatwo łączy się z innymi
sieciowym. językami, np. C/C++. Dlatego jeśli planujemy tworzyć
serwer działający w ramach naszej platformy, to może
PODSUMOWANIE warto rozważyć zastosowanie Erlanga, który znakomicie
nadaje się do tworzenia serwerów ze względu na obec-
Na zakończenie warto jak zawsze zachęcać do dalszych ność systemu komunikatów. Choć tak naprawdę jest to
poszukiwań, wbrew pozorom o Erlangu znajdziemy sporo język uniwersalny i można w nim tworzyć wydajne aplika-
informacji, także różnego rodzaju przykłady, więc warto cje rozwiązujące różnego rodzaju problemy. Wydaje się,
poszukać. Trzeba także wspomnieć o dwóch książkach: iż jest również łatwiejszy do stosowania niż np. Prolog,
pierwsza to „Erlang Programming” oraz druga „Erlang and ze względu na dużą liczbę typowych konstrukcji sekwen-
OTP in Action”, które stanowią podręczniki dla tego języ- cyjnych, co tylko ułatwia tworzenie pierwszych własnych
ka programowania. Erlang, o czym nie było okazji wspo- programów w Erlangu.

W SIECI: Główna strona języka Erlang: http://www.erlang.org/


PP Serwis o programowaniu w języku Erlang: http://trapexit.org/
PP Nauka Erlanga dla początkujących: http://learnyousomeerlang.com/
PP Podręcznik Wiki o programowaniu w Erlangu: http://en.wikibooks.org/wiki/Erlang_Programming
PP Komercyjne wsparcie dla zastosowań Erlanga: http://www.erlang-solutions.com
PP Przykłady programów w Erlangu dot. sortowania szybkiego: http://en.literateprograms.org/Quicksort_(Erlang)
PP Dodatek do edytora Emacs umożliwiający pracę z Erlangiem: https://github.com/massemanet/distel
PP Wtyczka dla Eclipse pozwalająca na wygodną pracę ze środowiskiem Erlang: http://erlide.org/

Marek Sawerwain redakcja@programistamag.pl

Autor jest pracownikiem naukowym Uniwersytetu Zielonogórskiego, na co dzień


zajmuje się teorią kwantowych języków programowania, a także tworzeniem
oprogramowania dla systemów Windows oraz Linux. Zainteresowania: teoria
języków programowania oraz dobra literatura.

48 / 1 . 2012 . (1)  /


KOMIKS

/ www.programistamag.pl / 49
PROGRAMOWANIE GRAFIKI
Wojciech Sura

Direct3D – podstawy
Użytkownicy komputerów często nie mają pojęcia, jak wielka moc obliczeniowa
drzemie w ich karcie graficznej. Współczesne procesory mają zwykle dwa lub cztery
rdzenie, gdy tymczasem liczba rdzeni przeciętnej karty graficznej przekracza już licz-
bę 200. Ujarzmienie takiej potęgi daje programiście bardzo dużo możliwości – i co
ciekawe, wcale nie jest takie trudne.

N
a początku zadajmy sobie proste pytanie: czy na trójkąt. Nie ma mowy o czymś takim, jak bryła, punkt
ekranie komputera można wyświetlić trójwymia- zaczepienia, kamera czy światło. Każdy z tych elemen-
rową grafikę? Mnogość gier komputerowych i pro- tów musi zostać - w oparciu o mechanizmy Direct3D
gramów typu CAD sugerowałaby odpowiedź twierdzącą, - zaimplementowany przez programistę.
a przecież jest dokładnie na odwrót: ekran komputera Przyjrzyjmy się więc, jak – z perspektywy programisty
może wyświetlić tylko płaskie obrazy. Innymi słowy to, co – wygląda proces renderowania grafiki 3D.
widzimy na ekranie, jest tylko dwuwymiarową projekcją
pewnej trójwymiarowej sceny. Właściwą analogią może RENDERING PIPELINE
być zdjęcie lub film, które utrwalają trójwymiarowy świat
na dwuwymiarowym medium. Proces renderowania podzielony jest na etapy, ułożone
Wykonanie takiej projekcji nie jest, wbrew pozorom, wzdłuż specyficznej ścieżki, nazywanej rendering pipeli-
aż tak trudne - dosyć powiedzieć, że już w latach 80' po- ne. Niektóre etapy realizowane są w całości przez kartę
wstawały gry na Atari, które potrafiły w czasie rzeczywi- graficzną, inne mogą być częściowo kontrolowane przez
stym zobrazować pewien prymitywny, ale już trójwymia- programistę, zaś jeszcze inne muszą zostać przygotowa-
rowy świat. Jednak w przypadku bardziej skomplikowanej ne przez niego w całości. Ponadto, niektóre elementy tej
i realistycznej grafiki rośnie znacząco złożoność algoryt- ścieżki są opcjonalne, zaś obecność innych jest wyma-
mów ją renderujących, co dramatycznie odbija się na ich gana do prawidłowego przeprowadzenia całego procesu.
wydajności. Osiągnięcie kilkudziesięciu klatek na sekun- Rysunek 1 przedstawia najprostszą ścieżkę zawierającą
dę dla sceny składającej się z tysięcy trójkątów wypeł- wszystkie elementy wymagane do wyrenderowania sceny
nianych setkami megabajtów tekstur tylko przy pomocy zawierającej trójwymiarowe bryły. Proces renderowania
procesora (nie zapominając o logice gry) jest nierealne.
Aby rozwiązać ten problem, powstały dedykowane układy Rysunek 1. Ścieżka renderowania
wspomagające renderowanie grafiki, montowane na kar-
tach graficznych. W ten sposób karta, która pierwotnie
służyła tylko do wyświetlania obrazu na ekranie, stała się
w pewnym momencie niewielkim centrum obliczeniowym
zdolnym wykonywać ogromne ilości operacji zmienno-
przecinkowych w czasie rzeczywistym.
Architektura jednostek obliczeniowych karty graficz-
nej rożni się znacząco od architektury procesora. Aby nie
zmuszać programisty do uczenia się programowania na
zupełnie nową platformę i jednocześnie by standaryzo-
wać dostęp do mocy obliczeniowej karty graficznej, po-
wstały specjalne API - pomosty, pośredniczące pomiędzy
aplikacją a sterownikiem i umożliwiające wykorzystanie
możliwości karty podczas programowania w języku wyż-
szego poziomu. Jednym z takich pomostów jest właśnie
Direct3D, składnik biblioteki DirectX rozwijanej przez
Microsoft.
Z perspektywy programisty, Direct3D jest rendererem,
czyli mechanizmem odpowiedzialnym za przetworzenie
i wyświetlenie na ekranie przekazanych mu danych. API
Direct3D jest stosunkowo niskopoziomowe: dosyć za-
uważyć, ze najbardziej zaawansowanym obiektem, z któ-
rym mamy do czynienia podczas pracy z tą biblioteką jest

50 / 1 . 2012 . (1)  /


DIRECT3D – PODSTAWY

może być wywołany kilkukrotnie dla pojedynczej klatki,


a programista ma pełną kontrolę nad tym, w jaki sposób
będą składane wyniki kolejnych przebiegów.

Input assembly

Pierwszym etapem ścieżki renderowania jest input assem­


bly (IA). W etapie tym dostarczone przez programistę
dane konwertowane są na prymitywy – czyli punkty, od-
cinki i trójkąty – a następnie w takiej postaci przekazywa-
ne są do dalszego przetworzenia w późniejszych etapach.
Struktura danych pojedynczego wierzchołka nie jest na-
rzucona – programista ma pełną dowolność w zdefiniowa-
niu, jakie dane i jakiego typu będą przekazywane karcie
graficznej. Zazwyczaj jednak są to: pozycja wierzchołka,
jego normalna oraz kolor lub współrzędne tekstury. Rysunek 2. Rasteryzacja
Etap input assembly jest w całości realizowany przez
renderer, a programista – poza dostarczeniem danych jako pozycja wierzchołka. Jest to informacja konieczna do
– nie ma nad nim żadnej kontroli. przeprowadzenia kolejnych etapów obliczeń.
Zadaniem vertex shadera jest wykonanie przekształ-
Vertex shader ceń geometrycznych na wierzchołku: przesunięciu, ob-
róceniu lub przeskalowaniu, a następnie obliczeniu jego
W kolejnym kroku dane przygotowane przez input as- docelowego położenia na dwuwymiarowej projekcji. Choć
sembly przetwarza vertex shader i przy nim zatrzymamy może to brzmieć mało zachęcająco, to na szczęście bi-
się na chwilę dłużej. blioteka Direct3D w parze z językiem HLSL udostępniają
Shadery są podstawową siłą roboczą, która umożliwia dużo narzędzi pozwalających zrealizować te zadania przy
karcie graficznej osiągnięcie tak dużej wydajności. Sha- naprawdę niewielkim wysiłku ze strony programisty.
der jest niewielkim programem napisanym w jednym ze W Direct3D programista jest zobligowany do samo-
specjalnie do tego celu przygotowanych języków progra- dzielnego napisania vertex shadera i włączenia go do
mowania (w DirectX jest to HLSL - High Level Shading ścieżki renderowania.
Language; innymi są na przykład GLSL czy Cg), kompi-
lowanym dla karty graficznej. Kompilacja shadera może Rasteryzator
zostać wykonana w trakcie pracy programu (w takiej sy-
tuacji kody shaderów muszą być przechowywane wraz Zaraz po zakończeniu pracy vertex shadera uruchamia-
z programem, co jest – wbrew pozorom – dosyć popu- ny jest rasteryzator. Jego zadanie polega na przeanalizo-
larną techniką, spotykaną nawet w niektórych grach), ale waniu prymitywów przygotowanych przez vertex shader
możliwe jest też skorzystanie ze wcześniej skompilowa- i obliczenie, które piksele ekranu zostaną przez nie zajęte.
nych kodów binarnych. Zadanie to jest realizowane przez kartę graficzną, jednak
Shadery pracują w architekturze SIMD (Single Instruc- przed rozpoczęciem renderowania programista ma moż-
tion, Multiple Data): program shadera jest jeden, ale jest liwość ustalić tryb pracy rasteryzatora – uzyskując w ten
on wywoływany niezależnie i równolegle na dużej ilości sposób różne rezultaty w zależności od potrzeb.
danych. W praktyce, jeśli dwie różne bryły mają być prze-
tworzone w różny sposób, stosuje się jedno z dwóch roz- Pixel shader
wiązań: shader może pracować w trybie warunkowym,
regulowanym dodatkowymi danymi przekazanymi razem Każdy z pikseli obliczonych przez rasteryzator przetwa-
z wierzchołkami bryły lub po prostu klatka może być ren- rzany jest przez kolejny shader – z racji swojego zadania
derowana kilkoma przebiegami, z wykorzystaniem róż- zwany pixel shaderem. Pixel shader, podobnie jak vertex
nych shaderow dla różnych elementów sceny podczas shader, musi zostać napisany przez programistę, skompi-
każdego przebiegu. lowany i umieszczony w rendering pipeline przed rozpo-
Główna funkcja (entry point) vertex shadera dosta- częciem renderowania. Dane wejściowe do pixel shadera
je na wejściu pojedynczy wierzchołek w takim forma- wymagają jednak dodatkowego wyjaśnienia.
cie, jaki programista zdefiniował w fazie IA. Rezultatem Pixel shader przyjmuje jako parametr dane dokładnie
zwracanym przez ten shader powinien również być do- takiego samego typu, jakie zostały zwrócone przez ver-
kładnie jeden wierzchołek. Także i tu programista może tex shader. Jak pamiętamy, oprócz wymaganej pozycji
zdefiniować, jaka będzie struktura danych, którą shader wierzchołka, programista ma pełną dowolność w zdefi-
przekaże dalej, ale z jednym wyjątkiem: pośród zwraca- niowaniu informacji generowanych przez vertex shader.
nych danych należy wyróżnić jedną strukturę, która bę- Kłopot polega jednak na tym, że vertex shader jest w sta-
dzie interpretowana później przez mechanizmy Direct3D nie jasno zdefiniować wartości tylko dla wierzchołków da-

/ www.programistamag.pl / 51
PROGRAMOWANIE GRAFIKI

w docelowym buforze (zwykle na ekranie, ale może to


  być również na przykład bitmapa).
Output merger kończy proces renderowania. Choć trudno
 
   w to uwierzyć, cały proces, począwszy od fazy IA aż do OM
wywoływany jest przynajmniej raz dla każdej klatki – czyli
nawet kilkadziesiąt razy na sekundę! Szybkość obliczeń

  
 może zrobić jeszcze większe wrażenie, jeśli uświadomimy
sobie, że opisana ścieżka zawiera tylko wymagane kroki,
    
  gdy tymczasem programista może skorzystać jeszcze z kil-
ku dodatkowych rodzajów shaderów, które można włączyć
do rendering pipeline. Daje to całkiem niezłe wyobrażenie
       
 o mocy obliczeniowej karty graficznej.

Rysunek 3. Interpolacja wykonana przez rasteryzator


TROCHĘ MATEMATYKI
nego prymitywa (trójkąta, odcinka), natomiast brakuje Aby prawidłowo oprogramować cały proces renderowa-
informacji na temat punktów znajdujących się w jego nia, nie można obejść się bez wiedzy o przekształceniach,
wnętrzu. W tym miejscu ratuje nas jednak rasteryzator, które w jego trakcie powinny zostać wykonane.
który w trakcie przetwarzania prymitywów inteligentnie
interpoluje wartości zwrócone przez vertex shader w taki Układ współrzędnych
sposób, że pixel shader otrzymuje komplet danych dla
każdego wygenerowanego piksela. Direct3D operuje na kartezjańskim układzie współrzęd-
Przyjmijmy na przykład, że vertex shader oprócz współ- nych, w którym każdy punkt ma trzy współrzędne: x, y
rzędnych wierzchołka zwraca dodatkowo zdefiniowaną i z. Osie OX i OY pokrywają się z osiami OX i OY ekranu
przez programistę pojedynczą liczbę rzeczywistą oraz że komputera, zaś oś OZ jest osią dodatkową, wskazującą w
pierwszemu wierzchołkowi została przyporządkowana głąb monitora. Istnieje ważna różnica pomiędzy oboma
wartość 0.0, drugiemu – 0.5, trzeciemu zaś 1.0. Rysunek układami: w układzie ekranowym wartości współrzędnej
3 obrazuje, w jaki sposób wartość ta będzie interpolowana y rosną w dół, natomiast Direct3D korzysta z matema-
dla wszystkich pikseli zrasteryzowanego trójkąta. tycznej definicji, gdzie wartości te rosną w górę. Niezgod-
ność kierunków osi y obu układów nie stanowi jednak
Output merger większego problemu, ponieważ to Direct3D jest odpowie-
dzialny za przeniesienie punktu z układu kartezjańskiego
Ostatnim etapem ścieżki renderowania jest output mer- do komputerowego, o czym za chwilę.
ger (OM), którego zadaniem jest końcowa obróbka efek- Czeka nas jednak jeszcze jedna niespodzianka – otóż
tów pracy vertex i pixel shadera. wbrew naszym oczekiwaniom, punkty przestrzeni opisywa-
Jednym z kluczowych zadań output mergera jest wy- ne są w Direct3D nie przez trzy, a przez cztery współrzęd-
konanie tzw. depth testu – testu głębi. Direct3D nie daje ne. Jednak bez strachu: czwarta współrzędna (nazywana
żadnej kontroli nad tym, w jakiej kolejności przetwarzane w) wprowadzona została głównie po to, aby można było
będą przekazane mu prymitywy. Co więcej, część proce- wygodnie i szybko wykonywać przekształcenia, których
su renderowania wykonywana jest równolegle, więc może w przestrzeni trójwymiarowej w ten sposób zrealizować się
się zdarzyć, że w jednej klatce pewien prymityw będzie nie da. W praktyce rendererowi można przekazywać punk-
przetworzony wcześniej, zaś w innej – później. Powsta- ty trójwymiarowe, a ten automatycznie przekształci je
je tu więc problem związany z zasłanianiem się prymity- w punkty czterowymiarowe, inicjując czwartą współrzędną
wów: może zdarzyć się, że element, który znajduje się wartością 1.0. Taką też wartością powinien zainicjować tę
bliżej, zostanie pechowo wyrenderowany wcześniej niż współrzędną programista, jeśli zdecyduje się na przekazy-
inny, znajdujący się dalej od obserwatora, i w efekcie zo- wanie rendererowi kompletu danych. Inne wartości mogą
stanie przez niego zasłonięty. spowodować nieprzewidziane zachowanie niektórych prze-
Output merger rozwiązuje ten problem poprzez zasto- kształceń, więc należy je stosować z ostrożnością.
sowanie tzw. z-buffera. Jest to bufor o rozmiarach rów- Macierze przekształceń są oczywiście również cztero-
nych wyjściowemu obrazowi, w którym output merger wymiarowe (4x4).
rejestruje odległość wszystkich renderowanych piksli. Prześledźmy teraz, w jaki sposób przekształcane są
W momencie, gdy jeden z nich pokrywa się z innym, mer- współrzędne wierzchołków w trakcie wykonywania ścieżki
ger jest w stanie zweryfikować, który z nich znajduje się renderowania.
bliżej i to on zostaje zapisany jako wynikowy, eliminując
w ten sposób opisany wcześniej problem. Przekształcenia bryły
Oprócz testowania głębi, output merger odpowiedzial-
ny jest też za mieszanie kolorów (w przypadku, gdy pi- Bryły przechowywane są w pamięci zazwyczaj w tzw.
kesle są półprzezroczyste) oraz zapisywanie wyników współrzędnych lokalnych. W każdej bryle wyróżniany

52 / 1 . 2012 . (1)  /


DIRECT3D – PODSTAWY

jest pomocniczo pewien wirtualny punkt (nazywany za-


zwyczaj punktem zaczepienia), który staje się środkiem
układu współrzędnych bryły i to względem niego usta- 
lane są współrzędne jej wierzchołków. Na przykład, je- 
śli rozważymy jednostkowy sześcian, którego punkt za-
czepienia znajduje się w jego geometrycznym środku, to
wierzchołki rozpięte będą na współrzędnych (-0.5, -0.5,
-0.5); (0.5, 0.5, 0.5) (Rysunek 4). Dzięki takiej, a nie
innej organizacji danych możemy później umieścić ów
sześcian w dowolnym miejscu sceny, nakładając na jego
wierzchołki odpowiednie przekształcenia.
Załóżmy na przykład, że chcemy, aby środek rende-
rowanego sześcianu znalazł się w punkcie (1, 1, 1); wy-
starczy wówczas na wszystkie jego wierzchołki nałożyć
przekształcenie translacji (przesunięcia) o wektor (1, 1,
1), a sześcian znajdzie się dokładnie tam, gdzie chce-
my (Rysunek 5). Mechanizm ten przydaje się szczególnie
w przypadku, gdy jedną bryłę chcemy powielić i umieścić
na scenie w wielu miejscach naraz – wystarczy wówczas
przechować w pamięci tylko jedną jej definicję, ale rende- 
rować ją wielokrotnie, używając za każdym razem innego
Rysunek 4. Współrzędne lokalne
przekształcenia.
Macierz przekształcenia, która przenosi współrzędne
wierzchołków bryły z układu lokalnego do układu świa-
ta (globalnego) nazywana jest macierzą świata (World
matrix) i zwykle jest nakładana na wierzchołek przed
wszystkimi innymi przekształceniami w vertex shaderze. 

Projekcja perspektywiczna

Stajemy teraz przed głównym problemem całego proce- 


su, czyli w jaki sposób przedstawić trójwymiarowy świat
na płaskim obrazie? Szukane przekształcenie, które trój-
wymiarowemu punktowi przyporządkowuje dwuwymia- 
rowy odpowiednik, nosi miano projekcji, zaś najczęściej
stosowanym rodzajem projekcji jest projekcja perspek-
tywiczna, która w dużym stopniu odzwierciedla sposób,
w jaki normalnie postrzegamy świat.
W Direct3D przyjęto, że docelowym obszarem, w któ-
rym muszą znaleźć się wyniki ścieżki renderowania, jest
prostopadłościan rozpięty na przedziałach: [-1.0, 1.0]
na osi OX, [-1.0, 1.0] na osi OY oraz [0.0, 1.0] na osi
OZ. Tak wygenerowane dane są liniowo przekształcane
na współrzędne ekranowe, to znaczy punkt (-1.0, 1.0, Rysunek 5. Konwersja na współrzędne świata
0.0) zostanie przekształcony na lewy, górny piksel, punkt
(1.0, -1.0, 0.0) – na prawy dolny i tak dalej. Współrzędna
z nie odgrywa żadnej roli w tym przekształceniu, jednak tafli, która ograniczy obszar pola widzenia od tyłu (czer-
jest używana na przykład podczas testów głębi. Punkt, wony prostokąt na Rysunku 6). Z sześciu wspomnianych
który znajdzie się poza opisanym wcześniej obszarem, płaszczyzn otrzymamy ostrosłup ścięty (ang. perspective
nie jest wyświetlany. frustum), w którym zawierają się wszystkie obiekty, któ-
Jak więc działa projekcja perspektywiczna? re możemy obserwować przez naszą taflę. Idea projekcji
Wyobraźmy sobie, że stoimy gdzieś w plenerze, trzy- perspektywicznej polega na obserwacji, że wspomniany
mając przed sobą prostokątną, szklaną taflę (niebieski ostrosłup ścięty wraz z jego zawartością odpowiednimi
prostokąt na Rysunku 6). Kiedy popatrzymy na otacza- przekształceniami można sprowadzić do wymaganego
jący nas krajobraz przez narożniki tej tafli, wyznaczymy przez Direct3D prostopadłościanu (Rysunek 7).
tym samym cztery płaszczyzny, ograniczające pole wi- Przekształcenie to jest dosyć skomplikowane, jednak
dzenia. Aby wykonać projekcję perspektywiczną, potrze- ratują nas tutaj funkcje biblioteczne Direct3D, które na
bujemy jeszcze jednej płaszczyzny, równoległej do naszej bazie kilku parametrów generują odpowiednią macierz

/ www.programistamag.pl / 53
PROGRAMOWANIE GRAFIKI

Rysunek 6. Pole widzenia obserwatora

Rysunek 7. Pole widzenia przekształcone w obszar


renderowania

Rysunek 8. W tym przypadku proporcje pola


widzenia projekcji perspektywicznej i okna są
takie same

Rysunek 9. Niezgodność proporcji


spowoduje zniekształcenia

54 / 1 . 2012 . (1)  /


DIRECT3D – PODSTAWY

Przygotuj dane sceny do wyrenderowania Proces rende-


Dla każdej bryły (lub zestawu brył): rowania obrazu
Procesor

Przekaż do Direct3D informację o buforze z danymi bryły


Skonfiguruj etapy renderowania
Przekaż do Direct3D skompilowany vertex shader
Przekaż do Direct3D skompilowany pixel shader
Uruchom renderer
[Input Assembly]
Zbierz informacje o prymitywach do wyrenderowania
Dla każdego prymitywu:
Dla każdego wierzchołka prymitywu:
[Vertex Shader]
Karta graficzna

Przekształć wierzchołek ze współrzędnych lokalnych na globalne przy pomocy macierzy świata


Przekształć wierzchołek ze współrzędnych globalnych na współrzędne ekranowe przy pomocy macierzy projekcji
[Rasteryzacja]
Oblicz, które piksele zostaną zajęte przez prymityw
Dla każdego piksela:
[Pixel Shader]
Oblicz docelowy kolor piksla
[Output Merger]
Wykonaj test głębi i zapisz piksel w buforze wyjściowym.

przekształcenia, którą wystarczy nałożyć w vertex shade- Renderowanie w pigułce


rze na przetwarzane wierzchołki.
Poskładajmy teraz całą teorię w całość i zarysujmy
Konwersja na współrzędne ekranowe w pseudokodzie, jak wygląda prosty program rende-
rujący pojedynczą trójwymiarową scenę (na diagramie
Ostatnim etapem przetwarzania danych, w którym nie bierze powyżej).
już udziału programista, jest wspomniane przed chwilą linio-
we przekształcenie współrzędnych punktów z prostopadło- NA KONIEC...
ściennego obszaru renderowania na współrzędne ekranowe.
Należy tu zauważyć pewien ważny fakt. Nikt nie pilnuje Opisany przeze mnie mechanizm dotyczy wprawdzie Di-
automatycznie zależności pomiędzy prostokątem definiu- rect3D, jednak jego ogólne założenia są bardzo podobne
jącym pole widzenia (niebieski prostokąt na rysunku 6), w przypadku innych rendererów (na przykład OpenGL).
a docelowym oknem (lub bitmapą), na którym rysowany Aby rozpocząć pisanie programów wykorzystujących
jest wynik całego procesu. W interesie programisty leży sprzętową akcelerację grafiki 3D, potrzeba jeszcze tyl-
więc, aby ich proporcje (to znaczy stosunki szerokości do ko wiedzy na temat API renderera, o czym postaram się
wysokości) były zachowywane – w przeciwnym razie ob- opowiedzieć w kolejnym artykule poświęconym podsta-
raz rysowany w oknie będzie zniekształcony. wom renderowania grafiki z udziałem Direct3D 11.

Wojciech Sura wojciechsura@gmail.com

Programista w firmie Optopol S.A. zajmującej się produkcją sprzętu i oprogramowania


do pozyskiwania, analizy i wizualizacji obrazów medycznych. Programuje od przeszło
dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne projekty.

reklama

CYBERCOM POLAND oferuje swoje usługi


w Polsce już od 15 lat. Główne obszary działań to
telekomunikacja, przemysł, media, bankowość
char[][] l = new char[3][]; i finanse, handel detaliczny oraz sektor publiczny.
while (true) { Specjalizuje się w rozwiązaniach internetowych,
l[0]=("Tworzymy innowacyjne rozwiązania B2B "+ bezpieczeństwa, usług mobilnych oraz teleko­
"na rynki UE od 2002 roku").toCharArray();
l[1]= "Android, JEE, Oracle, RoR".toCharArray(); munikacyjnych. Świadczy pełen zakres usług
l[2]= "Zapraszamy do wspołpracy!".toCharArray(); consultingowych i outsourcingowych, testowa­
log.severe("www.I"+l[1][1]+"f"+l[1][5]+l[1][1]+ nia oraz R&D dla dużych i średnich firm.
l[1][5]+l[0][0]+l[0][19]+".pl/l00p");
} www.cybercom.com/pl/
PROGRAMOWANIE URZĄDZEŃ
Łukasz Górski

Wykorzystanie sensora Kinect


w systemie Windows
Sensor Kinect, umożliwiający interakcję z konsolą Xbox 360 za pomocą gestów i po-
leceń głosowych, zyskał wielką popularność i przychylne recenzje na rynku elektro-
niki użytkowej. Nie powinno więc dziwić, że firma Microsoft postanowiła rozszerzyć
bazę użytkowników opracowanej przezeń technologii. Wraz z opublikowaniem od-
powiedniego SDK dla systemu Windows możliwe stało się wykorzystanie tej techno-
logii również w oprogramowaniu działającym na komputerach osobistych. Co więcej,
1 lutego 2012 r. na rynek wypuszczona została nowsza wersja sensora, przeznaczo-
na do współpracy z systemem Windows, lepiej wspierana przez SDK i zawierająca
pewne ulepszenia firmware’u. Tym samym Microsoft tym bardziej podkreślił swoje
wsparcie dla rozwoju technologii NUI (Natural User Interface – naturalny interfejs
użytkownika) również w segmencie komputerów PC.

W
ramach artykułu wskażemy, w jaki sposób uzy- ■■ strumień głębi – pozwala pobrać informacje o odległo-
skać dostęp do strumieni danych przesyłanych ści obiektów od sensora; Kinect identyfikować może te
przez sensor; nabytą wiedzę wykorzystamy do znajdujące się w odległości od ok. 800 mm do 4000
zaimplementowania prostego programu analizującego mm (w wersji dla Windows – od ok. 400 mm, z pewny-
gesty oraz mowę użytkownika i pokażemy, jak wyko- mi ograniczeniami),
rzystać go do sterowania aplikacjami uruchomionymi na ■■ strumień danych szkieletowych – transmituje infor-
komputerze – takimi, jak przeglądarka zdjęć czy program macje o położeniu wyróżnionych elementów ciała (jo-
prezentacyjny. ints w terminologii Kinect SDK) maksymalnie dwóch
Wcześniej jednak wskażemy na możliwości techniczne osób (śledzenie aktywne). Elementy te przedstawia
urządzenia. rysunek 2. Dodatkowo sensor może udostępnić dane
o położeniu sylwetek czterech dodatkowych osób (bez
BUDOWA I MOŻLIWOŚCI SENSORA szczegółowych informacji o położeniu poszczególnych
joints – śledzenie pasywne). Funkcjonalność ta zwalnia
Generalnie w sensorze wyróżnić można następujące ele- programistę z konieczności implementacji algorytmów
menty (por. rysunek 1): wykrywających, na podstawie strumieni wideo i głębi,
sylwetkę ludzką i dokonywania analizy jej położenia.
1. Czteroelementowy zestaw mikrofonów – umożliwia Pozwala to skupić się na implementacji rozwiązań re-
rejestrację dźwięku i lokalizację jego źródła w prze- alizujących docelową funkcjonalność tworzonego przez
strzeni; mikrofony wyposażone są w funkcję redukcji programistę oprogramowania,
hałasu. ■■ strumień danych audio – umożliwiający redukcję ha-
2. Emiter podczerwieni – generujący wiązkę promieni łasu, analizę źródła dźwięku i rozpoznawanie mowy
podczerwonych, wykorzystywanych przez kamerę głę- (wraz z Microsoft Speech SDK).
bokości (4) do pomiaru odległości obiektów znajdują-
cych się w polu widzenia sensora. INSTALACJA SDK I KONFIGURACJA
3. Kamera rejestrująca strumień wideo. ŚRODOWISKA
4. Kamera głębokości, opisana wyżej w pkt. 2.
Przejdziemy teraz do praktycznej części artykułu. Czytel-
Wskazane wyżej możliwości techniczne powodują, iż nicy chcący wypróbować prezentowane programy powin-
programista ma dostęp do następujących strumieni ni (oprócz oczywiście posiadania sensora Kinect – czy to
danych: w wersji dla Xbox-a, czy to dla Windowsa) zainstalować
SDK, dostępne na stronie http://kinectforwindows.org.
■■ strumień wideo – w rozdzielczościach 640x480 (RGB Wykorzystanie SDK wymaga odpowiedniego skonfigu-
i YUV) i 1280x960 (wyłącznie RGB), rowania środowiska programistycznego. Zadbać należy

56 / 1 . 2012 . (1)  /


WYKORZYSTANIE SENSORA KINECT W SYSTEMIE WINDOWS

o dodanie do projektu referencji do zestawu Microsoft.


Research.Kinect. Odpowiednia biblioteka dynamiczna do-
myślnie znajduje się w folderze C:\Program Files\Micro-
soft SDKs\Kinect\v1.0\Assemblies.

Rysunek 3. Niezbędne referencje


Rysunek 1.
Elementy sensora (źródło: J. Webb, J. Ashley, Beginning Kinect
PIERWSZY PROGRAM – STRUMIEŃ Programming with the Microsoft Kinect SDK, wyd. Apress
2012, s. 10)
WIDEO
Mając za sobą niezbędne przygotowania, możemy przejść
do prezentacji przykładów wykorzystania urządzenia.
Na samym początku pokażemy, w jaki sposób uzyskać
w aplikacji dostęp do strumienia wideo, implementując
następujące proste GUI:

Rysunek 2.
Śledzone elementy (źródło: http://i.msdn.microsoft.com/dynimg/
Rysunek 4. Pierwszy program w działaniu IC539011.png)

Interfejs wykorzystuje bibliotekę Windows Forms i składa danych. Obsługę strumienia wideo prezentuje Listing 2
się z pojedynczej formy wraz z kontrolką PictureBox, któ- (str. 59).
rą nazwiemy videoStream. Na początku otwieramy ramkę wideo i sprawdzamy,
Kod inicjujący w naszym wypadku wyglądał będzie tak, czy nie jest pusta. W wypadku bowiem, gdyby kod obsłu-
jak pokazano w Listingu 1 (str. 59). gujący zdarzenie działał zbyt powoli i nie nadążał z sukce-
Rozpoczynamy od pobrania obiektu reprezentującego sywną obsługą kolejnych zgłoszeń, zmienna image miała-
sensor – z kolekcji KinectSensors, statycznego pola kla- by wartość null.
sy KinectSensor, reprezentującego wszystkie urządzenia Następnie tworzymy bufor, w którym zapisane zostaną
Kinect dostępne w systemie. Następnie inicjujemy stru- dane pochodzące ze strumienia wideo – i wypełniamy go.
mień danych wideo (ColorStream), a także dodajemy ob- W dalszej części konwertujemy „surowe” dane pochodzą-
sługę zdarzenia AllFramesReady – wywoływanego, gdy ce z sensora do postaci bitmapy colorBitmap, odwracamy
dostępna jest nowa ramka danych. Możliwe byłoby rów- (dane stanowią „lustrzane odbicie” rzeczywistych) i wy-
nież obsłużenie zdarzenia ColorFrameReady, związanego świetlamy w kontrolce videoStream.
wyłącznie ze strumieniem danych wideo.
Rozwiązanie oparte na zdarzeniach zalecane jest jako DRUGI PROGRAM – STRUMIEŃ GŁĘBI
zapewniające największą responsywność aplikacji; alter- I DANYCH SZKIELETOWYCH
natywnie możliwe jest odpytywanie w pętli o zawartość
kolejnych ramek, korzystając z wywołań w rodzaju ki- W następnym przykładowym programie pokażemy,
nect.ColorStream.OpenNextFrame(1000) – gdzie jako w jaki sposób uzyskać dostęp do danych z czujnika głę-
argument podajemy czas oczekiwania na nowy zestaw bi. GUI będzie analogiczne do poprzedniego i składać

/ www.programistamag.pl / 57
PROGRAMOWANIE URZĄDZEŃ

się będzie z pojedynczej kontrolki PixtureBox o nazwie Przystąpimy teraz do naniesienia na bitmapę punktów
depthStream: położenia joints. Służy do tego następujący kod (zobacz
str. 60).
Otwieramy ramkę danych szkieletowych i również
sprawdzamy, czy nie jest pusta. Podobnie jak wyżej,
dane zapisane w ramce kopiujemy do bufora skeletons
– i iterujemy po zapisanych danych szkieletowych użyt-
kowników śledzonych aktywnie (w wypadku których pole
Track­ingState ma wartość SkeletonTrackingState.
Tracked). Zaznaczenie pozycji każdego joint na ekranie
wymaga skonwertowania pozycji ze współrzędnych 3D
(w ten sposób ich pozycja identyfikowana jest w strumie-
niu danych szkieletowych) na dwuwymiarowe współrzęd-
ne bitmapy. Służy do tego metoda MapSkeletonPoint-
ToDepth klasy KinectSensor. Wypisujemy pozycje tych
tylko joints, które znajdują się w polu widzenia sensora
Rysunek 5. Drugi program w działaniu i są przezeń śledzone (pole TrackingState o wartości Jo-
intTrackingState.Tracked).
Obiekt będzie tym jaśniejszy, im bardziej oddalony będzie Wreszcie, w ostaniej linii, (odwrócona) bitmapa wy-
od sensora. Sylwetki użytkowników oznaczane będą kolo- świetlana jest w kontrolce PictureBox.
rem, tło zaś prezentowane będzie w odcieniach szarości. Dla porządku należy dodać, że obsługa zdarzeń
Dodatkowo – zaznaczone zostaną punkty ciała (joints) w osobnym wątku wykonania znacznie podniosła wydaj-
śledzone przez sensor. ność aplikacji na komputerze, na którym była testowana.
Kod inicjujący przedstawia się analogicznie do tego Nie stanowi to jednak istoty niniejszego artykułu, dlate-
z pierwszego programu (Listing 3). go też część aplikacji związana z obsługą wątków została
Włączenie strumienia danych szkieletowych pozwoli pominięta.
nie tylko zaznaczać śledzone punkty ciała, ale również
identyfikować sylwetki użytkowników w strumieniu głębi, TRZECI PROGRAM –
a co za tym idzie – odpowiednio je kolorować. ROZPOZNAWANIE GESTÓW I MOWY
Kod obsługujący zdarzenie, dla jasności, podzielono na
dwie logiczne części, związane z obsługą odpowiedniego Wykorzystując powyższą wiedzę, przejść możemy do
strumienia. Zacznijmy od strumienia głębi. implementacji zapowiedzianego na wstępie programu
Ze względu na konieczność zdekodowania danych, kod rozpoznającego gesty. Niniejszy program ma chara­kter
jest nieco bardziej skomplikowany niż ten dla strumienia przykładowy, toteż jego funkcjonalność będzie nieco
wideo. Wygląda w sposób następujący (Listing 4). ograniczona: śledzić będziemy gesty wykonywane przez
Na początku wprowadzamy siedem barw bazowych, prawą rękę; aplikacja rozpozna gest polegający na jej
identyfikujących tło i sześciu użytkowników. Nastę­pnie przesunięciu w lewo i prawo (podobne gesty rozpoznawa-
definiujemy bitmapę, którą wyświetlać będziemy w kon- ne są w interfejsach telefonów komórkowych jako „dalej”
trolce depthStream GUI programu, a także bufor na i „wstecz”). Aby zbytnio nie komplikować kodu źródłowe-
strumień danych głębi. Pobieramy także informacje go, zakładamy, że z sensora korzystać będzie pojedynczy
o zakresie, w którym sensor jest w stanie identyfikować użytkownik, który nie będzie opuszczał jego pola widze-
oddalenie obiektów. W wypadku Kinecta konsoli Xbox nia. Dla prostoty pominiemy również kwestie związane
uzyskano przedział od 800 mm do 4000 mm. Następnie z architekturą aplikacji rozpoznającej gesty.
otwieramy strumień i kopiujemy dane do bufora. Czytelnik nie powinien mieć problemów z ominięciem
W tym miejscu kod nieco się komplikuje. Nie może- tych ograniczeń w swojej aplikacji. Bardziej rozbudowa-
my, jak w poprzednim programie, po prostu skopiować ne przykłady w tym względzie znaleźć można w Interne-
danych z bufora do bitmapy, lecz musimy poddać je ob- cie – por. wykaz adresów na końcu niniejszego artykułu.
róbce. Każdy „piksel” danych strumienia głębi to poje- W tym wypadku inicjować musimy jedynie strumień
dyncza dana typu short. Jej pierwszych 13 bitów zawie- danych szkieletowych (Listing 6, str. 60).
ra informacje o oddaleniu (w mm) punktu od sensora, Rozpoznawanie gestów założonych na wstępie niniej-
a ostatnie 3 – identyfikują piksele, które składają się na szego artykułu nie jest trudne. Dla zbadania, czy ręka
sylwetkę użytkownika (każdy użytkownik ma przyznaną wykonała gest „dalej”, wystarczy sprawdzić, czy w ciągu
liczbę od 1 do 7; 0 przeznaczone jest dla tła). Informacje ostatnich dwóch sekund spełniono łącznie następujące
te wydobywamy w pętli i przechowujemy w zmiennych warunki:
distance i idx. Następnie sprawdzamy, czy odczytana
odległość zawiera się w zakresie śledzenia sensora i jeśli ■■ użytkownik przesuwał dłoń wyłącznie w prawo,
tak – odpowiadający piksel wyjściowej bitmapy „cieniuje- ■■ na odległość co najmniej 40 cm,
my” odpowiednią barwą. ■■ dłoń pozostawała stabilna w poziomie,

58 / 1 . 2012 . (1)  /


WYKORZYSTANIE SENSORA KINECT W SYSTEMIE WINDOWS

Listing 1. Inicjalizacja strumienia wideo

KinectSensor kinect = KinectSensor.KinectSensors[0];


kinect.ColorStream.Enable(ColorImageFormat.RgbResolution640x480Fps30);
kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(nui_AllFramesReady);
kinect.Start();

Listing 2. Obsługa strumienia wideo

ColorImageFrame image = e.OpenColorImageFrame();


if (image == null) return;
byte[] colorPixels = new byte[image.PixelDataLength];
image.CopyPixelDataTo(colorPixels);
Bitmap colorBitmap = new Bitmap(image.Width, image.Height, PixelFormat.Format32bppRgb);
BitmapData data = colorBitmap.LockBits(new Rectangle(0, 0, image.Width, image.Height), ImageLockMode.
WriteOnly, colorBitmap.PixelFormat);
IntPtr ptr = data.Scan0;
Marshal.Copy(colorPixels, 0, ptr, image.Width * image.Height * image.BytesPerPixel);
colorBitmap.UnlockBits(data);
colorBitmap.RotateFlip(RotateFlipType.RotateNoneFlipX);
videoStream.Image = colorBitmap;

Listing 3. Inicjalizacja strumieni głębi i danych szkieletowych

kinect = KinectSensor.KinectSensors[0];
kinect.DepthStream.Enable(DepthImageFormat.Resolution320x240Fps30);
kinect.SkeletonStream.Enable();
kinect.AllFramesReady += new EventHandler<AllFramesReadyEventArgs>(kinect_AllFramesReady);
kinect.Start();

Listing 4. Obsługa strumienia głębi

Color[] colors = { Color.White, Color.Red, Color.Green, Color.Blue, Color.Yellow, Color.Orange, Color.


Magenta };
Bitmap bitmap = new Bitmap(320, 240);
short[] bytes = new short[320 * 240];
int min = kinect.DepthStream.MinDepth,
max = kinect.DepthStream.MaxDepth;
var image = e.OpenDepthImageFrame();
if (image == null) return;
BitmapData bits = bitmap.LockBits(new Rectangle(0, 0, bitmap.Width, bitmap.Height), ImageLockMode.
WriteOnly, PixelFormat.Format32bppRgb);
image.CopyPixelDataTo(bytes);
unsafe
{
byte* pixels = (byte*)bits.Scan0.ToPointer();
for (int x = 0; x < bitmap.Width; x++)
for (int y = 0; y < bitmap.Height; y++)
{
short distance = (short)(bytes[x * 240 + y] >> DepthImageFrame.PlayerIndexBitmaskWidth);
int idx = bytes[x * 240 + y] & DepthImageFrame.PlayerIndexBitmask;
byte red = 0, green = 0, blue = 0;
if (distance != kinect.DepthStream.TooNearDepth && distance != kinect.DepthStream.
TooFarDepth && distance != kinect.DepthStream.UnknownDepth)
{
red = (byte)(colors[idx].R * (distance - min) / (max - min));
green = (byte)(colors[idx].G * (distance - min) / (max - min));
blue = (byte)(colors[idx].B * (distance - min) / (max - min));
}
pixels[x * 4 * 240 + y * 4] = blue;
pixels[x * 4 * 240 + y * 4 + 1] = green;
pixels[x * 4 * 240 + y * 4 + 2] = red;
}
}
bitmap.UnlockBits(bits);

/ www.programistamag.pl / 59
PROGRAMOWANIE URZĄDZEŃ

Listing 5. Obsługa strumienia danych szkieletowych

SkeletonFrame skeletonFrame = e.OpenSkeletonFrame();


if (skeletonFrame == null) return;
Skeleton[] skeletons = new Skeleton[skeletonFrame.SkeletonArrayLength];
skeletonFrame.CopySkeletonDataTo(skeletons);
using (Graphics graphics = Graphics.FromImage(bitmap))
foreach (Skeleton skeleton in skeletons)
if (skeleton.TrackingState == SkeletonTrackingState.Tracked)
foreach (Joint joint in skeleton.Joints)
if (joint.TrackingState == JointTrackingState.Tracked)
{
DepthImagePoint point = kinect.MapSkeletonPointToDepth(joint.Position, kinect.DepthStream.
Format);
graphics.FillEllipse(Brushes.Red, point.X - 3, point.Y - 3, 7, 7);
}
bitmap.RotateFlip(RotateFlipType.RotateNoneFlipX);
depthStream.Image = bitmap;

Listing 6. Inicjalizacja programu rozpoznającego gesty

kinect = KinectSensor.KinectSensors[0];
kinect.SkeletonStream.Enable();
kinect.SkeletonFrameReady += new EventHandler<SkeletonFrameReadyEventArgs>(kinect_SkeletonFrameReady);
kinect.Start();

Listing 7. Klasa przechowująca historię położeń śledzonych punktów ciała

class Positions
{
public SkeletonPoint Hand { get; set; }
public SkeletonPoint HipCenter { get; set; }
public SkeletonPoint ShoulderCenter { get; set; }
public DateTime Time { get; set; }
}

Listing 8. Rejestrowanie położeń śledzonych punktów ciała

SkeletonFrame frame = e.OpenSkeletonFrame();


if (frame == null) return;
Skeleton[] skeletons = new Skeleton[frame.SkeletonArrayLength];
frame.CopySkeletonDataTo(skeletons);
var tracked = (from skeleton in skeletons where skeleton.TrackingState == SkeletonTrackingState.
Tracked select skeleton).FirstOrDefault();
if (tracked == null) return;
var position = new Positions { Hand = tracked.Joints[JointType.HandRight].Position,
HipCenter = tracked.Joints[JointType.HipCenter].Position,
ShoulderCenter = tracked.Joints[JointType.ShoulderCenter].Position,
Time = DateTime.Now };
if (_positions.Count == 90) { _positions.RemoveAt(0); }
_positions.Add(position);

if (IsSwypeRight())
{
_positions.Clear();
}
if (IsSwypeLeft())
{
_positions.Clear();
}

■■ przemieszczała się wyłącznie dłoń użytkownika; reszta cy i górnej części tułowia – w celu sprawdzenia założenia
sylwetki pozostawała (względnie) nieruchomo. o stabilności całej sylwetki.
W funkcji obsługi zdarzenia wybieramy (jedyną) śle-
W celu ich weryfikacji w programie rejestrować będziemy dzoną aktywnie sylwetkę i zapisujemy rejestrowane in-
dane z ostatnich 90 ramek danych szkieletowych. Prze- formacje w liście _positions (Listing 8).
chowamy je w następującej klasie (Listing 7). Sprawdzenie, czy wykonano gest, dokonywane jest
Zapisujemy więc informacje o położeniu dłoni i reje- w metodach IsSwypeRight oraz IsSwypeLeft. Pierwsza
strujemy czas, w którym położenie to zostało odczytane. z nich prezentuje się następująco (Listing 9).
Dodatkowo przechowujemy informacje o pozycji miedni- W powyższym kodzie za pomocą komentarzy oznaczo-

60 / 1 . 2012 . (1)  /


WYKORZYSTANIE SENSORA KINECT W SYSTEMIE WINDOWS

Listing 9. Detekcja gestu

private bool IsSwypeRight()


{
if (_positions.Count == 0) return false;
Positions last = _positions.Last();
for (int i = _positions.Count - 2; i >= 0 && last.Time - _positions[i].Time <= TimeSpan.
FromSeconds(2); i--) //1
{
if (!(_positions[i + 1].Hand.X > _positions[i].Hand.X) || //2
!(Math.Abs(_positions[i].Hand.Y - last.Hand.Y) <= 0.1f) || //3
!(Math.Abs(_positions[i].HipCenter.X - last.HipCenter.X) <= 0.1f) || //4
!(Math.Abs(_positions[i].ShoulderCenter.X - last.ShoulderCenter.X) <= 0.1f)) //5
return false;
if (last.Hand.X - _positions[i].Hand.X >= 0.4f) //6
return true;
}
return false;
}

Listing 10. Reakcja na gesty

if (IsSwypeRight())
{
_positions.Clear();
SendKeys.Send("{RIGHT}");
}
if (IsSwypeLeft())
{
_positions.Clear();
SendKeys.Send("{LEFT}");
}

Listing 11. Inicjalizacja rozpoznawania mowy

var info = (from ee in SpeechRecognitionEngine.InstalledRecognizers() where ee.Id == "SR_MS_en-US_


Kinect_11.0" select ee).FirstOrDefault();
var engine = new SpeechRecognitionEngine(info.Id);
var gb = new GrammarBuilder();
gb.Culture = info.Culture;
gb.Append("rotate");
var directions = new Choices(new string[] { "left", "right" });
gb.Append(new SemanticResultKey("direction", directions));
engine.LoadGrammar(new Grammar(gb));
engine.SpeechRecognized +=new EventHandler<SpeechRecognizedEventArgs>(engine_SpeechRecognized);

Listing 12. Rozpoznawanie mowy

public void engine_SpeechRecognized (object sender, SpeechRecognizedEventArgs e)


{
if (e.Result.Confidence <= 0.8)
return;
if (""+e.Result.Semantics["direction"].Value == "left")
SendKeys.Send("^,");
else
SendKeys.Send("^.");
}

Listing 13. Inicjalizacja strumienia audio

var stream = kinect.AudioSource.Start();


engine.SetInputToAudioStream(stream,
new SpeechAudioFormatInfo(
EncodingFormat.Pcm, 16000, 16, 1,
32000, 2, null));

engine.RecognizeAsync(RecognizeMode.Multiple);

/ www.programistamag.pl / 61
PROGRAMOWANIE URZĄDZEŃ

no miejsca, gdzie badane są wskazane na wstępie warun- W pierwszej kolejności przygotowujemy silnik rozpo-
ki co do pozycji poszczególnych joints: znawania mowy, opierając się na tym zainstalowanym już
w systemie, dostępnym dla języka angielskiego (polski
1. badamy ramki z ostatnich 2 sekund, nie jest niestety dostępny), identyfikowanym przez ciąg
2. sprawdzamy, czy użytkownik przemieszczał prawą dłoń znaków „SR_MS_en-US_Kinect_11.0”. Następnie przygo-
wyłącznie w prawo, towujemy strukturę gramatyczną rozpoznawanych przez
3. sprawdzamy, czy dłoń pozostała stabilna w poziomie, sensor poleceń. Wskazujemy, iż składać się one będą ze
tj. nie przemieściła się o więcej niż o 10 cm, słowa „rotate” oraz kierunku tego obrotu – alternatywnie
4. sprawdzamy stabilność położenia punktu odpowiada- „left” lub „right”. Obsługa zdarzenia polegającego na
jącego miednicy – również, czy nie przemieściła się rozpoznaniu polecenia głosowego miała będzie miejsce
o więcej, niż o 10 cm, w metodzie engine_SpeechRecognized (Listing 12, po-
5. badamy stabilność górnej części tułowia, analogicznie przednia strona).
do położenia miednicy, Badamy najpierw poziom pewności, że dane polecenie
6. ostatecznie: sprawdzamy, czy dłoń przesunięto na od- głosowe zostało w istocie rozpoznane. Jeżeli przekracza
ległość co najmniej 40 cm. on – dobrany doświadczalnie – poziom 80%, to przesyła-
my do systemu odpowiedni kod odpowiedzialny za obrót
Łączne spełnienie wszystkich powyższych warunków obrazka, uzależniony od wskazanego przez użytkownika
spowoduje, iż odczytany zostanie gest przesunięcia dło- kierunku.
ni w prawo. Wszelkie wartości liczbowe powyżej zostały Pozostaje jeszcze skonfigurowanie biblioteki Micro-
dobrane eksperymentalnie. Kod dla gestu przesunięcia soft Speech do współpracy z sensorem – wskazanie, że
w lewo będzie, oczywiście, analogiczny. źródłem rozpoznawanego dźwięku jest strumień audio
Pozostało jedynie wskazać, w jaki sposób wykorzystać Kinecta.
odczytane gesty. Proponuję zmodyfikować końcową część Uruchomienie strumienia następuje poprzez wywołanie
funkcji obsługi zdarzenia SkeletonFrameReady w nastę- odpowiedniej metody: kinect.AudioSource.Start();
pujący sposób (Listing 10, poprzednia strona). powinno ono nastąpić co najmniej cztery sekundy po
Spowoduje to wysłanie do aktywnego okna zdarze- wywołaniu metody kinect.Start() (por. np. Listing
nia odpowiadającego za naciśnięcie lewego lub prawego 1) – dopiero bowiem po takim czasie Kinect gotowy jest
klawisza kursora. Jeżeli aktywną aplikacją byłaby prze- przesyłać dane audio. Następnie strumień ten wskazuje-
glądarka zdjęć, spowodowałoby to zmianę oglądanej fo- my jako źródłowy dla biblioteki Microsoft Speech i inicju-
tografii; w wypadku aktywnego PowerPoint’a – zmianę jemy (asynchroniczne) rozpoznawanie mowy (Listing 13,
aktualnego slajdu. poprzednia strona).
Przykład ten rozbudujemy teraz o możliwość rozpo- Warto zauważyć, że – podobnie j/w – uruchomienie
znawania mowy – dodamy możliwość obracania aktualnie rozpoznawania mowy w osobnym wątku wykonania zna­
oglądanego w przeglądarce zdjęć obrazka poprzez wyda- cznie podnosi efektywność całej aplikacji.
nie głosowego polecenia „rotate left/right”.
W tym celu skorzystamy z biblioteki Microsoft Speech ZAKOŃCZENIE
Platform SDK w wersji 11, dostępnej bez opłat na stro-
nie firmy Microsoft. W tym miejscu należy zaznaczyć, że W artykule przedstawiono sposób uzyskania dostępu do
większość źródeł wskazuje na konieczność skorzystania strumieni danych udostępnionych przez SDK. Czytelników
z 32-bitowej wersji SDK; następujący przykład z powo- chcących pozyskać więcej informacji zachęcam do odwie-
dzeniem jednak współpracuje z jego 64-bitową odmianą. dzenia adresów internetowych wymienionych w ramce
Po zainstalowaniu biblioteki należy dodać do projek- „W Sieci”. Polecam również dalsze eksperymentowanie
tu odpowiednią referencję, analogicznie jak w wypadku z urządzeniem.
SDK Kinecta. Odpowiedni plik DLL domyślnie dostępny
jest w następującej lokalizacji: C:\Program Files\Micro-
„W SIECI”:
soft SDKs\Speech\v11.0\Assembly\Microsoft.Speech.dll.
Z biblioteki korzysta następujący kod (Listing 11, po- PP http://kinectforwindows.org
przednia strona). PP http://msdn.microsoft.com/pl-pl/library/hh428637.aspx
PP http://kinecttoolbox.codeplex.com/

Łukasz Górski lukasz.gorski@gmail.com

Interesuje się zagadnieniami programowania równoległego i funkcyjnego. Na co dzień


korzysta z Pythona, Javy i C++. Posiada doświadczenie w pracy z rozwiązaniami firmy
Oracle

62 / 1 . 2012 . (1)  /


PRENUMERATA

Prenumerata - korzyści:
»» Prenumerata magazynu Programista to wygodny sposób na dostęp do aktualnej i przydatnej wiedzy dla specjalistów IT.
»» Nie wychodząc z domu będziesz otrzymywał czasopismo tradycyjną pocztą lub dostaniesz e-mail z informacją o
nowym wydaniu do pobrania.
»» Cena egzemplarza w prenumeracie jest niższa względem pojedyńczego numeru.
»» Prenumerując magazyn Programista zyskujesz czas i pieniądze.

Prenumerata dla firm


Zadbaj by w Twojej firmie specjaliści mieli dostęp do aktualnej wiedzy! W Prenumeracie dla firm możesz otrzymać:
»» Kilka egzemplarzy magazynu Programista na papierze, aby wszyscy członkowie zespołu mieli dostęp do czasopisma.
»» Licencje na wiele stanowisk do wydań elektronicznych (.pdf, ePub, .mobi).
»» Atrakcyjne rabaty.
»» Reklamę w magazynie.
W sprawie oferty prosimy o kontakt: prenumerata@programistamag.pl

ZAMAWIAM
CZAS TRWANIA PRENUMERATĘ OD
RODZAJ PRENUMERATY PRENUMERATY CENA BRUTTO NUMERU:
Wydanie drukowane Roczna 12 wydań 199,00 zł + 36 zł przesyłka*
Wydanie drukowane Dwuletnia 24 wydania 398,00 zł + 72 zł przesyłka*
Wydanie elektroniczne Roczna 12 wydań 99,00 zł
Wydanie elektroniczne Dwuletnia 24 wydania 198,00 zł
Dane zamawiającego i Imię i nazwisko:
adres wysyłki
Adres wysyłki:
e-mail:
Telefon:
Dla firm (faktura) Nazwa firmy:
Adres siedziby firmy:
NIP:
Prenumerata zostanie uruchomiona po zaksięgowaniu wpłaty na konto wydawcy:
Anna Adamczyk, ul.Dereniowa 4/47, 02-776 Warszawa,
Bank Pekao S.A. 34 1240 1125 1111 0010 2401 3333

Data i podpis zamawiającego:

*Do ceny wydań drukowanych doliczamy 3 zł za wysyłkę jednego egzemplarza

W przypadku pytań, napisz do nas: prenumerata@programistamag.pl


INŻYNIERIA OPROGRAMOWANIA
Sławomir Sobótka

Domain Driven Design krok po kroku


Część II: Zaawansowane modelowanie DDD
– techniki strategiczne: konteksty i architektu-
ra zdarzeniowa
Modele nietrywialnych domen wymagają struktury. Struktury, która pomaga nam
okiełznać chaos. W zestawie technik DDD znajdują się podejścia strukturyzacji sys-
temu na każdym poziomie abstrakcji. Techniki DDD stanowią również „rusztowania
mentalne”, które prowadzą procesy myślowe modelarza w kierunku bardziej ade-
kwatnych modeli. W artykule zostaną przedstawione techniki destylacji domen, wy-
dzielania kontekstów, komunikacji pomiędzy kontekstami, porządkowania złożonych
kontekstów oraz na poziomie mikro – efektywnego modelowania Agregatów.

Plan serii stwową strukturę, która separuje elementy modelu wg


ich podatności na zmiany.
Niniejszy tekst jest drugim z serii artykułów mających na celu Na zakończenie naszych rozważań powrócimy do oma-
szczegółowe przedstawienie kompletnego zestawu technik wianych w poprzedniej części Agregatów i zastanowimy
modelowania oraz nakreślenie kompletnej architektury apli­ się nad ich granicami. Poznamy techniki efektywnego
kacji wspierającej DDD. modelowania granic Agregatów oraz typowe błędy.
Część 1: Podstawowe Building Blocks DDD;
Część 2: Zaawansowane modelowanie DDD – techniki stra­ DESTYLACJA CORE DOMAIN - KIEDY
tegiczne: konteksty i architektura zdarzeniowa; NIE STOSUJEMY DDD
Część 3: Szczegóły implementacji aplikacji wykorzystującej
DDD na platformie Java (dwa podejścia: Spring i EJB 3.1 oraz Stosowanie technik DDD wnosi pewien narzut w proce-
JPA); sie tworzenia oprogramowania. Narzut ten jest inwesty-
Część 4: Skalowalne systemu w kontekście DDD – architek­ cją, która jednak nie zwraca się w każdym przypadku.
tura CqRS; W zwiążku z tym na wstępie modelowania DDD określa-
Część 5: Kompleksowe testowanie aplikacji opartej o DDD; my miejsca w systemie, w których stosujemy techniki
Część 6: Behavior Driven Development – Agile drugiej ge­ DDD, oraz miejsca, w których ich nie stosujemy.
neracji.
Supporting Domain – świadomy dług techniczny

STRATEGICZNA STRUKTURYZACJA – Relatywnie proste domeny nie wymagają technik DDD


RÓŻNE SKALE, JEDEN CEL – ich modelowanie z definicji nie wymaga złożonego
procesu mentalnego. Możemy je z powodzeniem zaim-
„Wszystko jest na miejscu i wszystko ma swoje miejsce” plementować w uproszczonej (1,2 – warstwowej) archi-
– cytat Benjamina Franklina jest chyba najlepszym pod- tekturze. Zwykle okazuje się, że nad prostymi domenami
sumowaniem strategicznych technik DDD. wykonujemy jedynie operacje CRUD (Create, Read, Upda-
Pierwszą decyzją, jaką podejmujemy podczas modelo- te, Delete), zatem warto skorzystać z mnogości wyboru
wania z wykorzystaniem DDD, jest określenie tych miej- framework'ów pozwalających generować niemal automa-
sce w całej „rozciągłości” systemu, w których będziemy tycznie aplikacje klasy „przeglądarka do bazy danych”.
stosować techniki DDD. Te relatywnie proste domeny nazywamy Supporting
Kolejnym poziomem strukturyzacji jest wydzielenie ja- Domain. W tych miejscach systemu decydujemy się na
snych granic Bounded Context – kontekstów, w których kompromisy jakościowe (jakość modelu, jakość kodu)
obowiązują osobne modele. – świadomie zaciągamy dług techniczny.
Separacja modeli jest ze wszech miar pożyteczna, jed- Przykładowo w naszym systemie możemy zdecydować
nak zwykle rodzi problem: domeny muszą ze sobą współ- się na wprowadzenie systemu komentarzy. Zakładając,
pracować. Zatem zastanowimy się, jak technicznie doko- że jego działanie nie jest krytyczne dla powodzenia całe-
nać mapowania kontekstów. Poszczególne modele mogą go przedsięwzięcia biznesowego, klasyfikujemy ten pod-
być wciąż złożone, dlatego nadamy im głębszą – war- system jako Supporting Domain. Warto zwrócić uwagę,

64 / 1 . 2012 . (1)  /


DOMAIN DRIVEN DESIGN KROK PO KROKU

że w innym kontekście podsystem komentarzy może być Modele odseparowane przez Bounded Context nie
krytyczny (ponieważ np. buduje zaufania) – wówczas mogą odnosić się do siebie wprost. Są to niezależne byty,
traktujemy go jako Core Domain. które mogą być rozwijane przez osobne zespoły. Oma-
wiane w dalszej części Wzorce Architektoniczne pozwa-
Generic domain – modele domen bazowych lają na hermetyzację modeli, która redukuje problemy
związane ze zmianami i pozwala na niezależne rozwijanie
Pewne domeny są generyczne w stosunku do naszej głów- modeli przez zespoły, które nie „wchodzą sobie w drogę”.
nej domeny. Przykładowo tworząc system służący do han- Projekt referencyjny (link w ramce „w sieci”) zawiera
dlu zawartością multimedialną, nie będziemy skupiać wy- 3 przykładowe Bounded Context: Sales, CRM, Shipping.
siłku mentalnego, pracując nad modułem fakturowania. W przypadku tego projektu BC odpowiadają modułom
Fakturowanie jest krytyczne (zależy nam na jakości tego systemu.
modułu), ale przede wszystkim nie stanowi ono głównej
wartości, oraz ponadto tego typu systemy istnieją na ryn- Granice kompetencji Ekspertów domenowych
ku (zarówno Open Source, jak i komercyjnym). Dlatego
też przykładowe fakturowanie możemy potraktować jako W systemach o większej skali mamy często do czy-
Generic Domain. Z podsystemami zaliczanymi do Generic nienia z sytuacją, gdzie korzystamy z wiedzy wie-
Domains integrujemy się w taki sposób, aby ich modele lu Ekspertów Domenowych. Eksperci ci specjalizują się
nie „przeciekały” do naszego głównego modelu. w hermetycznych domenach (sferach) wiedzy. Możliwość
Innym przykładem Generic Domain może być biblio- komunikacji merytorycznej pomiędzy ekspertami jest
teka do operowania na Grafach. Być może będziemy ją mocno ograniczona.
wykorzystywać w Module Magazynowym, aby sugerować W kontekście naszego przykładowego modelu: pojęcie
najkrótsze ścieżki, jakimi należy się poruszać pomiędzy „produktu” w Module Sprzedaży oznacza coś, co ma cha-
regałami w magazynach, aby jak najszybciej skomple- rakterystykę handlową, pewne atrybuty promocyjne, być
tować zamówienie. W tym przypadku nadbudowujemy może jest powiązane z profilami behawioralnymi klientów.
domenę Magazynu (regały, półki, korytarze) nad gene- Natomiast w Module Magazynowym jest to przedmiot,
ryczną domeną grafów (węzły, ścieżki, algorytmy wyszu- którego główną cechą jest przykładowo to, że zajmuje
kiwania ścieżek). określoną przestrzeń na regałach i być może wymaga
specjalnego traktowania podczas transportu.
Core Domain – miejsce dla DDD Jedynie ubóstwo naszego języka powoduje, że eksper-
ci domenowi socjalizujący się w sprzedaży i eksperci spe-
Omówiliśmy rodzaje domen, dla których nie stosujemy cjalizujący się w domenie magazynów używają podobnej
technik DDD. Techniki DDD mają zastosowanie (jako in- fali akustycznej, która brzmi „produkt”. Jednak na pozio-
westycja) w Core Domain. Core Domain to sfera logiki mie znaczenia to brzmienie budzi zupełnie różne skoja-
biznesowej, która jest głównym powodem, dla którego rzenia – skojarzenia z pojęciami pojawiające się w umy-
system powstaje. Być może model z Core Domain sta- śle jednego eksperta są w ogóle niedostępne dla umysłu
nowi przewagę systemu (klienta zamawiającego system) innego eksperta.
nad konkurencją. W Core Domain inwestujemy czas na- Zatem próba uogólnienia i „zsumowania” kilku mode-
szych najlepszych ludzi – Developerów oraz Ekspertów li wynikających z wiedzy osób o rozłącznych kompeten-
Domenowych. W DDD zakładamy, że modelowanie Core cjach jest zajęciem karkołomnym. W DDD takie podejście
Domain stanowi największe wyzwanie w cyklu produk- jest zaliczane do anty-wzorców i nazywa się Corporate
cji systemu. Zakładamy również, że słaby model stanowi Model Anti-Pattern.
wysokie ryzyko niepowodzenia przedsięwzięcia. Jeżeli eksperci domenowi znający na wskroś swe spe-
Podstawową techniką destylacji Core Domain jest cjalizacje nie są w stanie się porozumieć, to w jaki sposób
spisanie Dokumentu Wizji. Dokument ten powinien być możemy sobie wyobrazić sytuację, w której developerzy
krótki (np. 1/2 A4), dzięki temu skłoni jego twórcę do – nie znający zwykle żadnej z tych domen – mogą być
skupienia się na głównych (Core) założeniach, które zwy- w stanie stworzyć wspólny model kilku domen...?
kle dokładnie mapują się na czynniki przewagi, czyli Core Granice słownictwa Ekspertów Domenowych z Ubiquitous
Domain. Language wyznaczają granice modeli - Bounded Context.

GRANICE BOUNDED CONTEXT – Techniki uwspólniania modeli


GRANICE WSPÓLNEGO JĘZYKA
Część pojęć jest wspólna dla wszystkich Bounded Con-
Drugim poziomem strukturyzacji modelu jest wydzielanie text. Przykładowo klasa Money z poprzedniego artykułu
Bounded Context – kontekstów, do których jest przypisa- modeluje pojęcie, co do którego wszyscy Eksperci Dome-
ny Ubiquitous Language. Przypomnijmy w tym miejscu: nowi zgadzają się w każdym szczególe. Wspólne pojęcia
istnienie „wspólnego języka”, którym posługują się wszy- umieszczamy w tak zwanym Shared Kernel.
scy uczestnicy projektu – od Eksperta Domenowego po Inną techniką uwspólniania modeli jest podejście na-
Developerów - jest jednym z głównych założeń DDD. zywane w DDD: Conformist. Polega na tym, że jeden

/ www.programistamag.pl / 65
INŻYNIERIA OPROGRAMOWANIA

model jest zależnością do drugiego. Zmiany w jednym Współpraca poprzez dane


modelu powodują „katastrofę” w drugim. Z tego wzglę-
du jest to podejście, którego rozważenie zaleca się w W najbardziej podstawowym przypadku jeden BC po-
przypadków projektów wspieranych przez zespoły trzebuje pewnych danych z innego BC. W takim wypadku
outsourcingowe. komunikację możemy modelować poprzez Repozytoria.
Implementacja repozytorium w BC1 wywołuje Serwis
WSPÓŁPRACA KONTEKSTÓW Aplikacyjny (de facto API) BC2. Innymi słowy API BC2
– W KIERUNKU ARCHITEKTURY staje się źródłem danych w BC1. Pamiętajmy, że z za-
ZDARZENIOWEJ łożenia model domenowy nie „wycieka” ponad warstwę
Serwisów Aplikacyjnych. Zatem komunikacja musi nastę-
Modele domen wyznaczone przez Bounded Context są pować poprzez Obiekty Transferowe.
hermetyczne – oznacza to, że pojęcia z jednego modelu Dzięki takiemu podejściu uzyskujemy hermetyzację
nie „wyciekają” do innych modeli. Jednak systemy mają modeli. Model domeny BC2 nie jest znany w dome-
to do siebie, że ich składowe muszą współpracować. nie BC1. Zespół pracujący nad BC2, publikując API,
W poprzedniej części artykułu wprowadziliśmy wstęp- „podpisuje kontrakt” ze „światem zewnętrznym” i od
ny szkic warstwowej architektury aplikacji. Nad warstwą tej pory szczegółem implementacyjnym jest sposób,
logiki domenowej umieszczamy logikę aplikacji, modelu- w jaki kontrakt ten jest spełniony. Dzięki temu model
jącą Use Case/Sser Story/Serwisy SAO. Poniżej domeny domeny BC2 może rozwijać się niezależnie i we wła-
umieszczamy warstwę infrastruktury, która zawiera mię- snym tempie.
dzy innymi implementacje repozytoriów. Praktyka ta różni się w zasadniczy sposób od popular-
Zobaczmy, jak na bazie tych założeń możemy podejść nego podejścia, w którym moduły „gmerają sobie nawza-
do komunikacji pomiędzy Bounded Contexts. jem” w modelach danych.

Rysunek 1. Podejścia architektoniczne do współpracy pomiędzy Bounded Context

66 / 1 . 2012 . (1)  /


DOMAIN DRIVEN DESIGN KROK PO KROKU

Współpraca poprzez operacje kontrakt na poziomie implementacji technicznej musi-


my posłużyć się transakcyjnym silnikiem zdarzeń, który
Innym przypadkiem współpracy BC jest wywołanie API zagwarantuje nam, że zdarzenie zostanie propagowane
w celu wykonania pewnych operacji. Nie operujemy więc dopiero wówczas, gdy transakcja, w której go wygenero-
wprost na modelu domenowym innego BC, a wywołujemy wano, powiodła się. Przykładem standardu wspierającego
zbudowane nad nim API. Jest to klasyczna praktyka, ale komunikaty transakcyjne jest Java Message Service.
wspominamy o niej dla zachowania kompletności.
Listing 2. Listener zdarzenia w Bounded Context Sales
Współpraca poprzez sygnały – Zdarzenia – listener znajduje się na poziomie aplikacji
biznesowe
@EventListeners
public class CustomerStatusChangedListener{
Wywoływanie serwisów z innych BC wprowadza silny Co-
upling. W przypadku większej ilości współpracujących ze @EventListener(asynchronous=true)
public void handle(CustomerStatusChangedEvent
sobą modułów skończymy z Architekturą o strukturze
event) {
klasycznego Spaghetti. if (event.getStatus() == CustomerStatus.VIP){
Klasyczną techniką Decouplingu jest wprowadzenie calculateReabteForAllDraftOrders(
event.getCustomerId(), 10);
modelu zdarzeń. Zdarzenia są jedną z technik Inversion
}
of Control – obok Dependency Injection i Aaspect Orien- }
ted Programming. Musimy pamiętać jednak, że odwra-
cając kontrolę, jednocześnie ją tracimy. Zatem zdarzeń private void calculateReabteForAllDraftOrders(
Long customerId, int i) {
używamy wówczas, gdy chcemy modelować współpracę, // TODO Auto-generated method stub
gdzie nie zależy nam na kontroli kolejności, czasu ani re- }
zultatu wykonania. }
W DDD przy pomocy zdarzeń modelujemy istotne
z biznesowego punktu widzenia fakty, które zaszły w cy- Listing 2 przedstawia kod jednego z Listenerów naszego
klu życia Agregatu. przykładowego zdarzenia. Listener ten wprowadza nastę-
pującą funkcjonalność: jeżeli w module CRM klient stanie
Listing 1. Zgłoszenie zdarzenia w Bounded Context CRM w się VIPem, wówczas w module Sales nadajemy mu 10%
Agregacie Customer rabatu na wszystkie niezatwierdzone zamówienia.
Warto zwrócić uwagę na fakt, iż nasz przykładowy Li-
@Entity
public class Customer extends BaseAggregateRoot{ stener posiada jedynie logikę filtrowania zdarzeń, nato-
miast całą pracę deleguje do Serwisu Aplikacyjnego (API)
public enum CustomerStatus{ swojego modułu. Jest to zatem jedynie kod „klejący”.
STANDARD, VIP, PLATINUM
Jak widzimy na Rysunku 1, nasz przykładowy Listener
}
jest klientem do API na takim samym poziomie jak np.
@Enumerated(EnumType.STRING) klienty GUI. Jest to zatem jeden z wielu aktorów, którzy
private CustomerStatus status; mogą uruchamiać API. Przykładowo rabaty na zamówie-

public void changeStatus(CustomerStatus status){ nia mogą być naliczane nie tylko automatycznie podczas
if (status.equals(this.status)) promowania klienta do statusu VIP, ale również ręcznie,
return; poprzez GUI np. panelu administracyjnego.
this.status = status;
eventPublisher.publish( Zdarzenia wprowadzamy do architektury systemu z
new CustomerStatusChangedEvent( kilku powodów:
getEntityId(), status));
}
■■ decoupling modeli – opisany przykład
}
■■ asynchroniczne wykonanie dodatkowych operacji
– przykładowo w module CRM możemy wysyłać maile
Listing 1 ilustruje przykład zgłoszenia zdarzenia przez do klientów, których statusy zmieniono; wysyłka maili
Agregat Customer w momencie zmiany jego statusu. to czynność z jednej strony dodatkowa, a z drugiej po-
Klasa CustomerStatusChangedEvent jest nośnikiem in- tencjalnie długotrwała
formacji o tym, który klient (ID) zmienił status. Mode- ■■ otwarcie architektury na pluginy – „wpinanie” kolej-
lując zdarzenia, musimy podjąć decyzję o tym, jakie in- nych listenerów można potraktować jako dodawanie
formacje umieszczamy w klasach zdarzeń – w naszym pluginów
przykładzie jest to techniczne ID – być może lepszą de- ■■ rejestrowanie zdarzeń w celu ich przetwarzania i analizy
cyzją byłoby przeniesienie informacji o biznesowym nu- - np. z wykorzystaniem silników CEP (Complex Events
merze klienta... Processing) pozwalających na deklarowanie kwerend
Zdarzenie modeluje fakt, który miał miejsce i nie moż- filtrujących określone wzorce zdarzeń w czasie
na go zawetować – możemy co najwyżej zareagować ■■ składowanie zdarzeń jako modelu danych alternatyw-
w dodatkowy sposób na zaistniały fakt. Aby osiągnąć taki nego dla bazy relacyjnej – Event Sourcing

/ www.programistamag.pl / 67
INŻYNIERIA OPROGRAMOWANIA

Saga – model czasu w procesie biznesowym Każda metoda nasłuchująca konkretnego zdarzenia
w Sadze modyfikuje jej wewnętrzny stan (wzorzec pro-
Rozszerzeniem modelu zdarzeń jest Saga biznesowa. jektowy Memento) oraz sprawdza, czy warunki bizneso-
Technicznie saga jest persystentnym multi-listenerem. we potrzebne do zakończenia Sagi zostały spełnione.
Oznacza to, że obiekty Sagi nasłuchują wielu zdarzeń Więcej o Sagach na stronie projektu Levan oraz na
oraz ich stan jest utrwalany pomiędzy kolejnymi zdarze- stronie szyny NServiceBus, która wspiera model Sag po-
niami – z uwagi na potencjalnie długi czas upływający przez API silnika.
pomiędzy kolejnymi zdarzeniami.
Natomiast na poziomie koncepcyjnym Saga modeluje POZIOMY MODELU – OKIEŁZNAĆ
de facto czas. CHAOS
Listing 3 zawiera przykładowy kod Sagi, która mode-
luje przepływ zamówienia. Saga reaguje na zdarzenia: Mając do czynienia ze złożonymi modelami, możemy potrze-
stworzenia zamówienia, zatwierdzenia zamówienia, na- bować dodatkowego rusztowania „rozpinającego” strukturę
stąpienia wysyłki oraz dostarczenia zamówienia. Zakłada- modelu. Z czasem, gdy nasze rozumienie modelu pogłębia
my, że nie możemy przewidzieć kolejności pojawienia się się, zauważamy, że pewne jego elementy są ogólnym fun-
tych zdarzeń, oraz czas pomiędzy ich wystąpieniem może damentem, na którym opierają się specyficzne czynności.
sięgać miesięcy. Specyficzne czynności mają znowuż swe wariacje.

Listing 3. Saga modelująca przepływ zamówienia

@Saga
public class OrderShipmentStatusTrackerSaga extends
SagaInstance<OrderShipmentStatusTrackerData> {

@Inject
private OrderRepository orderRepository;

@SagaAction
public void handleOrderCreated(OrderCreatedEvent event) {
data.setOrderId(event.getOrderId());
completeIfPossible();
}

@SagaAction
public void handleOrderSubmitted(OrderSubmittedEvent event) {
data.setOrderId(event.getOrderId());
// do some business
completeIfPossible();
}

@SagaAction
public void orderShipped(OrderShippedEvent event) {
data.setOrderId(event.getOrderId());
data.setShipmentId(event.getShipmentId());
completeIfPossible();
}

@SagaAction
public void shipmentDelivered(ShipmentDeliveredEvent event) {
data.setShipmentId(event.getShipmentId());
data.setShipmentReceived(true);
completeIfPossible();
}

private void completeIfPossible() {


if (data.getOrderId() != null
&& data.getShipmentId() != null
&& data.getShipmentReceived()) {
Order shippedOrder = orderRepository.load(data.getOrderId());
shippedOrder.archive();
orderRepository.save(shippedOrder);
markAsCompleted();
}
}
}

68 / 1 . 2012 . (1)  /


DOMAIN DRIVEN DESIGN KROK PO KROKU

Różne części modelu charakteryzują się różną podat- Rozwarstwienie logiki to dopiero początek
nością na zmiany. Czytelnicy zaznajomieni z Archetypa-
mi Modeli Biznesowych znają pojęcie dwóch poziomów W poprzedniej części dokonaliśmy rozwarstwienia logiki
modelu: Operational Level i Knowledge Level. Natomiast na warstwę aplikacji i warstwę logiki domenowej. War-
w DDD nadajemy modelowi jeszcze głębszą strukturę stwa aplikacji modelu (czylu Use Case/User Story/Serwi-
i wyłaniamy w nim aż cztery poziomy... sy SAO) jest odpowiedzialna za:

Rysunek 2. Poziomy modelu domenowego (wraz z mapowaniem na Archetypowe Operational i Knowlege Level)

/ www.programistamag.pl / 69
INŻYNIERIA OPROGRAMOWANIA

■■ orkiestrację domeny – scenariusz sterowania obiekta- ku – sugestie na postawie analizy behawioralnej klien-
mi domenowymi ta, jego znajomych lub wszystkich klientów (polityka!).
■■ dodatkową logikę typową dla tej konkretnej aplikacji Innym przykładem jest model dobierania rabatu (jeżeli
■■ technikalia takie jak transakcje i bezpieczeństwo klientowi przysługuje wiele rabatów, np. z uwagi na: po-
zycję zamówienia, zawartość całego zamówienia, histo-
Warstwa logiki domenowej to miejsce, w którym mode- rię zamówień klienta oraz dodatkowe rabaty: dla VIPów,
lujemy przy pomocy Building Blocks logikę ograniczoną z okazji zimy itd.). Model dobierania rabatów może być
przez Bounded Context. dostrojony (policy!) tak, aby działał na korzyść klienta lub
W tym rozdziale zajmiemy się dalszą strukturyzacją właściciela systemu...
złożonych modeli w warstwie logiki domenowej.
GRANICE AGREGATÓW –
Capability MODELUJEMY NIEZMIENNIKI
Poziom Capability zawiera klasy modelujące potencjalne Na zakończenie omawiania zaawansowanego modelowa-
możliwości, jakie oferuje nasz system. W naszym przy- nia DDD poruszymy zagadnienie określania granicy Agre-
kładowym systemie umieścimy tutaj agregaty Product gatów. Jest to najważniejszy aspekt modelowania DDD,
i Client oraz Value Object Money. Posiadając produkty, który decyduje o powodzeniu lub klęsce modelowania.
użytkowników i pieniądze, możemy potencjalnie dalej Nieodpowiednio „zakreślone” granice Agregatów powo-
modelować: handel, usługi, reklamacje itd. Na tym po- dują powstanie modeli słabo podatnych na zmiany oraz
ziomie zmiany są relatywnie niezbyt częste. nieefektywnych z wydajnościowego punktu widzenia.
W części pierwszej przyjęliśmy dosyć intuicyjne grani-
Operations ce naszych Agregatów, natomiast teraz zastanowimy się
bardziej świadomie nad ich modelem.
Poziom Operations zawiera klasy modelujące konkretne Czytelnicy doświadczeni w modelowaniu DDD mogli
operacje, jakie aktualnie wspiera nasz system. W na- zwrócić uwagę na przykładowy Agregat Order, którego
szym przykładowym systemie umieścimy tutaj agregaty granica była zakreślona dosyć „chciwie”, co mogło poten-
Order, Invoice oraz Serwis Biznesowy BookKeeper. Skła- cjalnie powodować problemy z utrzymaniem i wydajno-
danie zamówień oraz wystawianie na ich podstawie przez ścią modelu.
księgowego faktur to konkretne operacje, jakie zbudo- Agregat w definicji DDD jest spójną jednostką zmiany
waliśmy nad modelem potencjału (produktami, klientami (pracy). Od strony praktycznej Agregat powinien modelo-
i pieniędzmi). Ten poziom jest średnio podatny na zmiany. wać i enkapsulować w swym wnętrzu niezmienniki. Przy-
kładowo, jeżeli model domenowy zakłada, że zawsze:
Policy
a+b=c
Poziom polityk niejako „dostraja” poziom Operacji. Przy-
kładowo Księgowy (Serwis Domenowy BookKeeper) to wówczas Agregat powinien enkapsulować a, b, c oraz
z poziomu Operacji nalicza podatki na różne sposoby zapewniać, że po wywołaniu każdej metody Agregatu nie-
– w zależności od wdrożenia systemu w różnych krajach zmiennik jest zachowany. W naszym przykładowym Agre-
lub w zależności od tego, kto jest klientem na fakturze. gacie Order modelujemy niezmienniki: 1) dodatnie/usu-
Zwróćmy uwagę, że sposób wybrania polityki (konfigura- nięcie produktu powoduje przeliczenie ceny po rabatach,
cja wdrożenia bądź decyzja w runtime) to funkcjonalność 2) dodatnie produktu już istniejącego nie powoduje poja-
aplikacji. wienia się nowej pozycji zamówienia, a jedynie zwiększe-
Obiekty na poziomie polityk modelują wariacje operacji nie ilości na już istniejącej pozycji.
biznesowych. Model ten jest mocno podatny na zmiany.
Warto zauważyć, że polityki na poziomie technicz- Technika analizy przypadków użycia
nym to technicznie rzecz biorąc domknięcia Operacji.
Domknięcia w sensie popularnych od pewnego czasu ję- Dosyć łatwo możemy doprowadzić do nieodpowiednie-
zyków funkcyjnych. Natomiast w starszych językach im- go modelowania granicy Agregatów, jeżeli zastosujemy
plementujemy je poprzez Wzorzec Projektowy Strategii klasyczne podejście polegające na grupowaniu rzeczow-
(zob. część I). ników w „worki”. Przykładowo, jeżeli odnajdziemy w mo-
delu rzeczownik Zamówienie, to „wrzucamy” do nie-
Decission Support go kolejne, „mniejsze” rzeczowniki, tworząc zbyt duży
agregat.
Niektóre systemy posiadają rodzaj „sztucznej inteli- Techniką, która sprawdza się lepiej w DDD, jest po-
gencji” - mniej lub bardziej wyrafinowane mechaniczny wstrzymanie się od wstępnego grupowania pojęć w tego
analityczne wspierające lub wręcz podejmujące decyzje. typu „worki” do momentu analizy przypadków użycia/
W naszym przykładowym systemie mógłby być to mo- operacji domenowych i grupowania ich pod kątem spój-
del sugerowania zamienników produktów w razie ich bra- nych jednostek zmiany.

70 / 1 . 2012 . (1)  /


DOMAIN DRIVEN DESIGN KROK PO KROKU

PODSUMOWANIE – O CZYM NIE ■■ efektywne pobieranie danych – Lazy Loading rozwiązu-


POWIEDZIELIŚMY je jedynie część z nich,
■■ równoległym dostępem do danych – im więcej danych
Omówiliśmy kolejne poziomy strategicznego modelowa- w agregacie, tym większe prawdopodobieństwo, że
nia domeny, począwszy od Destylacji Core Domen, czyli wielu użytkowników będzie miało interes w jego rów-
miejsca, gdzie stosujemy techniki DDD, poprzez wyła- noległej modyfikacji; Optimistic Locking jedynie chroni,
nianie na podstawie Ubiquitous Language granic mode- nas przed konsekwencjami, nie rozwiązując problemu
lu – Bounded Context, aż po granice poszczególnych ■■ skalowanie – jesteśmy zmuszeni, aby cały Agregat
Agregatów. persystować w tym samym modelu danych oraz na tej
Poruszyliśmy również zagadnienie struktury wielko- samej maszynie (zakładając, że rozproszone transak-
skalowej modelu, gdzie separujemy potencjał modelu cje są błędem w sztuce projektowania skalowalnych
od konkretnych operacji i modeli wspierających decyzje systemów).
oraz dostrajających je polityk. Kluczem nadawania tej
struktury jest zakres odpowiedzialności oraz podatność Zainteresowanych tą tematyką odsyłam do pierwszej
na zmiany. pozycji w ramce „w sieci”.
Warto zawrócić uwagę, iż zbyt chciwie zakreślone
Agregaty powodują następujące problemy:

W SIECI:
PP strategiczne modelowanie agregatów: http://dddcommunity.org/library/vernon_2011
PP oficjalna strona DDD http://domaindrivendesign.org
PP wstępny artykuł poświęcony DDD http://bottega.com.pl/pdf/materialy/sdj-ddd.pdf
PP przykładowy projekt: http://code.google.com/p/ddd-cqrs-sample/
PP sagi w NServiceBus http://www.nservicebus.com/sagas.aspx

Sławomir Sobótka slawomir.sobotka@bottega.com.pl

Programujący architekt aplikacji specjalizujący się w technologiach Java i efektyw-


nym wykorzystaniu zdobyczy inżynierii oprogramowania. Trener i doradca w firmie
Bottega IT Solutions. W wolnych chwilach działa w community jako: prezes Stowar-
zyszenia Software Engineering Professionals Polska (http://ssepp.pl), publicysta w
prasie branżowej i blogger (http://art-of-software.blogspot.com).

reklama

Loremipsumdolorsit
a m CHCESZ
e t , cBYĆ
o nNA se BIEŻĄCO?
ctetur
DOŁĄCZ DO NEWSLETTERA
adipiscingelit.Mauris
pos MAGAZYNU
u e r e c "PROGRAMISTA"
ondimentum
juston onvestibulum.
www.programistamag.pl
ullamgravidadolor
feugiatdiamfringilla
/ www.programistamag.pl / 71
KLUB LIDERA - IT
Michał Bartyzel, Mariusz Sieraczkiewicz

Dokumentowanie architektury
Jak zorganizować proces rozwoju architektury?
Jeśli skrupulatnie przejdziesz razem z nami przez opisane kroki, gwarantujemy, że
Ty i ludzie, z którymi współpracujesz, będziecie mieć całkowitą jasność co do tego,
jakiej konkretnie dokumentacji potrzebujecie. Zdefiniujesz kryteria, dzięki którym
określisz, czy tworzenie danego fragmentu dokumentacji będzie dla Twojego
zespołu przydatne czy nie.

T
o, co za chwilę przeczytasz, będzie mieć sens wy- problemy. Wciąż nie można połapać się w architekturze
łącznie wtedy, gdy już wielokrotnie spotkałeś się systemu. Już nie z powodu bezużyteczności dokumenta-
z problemami dotyczącymi dokumentowania i do- cji, lecz z powodu jej braku.
kumentacji w architekturze systemów informatycznych. W tej całej pogoni za nowym i lepszym jakoś zawieru-
Tak to już jest, że dopóki nie spotkasz się z określonymi szyło się ostatnie zdanie Manifestu Agile: „That is, while
trudnymi sytuacjami, pewne rzeczy mogą nie mieć dla there is value in the items on the right, we value the
Ciebie znaczenia praktycznego. Zatem jeśli: items on the left more”. Nie oznacza to, że dokumen-
tacja jest złem, którego należy się wystrzegać, lecz że
■■ pracujesz samotnie albo w bardzo małym zespole, celem projektu programistycznego jest przede wszyst-
■■ rotacja w Twoim zespole jest bardzo mała, kim działające oprogramowanie, a dokumentacja pełni
■■ nigdy nie przyszło Ci do głowy, aby w jakiś szczególny rolę pomocniczą. Stanie na stanowisku, że dobry kod
sposób dbać o dokumentowanie architektury, i częsta komunikacja całkowicie zastąpią dokumenta-
cję, jest równie nonsensowne, co opinia, że za pomo-
to spokojnie możesz przejść do kolejnego ciekawego cą UMLowych diagramów można napisać cały system,
artykułu w magazynie lub potraktuj ten tekst poglądo- a potem wystarczy już „tylko” wygenerować kod. Rze-
wo i jako przedstawienie problemów pojawiających się czywistość tak nie działa. Nie ma jednego sposobu na
w projektach. Lecz jeśli: załatwienie wszystkich problemów. Wspomniane zdanie
z Manifestu mówi, że:
■■ w Twoim zespole często należy wprowadzać nowe oso-
by do projektu, ■■ poszerz swoje myślenie o dokumentacji; dokumenta-
■■ próbowałeś już wielokrotnie dokumentować archite­ cja to nie tylko tekst, obrazki i diagramy, lecz również
kturę i nie zdawało to egzaminu, kod źródłowy, testy;
■■ masz wrażenie, że automatyczne generowanie tysięcy ■■ celem projektu jest stworzenie oprogramowania, a do-
stron dokumentacji jest absolutnie bezcelowe, kumentacja jest o tyle przydatna, o ile wspomaga ten
■■ w Twoim zespole wiedza o systemie mieści się w więk- cel;
szości w głowach programistów, ■■ są różne konteksty użycia dokumentacji, w zależności
od kontekstu dokumentacja może się różnić; raz może
to znajdziesz tu kilka przepisów, jak rozwiązać wspo- to być prosty odręczny rysunek, raz złożony diagram
mniane problemy. W tym artykule chcemy przeprowadzić klas, innym razem kod źródłowy jest wystarczającą
Ciebie i Twój zespół przez fragment procesu zmierzają- dokumentacją.
cego do odtworzenia dokumentacji złożonego systemu
informatycznego. W związku z powyższym, zanim rozpoczniesz działa-
nia dokumentowania systemu, absolutną koniecznością
SENS DOKUMENTOWANIA jest precyzyjne określenie po co dokumentacja w ogóle
powstaje.
We have come to value working software over compre-
hensive documentation – stwierdza Manifest Agile. Zaraz CEL TWOJEJ DOKUMENTACJI
potem rzesze zespołów uwolnionych spod jarzma z ze-
garmistrzowską dokładnością zdefiniowanych procesów, Tak, właśnie Twojej. Podejście do dokumentowania archi-
każących im dokumentować nawet mrugnięcie okiem, ru- tektury systemu w każdym zespole, w każdym projekcie
szyły w stronę Agile z nowymi hasłami na ustach „Precz będzie inne. To jest właśnie główny kłopot w tym obsza-
z dokumentacją!”, „Dobry kod i testy to również doku- rze. Mamy UML, mamy narzędzia, lecz jak je wykorzy-
mentacja!”. Pojawiły się nowe praktyki, lecz zostały stare stać, aby stworzyć coś rzeczywiście przydatnego?

72 / 1 . 2012 . (1)  /


DOKUMENTOWANIE ARCHITEKTURY

Zwróć uwagę, że jeśli postawimy kwestię właśnie w ten Krok 2. Zbieranie informacji o problemach
sposób: „jak używać dostępnych narzędzi, aby stworzyć
użyteczną dokumentację”, niemal automatycznie nasu- W drugim kroku zbierzesz informacje o tym, jakiego ro-
wa się pytanie: „co i komu jest właściwie potrzebne?”. dzaju problemy wystąpiły w tracie prac, którym mogłaby
I o to chodzi! Jeśli będziemy po prostu dokumentować, zaradzić dokumentacja. Możesz ten krok wykonać samo-
to powstanie duża ilość dokumentacji bez żadnego spre- dzielnie, lecz więcej różnorodnych informacji uzyskasz,
cyzowanego zastosowania. Jeśli najpierw określmy kon- gdy zaangażujesz w niego cały swój zespół. Zbieranie
tekst użycia dokumentacji, kto, kiedy, do czego będzie jej problemów odbywa się poprzez wypełnienie treścią na-
używał, to będzie powstawało tylko tyle dokumentacji, ile stępującego zdania:
trzeba i ani bajta więcej.
■■ W trakcie…
ETAP 1. POSZUKIWANIE PROBLEMÓW ■■ (kto?)…
■■ miał problem z…
W przypadkach, z którymi mamy do czynienia najczęściej, ■■ ponieważ…
czyli podczas odtwarzania dokumentacji już istniejącego ■■ i spowodowało to…
systemu, cele dokumentacji najłatwiej zdefiniować, wy-
chodząc od problemów, które występują, gdy dokumen- Każda z części powyższego zdania ma swoje specyficzne
tacji nie ma. zadanie, dlatego każdą z nich omówimy osobno.
„W trakcie…” uwypukla miejsce, w którym problem się
Krok 1. Proces wytwarzania oprogramowania pojawia. Wpisz tu nazwę etapu z narysowanego wcześniej
procesu wytwarzania oprogramowania, w którym bierzesz
Żeby namierzyć problem z dokumentacją, trzeba wskazać udział. Od miejsca w procesie może zależeć forma doku-
miejsce, w którym występuje. Z tego powodu pierwszym mentacji oraz sposób jej udostępniania. Na przykład jeśli
krokiem będzie narysowanie procesu wytwarzania opro- kłopot występuje podczas etapu wdrożenia, to ten fragment
gramowania, w którym zespół bierze udział. Niestety nie dokumentacji najpewniej przybierze bardzo formalną po-
wystarczy, że będziesz „miał go w głowie”. Jakoś tak to stać, jeśli jednak dotyczy etapu implementacji, to być może
magicznie działa, że gdy narysujesz proces, to zaczynasz wystarczy odręczny schemat na dużej kartce przyklejonej
patrzeć na niego z innej perspektywy – jako obserwator, na ścianie w pomieszczeniu, w którym pracuje zespół.
nie jako uczestnik. Twoje postrzeganie jest wtedy szersze Część „(kto?)…” identyfikuje osobę, która miała opi-
i nieco bardziej obiektywne. Nawet jeśli używasz znanej sywany problem. W tym miejscu mogą pojawić się: imię
metodyki, również przygotuj rysunek. Proces, w którym faktycznej osoby, rola pełniona w projekcie, stanowisko.
bierzesz udział, z pewnością wygląda nieco inaczej, niż Ta część definiuje odbiorcę dokumentacji. Jest o tyle istot-
wzorcowy diagram w materiałach szkoleniowych. To wła- na, że innych informacji potrzebuje programista, innych
śnie „nieco” może sprawić, że kłopoty występują albo zni- projektant, a jeszcze innych architekt. Dzięki precyzyj-
kają. Efektem tego kroku będzie schemat blokowy, które- nemu nazwaniu odbiorcy, będziemy potrafili uwypuklić te
go przykład znajdziesz na Rysunku 1. elementy systemu, które są istotne dla tej właśnie osoby.

Rysunek 1. Przykładowy schemat procesu wytwarzania oprogramowania

/ www.programistamag.pl / 73
KLUB LIDERA - IT
Autor

Rysunek 2. Definiowanie kryteriów na podstawie zidentyfikowanych problemów

„Miał problem z…” określa faktyczny moment poja- menty, które możesz przedstawić jako poparcie swoich
wienia się problemu. Zatrzymaj się na chwilę, bo trze- działań odtwarzania dokumentacji.
ba powiedzieć o bardzo ważnej rzeczy. W tym miejscu Jeszcze mała podpowiedź na koniec. Zbieranie powyż-
mogą pojawić się wyłącznie czynności, z którymi miała szych informacji przebiega sprawniej, jeśli dasz ludziom
problem dana osoba. Czynność to coś, co możesz wy- tabelę do wypełnienia. W nagłówkach kolumn wpisz po-
konać, to określone działanie, które ma swój początek szczególne części zdania i ewentualne objaśnienia. W po-
i koniec, czynność odbywa się w czasie. Dlaczego jest to szczególnych wierszach ankietowani będą opisywać ko-
takie ważne? Jeśli określisz problemy następująco: lejne problemy. Forma tabeli sprawia, że nie trzeba za
każdym razem przepisywać powtarzalnych fragmentów,
■■ miał problem z bazą danych, a osoba ją wypełniająca może skupić się na myśleniu za-
■■ miał problem z testami, miast na pisaniu.
■■ miał problem z komunikacją z Product Ownerem,
Etap 2. Definiowanie kryteriów
to niewiele z tego wynika. Jedyne bowiem, co możesz
zaradzić w takich wypadkach, to: dokumentacja bazy Większość pracy masz już za sobą. Teraz pozostało je-
danych, dokumentacja testów, porozmawiać z Product dynie ostateczne sformułowanie kryteriów, które musi
Ownerem. Takie definiowanie problemów jest zbyt ogólne spełniać dokumentacja architektury Twoje go systemu.
i utrudnia znalezienie środka zaradczego. Jeśli natomiast Pojedyncze kryterium jest określone poprzez trzy nastę-
zgodnie z przedstawioną zasadą skoncentrujesz się na pujące pytania:
czynnościach np.:
■■ Kto będzie używał dokumentacji architektury,
■■ miał problem z określeniem, które procedury składo- które osoby, które role, które pełnione funkcje?
wane uruchamiają się w żądaniu X, ■■ Kiedy będzie używał dokumentacji architektury,
■■ miał problem z napisaniem testu jednostkowego do Y, na którym etapie procesu wytwarzania oprogramowania,
■■ miał problem z tym, że długo czekał na odpowiedź Pro- ■■ Czego musi się dowiedzieć z dokumentacji archi-
duct Ownera, tektury, na jakie konkretne pytania musi odpowiadać
dokumentacja.
to niemal od razu przychodzi do głowy, które elementy
systemu wymagają objaśnienia i które aspekty współpra- Podobnie jak poprzednio, wygodnie będzie zebrać wszyst-
cy z Product Ownerem należy poprawić. Definiując pro- kie kryteria w tabeli, której nagłówki kolumn będą za-
blemy, odnoś się do konkretnych czynności. wierały powyższe pytania. Aby wypełnić tabelę kryteriów,
Część „ponieważ…” dookreśla potencjalne braki w do- skorzystaj wyodrębnionych wcześniej problemów w spo-
kumentacji. Tutaj jest miejsce na wpisanie, czego dana sób przedstawiony na Rysunku 2.
osoba nie wiedziała lub jaki błąd popełniła. Zgodnie z obietnicą złożoną na początku, po sumien-
Na koniec została jeszcze część „i spowodowało nym wykonaniu wszystkich opisanych kroków, w tym
to…”. Na tym etapie odpowiadasz na pytania: Jaki skutek momencie dysponujesz listą kryteriów, które musi speł-
miał ten błąd dla: terminów, innych zależnych procesów, niać dokumentacja architektury Twojego systemu. Od tej
stabilności systemu, podwykonawców, klientów? Można pory za każdym razem, gdy zaplanujesz stworzenie ja-
powiedzieć, że ta część zdania wyodrębnia problemy biz- kiejś części dokumentacji, zweryfikuj to pod kątem swo-
nesowe związane z dokumentacją. Tutaj określisz argu- ich kryteriów.

Michał Bartyzel, m.bartyzel@bnsit.pl,


Mariusz Sieraczkiewicz m.sieraczkiewicz@bnsit.pl

Trenerzy i konsultanci w firmie BNS IT. Badają i rozwijają metody psychologii pro-
gramowania, pomagające programistom lepiej wykonywać ich pracę. Na co dzień
Autorzy zajmują się zwiększaniem efektywności programistów poprzez szkolenia,
warsztaty oraz coaching i trening.

74 / 1 . 2012 . (1)  /


Zaglądaj na:
www.serwerblog.com
www.facebook.com/serwerblog

Blog o infrastrukturze IT
WDROŻENIA
Wojciech Holisz

Highsky.com
– projekt, oprogramowanie i wdrożenie pla­
tformy inwestycyjnej highsky.com zintegro­
wanej z platformą MetaTrader 5.
Projekt został zrealizowany przez zespół Positive Power Sp. z o.o. na zlecenie
czeskiego domu maklerskiego HighSky Brokers A.S. W artykule zaprezentowano
wybrane etapy realizacji projektu highsky.com, a także sposoby integracji plat-
formy MetaTrader 5 z zewnętrznymi aplikacjami. Opisano również język skryp-
towy MQL5, który został wbudowany w platformę i umożliwia rozszerzenie jej
funkcjonalności.

P
latforma MetaTrader 5 to zestaw nowoczesnych in- Język MQL5 jest językiem wysokiego poziomu, zorien-
strumentów umożliwiających handel na rynku wa- towanym obiektowo. Składania przypomina język C++.
lutowym oraz giełdowym. Oprócz możliwości za- Podobnie jak C++, język MQL5 cechuje się statyczną
wierania transakcji platforma oferuje również szereg typizacją. Do naszej dyspozycji zostały oddane typy da-
różnorodnych narzędzi analitycznych, pozwalających na nych pozwalające na wykonywanie operacji na liczbach
ocenę bieżących oraz historycznych danych. W ich skład całkowitych (int, uint, long), zmiennopozycyjnych (flo-
wchodzą m.in. dynamiczne wykresy cenowe. at, double) oraz wartościach logicznych (bool), a także
Platforma składa się z wielu elementów. Wśród nich możliwość definiowania własnych typów danych w postaci
wyróżniamy m.in. aplikacje klienckie (terminale), służą- struktur oraz klas.
ce do zlecania operacji handlowych i analizy danych oraz Skrypty tworzone w MQL5 dzielą się na kilka typów.
wielofunkcyjne serwery, m.in.: Pierwszy rodzaj to tzw. Expert Advisor, czyli roboty, dzię-
ki którym można całkowicie zautomatyzować procesy
■■ przetwarzające operacje handlowe, handlowe. Roboty potrafią na bieżąco śledzić sytuację na
■■ udostępniające dane historyczne, giełdzie i na podstawie dokonanej analizy zlecać opera-
■■ przechowujące kopie zapasowe, cje kupna bądź sprzedaży. Drugi rodzaj to proste skrypty,
których zadaniem jest zwykle wykonanie jednego specy-
dostępowe (access points), z którymi łączą się terminale. ficznego zadania, a następnie zakończenie pracy. Z kolei
Użytkownik końcowy uzyskuje dostęp jedynie do ter- biblioteki (Library) nie są typowymi skryptami, ale zbio-
minala, a pozostała infrastruktura jest ukryta za serwe- rem funkcji, które można wielokrotnie wykorzystywać w
rami dostępowymi. Dzięki takiej architekturze systemu innych skryptach.
znacznie podnosi się bezpieczeństwo danych przechowy- W listingu 1 przedstawiam kod prostego programu na-
wanych na serwerach. pisanego w C++, który wyświetla na ekranie tekst oraz
Posiadacze smartphone’ów i tabletów mogą skorzy- jego odpowiednik w języku MQL5.
stać z aplikacji mobilnej, która umożliwia przeglądanie Jak widać, języki te są do siebie bardzo zbliżone. Skrypt
wykresów oraz handel. Aplikacja jest dostępna na tele- MQL5 zawiera funkcję o nazwie “OnStart”, która jest od-
fony iPhone oraz z systemem Android, można je pobrać powiednikiem funkcji “main” w języku C++ oraz definicję
nieodpłatnie ze strony http://www.metatrader5.com/en/ klasy. Skrypt można przetestować w aplikacji MetaEditor.
download. Aby utworzyć nowy skrypt, należy wybrać opcję “New” z
menu “File”, w oknie zaznaczamy “Script” i przechodzimy
MQL5 dalej. W kolejnym oknie możemy określić nazwę skryptu,
jego autora oraz parametry wejściowe. W pierwszym polu
Aplikacja kliencka MetaTradera została wyposażona w wpisujemy nazwę i zatwierdzamy przyciskiem “Finish”.
język skryptowy MQL5. Przy pomocy języka MQL5 moż- Otworzy się nowe okno z domyślnym szablonem, który
na tworzyć zarówno proste skrypty agregujące dane czy usuwamy i wklejamy kod naszego skryptu.
powiadamiające o zmianie kursów, jak i zaawansowane Skrypt kompilujemy przy pomocy przycisku “Compi-
automaty, które potrafią samodzielnie przeprowadzać le” lub klawisza F7, a uruchamiamy, wybierając pozycję
operacje handlowe. “Start” z menu “Debug” lub klawiszem F5. Jeżeli nie uru-

76 / 1 . 2012 . (1)  /


PROJEKT, OPROGRAMOWANIE I WDROŻENIE PLA­TFORMY INWESTYCYJNEJ HIGHSKY.COM
TYTUŁ

Rysunek 1. Aplikacja kliencka z uruchomionymi wykresami. Rysunek 2. Edytor skryptów MetaEditor.

Listing 1. Przykładowy program w C++ oraz jego odpowie­ chomiliśmy jeszcze MetaTradera, edytor zrobi to za nas,
dnik w MQL5 a następnie otworzy jeden wykres. W oknie “Toolbox”
umieszczonym przy dolnej krawędzi ekranu w zakład-
#include <iostream>
#include <string> ce “Eksperci”, znajdziemy tekst wyświetlony przez nasz
skrypt.
class Person
{
Okno z wykresem otwiera się, ponieważ skrypty nie
public: mogą zostać uruchomione jako samodzielne instancje.
Person(const std::string& name) : name(name) Każdy skrypt musi zostać “załączony” do wykresu, aby
{
} możliwe było jego wykonanie. Chcąc uruchomić skrypt z
poziomu MetaTradera, wyszukujemy go w oknie “Naviga-
const std::string& getName() const
{ tor” i wybieramy z menu podręcznego pozycję “Attach to
return name; Chart”.
}
protected: Oczywiście język zawiera również instrukcje sterujące.
std::string name; Instrukcje warunkowe, pętle oraz instrukcje wyboru defi-
};
niujemy identycznie jak w języku C++.
int main(int argc, char* argv[])
{ Listing 2. Przykład ze skryptu z pętlą oraz parametrami
Person* person = new Person("John");
wejściowymi
std::cout
<< "Hello World! My name is " #property copyright "Copyright 2012, MetaQuotes
<< person->getName() Software Corp."
<< "." << std::endl; #property link "http://www.mql5.com"
#property version "1.00"
delete person;
#property script_show_inputs
return 0;
} input uint from;
input uint to;
// MQL5
class Person void OnStart()
{ {
public: for (uint i = from; i <= to; ++i)
Person(string name) : name(name) {
{
if (i % 15 == 0)
}
{
string getName() const Print("FizzBuzz");
{ }
return name; else if (i % 3 == 0)
} {
protected: Print("Fizz");
string name; }
}; else if (i % 5 == 0)
{
void OnStart()
{ Print("Buzz");
Person* person = new Person("John"); }
else
Print("Hello World! My name is " + person. {
getName() + "."); Print(i);
}
delete person; }
} }

/ www.programistamag.pl / 77
WDROŻENIA

Podobnie jak do programów pisanych w innych językach, zapoznaniu się z tajnikami języka MQL5 można spró-
do skryptów MQL5 również możemy przekazywać parame- bować swoich sił w organizowanych co roku zawodach
try wejściowe. Parametry wejściowe definiowane są tak Automated Trading Championship (http://championship.
samo, jak zmienne globalne, ale poprzedzamy je słowem mql5.com).
kluczowym “input”. W przykładzie zdefiniowane zostały dwa
parametry wejściowe o nazwach “from” oraz “to”. WORTAL HIGHSKY BROKERS
W powyższych przykładach zostały umieszczone także
“właściwości skryptu”. Każdy skrypt może zostać opatrzony Wortal inwestycyjny https://www.highsky.com/ został
szeregiem właściwości, w których możemy umieścić m.in. wykonany na potrzeby działającego na terenie Czech
informacje o autorze skryptu lub jego wersji. Właściwości domu maklerskiego HighSky Brokers, A.S. Serwis, skie-
definiujemy przy pomocy polecenia “#property”, po któ- rowany do użytkowników czesko- i anglojęzycznych, ofe-
rym występuje nazwa oraz wartość właściwości. Umiesz- ruje dostęp do aplikacji MetaTrader oraz wielu materiałów
czenie w skrypcie właściwości “script_show_inputs” powo- szkoleniowych na temat działania platformy i funkcjono-
duje, że podczas jego uruchomienia wyświetli się okno z wania giełdy.
możliwością wprowadzenia parametrów wejściowych. Dzięki integracji z systemem transakcyjnym MetaTra-
Przedstawione przykłady zawierały jedynie podsta- der, w serwisie istnieje możliwość założenia rachunku
wowe elementy samego języka MQL5. Utworzymy teraz inwestycyjnego “demo” bądź rachunku “rzeczywistego”.
skrypt Expert Advisor, który umożliwi integrację z apli- Rachunki demo służą wyłącznie celom edukacyjnym.
kacją MetaTrader 5. Z menu “File” wybieramy pozycję Dzięki nim użytkownicy mogą zapoznać się z zasadami
“New”. W kreatorze zaznaczamy “Expert Advisor (templa- funkcjonowania giełdy oraz testować różne strategie gry
te)” i podajemy nazwę skryptu. W kolejnym etapie wybie- bez obawy o utratę środków, gdyż wszelkie operacje han-
ramy zdarzenia, na które reagować będzie nasz skrypt. dlowe przeprowadzane na takich rachunkach są traktowa-
Po zatwierdzeniu przyciskiem “Finish” otworzy się nowe ne jako fikcyjne i nie skutkują faktycznym zakupem lub
okno z szablonem skryptu. Usuwamy jego zawartość i sprzedażą. Rachunki demo wiążą się z jeszcze jedną funk-
wklejamy kod z listingu 3. cjonalnością serwisu - za ich pośrednictwem użytkownicy
Metody “OnInit” oraz “OnDeinit” są wywoływane od- wortalu mogą wziąć udział w organizowanym przez dom
powiednio podczas uruchamiania oraz zamykania skryp- maklerski HighSky Brokers konkursie. Każdy jego uczest-
tu. W powyższym przykładzie zostały wykorzystane do nik rejestruje testowy rachunek demo, a następnie stara
uruchomienia oraz zatrzymania zegara. Zegary generują się zgromadzić na swoim koncie jak największy kapitał.
zdarzenia co określoną liczbę sekund. Nasz zegar urucha- Swoją aktualną pozycję można sprawdzić w codziennie
mia się co 10 sekund i wyświetla ile razy został wywołany aktualizowanej liście rankingowej. Konkurs trwa kilka ty-
od momentu uruchomienia skryptu. godni, a na zwycięzców czekają nagrody.
Metoda “OnTick” jest wywoływana w momencie, gdy W przeciwieństwie do rachunków demo, rachunki rze-
nastąpi aktualizacja cen na wykresie, do którego załączo- czywiste umożliwiają faktyczną grę na giełdzie i są aktyw-
ny jest nasz skrypt. Metoda z przykładu pobiera oraz wy- ne dopiero po podpisaniu stosownych umów o współpracy
świetla w konsoli najnowsze dostępne wartości cen kupna z domem maklerskim. Po założeniu rachunku użytkow-
oraz sprzedaży. nik otrzymuje jednoczesny dostęp do aplikacji klienc-
Skrypty Expert Advisor pozwalają również na integrację kiej MetaTrader 5 oraz specjalnie przygotowanej strefy
z elementami interfejsu aplikacji. Ostatnia z przykładowych klienta, gdzie może zweryfikować swoje dane oraz drogą
metod “OnChartEvent” wyświetla współrzędne punktu, w elektroniczną uzupełnić stan konta. Płatności on-line są
którym nastąpiło kliknięcie oraz kod naciśnięcia klawisza. realizowane przy pomocy systemu Global Payments, za-
Możliwości naszych skryptów możemy rozszerzać po- pewniającego wysokie bezpieczeństwo transakcji. Jedną
przez import funkcji z zewnętrznych bibliotek DLL. Poniż- z cech - wyróżniającą płatności GP na tle innych - jest
szy skrypt wywołuje popularną funkcję “MessageBox” z konieczność cyfrowego podpisywania wszystkich danych
biblioteki systemowej. przesyłanych pomiędzy serwisem a systemem płatności,
dzięki czemu płatności są odporne na niepożądane mody-
Listing 4. Wywołanie funkcji z zewnętrznej biblioteki DLL fikacje danych.
Wortal pełni również funkcję informacyjną oraz edu-
#import "User32.dll" kacyjną. Zawarte w nim artykuły szkoleniowe pozwalają
int MessageBoxW(long, string, string, int);
#import nowym użytkownikom zapoznać się z zasadami gry na
giełdzie, uczą także obsługi aplikacji MetaTrader. Z kolei
void OnStart() bieżące wiadomości ze świata giełdy zapewniają dopływ
{
cennych informacji pomocnych podczas zawierania trans-
MessageBoxW(0, "Hello World!", "Message", 0);
} akcji handlowych.
Serwis internetowy udostępnia również aktualne no-
towania w postaci przewijanego paska widocznego na
Dokumentacja wraz z przykładami użycia języka MQL5 każdej podstronie oraz stale uaktualnianych wykresów
jest dostępna pod adresem http://www.mql5.com/. Po (liniowych, słupkowych oraz świecowych). Dodatkowo na

78 / 1 . 2012 . (1)  /


PROJEKT, OPROGRAMOWANIE I WDROŻENIE PLA­TFORMY INWESTYCYJNEJ HIGHSKY.COM
TYTUŁ

Listing 3. Przykład skryptu Expert Advisor

int OnInit()
{
Print("Bot started.");

EventSetTimer(10);

return 0;
}

void OnDeinit(const int reason)


{
Print("Bot killed.");

EventKillTimer();
}

void OnTick()
{
const string symbol = Symbol();

MqlTick tick;
SymbolInfoTick(symbol, tick);

Print("Symbol: ", symbol, ", Bid: ", tick.bid, ", Ask: ", tick.ask);
}

void OnTimer()
{
static uint i = 1;

Print("Timer was called ", i++, " times.");


}

void OnChartEvent(const int id, const long& lparam,


const double& dparam, const string& sparam)
{
switch (id)
{
case CHARTEVENT_CLICK:
Print("You clicked at ", lparam, ":", (int)dparam, ".");
break;

case CHARTEVENT_KEYDOWN:
Print("You pressed key with code ", lparam, ".");
break;
}
}

wykresach liniowych prezentowane są dane makroeko- serwera, jeżeli wystąpi konieczność np. zwiększenia mocy
nomiczne obrazujące stan gospodarki. Do generowania obliczeniowej lub udostępnienia większej powierzchni
wszystkich wykresów w wortalu wykorzystano zewnętrz- dyskowej. Serwis WWW jest hostowany na serwerze pra-
ne narzędzie Google Charts Tools, dzięki czemu zoptyma- cującym pod kontrolą systemu Debian, a aplikacja inte-
lizowano zapotrzebowanie na moc obliczeniową serwera, grująca serwis WWW z platformą MetaTrader 5 w syste-
na których hostowany jest serwis. mie Windows Server.
Publikowane artykuły to nie jedyne źródło informacji na Strona WWW została wykonana w języku PHP 5, a dane
temat giełdy. W serwisie znajdziemy również wykaz semi- przechowywane są w relacyjnej bazie MySQL 5. W celu
nariów wraz z możliwością rejestracji. Na seminariach - za- podniesienia bezpieczeństwa zainstalowano rozszerzenie
równo w formie tradycyjnych spotkań, jak i elektronicznych Suhosin dla języka PHP oraz rozszerzenie mod_security
konferencji - poruszane są tematy związane z giełdą oraz do serwera Apache. Rozszerzenie Suhosin to zaawanso-
obsługą inwestycji za pośrednictwem platformy MetaTrader. wany system, którego zadaniem jest ochrona serwerów
oraz użytkowników przed potencjalnymi zagrożeniami
TECHNOLOGIE występującymi w języku PHP i aplikacjach tworzonych
przy jego użyciu.
Aplikacje umieszczono na dwóch serwerach wirtualnych. Bezpieczeństwo danych podnosi również zastosowanie
Wirtualizacja daje możliwość łatwej zmiany parametrów certyfikatów SSL. Dostęp do wszystkich podstron witry-

/ www.programistamag.pl / 79
WDROŻENIA

ny highsky.com jest możliwy wyłącznie poprzez protokół Jedną z zastosowanych optymalizacji jest instalacja
HTTPS. Częstym niedopatrzeniem popełnianym podczas rozszerzenia APC. Moduł APC pełni dwie funkcje. Pierwsza
tworzenia serwisów WWW jest wymuszanie szyfrowania to kompilacja kodu do postaci kodu operacji (opcode),
SSL jedynie przy formularzach logowania czy rejestracji, dzięki czemu kod nie musi być przetwarzany za każdym
a wyłączanie go na pozostałych podstronach. Zabezpie- razem, gdy skrypt jest wywoływany. Druga funkcja APC
cza to wprawdzie przed przechwyceniem hasła wpisywa- to możliwość korzystania z pamięci operacyjnej do prze-
nego podczas logowania, ale pozwala na przechwycenie chowywania danych (np. wyników zapytań wykonywa-
ciasteczka identyfikującego zalogowanego użytkownika, nych na bazie danych). Dzięki cache’owaniu wyników wy-
jeżeli użytkownik przejdzie na nieszyfrowaną podstro- konywanych zapytań, serwer może szybciej generować
nę. Pomimo pewnych słabości protokołu HTTPS należy dynamiczne strony WWW. Zastosowanie APC zwiększa
go włączać w całym serwisie. Takie rozwiązanie wymu- szybkość wykonywania skryptów oraz zmniejsza zapo-
sza szyfrowanie wszystkich treści, łącznie z obrazami, co trzebowanie na pamięć podczas ich wykonywania.
wiąże się z nieco większym zużyciem mocy obliczeniowej Integrację serwisu z platformą MetaTrader 5 wykonano
procesora, ale bezpieczeństwo danych powinno stać na przy pomocy API udostępnionego przez autorów oprogra-
pierwszym miejscu. mowania. W przeciwieństwie do języka MQL5, API nie jest
Dostęp do plików z kodem źródłowym oraz plików za- dostępne publicznie i korzystanie z niego jest możliwe
wierających ważne dane (np. hasła dostępu do bazy) zo- dopiero po wykupieniu odpowiedniej licencji. API umoż-
stał zabezpieczony poprzez umieszczenie ich w katalogu, liwia m.in. zakładanie rachunków demo oraz rachunków
do którego serwer WWW nie ma dostępu. Serwer WWW rzeczywistych, zarządzanie użytkownikami, dostęp do
posiada dostęp jedynie do zasobów statycznych np. gra- bieżących i archiwalnych notowań oraz zlecanie operacji
fik, arkuszy CSS oraz skryptów JavaScript. Istotne frag- handlowych.
menty kodu źródłowego zostały dodatkowo zaszyfrowane. Aplikacja integrująca serwis z platformą MetaTrader
Serwis WWW działa wydajnie dzięki zastosowaniu wer- 5 została wykonana przy użyciu technologii .NET w ję-
sji 5.3 języka PHP, która zyskała wiele poprawek związa- zyku C#. Technologia .NET została wybrana ze względu
nych z szybkością działania. Większą wydajność serwisu na możliwości łączenia kodu natywnego (API platformy)
osiągnięto poprzez zastosowanie wielopoziomowej pa- z kodem zarządzanym oraz możliwościami oferowanymi
mięci podręcznej (cache), czyli mechanizmu polegające- przez Windows Communication Foundation. Aplikacja wy-
go na zapamiętywaniu często wykorzystywanych danych, korzystuje API platformy do zakładania rachunków oraz
ale zwykle dostępnych w źródłach o wolniejszym czasie udostępniania najnowszych notowań. Dzięki zastosowa-
dostępu lub takich, których generowanie zajmuje więcej niu WCF aplikacja może zostać uruchomiona samodziel-
czasu. nie lub jako usługa działająca pod kontrolą serwera IIS.

Wojciech Holisz wojciech.holisz@gmail.com

Absolwent Politechniki Śląskiej, od 3 lat związany z agencją interaktywną Positive


Power Sp. z o.o., obecnie na stanowisku Team Leader PHP Developer. Swoje zawo-
dowe zainteresowania koncentruje wokół szeroko pojętej inżynierii oprogramowania
oraz grafiki komputerowej.

Positive Power
Positive Power Sp. z o. o. skupia kilkudziesięciu ekspertów z branży IT, webdesignu
oraz działań kreatywnych. Agencja specjalizuje się w tworzeniu stron internetowych,
dedykowanych platform e-commerce oraz zaawansowanych aplikacji oraz szeroko
pojętym marketingu internetowym. W ciągu 10 lat istnienia przekonała do siebie
kilkaset firm i instytucji, m.in.: Onet.pl, Libet SA, Mennica Polska SA, Mitsubishi Elec-
tric, Subway, Intersport SA, Greenpeace, 8a.pl.

80 / 1 . 2012 . (1)  /


PROJEKT, OPROGRAMOWANIE I WDROŻENIE PLA­TFORMY INWESTYCYJNEJ HIGHSKY.COM

Rysunek 3. Strona główna serwisu highsky.com.

/ www.programistamag.pl / 81

You might also like