Programista 1/2012

Page 1

Magazyn

programistów

i

liderów

zespołów

IT

www • programistamag • pl NR

1/ 2012 (1) marzec

C++11 w praktyce: Sygnały i sloty

Wyrażenia regularne w C++

Arduino Domain Driven Pierwszy kontakt Design

Praktyczne wykorzystanie RUP

W artykule omawiamy biblioteki boost::regex i boost::xpressive, dostarczające wyrażeń regu­ larnych dla C++

Arduino to platforma oparta o mikrokontroler z rodziny AVR. W artykule zaprezentowano podstawowe wiadomości o Arduino i przykłady tworze­ nia programów dla tego typu urządzenia

Artykuł opisuje prakty­ czne wyko­rzystaniw metod i narzędzi Inżynierii Opro­ gramowania na stanowisku pracy Analityka Biznesowego

W artykule przedstawiono podstawowe Building Blocks DDD wraz z przykładami ich implementacji w Javie z wykorzystaniem Spring i JPA

Pierwszy numer bezpłatny


REDAKCJA/EDYTORIAL

Startujemy! Szanowni Czytelnicy. Mam przyjemność oddać w Wasze ręce premierowe wydanie magazynu „Programista”. Programista to nowy projekt. Pismo skierowane jest do zawodowych programistów i członków zespołów IT. Przygotowania do tego dnia zajęły wiele miesięcy, ale jestem przekonany, że ostate­czny efekt zyska Wasze uznanie. Skąd pomysł na magazyn? Jesteśmy zespołem ludzi z wieloletnim doświadczeniem w branży wydawniczej związanej z IT i uznaliśmy, że na polskim rynku brakuje profesjonalnych wydawnictw skierowanych do tej grupy docelowej. Rozumiemy, jak ważny jest czas w pracy, dlatego naszym celem jest dostarczanie profesjonalnych i ciekawych materiałów wprost do ich rąk. Materiałów, dzięki którym będą mogli poszerzyć swoją wiedzę i zdobyć nowe umiejętności. Stawiamy na nowości, ale nie zapominamy o starszych rozwiązaniach. Jakość jest słowem, które jest nam bardzo bliskie. Dlatego zwracamy szczególną uwagę na to, w jaki sposób wiedza dociera do czytelnika. Nasz magazyn jest prosty i elegancki w formie. W premierowym wydaniu znajdziecie wiele interesujących artykułów. Przeczytacie teksty dotyczące C++11, bibliotek boost::regex i boost::xpressive, analizy obrazu, Arduino czy też wprowadzenie do AMFPHP. Podjęliśmy współpracę

z najlepszymi autorami, ale także z firmami z wieloletnim doświa­dczeniem i odpowiednią wiedzą dotyczącą pracy zespołów IT. W magazynie znajdziecie pierwszy artykuł z serii Klub Lidera IT. Będziemy zamieszczać w nim teksty koncentrujące się na tym co trapi liderów zespołów programistycznych. Będziemy zajmować się budowaniem i zarządzaniem zespołem programistycznym, kłopotami z architekturą i architektami, motywowaniem programistów, promowaniem własnych pomysłów w organizacji, zarządzaniem dokumentacją, konfli­ ktami z tak zwanym Biznesem i efektywnością zespołu programistycznego. Aby zadowolić naszych Czytelników, z uwagą będziemy wsłuchiwać się w ich uwagi i potrzeby. Dlatego wszelkie komentarze i sugestie są przez nas mile widziane. Nie obawiamy się również krytyki. Jesteśmy zdania że każda opinia jest wartościowa i dzięki każdej uwadze możemy ulepszać nasz magazyn. Od następnego numeru (już w maju!) "Programista" będzie ukazywał się również w wersji drukowanej i będzie liczył 80 stron. Zachęcamy do prenumeraty pisma. Więcej infomacji znajdą Państwo na stronie www.programistamag.pl. Zapraszam do lektury.

Łukasz Łopuszański

Redaktor naczelny lukaszlopuszanski@programistamag.pl

Magazyn Programista wydawany jest przez firmę Anna Adamczyk

Korekta:

Prenumerata:

Wydawca:

Kierownik produkcji:

Współpraca:

Redaktor naczelny:

DTP:

Anna Adamczyk annaadamczyk@programistamag.pl Łukasz Łopuszański lukaszlopuszanski@programistamag.pl

Z-ca Redaktora naczelnego:

Tomasz Łopuszański tomaszlopuszanski@programistamag.pl

Tomasz Łopuszański

prenumerata@programistamag.pl

Krzysztof Kopciowski

Rafał Kocisz, Michał Bartyzel, Mariusz Sieraczkiewicz, Sławomir Sobótka, Artur Machura

Dział reklamy:

Adres korespondencyjny:

Krzysztof Kopciowski bok@keylight.com.pl

reklama@programistamag.pl tel. +48 663 220 102 tel. +48 604 312 716

Magazyn „Programista”, ul.Meissnera 7 lok 5, 03-982 Warszawa


SPIS TREŚCI

AKTUALNOŚCI............................................................................................................................................4 Rafał Kocisz BIBLIOTEKI I NARZEDZIA Wyrażenia regularne w C++ biblioteka boost::regex i boost::xpressive............................. 6 Robert Nowak PROGRAMOWANIE GRAFIKI Analiza obrazu: rozpoznawanie obiektów...................................................................................10 Paweł Wolniewicz JĘZYKI PROGRAMOWANIA C++11 w praktyce: sygnały i sloty..................................................................................................20 Rafał Kocisz PROGRAMOWANIE SYSTEMÓW OSADZONYCH Arduino – Pierwszy kontakt.............................................................................................................28 Marek Sawerwain TECHNOLOGIE FLASH/FLEX Wprowadzenie do AMFPHP.............................................................................................................34 Paweł Murawski INŻYNIERIA OPROGRAMOWANIA Domain Driven Design krok po kroku Część I: Podstawowe Building Blocks DDD.......... 38 Sławomir Sobótka Praktyczne wykorzystanie RUP na stanowisku pracy Analityka Biznesowego................. 48 Artur Machura KLUB LIDERA IT Ewolucyjna architektura Jak zorganizować proces rozwoju architektury?........................ 54 Michał Bartyzel, Mariusz Sieraczkiewicz E-LEARNING DevCastZone – nowy wymiar e-edukacji dla programistów...................................................60 IT BOYZ ....................................................................................................................................................... 62 Zuch

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.


AKTUALNOŚCI

Objective-C czarnym koniem rankingu Tiobe w roku 2011

J

eśli ktoś ma jeszcze wątpliwości co do tego, że technologie wywierają przemożny wpływ na popularność języków programowania, to może przestać głowić się nad tym problemem. Jak pokazują wyniki rankingu popularności języków programowania TIOBE, ewidentnym zwycięzcą w kategorii języka, który odnotował najwię­ kszy wzrost zainteresowania wśród programistów w roku 2011, jest Objective-C. Nietrudno się domyśleć, iż wzrost ten jest bezpośrednio powiązany z falą sukcesów, które święcą ostatnio platformy iOS oraz OS X. A co z resztą tabeli? Na pierwszym miejscu króluje nieprzerwanie Java, jednakże jej notowania z roku na rok sukcesywnie maleją. Dalej mamy C, oraz C#, które wyparło z trzeciego miejsca C++. JavaScript powrócił do pierwszej dziesiątki, wypierając Ruby. Największymi przegranymi tego roku są niewątpliwie Python (zeszłoroczny lider w tabeli) oraz PHP. TIOBE to dość kontrowersyjny ranking, wiele osób nie traktuje go poważnie, krytykując chociażby stosowany przezeń mechanizm szacowania poziomu popularności. Jednakże, patrząc z perspektywy kilku ostatnich lat, TIOBE wydaje się być dość skutecznym aparatem wspomagającym monitorowanie głównych trendów czy też ogólnie pojętej mody w dziedzinie języków programowania. Podsumowanie tegorocznych wyników rankingu wydaje się potwierdzać powyższą tezę. H T T P://W W W.T I O B E.C O M

Zapowiedź Apple OS X Mountain Lion: marzenie użytkowników, utrudnienia programistów

W

dość nietypowy dla siebie sposób – w formie noty prasowej - firma Apple przedstawiła zapowiedź nowej wersji systemu z rodziny OS X: 10.8, noszącego kodową nazwę Mountain Lion. Warto na samym początku odnotować pewien ciekawy fakt; otóż z nazwy systemu usunięto słowo Mac. Od wersji 10.8 mamy do czynienia po prostu z OS X. W pierwszej kolejności dostęp do próbnej wersji nowego systemu otrzymali przedstawiciele najpopularniejszych blogów technicznych; mniej więcej tydzień później wersja Developer Preview została przekazana uczestnikom Mac Developer Program. Warto odnotować, że taka a nie inna kolejność wzbudziła pewne kontrowersje, gdyż w przypadku wcześniejszych wersji OS X’a to programiści otrzymywali w pierwszym rzędzie możliwość zapoznania się ze zmianami w nowej odsłonie systemu; dopiero zaś po przeanalizowaniu ich uwag i komentarzy produkt trafiał w ręce pierwszych recenzentów. Czym zamierza zadziwić nas Apple w nowej odsłonie OS X? Analizując pierwsze recenzje oraz zestawienia właściwości nowego systemu, można odnieść wrażenie,

4

/ 1 . 2012 . (1)  /

iż Mountain Lion to krok w kierunku… iOS. Praktycznie wszystkie nowe właściwości tego systemu, czyli Notification Center, AirPlay Mirroring, czy Game Center AirPlay mirroring, and Game Center, stanowią wierne odwzorowanie usług dostępnych u jego mobilnego kuzyna. Jak by tego było mało, Apple zdecydowało się zmienić nazwy istnie­jących aplikacji dostępnych w OS X: iChat, iCal, oraz Address Book, tak aby były one zgodne z nazwami ich odpowiedników w iOS, czyli Messages, Calendar oraz Contacts. Nowy OS X oferuje również pełniejszą integrację z usługą iCloud, w czego konsekwencji usługi Reminders oraz Notes stają się pełnoprawnymi aplikacjami zintegrowanymi z chmurą Apple. Całkowitą nowością oferowaną w ramach Mountain Lion’a jest Gatekeeper: mechanizm, który pozwala blokować uruchamianie podejrzanych aplikacji. Gatekeeper jest domyślnie skonfigurowany w taki sposób, że "przepuszcza" jedynie aplikacje pobrane za pośrednictwem AppStore bądź podpisane przez producenta. Apple zapowiada, że z punktu widzenia programistów tworzących aplikacje pod OS X’a proces ich podpisywania ma być bardzo prosty; jednakże, póki co, nie zostało jasno określone to, czy firma z Coupertino będzie rościć sobie prawo do ich moderowania. Jako główny czynnik motywacyjny implikujący wprowadzenia mechanizmu Gatekeeper’a Apple wskazuje chęć walki ze szkodliwym oprogramowaniem. Jednakże, dla programistów aplikacji OS X oznacza to, że mniej więcej do lata 2012 mają czas na podpisanie swoich produktów, gdyż w tym właśnie czasie Apple planuje oficjalnie wydać Mountain Lion’a. Podsumowując: patrząc z perspektywy użytkownika, no­ wy system ze stajni Apple wygląda absolutnie świetnie; patrząc z perspektywy twórców oprogramowania – sprawa przedstawia się nieco gorzej. Wygląda na to, że ci, którzy nie chcą zadzierać z Górskim Lwem, powinni zakasać rękawy i brać się do pracy. H T T P://W W W.E X T R E M E T E C H.C O M

Rust: nowy język programowania ze stajni Mozilla, który uratuje FireFox’a?

P

o ponad pięciu latach prowadzenia prac rozwojowych Mozilla Labs Rust udostępniła wersję Alpha (0.1) kompilatora dla języka programowania Rust. Autorzy tego języka postawili na wsparcie dla przetwarzanie równoległego oraz bezpieczne zarządzanie zasobami pamięci. Jeśli wszystko pójdzie zgodnie z planem, to Rust w niedalekiej przyszłości ma zastąpić C++ jako główny język kompilowany, wykorzystywany przy produkcji oprogramowania ze stajni Mozilla. W praktyce oznacza to, że cały FireFox, lub jego części mogą być przepisane w Rust. Rust jest kompilowanym, statycznie typowanym, obiektowym językiem programowania. Zgodnie z intencją autorów, nie wprowadza on żadnych rewolucyjnych konstrukcji syntaktycznych, bazuje raczej na znanych, wypróbo-


wanych w innych językach rozwiązaniach. Rust posiada opcjonalny mechanizm odśmiecania pamięci, który można wykorzystywać w sposób wybiórczy (per zadanie). Twórcy Rust’a wskazują Newsqueak, Alef oraz Limbo jako języki, które najmocniej wpłynęły na jego rozwój. Kompilator Rust póki co dostępny jest dla systemów Windows, Linux oraz OS X. Analizując decyzję zastąpienia C++ przez Rust trudno oprzeć się wrażeniu, że Mozilla wymyśliła dość skomplikowany sposób na wymyślenie od nowa koła, wyważając przy tym niejedne otwarte drzwi… Z drugiej strony – jak wiadomo, do odważnych świat należy, a niekonwencjonalne pomysły są nierzadko kluczem do sukcesu. Bardziej niepokoi jednak fakt, iż to, co autorzy Firefoxa pragną uzyskać, korzystając z nowego języka, czyli lepszą wydajność i stabilność aplikacji, przynajmniej w teorii powinno dać się uzyskać stosując język C++ we właściwy sposób. Pojawia się również pytanie, czy odzyskanie stabilności i wydajności pozwoli dogonić Ognistemu Liskowi konkurencję, która skupia się raczej na dodawaniu nowych, ciekawych właściwości...

deo już od dawna sprawia, iż gracze potrafią raz po raz powracać do tych samych fragmentów rozgrywki tylko po to, aby znaleźć ukryte przejścia, zdobyć trudno dostępne monety czy przejść cały etap, korzystając tylko z wybranego rodzaju broni. Rzecz staje się jeszcze ciekawsza, jeśli gracz otrzymuje możliwość podzielenia się swoimi wynikami ze znajomymi w sieci. Czemu podobnej frajdy nie mogą sobie zafundować programiści? Otóż mogą! Użytkownicy pakietu Visual Studio 2010 (Professional, Premium oraz Ultimate) od niedawna mogą cieszyć się darmową wtyczką Visual Studio Achieve­ ments, która pozwoli im cieszyć się podobną funkcjonalnością. Póki co do odblokowania są 32 osiągnięcia, a według zapewnień autorów liczba ta ma być sukcesywnie zwiększana. Wtyczka daje też możliwość publikowania osiągnięć na Facebook’a czy na własnej stronie WWW. Kryteria doboru wymagań dla poszczególnych osiągnięć zostały potraktowane ze sporym przymrużeniem oka – wydaje się, że autorzy wtyczki za cel postawili sobie wprowadzenie nieco luzu i zabawy w trudną i nierzadko stresującą pracę programisty. W tym przypadku można zaryzykować stwierdzenie, że cel ten udało się im osiągnąć.

H T T P://W W W.E X T R E M E T E C H.C O M H T T P://C H A N N E L9.M S D N.C O M/A C H I E V E M E N T S/

Microsoft udostępnia specyfikację V I S U A L S T U D I O rozszerzenia C++ AMP dla producentów kompilatorów Polscy deweloperzy w czołówce twórców aplikacji na Windows godnie z zeszłorocznymi zapowiedziami Firma MiPhone

Z

crosoft postanowiła otworzyć specyfikację C++ AMP (C++ Accelerated Massive Parallelism) tak aby twórcy konkurencyjnych kompilatorów (Embarcadero, Intel czy Free Software Foundation) mogli zaimplementować to rozszerzenie w swoich produktach. Wstępna implementacja tej specyfikacji przedstawiona została społeczności programistów we wrześniu 2011, jako część demonstracyjnej wersji pakietu Microsoft Visual Studio 2011. Głównym zadaniem rozszerzenia jest przyśpieszenie kodu pisanego w języku C++ za pośrednictwem GPU, przy czym inżynierowie pracujący nad stworzeniem tej specyfikacji dołożyli szeregu starań, aby zachować jak najwyższą kompatybilność z kanoniczną wersją języka C++. Nie jest tajemnicą, że Microsoft współpracuje dość ściśle z komitetem standaryzacyjnym języka C++ i liczy na to, że członkowie tego komitetu wykorzystają pomysły zastosowane w AMP, pracując nad kolejnymi rozszerzeniami specyfikacją języka. Fakt otwarcia specyfikacji AMP potwierdza konsekwencję, z jaką Gigant z Redmond dąży do tego celu.

P

H T T P://D R D O B B S.C O M

olska jest trzecim na świecie krajem pod względem ilości aplikacji wydanych na platformę Windows Phone. Do końca zeszłego roku w Marketplace, wirtualnym sklepie z programami na telefon z systemem operacyjnym Microsoftu, opublikowano 2322 aplikacje autorstwa Polaków. Od naszych rodzimych programistów prężniej działają jedynie przedstawiciele Stanów Zjednoczonych i Indii, czyli krajów znacznie większych. Wirtualny sklep Windows Phone przekroczył pod koniec 2011 roku liczbę 50 tysięcy aplikacji gotowych do pobrania. Dziennie pojawia się tam około 200-300 nowych aplikacji, zaś liczba ta nadal rośnie. Liczby te nie są tak imponujące jak w przypadku iOS, jednakże widać, iż ostatnie wydarzenia związane z mobilną platformą Microsoftu (geograficzne rozszerzenie dostępności do Marketplace oraz silne techniczno/marketingowe wsparcie ze strony Nokii) pobudziły jej witalność. Na pytanie, czy Windows Phone da radę przebić się na rynku i stać się konkurencją dla iOS’a oraz Androida, jedynie przyszłość może odpowiedzieć, jednakże na wszelki wypadek warto trzymać rękę na pulsie.

Osiągnięcia w Visual Studio

H T T P://W E B H O S T I N G.P L

T

rudno znaleźć miłośnika gier komputerowych, który nie wiedziałby, co to są osiągnięcia (ang. achievements). Ten sprytny mechanizm stosowany w grach vi-

Przygotował i zredagował Rafał Kocisz

/ www.programistamag.pl /

5


BIBLIOTEKI I NARZĘDZIA Robert Nowak

Wyrażenia regularne w C++

biblioteka boost::regex i boost::xpressive Wyrażenia regularne uważa się za elementy programowania deklaratywnego, ponieważ nie dostarcza się sposobu postępowania (algorytmu), tylko danych wejściowych i opisu wyniku. Są one przydatne przy obróbce danych tekstowych, pozwalają zna­ cznie uprościć kod programu. W artykule omawiamy dwie biblioteki dostarczające wyrażeń regularnych dla C++ mając na uwadze fakt, że udogodnienia takie zostały dodane do standardu tego języka.

J

ęzyk C++ pozwala na przetwarzanie danych tekstowych, biblioteka standardowa dostarcza klasę do reprezentacji napisu oraz wyrażenia regularne, są one częścią standardu C++11. Tradycyjne napisy przetwarza się za pomocą języków skryptowych (AWK, sed, Python, Perl), które stanowią rozszerzenie systemu operacyjnego i są używane do kontroli pracy aplikacji, co często wymaga obróbki tekstowych plików konfiguracyjnych i tekstowych plików z logami. Jeżeli wydajność modułu operującego na tekście i utworzonego w tych jezykach jest niewystarczająca, warto rozważać użycie C++ do jego implementacji, wtedy kod jest translowany do kodu maszynowego, natomiast języki skryptowe są interpretowane – więc zazwyczaj dużo mniej wydajne. Napisy w C++ są obiektami standardowej klasy std::string. Klasa ta jest konkretyzacją szablonu basic_ string dla typu char (znaki 8 bitowe). Klasa std::wstring jest generowana na podstawie tego samego szablonu – znaki są przechowywane przez obiekty typu wchar_t (znaki 16 bitowe). Oprócz możliwości przechowywania napisów (o zmiennej długości) szablon basic_string dostarcza podstawowych operacji: porównywania, łączenia napisów, wyszukiwania znaków. Język C++ pozwala na zapis stałych napisowych bezpośrednio w kodzie programu. Ciąg znaków w cudzysłowach jest zamieniany na tablicę znaków, którą można inicjować obiekty typu string (w opisie będziemy pomijali przestrzeń nazw). Można wskazać sposób kodowania oraz reprezentację znaku: ■■ „ASCII” tablica obiektów typu char (8 bitowe); ■■ L”wchar_t” tablica obiektów wchar_t (16 bitowe); ■■ u8”UTF-8 String \u03CO” tablica obiektów typu char, kodowanie UTF-8, znak UTF 03C0 - to pi; ■■ u”UTF-16 String \u03C0” tablica obiektów char16_t (16 bitowe), kodowanie UTF-16; ■■ U”UTF-32 String \u03C0”. tablica obiektów char32_t (32 bitowe), kodowanie UTF-32. Tablice znaków są zakończone znakiem końca napisu, tzn. znakiem \0, jest to standard stosowany w C. Tablicę znaków można utworzyć za pomocą metody c_str() dla

6

/ 1 . 2012 . (1)  /

Szybki start Aby uruchomić przedstawione przykłady, należy mieć dostęp do kompilatora C++ oraz edytora tekstu. Przykłady wykorzystują biblioteki boost (www.boost.org). Aby poprawnie uruchomić te, które wykorzystują bibliotekę boost::regex, należy dodać odpowiednie zależności dla konsolidatora (linkera), dla g++ należy dodać opcję: -lboost_regex; dla Visual Studio (program link) biblioteka ta jest dodawana automatycznie. Przykłady wykorzystujące boost::xpressive wymagają jedynie poprawnej instalacji tej biblioteki. Na wydrukach pominięto dołączanie odpowiednich nagłówków oraz udostępnianie przestrzeni nazw, pełne źródła umie­ szczono jako materiały pomocnicze. obiektu std::string, można także użyć takiej tablicy do inicjacji obiektu std::string. Wyrażenia regularne pozwalają opisywać grupę „podobnych” napisów. Opisy takie są przydatne, gdy chcemy wykonywać pewne operacje na napisach, które reprezentują pewien szablon. Wyrażenia regularne są przydatne przy przetwarzaniu tekstów, dlatego są dostępne standardowo w tych językach skryptowych. Zostały także dodane do biblioteki standardowej w nowej wersji standardu C++11, oferują ją m.in. kompilatory Visual Studio 2010 oraz GNU GCC 4.6. Dla innych, stosowanych obecnie kompilatorów, możemy wykorzystać jedną z kilku bibliotek boost. Wyrażenia regularne można opisać napisem, który zawiera symbole specjalne, patrz ramka. Przykładowo: .a oznacza dwuliterowy napis kończący się literą a (1a, aa, Aa, ...); [AEO]la oznacza napis Ala, Ela lub Ola; \da albo [[:digit:]]a oznacza dwuliterowy napis rozpoczynający się cyfrą, a kończący literą a (1a, 2a, ... ,9a); a*b oznacza dowolną liczbę liter a, a następnie literę b (b, ab, aab, ...); a? b oznacza opcjonalne wystąpienie litery a, a następnie literę b, czyli opisuje dwa napisy: b i ab; (a|b)b oznacza wystąpienie litery a lub b na pierwszej pozycji, a następnie litery b; (ab)+ oznacza przynajmniej jedno powtórzenie ciągu ab (ab, abab, ababab, ...); a(a|b)*b oznacza wyrażenie regularne, które opisuje napisy rozpoczynające się literą a, zakończone literą b, składające się jedynie z liter a i b (napisy ab, aab, abb, aaab, aabb, abab, abbb, ...).


WYRAŻENIA REGULARNE W C++

ZNAKI PISARSKIE są kodowane w sposób standardowy od XIX wieku, używano wtedy kodu Morsa oraz kodów telegraficznych (5 bitów na znak). W systemach komputerowych dominującą rolę odgrywa ASCII (American Standard Code for Information Interchange, 7 bitowy), a ostatnio Unicode (znaki 16- i 32 bitowe oraz o zmiennej długości od 8 do 32 bitów). Taka różnorodność wynika z faktu istnienia wielu alfabetów narodowych i wielu metod ich obsługi. Jeżeli wykorzystujemy ASCII, 7 bitów jest wykorzystywane na kodowanie znaków alfabetu łacińskiego, używanych w tekstach utworzonych w języku angielskim, cyfr i znaków przestankowych. Do reprezentacji innych znaków, na przykład znaków narodowych wykorzystano ósmy bit, co daje możliwość zakodowania dodatkowych 128 symboli. Niestety, dodatkowych znaków, które chcieliby-

śmy zakodować jest znacznie więcej niż 128, więc istnieje wiele standardowych zbiorów konwertujących 8 bitów na znak pisarski, zbiór taki nazywamy stroną kodową. Reprezentacja 8 bitowa oraz mechanizm stron kodowych, pozwala reprezentować teksty w wię­ kszości używanych języków. Niestety takie kodowanie nie pozwala, w prosty sposób, umieszczać w tym samym tekście znaków z różnych stron kodowych, więc jeżeli tekst zawiera napisy w dwóch różnych językach jest kłopotliwy do kodowania. Z drugiej strony, te same ciągi bajtów mogą reprezentować różne ciągi znaków pisarskich, co może prowadzić do trudności z odczytaniem tekstu. Wymienione problemy rozwiązano, stosując dłuższe, niż 1-bajtowe, reprezentacje znaków. Znaki 16 bitowe (UTF16, UCS2) pozwalają zakodować symbole używane w większości współ-

Za pomocą przedstawionego opisu można utworzyć obiekt reprezentujący wyrażenie regularne. Obiekt taki pozwala typowo na następujące operacje: ■■ badanie, czy dany napis jest opisywany przez wyrażenie regularne; ■■ badanie, czy dany napis zawiera ciąg symboli (pod-napis) opisywany przez wyrażenie regularne; ■■ zamiana ciągu symboli opisywanych wyrażeniem regularnym na inny ciąg symboli. Dodatkowo dostępne są metody iteracji po odnalezionych pod-napisach opisywanych wyrażeniem regularnym oraz dzielenia tekstu na grupy. W artykule będziemy wykorzystywali bibliotekę boost::regex, która jest dostępna od kilku lat dla wielu platform i jest w dużej mierze zgodna ze standardem. Dodatkowo opisana będzie biblioteka boost::xpressive, która pozwala tworzyć obiekty reprezentujące wyrażenia regularne w czasie kompilacji. Obiekty reprezentujące wyrażenie regularne przedstawione w tym artykule mają semantykę wartości, można je kopiować, przekazywać jako argument czy zwracać z funkcji lub metody.

BIBLIOTEKA BOOST::REGEXP Wyrażenie regularne jest obiektem typu szablonowego basic_regex, gdzie parametrem jest typ znaku. Dla znaków char klasa ta nazywa się regex, dla wchar_t – wregex. Napis opisujący wyrażenie regularne jest przekazywany w konstruktorze, gdzie tworzony jest wewnętrzna reprezentacja wyrażenia regularnego (jest nią automat skończony). Jeżeli obiekt jest prawidłowo zainicjowany, co oznacza m.in., że opis jest poprawny, to metoda basic_

czesnych języków, znaki 32 bitowe (UTF32, UCS-4) pozwalają kodować wszystkie symbole, także występujące w alfabetach obecnie nie stosowanych (np. hieroglify). Wadą napisów wykorzystujących kodowanie szerokimi znakami, jest zajętość pamięci, gdy kodujemy napisy w języku angielskim. Wadę tę można eliminować, stosując kodowanie znaków przez obiekty o różnej długości: znaki ASCII przez symbole 1-bajtowe (8 bitów), znaki z alfabetów narodowych przez symbole dłuższe (2, 3 lub 4 bajty). Takie kodowanie jest opisane przez standard UTF8. Zaletą UTF8 jest zgodność z ASCII, wadą – niebanalne obliczanie długości napisu, nie ma prostej zależności liczby znaków w napisie od liczby bajtów zajmowanych przez napis. Obecnie najbardziej popularne jest kodowanie UTF-8. Język C++ wspiera wszystkie wymienione wyżej standardy.

regex::empty() zwraca false. Opis wyrażenia regularnego to napis zawierający symbole specjalne, tak jak pokazano wcześniej. Jeżeli symbol posiadający specjalne znaczenie chcemy wykorzystać wprost, stawiamy przed nim lewy ukośnik (backslash, \). Proszę zwrócić uwagę, że kasowanie specjalnego znaczenia symbolu w wyrażeniu regularnym odbywa się za pomocą tego samego znaku, co kasowanie specjalnego znaczenia wewnątrz stałych napisowych języka C++, więc jeżeli wyrażenie regularne

Wybrane na potrzeby demonstracji symbole specjalne stosowane przy opisie wyrażeń regularnych. Pełna lista (kilkadziesiąt symboli) jest dostępna w dokumentacji bibliotek. Symbol

Opis

^$

początek i koniec linii

.

dowolny pojedynczy znak

[aeo]

zbiór znaków

[a-z]

zakres znaków

[^0-9]

znak spoza zakresu

[[:alpha:]]

litera (predefiniowany zbiór znaków)

[[:digit:]]

cyfra (predefiniowany zbiór znaków)

[[:space:]]

biały znak (predefiniowany zbiór znaków)

\d

to samo co [[:digit:]]

\s

to samo co [[:space:]]

*

dowolna liczba (zero lub więcej) powtórzeń poprzedniego symbolu

+

jedno lub więcej powtórzenie poprzedniego symbolu

?

opcjonalność: zero lub jedno powtórzenie

{m,n}

od m do n wystąpień

(wyr)

grupa, do której odnoszą się inne operacje

|

alternatywa

/ www.programistamag.pl /

7


BIBLIOTEKI I NARZĘDZIA jest dostarczone jako stała napisowa (napis w cudzysłowach umieszczony w kodzie), to musimy backslash pisać podwójnie! Po poprawnym utworzeniu obiektu reprezentującego dane wyrażenie regularne możemy używać funkcji opisanych poniżej. Badanie, czy napis jest opisywany wyrażeniem regularnym jest realizowane przez funkcję regex_match, wyszukiwanie ciągu znaków (pod-napisu) opisywanego danym wyrażeniem wykonuje regex_search, regex_replace pozwala zastępować fragmenty opisane danym wyrażeniem regularnym innym napisem. Funkcja regex_match wymaga podania dwóch argumentów: napisu który będzie badany oraz obiektu reprezentującego wyrażenie regularne. Zwraca ona wartość logiczną, czy napis jest opisywany wyrażeniem regularnym. Przykład użycia tej funkcji zawiera Listing 1, gdzie pokazano funkcję correctIP sprawdzającą, czy napis jest poprawnym adresem IP. Przedstawiony kod sprowadza badanie tej poprawności do badania warunku, czy napis składa się z 4 sekcji zawierających od 1 do 3 cyfr i rozdzielonych kropką, nie jest to badanie doskonałe, za poprawny adres IP zostaną uznane napisy takie jak 256.0.0.0 czy 0.0.0.999. Wyrażenia regularne pozwalają taką funkcję zapisać za pomocą dwóch linii kodu. Funkcja regex_search bada, czy dany napis zawiera ciąg symboli (pod-napis) opisywany przez wyrażenie regularne, zwraca ona prawdę logiczną (true), jeżeli taki ciąg udało się odnaleźć. Istnieje możliwość uzyskania wskazania na ten pasujący pod-napis, obiekt typu smatch, przekazywany jako argument referencyjny do funkcji rebool correctIP(const std::string& ip) { static const regex e("\\d{1,3}\\.\\d{1,3}\\.\\ d{1,3}\\.\\d{1,3}"); return regex_match(ip, e); } Listing 1. Badanie, czy napis opisuje poprawny adres IP

gex_search jest wypełniany odpowiednimi wartościami. Przykład zawiera Listing 2, który pokazuje kod wyszukujący odpowiedni znacznik html w łańcuchu znaków. Obiekty generowane przez szablon match_result, zawierające kolekcję par wskaźników lub par iteratorów, są wykorzystywane do wskazywania na ciąg znaków. Pierwszy wskaźnik (lub pierwszy iterator) z pary wskazuje na pierwszy znak opisywanego ciągu, zaś drugi – na znak następny po ostatnim znaku opisywanego ciągu (stosuje się tutaj konwencję taką, jak dla zakresu iteratorów w bibliotece standardowej C++). Biblioteka definiuje cztery różne typy, generowane na podstawie tego szablonu (różne typy znaków, wskaźnik lub iterator): ■■ ■■ ■■ ■■

typedef match_results<const char*> cmatch; typedef match_results<const wchar_t*> wcmatch; typedef match_results<string::const_iterator> smatch; typedef match_results<wstring::const_iterator> wsmatch.

8

/ 1 . 2012 . (1)  /

Pierwszy element kolekcji wskazuje na ciąg opisywany całym wyrażeniem regularnym, drugi element kolekcji (element o indeksie 1) opisuje ciąg „pasujący” do pierwszej grupy (grupy w wyrażeniu regularnym tworzone są za pomocą nawiasów), trzeci element kolekcji to ciąg odpowiadający drugiej grupie itd. string html = /* wczytaj stronę html */; regex mail("href=\"mailto:(.*?)\">"); smatch what;//przechowuje wyniki wyszukiwania if( regex_search(html,what,mail) ) { //what[0] napis pasujący do wyrażenia cout << "mail to" << what[1] << endl; } Listing 2. Wyszukiwanie wzorca opisywanego wyrażeniem regularnym. Tutaj kod, który przeszukuje tekst html w poszukiwaniu adresów e-mail

W przykładzie pokazanym na Listingu 2 wykorzystujemy niezachłanne dopasowywanie do grupy, co jest zapisane w wyrażeniu regularnym jako (.*? ). Symbole wieloznaczne, na przykład *, mogą reprezentować pod-napisy różnej długości, co pozwala na powstawanie przypadków, gdy ten sam napis wejściowy może być interpretowany na wiele sposobów. Biblioteka boost::regex wykorzystuje dwa rodzaje dopasowań: zachłanne, gdy symbol opisuje pod-napis o maksymalnej długości, oraz niezachłanne, gdy dopasowanie oznacza ciąg o minimalnej długości. W ten sposób usuwane są niejednoznaczności. Wyrażenie regularne <(.*)> wskaże na ciąg <p>Hello</p> dla wejścia xxx<p>Hello</p>yyy, zaś wyrażenie <(.*? )> znajdzie <p> dla tego samego wejścia. Funkcja regex_replace pozwala zastępować fragmenty opisane danym wyrażeniem regularnym wybranym napisem. Argumentami tej funkcji są: napis wejściowy, wyrażenie regularne oraz napis formatujący, czyli napis, który będzie wstawiany w miejsce ciągu opisanego wyrażeniem regularnym. Napis formatujący może zawierać kilka symboli specjalnych, między innymi znak &, który określa fragment tekstu opisany wyrażeniem regularnym; znak \D oznacza fragment tekstu dopasowany do

Operatory reprezentujące niektóre symbole specjalne dla boost::xpressive wyrażenie

typowy symbol

opis

bos

^

początek sekwencji

a >> b

brak

konkatenacja

as_xpr(’a’)

a

znak

as_xpr(”abc”)

abc

ciąg znaków

(set=’a’,’b’,’c’)

[abc]

zbiór zbiór

_

.

dowolny znak

|

|

alternatywa

*

*

dowolna liczba wystąpień, - operator jest prefiksowy

+

+

jedno lub więcej wystąpienie

?

!

opcjonalność, prefiksowy

(s1= a), gdzie 1 to nr grupy

(a)

grupa


WYRAŻENIA REGULARNE W C++

Więcej w książce Omówienie współcześnie stosowanych technik, wzorce projektowe, programowanie generyczne, prawidłowe zarządzanie zasobami przy stosowaniu wyjątków, programowanie wielowątkowe, ilustrowane przykładami stosowanymi w bibliotece standardowej i bibliotekach boost, opisano w książce Robert Nowak, Andrzej Pająk ,,Język C++: mechanizmy, wzorce, biblioteki’’, BTC 2010. grupy D, gdzie D = 1, 2, ..., 9. Przykładowo regex_replace("(<p>)+", "<p><p>hello", "<p>") zwróci napis <p>hello, zaś regex_replace("a,bx", reg, "\\2 = \\1") zwróci napis b = ax.

BIBLIOTEKA BOOST::XPRESSIVE - WYRAŻENIA W CZASIE KOMPILACJI Biblioteka boost::xpressive jest inną biblioteką, wchodzącą w skład zbioru boost, która dostarcza obiektów i funkcji pozwalających wykorzystywać wyrażenia regularne. Oferuje ona udogodnienia takie jak opisane poprzednio, a dodatkowo pozwala tworzyć obiekty reprezentujące wyrażenie regularne w czasie kompilacji. Biblioteka boost::regex umożliwia jedynie zaszycie napisu opisującego wyrażenie w kodzie, automat skończony (reprezentacja wyrażenia regularnego) jest tworzony w konstruktorze tego obiektu - czyli w czasie działania. Tworzenie reprezentacji wyrażenia regularnego w czasie kompilacji pozwala zaoszczędzić czas podczas działania aplikacji, nie trzeba wtedy przetwarzać napisu opisującego wyrażenie i budować reprezentacji wewnętrznej. Budowa obiektu reprezentującego wyrażenie regularne wykorzystuje techniki związane z meta-programowaniem, opisane m.in. w książce Nowak, Pająk „Język C++: mechanizmy, wzorce, biblioteki”. Obiekt taki jest tworzony na podstawie wyrażenia, a nie na podstawie napisu. Wyrażenie to wykorzystuje obiekty klas dostarczanych przez bibliotekę, dla których przeciążono operatory reprezentujące symbole specjalne używane w wyrażeniach regularnych. Opis wyrażenia regularnego jest wyrażeniem w C++, co wymusza stosowanie innych, niż typowe, symboli w niektórych przypadkach. Podstawowe elementy takich wyrażeń zostały przedstawione w ramce, pozostałych kilkadziesiąt jest opisanych w dokumentacji biblioteki. Przy tworzeniu opisu wyrażenia regularnego za pomocą wyrażeń C++ jesteśmy zmuszeni zapisywać symbol konkatenacji (łączenia kolejnych części opisujących wyrażenie regularne), czyli stosować operator >>. Opisując wyrażenie regularne napisem nie było takiej konieczności, napis ^a oznaczał wystąpienie litery a na początku linii, tutaj musimy to zapisać jako bos >> a, gdzie bos oznacza obiekt (stałą) reprezentujący początek napisu.

Robert Nowak

string subject(const string& in) { smatch w; //pod-napisy dopasowane do grup if( regex_match(in, w, bos>>"Subject: " >>(s1=*(as_xpr("Re: ")|as_xpr("Odp: "))) >>(s2=*_) ) ) { return string(w[2].first, w[2].second); } return ””; } Listing 3. Funkcja dostarcza tytuł listu, pomijając przedrostki dodawane przez programy pocztowe

Znak lub ciąg znaków zapisujemy jako wynik działania funkcji as_xpr, operatory działają na obiektach pewnych typów, a nie na stałych znakowych i stałych napisowych. Jeżeli kompilator jest w stanie jednoznacznie zastosować konwersję, to możemy pominąć zapis as_xpr('a') i pisać 'a'. Dowolny znak nie jest oznaczany kropką, a podkre­ ślnikiem, więc _ >> ’a’ opisuje dwuliterowe napisy kończące się literą a; operator * i + jest prefiksowy, więc * as_xpr(’a’) >> ’b’ oznacza dowolną liczbę liter a, a nastę­ pnie literę b (b, ab, aab, ...); opcjonalne wystąpienie symbolu (lub grupy) zapisujemy jako ! , więc !as_xpr(’a’) >> ’b’ oznacza opcjonalne wystąpienie litery a, a nastę­pnie literę b, czyli opisuje dwa napisy: b i ab. Wyrażenie bos >> "Subject: " >> (s1=*(as_xpr("Re: ")|as_xpr("Odp: "))) >> (s2=*_) wyszukuje w treści listu (e-mail) tytułu, pomijając przedrostki dodawane przez programy pocztowe. Identyczny obiekt można reprezentować napisem: Subject: (Re: |Odp: )*(.*). Wyrażenia takiego użyto na Listingu 3. Funkcja subject zwróci napis witajcie dla wejścia Subject: Re: Re: Odp: witajcie.

PODSUMOWANIE Biblioteka boost::regex jest dostarczana jako biblioteka binarna, trzeba ją konsolidować. Biblioteka boost::xpressive zawiera tylko nagłówki, do jej użycia nie jest potrzebna konsolidacja. Wyrażenia regularne (takie jak boost::regex) są już dostępne dla popularnych kompilatorów, a będą powszechne, ponieważ są częścią biblioteki standardowej języka (standard C++11).

W Sieci PP http://www.boost.org – dokumentacja bibliotek boost, PP http://www.open-std.org/jtc1/sc22/wg21/docs/papers/2011/n3242.pdf – standard C++11 (working draft).

rno@o2.pl

Adiunkt w Zakładzie Sztucznej Inteligencji Instytutu Systemów Elektronicznych Politechniki Warszawskiej, zainteresowany tworzeniem aplikacji wykorzystujących algorytmy sztucznej inteligencji i fuzji danych. Autor biblioteki faif.sourceforge.net. Programuje w C++ od ponad 15 lat. / www.programistamag.pl /

9


PROGRAMOWANIE GRAFIKI Paweł Wolniewicz

Analiza obrazu:

rozpoznawanie obiektów Funkcja programowego wykrywania i rozpoznawania obiektów znajdujących się na obrazach oraz klatkach sekwencji wideo znajduje zastosowanie w wielu aplikacjach. Dzisiaj oprogramowanie komputerowe nie ogranicza się bowiem do katalogowania, wyświetlania i edycji grafiki, ale analizuje również zawartość zdjęć i filmów.

P

owstanie algorytmów i bibliotek umożliwiających rozpoznawanie obiektów na obrazach pozwoliło między innymi na stworzenie aplikacji służących do wykrywania twarzy, przeszkód, dowolnych kształtów o charakterystyce zbliżonej do zadanej przez programistę. Początkowo rozwiązania takie były używane w robotyce, biometrii oraz innych zastosowaniach przemysłowych. Obecnie z komputerową analizą obrazu ma styczność każdy posiadacz kamery cyfrowej wykrywającej ludzkie twarze, użytkownik oferującej identyczne funkcje usługi Google Picasa, a także wielu innych aplikacji. Nowe możliwości otworzyło pojawienie się smartfonów wyposażonych w aparaty pozwalające na wykonywanie zdjęć w przyzwoitej jakości. Powstał nowy rynek aplikacji, które nie miały racji bytu w przypadku komputerów stacjonarnych, a nawet laptopów. Przykładowe zastosowania to między innymi: ■■ wykrywanie i rozpoznawanie budynków na obrazie z kamery (na potrzeby wzbogaconej rzeczywistości – augmented reality), ■■ odnajdywanie twarzy na obrazie jeszcze przed wykonaniem zdjęcia, w celu poprawienia jakości wykonywanych fotografii, ■■ korekcja zniekształceń wynikających z fotografowania płaskich powierzchni pod kątem (co ułatwia wykorzystanie smartfonu w roli skanera dokumentów). Wszystkie wymienione przykłady wymagają opracowania kodu wykrywającego na obrazie krawędzie, linie lub figury geometryczne o określonym kształcie, a także obiekty o dużym stopniu komplikacji. Nie wystarczy tutaj użycie filtrów, progowania bądź binaryzacji. Trzeba sięgnąć po metody umożliwiające precyzyjne wskazanie na obrazie konkretnych obiektów.

WYBÓR BIBLIOTEK Rozpocznijmy od przygotowania środowiska pracy. Pisanie od podstaw kodu służącego do analizowania obrazu mija się z celem. Istnieją gotowe biblioteki pozwalające zarówno na przygotowanie grafiki źródłowej, jak i wydobycie z odpowiednio przetworzonych zdjęć i klatek filmu interesujących nas obiektów.

10

/ 1 . 2012 . (1)  /

Istnieją pakiety narzędzia programistyczne przeznaczone dla konkretnych platform. Przykładowo, programiści systemu iOS mogą wykorzystać niewielką klasę simple-iphone-image-processing. Zawiera ona funkcje pozwalające na binaryzację grafiki, wykrywanie krawędzi, rozmywanie, manipulowanie jasnością, a także na wykrywanie obiektów w obrazach binarnych. Nie jest to wiele – w bardziej zaawansowanych zastosowaniach trzeba pisać własny kod lub sięgnąć po dodatkowe narzędzia. Bardziej sensownym rozwiązaniem jest skorzystanie z rozbudowanych bibliotek, oferujących pełny pakiet narzędzi do analizy obrazu – od pobrania go z kamery, przez operacje na kanałach i pojedynczych pikselach, progowanie, aż po tworzenie bazy wykrytych konturów i przedmiotów. Jednym z takich uniwersalnych narzędzi jest OpenCV (Open Source Computer Vision). Biblioteka ta jest rozpowszechniana na warunkach licencji BSD i zawiera ponad 2,5 tysiąca zoptymalizowanych algory­ tmów do przetwarzania obrazu. Olbrzymią zaletą OpenCV jest jej wieloplatformowość. Biblioteka działa z powodzeniem zarówno na urządzeniach stacjonarnych (wspierane systemy to Windows i Unix), jak i na smartfonach oraz tabletach. Oprócz obsługi Androida OpenCV doczekało się także wersji przeznaczonej dla iOS. Niewykluczone, że w przyszłości otrzymamy również wsparcie dla kolejnych systemów operacyjnych dla urządzeń mobilnych. Kod źródłowy przedstawiony w dalszej części artykułu przeznaczony będzie dla OpenCV wykorzystywanego na komputerach stacjonarnych i laptopach. Porty biblioteki dla systemów mobilnych znajdują się na razie w fazie eksperymentalnej, stąd też korzystanie z nich jest nieco trudniejsze. Użytkownicy zainteresowani tworzeniem aplikacji dla iPhone oraz iPada powinni przejrzeć krótki samouczek (w języku angielskim), opisujący pierwsze kroki z pakietem. O ile same funkcje i struktury danych nie różnią się bez względu na platformę, o tyle instalacja biblioteki oraz sposób odwoływania się do plików nagłówkowych zależy od wykorzystywanego systemu dla urządzeń mobilnych. W przypadku Androida warto zapoznać się z podrozdziałem dokumentacji prezentującym sposób użycia biblioteki w środowisku Eclipse.


ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW

Wykorzystanie OpenCV w celu tworzenia oprogramowania dla komputerów stacjonarnych i laptopów jest najłatwiejsze i sprowadza się do pobrania pakietu dla systemu Windows lub zainstalowania paczek w dowolnej z popularnych dystrybucji Linuksa. W przypadku systemu Mac OS X sytuacja jest trudniejsza. Należy przede wszystkim odwiedzić stronę internetową pakietu na witrynie OpenCV. Znajdziemy tam podstawowe informacje instalacyjne oraz link do plików do pobrania. Bibliotekę OpenCV możemy użyć niezależnie od wykorzystywanego przez nas środowiska programistycznego. Dla uproszczenia będziemy przygotowywali kod gotowy do skompilowania za pomocą jednej komendy dowolnego kompilatora dostępnego na licencji open source, na przykład MinGW (w Windows) i GCC (w Linuksie). Oprócz OpenCV można skorzystać także z innych bibliotek o podobnym zastosowaniu. Warto wypróbować zwłaszcza ITK (Insight Segmentation and Registration Toolkit). Tak jak OpenCV, biblioteka została napisana w języku C++. Można wykorzystać ją jednak także podczas tworzenia aplikacji w innych językach, między innymi Java. ITK jest biblioteką wszechstronną. Olbrzymia lista klas zawiera narzędzia przeznaczone zarówno do wykonywania operacji na pojedynczych pikselach, korzystania z dziesiątek (setek?) filtrów, jak i przetwarzania konturów wykrytych na analizowanych obrazach. Pod względem funkcjonalności jest to najlepsza alternatywa na licencji open source dla biblioteki OpenCV. Problem stanowi je­ dnak wsparcie dla systemów operacyjnych dla urządzeń mobilnych. Pod tym względem o wiele lepiej prezentuje się OpenCV. Jeśli więc zamierzamy tworzyć oprogramowanie, które w przyszłości trafi do smartfonów i tabletów, to dobrym rozwiązaniem będzie wybór OpenCV.

ZALĄŻEK PROJEKTU Aplikacja rozpoznająca obiekty na zdjęciach / klipach wideo powinna wykonywać w tle następujące zadania:

Listing 1. Pobieranie obrazu z kamery cyfrowej

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { // pobranie obrazu z dowolnej kamery CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } // przygotowanie okienka cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); // pobieranie kolejnych klatek aż do naciśnięcia klawisza ESC while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } // wyświetlanie klatek w okienku cvShowImage( "okienko", klatka ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } // porządkowanie cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; }

■■ pobieranie obrazu ze źródła (kamera cyfrowa, plik na dysku), ■■ przetwarzanie uzyskanego obraz w taki sposób, by możliwe było bezbłędne wyodrębnienie potrzebnych obiektów, ■■ wykrywanie obiektów, ■■ wykonywanie operacji na wykrytych konturach / przedmiotach.

do projektu funkcję IplImage2QImage. Jest ona odpowiedzialna za konwersję obrazów OpenCV do postaci akceptowalnej przez bibliotekę Qt. Dodatkowa funkcja qImage2IplImage działa w sposób odwrotny. Jeżeli proces tworzenia obrazu QImage zakończy się niepowodzeniem, to konieczna będzie konwersja przestrzeni barw z poziomu OpenCV (funkcja cvCvtColor).

Niezależnie od tego powinniśmy również przygotować interfejs, za pomocą którego aplikacja będzie komunikowała się z użytkownikiem i przedstawiała mu efekty swojej pracy. Wszystko zależy od preferencji programisty i jego indywidualnych potrzeb. Jeśli decydujemy się na stosowanie przenośnych bibliotek open source, to graficzny interfejs może korzystać na przykład z Qt. Wystarczy dokonać konwersji pomiędzy obiektami IplImage i QImage, by uzyskać z poziomu Qt dostęp do funkcji oferowanych przez OpenCV. Potrzebny kod można znaleźć na stronach Nokii (właściciela Qt). Powinniśmy skopiować i włączyć

Tworzenie własnej aplikacji rozpoznającej obiekty znajdujące się na zdjęciach oraz klatkach sekwencji wideo musimy rozpocząć od pobrania obrazu źródłowego. W tym celu napiszemy prosty kod w języku C++. Uzyska on dostęp do kamery cyfrowej, pobierze kolejne klatki obrazu, a następnie wyświetli go na ekranie w osobnym oknie. Na razie nie będziemy stosowali żadnych funkcji służących do analizy obrazu ani szukania obiektów. Zbudujemy tylko szkielet aplikacji wykorzystującej OpenCV. Przykładowy kod zaprezentowany został na listingu 1. Za połączenie z kamerą internetową podłączoną do portu

POZYSKANIE OBRAZU

/ www.programistamag.pl /

11


PROGRAMOWANIE GRAFIKI

Rysunek 1. Oryginalny obraz wykorzystany w testach

Rysunek 2. Efekt dwukrotnego przeskalowania oryginalnego obrazu

Rysunek 3. Efekt binaryzacji z użyciem funkcji cvThreshold

Rysunek 4. Tak działa detekcja krawędzi metodą Canny’ego

Rysunek 5. Pułapki binaryzacji i detekcji krawędzi: źle dobrane progi w połączeniu z niewielkim kontrastem pomiędzy kartką papieru a jej tłem spowodowały, że brzegi papieru nie zostały uchwycone przez zastosowaną w dalszej kolejności metodę transformacji Hougha

Rysunek 6. Okręgi odnalezione przez funkcję cvHoughCircles

Rysunek 7. Efekt działania algorytmu szukającego prostokątów.

12

/ 1 . 2012 . (1)  /

USB odpowiedzialna jest funkcja cvCaptureFromCAM. Łączymy się z dowolnym urządzeniem z obecnych w systemie (parametr CV_CAP_ANY). Kolejne klatki pobierane są za pomocą funkcji cvQueryFrame wykonywanej wewnątrz pętli while. I wreszcie, linijki zaczynające się od cvNamedWindow oraz cvShowImage przygotowują okienko oraz wypełniają je treścią, czyli kolejnymi klatkami. Jeszcze łatwiejszym zadaniem jest wyświetlenie obrazu z pliku. Kod kompletnego programu ładującego grafikę z dysku ma zaledwie 20 linijek (listing 2). Jedyną nowość stanowi funkcja cvLoadImage, z parametrem w postaci ścieżki dostępu. Jest ona odpowiedzialna za otwieranie obrazu.


ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW

PRZYGOTOWANIE OBRAZU DO ANALIZY Zdjęcie z dysku lub obraz z kamery zazwyczaj muszą być odpowiednio przygotowane do analiz polegających na rozpoznawaniu obiektów. W przeciwnym wypadku uzyskane rezultaty będą dalekie od oczekiwań. Niezbędne operacje można podzielić na kilka grup: ■■ usuwanie z obrazu artefaktów mogących zakłócić analizy, ■■ konwersję do odcieni szarości i/lub binaryzację, ■■ wykrywanie krawędzi. Nie wszystkie te etapy należy wykonać. Zabiegiem potrzebnym najczęściej jest poprawianie czytelności źródłowego obrazu. W tym celu konieczne jest skorzystanie z filtrów lub zastosowanie prostego sposobu polegającego na zmniejszeniu obrazu, a następnie przeskalowaniu do pierwotnej rozdzielczości. Drobne artefakty zostaną wówczas przynajmniej częściowo usunięte na skutek interpolacji. Rozpocznijmy od techniki polegającej na dwukrotnym skalowaniu obrazu. Poprzedzimy ją konwersją klatki wideo do odcieni szarości. Większość algorytmów, które wykorzystamy później, i tak operuje na pojedynczym kanale. Konwersja i skalowanie grafiki zaprezentowane są na listingu 3. Za konwersję obrazu do innej przestrzeni barw w OpenCV odpowiedzialna jest funkcja cvCvtColor. Po wskazaniu grafiki źródłowej oraz docelowej określamy jeszcze rodzaj konwersji. Aby uzyskać obraz w odcieniach szarości należy ustalić wartość tego parametru jako CV_ BGR2GRAY. Skalowanie obrazu umożliwiają natomiast funkcje cvPyrDown i cvPyrUp. Wcześniej warto jeszcze ograniczyć rozmiar grafiki, jeśli szerokość lub wysokość Listing 2. Otwieranie obrazu z dysku

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { IplImage* obraz = 0; // wskazujemy nazwę pliku obraz=cvLoadImage("obrazek.tif"); if(!obraz){ printf("Problem z obrazkiem!"); exit(0); } // wyświetlanie obrazu z pliku cvNamedWindow("okienko", CV_WINDOW_ AUTOSIZE); cvShowImage("okienko", obraz ); cvWaitKey(0); // porządki cvReleaseImage(&obraz ); return 0; }

Listing 3. Skalowanie obrazu i konwersja przestrzeni barw w OpenCV

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); // zmienna przechowująca rozmiar oryginalnego obrazu, po zaokrągleniu w przypadku, gdy szerokość i/lub wysokość są wartościami nieparzystymi CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); // ograniczenie dalszych operacji do obszaru grafiki o rozmiarach będących liczbami podzielnymi przez 2 cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); // konwersja do odcieni szarości cvCvtColor(klatka, szary, CV_BGR2GRAY); // skalowanie obrazu "w cvPyrDown( szary, maly, // skalowanie obrazu "w cvPyrUp( maly, szary, 7

dół" 7 ); górę" );

cvShowImage( "okienko", szary ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; } / www.programistamag.pl /

13


PROGRAMOWANIE GRAFIKI Listing 4. Przygotowanie obrazu do wykrywania obiektów: filtry i binaryzacja

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } IplImage* szary = cvCreateImage (cvSize(klatka->width, klatka->height), IPL_ DEPTH_8U, 1 ); IplImage* koncowy = cvCreateImage (cvSize(klatka->width, klatka->height) , 8, 1 ); cvCvtColor(klatka, szary, CV_BGR2GRAY); // filtr Gaussa cvSmooth(szary, szary, CV_GAUSSIAN, 9, 9); // binaryzacja obrazu (progowanie) cvThreshold( szary, koncowy, 100, 255, CV_THRESH_BINARY ); // zamiast binaryzacji w niektórych zastosowaniach lepiej skorzystać z algorytmu Canny'ego do wykrywania krawędzi //cvCanny( szary, koncowy, 75, 150, 3 ); //cvDilate( koncowy, koncowy, 0, 1 ); cvShowImage( "okienko", koncowy ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; }

14

/ 1 . 2012 . (1)  /

mają wartości nieparzyste. Dokonujemy tego funkcją cvSetImageROI. Zamiast skalować obraz można skorzystać z filtrów, które wygładzą źródłową grafikę, likwidując drobne zakłócenia. W tym celu użyjemy funkcji cvSmooth. Do wyboru mamy kilka filtrów. Jeżeli obraz źródłowy powinien być rozmyty, to dobrym rozwiązaniem będzie filtr Gaussa. Aby z niego skorzystać, należy określić wartość parametru smoothtype jako CV_GAUSSIAN. Niewielkie zakłócenia na obrazie, które mogą utrudnić proces rozpoznawania obiektów, można usunąć za pomocą filtra medianowego. Zastępuje on wartości pikseli medianą z wartości sąsiednich punktów. Likwiduje to szum w niewielkiej skali, zachowując ogólne tendencje, dzięki czemu krawędzie nie są niszczone. Umożliwia to późniejszą detekcję obiektów w obrazie. Filtr medianowy stosujemy za pomocą funkcji cvSmooth, przy czym wartość parametru smoothtype powinna być określona jako CV_MEDIAN. Wszystkie narzędzia wchodzące w skład OpenCV i służące do usuwania zakłóceń oraz wygładzania obrazu opisane są w dokumentacji biblioteki. Użytkownik korzysta z gotowych rozwiązań oferowanych przez funkcję cvSmooth lub przygotowuje własny filtr konwolucyjny, definiując uprzednio jego maskę (cvFilter2D). Usuwanie niedoskonałości z pozyskanego obrazu ma ogromne znaczenie dla rozpoznawania obiektów. Wię­ kszość analiz tego typu oparta jest bowiem na wykrywaniu w badanej grafice wyraźnych krawędzi, nierzadko mających charakter prostych linii bądź okręgów. Artefakty mogą zatrzeć takie granice. Duża rozdzielczość powoduje ponadto, że istotne krawędzie zostają rozmyte do poziomu słabych gradientów. Poza tym ich przebieg jest zakłócony przez liczne, drobne elementy obrazu, które w niskiej rozdzielczości zostałyby po prostu pominięte. Ważnym problemem wpływającym na proces przygotowania obrazu są także odbiegające od optymalnych ustawienia kontrastu i jasności. Mogą one spowodować, że wyraźna krawędź stanie się mało widoczna – i w efe­kcie pominięta przez algorytmy odnajdujące kształty. Prosta zmiana parametrów kamery może niekiedy dopomóc więcej niż skomplikowane manipulacje z użyciem filtrów.

BINARYZACJA I WYKRYWANIE KRAWĘDZI Po wstępnym przygotowaniu obrazu można go zbinaryzować, o ile oczywiście jest to wymagane w kolejnych etapach pracy nad grafiką lub klatką filmową. Binaryzacja polega na ograniczeniu przestrzeni barw do czerni i bieli. Każdy piksel jest w efekcie opisany jednym bitem danych. Binaryzacja ma z reguły na celu oddzielenie istotnych elementów obrazu od ich tła. Wynikowy obraz może zostać w prosty sposób wykorzystany w celu analizy i rozpoznawania obiektów. Spełniony musi być tylko jedyny warunek: poprawne rozróżnienie istotnych elementów od tła. Biblioteka OpenCV umożliwia szybkie przeprowadzenie binaryzacji z wykorzystaniem tak zwanego progowania. Technika ta polega na analizowaniu kolejnych pikseli i po-


ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW

równywania ich z założoną wartością (lub wartościami). Punkty o wartościach znajdujących się poza ustalonym przez użytkownika zakresem są oznaczane zerem bądź jedynką, natomiast pozostałe – drugą z wartości. Tym samym wynikowy obraz nie zawiera żadnych barw innych niż czerń i biel – jest zatem zbinaryzowany. Kluczowe znaczenie dla procesu progowania ma wybór progu. Jego nieprawidłowe ustalenie spowoduje, że elementy obrazu staną się niemożliwe do wykrycia, lub ich krawędzie znajdą się w złym miejscu. Wybór odpowiedniego progu wymaga niekiedy wielu eksperymentów; w niektórych sytuacjach można pozostawić decyzję oprogramowaniu, poszukując obiektów na obrazie kilkakrotnie, dla różnych wartości granicznych. Daje to lepsze rezultaty, ale wymaga czasu, utrudniając analizę dostarczanych na bieżąco klatek filmu z kamery. Progowania dokonujemy za pomocą funkcji cvThreshold. Jeżeli chcemy uzyskać obraz binarny, to oprócz klatki/zdjęcia źródłowego i wynikowego, wartości progu

oraz maksymalnej, musimy jeszcze określić wartość piątego parametru jako CV_THRESH_BINARY. Progowanie nie musi bowiem koniecznie skutkować binaryzacją. Przykładowe polecenie wykorzystujące funkcję cvThreshold znaleźć można na listingu 4. Na zbinaryzowanym obrazie widoczne są nie tylko obiekty, ale także ich krawędzie. Nie jest to jedyny sposób, by odnaleźć na zdjęciach i klatkach wideo wyraźne granice, czyli linie wzdłuż których wartości sąsiadujących pikseli zmieniają się gwałtownie. Inne rozwiązanie polega na użyciu algorytmów wykrywania krawędzi. OpenCV oferuje kilka spośród nich. Pełną listę można znaleźć w dokumentacji biblioteki. Jednym z najbardziej znanych algorytmów wykrywania krawędzi jest metoda Canny’ego. W przypadku OpenCV detekcja granic pomiędzy elementami i tłem sprowadza się do użycia funkcji cvCanny z pięcioma parametrami – obrazami wejściowym i wyjściowym, dwoma progami oraz rozmiarem maski dla operatora Sobela. Progi można

Listing 5. Wykrywanie prostych metodą transformacji Hougha

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } // skalowanie obrazu parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); // wykrywanie krawędzi algorytmem Canny

IplImage* koncowy = cvCreateImage( parzysty, 8, 1 ); cvCanny( szary, koncowy, 75, 150, 3 ); cvDilate( koncowy, koncowy, 0, 1 ); // przygotowanie zmiennych przechowujących dane linii CvMemStorage* storage = cvCreateMemStorage(0); CvSeq* linie = 0; // transformacja Hougha linie = cvHoughLines2(koncowy, storage, CV_HOUGH_PROBABILISTIC, 1, CV_PI/180, 50, 50, 10 ); // wykreślanie linii na obrazie źródłowym for (int i = 0; i < linie->total; i++ ) { CvPoint* linia = (CvPoint*) cvGetSeqElem(linie,i); cvLine(klatka, linia[0], linia[1], CV_ RGB(255,0,0), 3, 8 ); } cvShowImage( "okienko", klatka ); // porządki cvReleaseImage( &maly ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; }

/ www.programistamag.pl /

15


PROGRAMOWANIE GRAFIKI określać w sposób dowolny (w zakresie od 0 do 255 w przypadku obrazach w odcieniach szarości), pamiętając jednak, że główne krawędzie są najpewniej wykrywane, gdy rozstaw tych parametrów jest duży (listing 4).

DETEKCJA PROSTYCH I OKRĘGÓW Binaryzacja i detekcja granic w obrębie obrazów prowadzi nas do głównego zagadnienia – czyli wykrywania oraz rozpoznawania obiektów. Także i w tym przypadku OpenCV oferuje spory pakiet możliwości. Jeżeli naszym zadaniem jest odnalezienie na obrazie prostych bądź okręgów, to powinniśmy skorzystać z kilku funkcji implementujących transformację Hougha. Dzięki nim cały proces wykrywania sprowadza się do wydania jednego polecenia, dzięki któremu otrzymamy kompletną listę odnalezionych elementów. Adresy konkretnych punktów na obrazie pozwolą nam na przetwarzanie tych informacji.

Transformacja Hougha służy do wykrywania kształtów opisanych równaniem określonym przez użytkownika. Początkowo wykorzystywano ją do detekcji prostych, krzywych i okręgów. Po opracowaniu uogólnionej transformacji Hougha metoda ta zaczęła się sprawdzać w wykrywaniu dowolnych kształtów. OpenCV zawiera kilka funkcji opartych na algorytmach Hougha. Podstawowe narzędzie to cvHoughLines2. Funkcja ta pozwala na wykrycie linii na odpowiednio przygotowanym obrazie, zwracając współrzędne znalezionych prostych. Na listingu 5 transformacja Hougha wykorzystana jest w celu detekcji prostych na klatkach wideo. Zostały one wcześniej przeskalowane i przetworzone algorytmem Canny’ego. Pierwsza z czynności może zostać zastąpiona filtrem bądź pominięta, w zależności od potrzeb. Efekty pracy metody Hougha nanoszone są na obraz źródłowy, wyświetlany na ekranie.

Listing 6. Wykrywanie okręgów za pomocą transformacji Hougha

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSize parzysty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; } parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); CvMemStorage* storage = cvCreateMemStorage(0);

16

/ 1 . 2012 . (1)  /

// zmiana w stosunku do kodu wykorzystującego funkcję cvHoughLines2 -obraz jest przygotowany za pomocą filtra Gaussa cvSmooth(szary, szary, CV_GAUSSIAN, 9, 9); // wykrywanie okręgów CvSeq* circles = cvHoughCircles(szary, storage, CV_HOUGH_GRADIENT, 2, szary>height/4, 200, 100); // wykreślanie wykrytych okręgów na obrazie źródłowym int i; for (i = 0; i < circles->total; i++) { float* p = (float*)cvGetSeqElem( circles, i ); cvCircle( klatka, cvPoint(cvRound(p [0]),cvRound(p[1])), cvRound(p[2]), CV_ RGB(255,0,0), 3, 8, 0 ); } cvShowImage( "okienko", klatka ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; }


ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW

W podobny sposób transformacja Hougha może zostać wykorzystana w celu wykrywania okręgów. Pozwala na to funkcja cvHoughCircles. Jej przykładowe zastosowanie można znaleźć na listingu 6. Użycie transformacji Hougha zostało tam poprzedzone filtrem Gaussa.

WYKRYWANIE DOWOLNYCH OBIEKTÓW Bibliotekę OpenCV możemy wykorzystać nie tylko w celu detekcji prostych i okręgów, ale także dowolnych kształtów geometrycznych. W tym celu należy zastosować funkcję cvFindContours, która odnajdzie wszystkie widoczne na obrazie kontury. Oczywiście konieczne jest odpowiednie przygotowanie zdjęcia lub klatki filmu. Obraz

powinien być poprawnie zbinaryzowany (tak, by wido­czne były wszystkie potrzebne obiekty). OpenCV pozwala na wygodne manipulowanie na wykrytych konturach, co zostało zaprezentowane na listingu 7. Znalezione kontury są tam poddawane analizie mającej na celu detekcję prostokątów. Wykryte kształty są generalizowane, tak by uzyskać wielokąty (funkcja cvApproxPoly). Następnie, o ile uzyskana figura ma dokładnie cztery wierzchołki, obliczane są kąty pomiędzy sąsiednimi bokami. Pozwala to na zidentyfikowanie prostokątów. Figury, które spełniają wymienione kryteria, są wykreślane na obrazie źródłowym. W podobny sposób można tworzyć kod rozpoznający inne wielokąty. W przypadku bardziej skomplikowanych figur jest to jednak trudne. Dodatkowym problemem sta-

Listing 7. Wykorzystanie OpenCV do detekcji wielokątów

#include <stdio.h> #include <opencv/highgui.h> #include <opencv/cv.h> // zmodyfikowana wersja demo "Square Detector" // oblicza cosinus kąta, umożliwiając odnalezienie kątów prostych double angle( CvPoint* pt1, CvPoint* pt2, CvPoint* pt0 ) { double dx1 = pt1->x - pt0->x; double dy1 = pt1->y - pt0->y; double dx2 = pt2->x - pt0->x; double dy2 = pt2->y - pt0->y; return (dx1*dx2 + dy1*dy2)/sqrt((dx1*dx1 + dy1*dy1)*(dx2*dx2 + dy2*dy2) + 1e-10); } int main() { CvCapture* obraz = cvCaptureFromCAM (CV_ CAP_ANY); if ( !obraz ) { fprintf( stderr, "Brak kamery\n" ); return -1; } CvSize parzysty; int rozmiary_klatki = (int) (cvGetCaptureProperty(obraz, CV_CAP_PROP_ FRAME_WIDTH) * cvGetCaptureProperty(obraz, CV_CAP_PROP_FRAME_HEIGHT) ); cvNamedWindow( "okienko", CV_WINDOW_ AUTOSIZE ); CvSeq* prostokaty; while ( 1 ) { IplImage* klatka = cvQueryFrame( obraz ); if ( !klatka ) { fprintf( stderr, "Brak klatki\n" ); break; }

parzysty = cvSize( klatka->width & -2, klatka->height & -2 ); IplImage* szary = cvCreateImage( parzysty, IPL_DEPTH_8U, 1 ); IplImage* maly = cvCreateImage( cvSize(parzysty.width/2, parzysty.height/2), IPL_DEPTH_8U, 1 ); cvSetImageROI( klatka, cvRect( 0, 0, parzysty.width, parzysty.height )); cvCvtColor(klatka, szary, CV_BGR2GRAY); cvPyrDown( szary, maly, 7 ); cvPyrUp( maly, szary, 7 ); IplImage* koncowy = cvCreateImage( parzysty, 8, 1 ); // deklaracje zmiennych przechowujących kontury oraz prostokąty CvSeq* kontury; CvPoint* rect; CvMemStorage* storage = 0;

// przygotowanie obrazu; binaryzacja, wykrywanie krawędzi int l, N = 11; for( l = 0; l < N; l++ ) { if( l == 0 ) { // wykrywanie krawędzi za pomocą algorytmu Canny cvCanny( szary, koncowy, 0, 50, 5 ); cvDilate( koncowy, koncowy, 0, 1 ); } else { // binaryzacja metodą progowania cvThreshold( szary, koncowy, (l+1)*255/N, 255, CV_THRESH_BINARY ); } cd. na kolejnej stronie... / www.programistamag.pl /

17


PROGRAMOWANIE GRAFIKI cd... Listing 7. Wykorzystanie OpenCV do detekcji wielokątów

// wykrywanie konturów na przygotowanym wcześniej obrazie storage = cvCreateMemStorage(0); cvFindContours(koncowy, storage, &kontury, sizeof(CvContour), CV_RETR_LIST, CV_CHAIN_APPROX_SIMPLE ); // dodatkowe deklaracje double powierzchnia_max = 0.0; CvPoint pt[4]; prostokaty = cvCreateSeq( 0, sizeof(CvSeq), sizeof(CvPoint), storage ); CvSeq* rezultat; double s, t; int i; while( kontury ) { rezultat = cvApproxPoly( kontury, sizeof(CvContour), storage, CV_POLY_APPROX_ DP, cvContourPerimeter(kontury)*0.02, 0 ); // poszukiwanie konturów o czterech wierzchołkach, o powierzchni większej od minimalnej dozwolonej (aby usunąć artefakty) // oraz mniejszej od całej powierzchni klatki if( rezultat->total == 4 && fabs(cvContourArea(rezultat,CV_WHOLE_ SEQ)) > 1000 && cvCheckContourConvexity(rezultat) && (fabs(cvContourArea(rezultat,CV_WHOLE_SEQ)) < ( (rozmiary_klatki / 10) * 9.5 )) ) { s = 0; for( i = 0; i < 5; i++ ) { if( i >= 2 ) { t = fabs(angle( (CvPoint*)cvGetSeqElem( rezultat, i ), (CvPoint*)cvGetSeqElem( rezultat, i-2 ), (CvPoint*)cvGetSeqElem( rezultat, i-1 ))); s = s > t ? s : t; } } // cosinus kątów bliski zeru, a więc kontur jest prostokątem if( s < 0.2 ) { for( i = 0; i < 4; i++ ) cvSeqPush( prostokaty, (CvPoint*)cvGetSeqElem( rezultat, i )); // wykryty prostokąt jest największym z widocznych na ekranie

18

/ 1 . 2012 . (1)  /

if ( fabs(cvContourArea(rezultat,CV_ WHOLE_SEQ)) > powierzchnia_max ) { powierzchnia_max = fabs(cvContourArea(rezultat,CV_WHOLE_SEQ)); CvSeqReader reader; cvStartReadSeq( rezultat, &reader, 0 ); for( int ii = 0; ii < rezultat>total; ii += 4 ) { rect = pt; memcpy( pt, reader.ptr, prostokaty->elem_size ); CV_NEXT_SEQ_ELEM( rezultat>elem_size, reader ); memcpy( pt + 1, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat>elem_size, reader ); memcpy( pt + 2, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat>elem_size, reader ); memcpy( pt + 3, reader.ptr, rezultat->elem_size ); CV_NEXT_SEQ_ELEM( rezultat>elem_size, reader ); } } } } // przejście do kolejnego konturu kontury = kontury->h_next; } // wykreślenie prostokąta na obrazie źródłowym if ( klatka && rect ) { int counting = 4; cvPolyLine( klatka, &rect, &counting, 1, 1, CV_RGB(0,255,0), 3, 8 ); } } cvShowImage( "okienko", klatka ); cvReleaseImage( &maly ); cvReleaseImage( &szary ); cvReleaseImage( &koncowy ); if ( ( cvWaitKey(10) & 255 ) == 27 ) break; } cvReleaseCapture( &obraz ); cvDestroyWindow( "okienko" ); return 0; }


ANALIZA OBRAZU: ROZPOZNAWANIE OBIEKTÓW

je się dokładna parametryzacja znalezionych konturów. Lepszym rozwiązaniem jest wykorzystanie klasyfikatora kaskadowego Haara. Umożliwi on rozpoznawanie dowolnych obiektów, bez konieczności pisania dodatkowego kodu C++. W OpenCV wymaga to skorzystania z funkcji cvHaarDetectObjects. Wcześniej należy jeszcze załadować plik klasyfikatora w formacie XML. Służy do tego funkcja cvLoad. Większa ilość kodu nie jest potrzebna. Przykładowy, kompletny program obejrzeć można w dokumentacji. Napisanie kodu korzystającego z algorytmu Haara to jednak nie wszystko. Przed rozpoczęciem rozpoznawania obiektów należy bowiem przygotować własny klasyfikator. W tym celu trzeba dostarczyć zestaw próbek zawierających szukany obiekt, a także takich, w których go nie ma. Obrazy te zostaną użyte w celu trenowania klasyfikatora. Narzędzia do tego przeznaczone rozpowszechniane są razem z biblioteką OpenCV. Jeżeli wystarczą nam gotowe klasyfikatory, pozwalające na wykrywanie sylwe­ tki ludzkiej oraz twarzy jej części, to możemy skorzystać z istniejących plików XML, umieszczonych w podkatalogu data\haarcascades. Tworzenie własnego klasyfikatora jest czynnością trójetapową. Pierwszy krok polega na skorzystaniu z progra-

Paweł Wolniewicz

mu opencv_createsamples. Na podstawie jednego lub kilku zdjęć obiektu utworzy on szereg obrazków, w których obiekt będzie umieszczony na innym tle. Po przygotowaniu próbek można przystąpić do trenowania klasyfikatora. Służy do tego program opencv_haartraining. Otrzymany klasyfikator warto przetestować za pomocą polecenia opencv_performance. Od tego momentu można już rozpoznawać interesujące nas obiekty. Nie trzeba nic zmieniać w kodzie – poza ścieżką dostępu do wygenerowanego przez nas pliku XML.

PODSUMOWANIE Rozpoznawanie obiektów na obrazach to obszerne zagadnienie, do niedawna trudne do zastosowania w praktyce. Pojawienie się aplikacji oraz usług wykorzystujących opisane techniki sprawiło, że znajomość komputerowej analizy obrazu stała się przydatna coraz większemu gronu programistów. Zmiany te idą w parze z pojawieniem się wszechstronnych i zaawansowanych bibliotek umożliwiających wykrywanie obiektów na obrazach i w sekwencjach wideo. Dzięki temu przygotowanie własnej aplikacji korzystającej z technik analizy obrazu jest stosunkowo proste i nie zajmuje wiele czasu.

pawelw@innodevel.net

Autor od pięciu lat wykorzystuje metody analizy obrazów biologicznych, przede wszystkim z użyciem biblioteki OpenCV.

reklama

/ www.programistamag.pl /

19


JĘZYKI PROGRAMOWANIA Rafał Kocisz

C++11 w praktyce: sygnały i sloty Sygnały i sloty to ciekawy wariant mechanizmu wywołań zwrotnych stanowiący elegancką alternatywę dla wzorca Obserwator. Czytając niniejszy artykuł zrozumiesz, jak działa ten mechanizm, przekonasz się, jak można zaoszczędzić sobie pracy, stosując go, i przekonasz się, jak można go łatwo zaimplementować, korzystając z nowoczesnych możliwości języka C++. Kilka miesięcy temu miałem okazję zmierzyć się z zadaniem polegającym na stworzeniu biblioteki służącej do budowania graficznych interfejsów użytkownika zintegrowanej z silnikiem do tworzenia gier na nowoczesne platformy mobilne oraz przenośne konsole do gier. Zmagając się z tym fascynującym i - jak się w praktyce okazało - dość złożonym zagadnieniem, napotkałem szereg ciekawych problemów do rozwiązania. Jednym z tych problemów było stworzenie uniwersalnego, a jednocześnie wygodnego w obsłudze mechanizmu wywołań zwrotnych (ang. callbacks) w języku C++.

W

szystko zaczęło się od zadania. W moim przypadku chodziło o stworzenie biblioteki kontrolek wielokrotnego użytku, za pomocą której będzie można szybko budować oraz utrzymywać przenośne interfejsy użytkownika w grach uruchamianych na szeregu urządzeń mobilnych. Przez "przenośność" rozumiem tutaj zestaw cech polegających na tym, iż wspomniane interfejsy potrafią się dopasowywać do różnych rozdzielczości wyświetlaczy, do liczby wyświetlaczy (niektóre mobilne konsole posiadają więcej niż jeden ekran), a także odpowiednio obsługiwać różne zestawy kontrolerów dostę­ pnych na danej platformie (np. ekran dotykowy, d-pad, ekran dotykowy + d-pad itd.). Ogólne założenia techniczne dotyczące tej biblioteki były takie, że klasa reprezentująca stany gry (GameState) "trzyma" w sobie drzewiastą hierarchię kontrolek interfejsu użytkownika (ControlTree) i przekazuje do niej zdarzenia wygenerowane w związku z interakcją użytkownika (np. wciśnięcie klawisza, dotknięcie ekranu czy wychylenie urządzenia). Komunikacja w tę stronę odbywa się bez problemu, np. poprzez wywoływanie wirtualnych funkcji z bazowej klasy reprezentującej abstrakcyjną kontrolkę (Control), obsługujących różne typy zdarzeń. Na Listingu 1 pokazane są uproszczone definicje wymienionych wyżej klas, obrazujące podstawowe relacje między nimi. Analizując Listing 1, warto zwrócić uwagę na to, w jaki sposób klasa BrowseMainMenuState deleguje zdarzenia OnKeyDown() oraz OnKeyUp() do drzewa kontrolek. Uważni Czytelnicy zauważą zapewne szybko dość istotną lukę w przedstawionej architekturze. Chodzi oczywiście o komunikację zwrotną. Np. załóżmy, że obiekt klasy BrowseMainMenuState przechwyci komunikat OnKey-

20

/ 1 . 2012 . (1)  /

Mechanizm taki w końcu udało mi się zbudować. Zaprogramowałem go, korzystając z konstrukcji języka C++ opisanych w standardzie ANSI/IOS z 1998 (głównie ze względu na brak wsparcia dla konstrukcji oraz biblioteki standardowej najnowszego standardu języka w starszych kompilatorach). Później jednak zdecydowałem się w ramach eksperymentu stworzyć podobny mechanizm, korzystając z możliwości, jakie oferuje C++Ox. Wyniki tego eksperymentu oraz związane z nim przemyślenia stały się impulsem, który skłonił mnie do napisania niniejszego artykułu, do lektury którego serdecznie zapraszam!

Down (jak to się dzieje, a bardziej ogólnie - w jaki sposób można efektywnie i wygodnie zarządzać stanami gry to oddzielny, bardzo ciekawy temat - być może wrócimy do niego w jednym z przyszłych odcinków tego cyklu). Komunikat ten zostanie natychmiast przekazany do drzewa kontrolek, a za jego pośrednictwem do poszczególnych kontrolek, aż trafi na tę właściwą kontrolkę, która powinna w odpowiedzi wygenerować konkretną akcję. Aczkolwiek tutaj pojawia się pytanie: co wtedy zrobić? A konkretnie, w jaki sposób klasa BrowseMainMenuState ma być powiadomiona o tym fakcie? Rozwiązaniem, które można by zastosować w tej sytuacji, jest wzorzec Obserwator (ang. Observer). W praktyce mogłoby wyglądać to następująco. Do przedstawionej hierarchii klas należałoby dodać interfejs ButtonObserver: class ButtonObserver { public: virtual void OnButtonActivated() = 0; }; Ogólna idea stosowania wzorca Obserwator polega na tym, że obiekt obserwowany (w naszym przypadku: Button) rejestruje obserwatorów (tj. obiekty klas dziedziczących z interfejsu ButtonObserver) i w odpowiednim momencie wywołuje na nich operacje notyfikujące o zaistnieniu określonego zdarzenia (w naszym przypadku: OnButtonActivated). Na Listingu 3 (patrz obok) przedstawione są definicje klas BrowseMainMenuState oraz Button zmodyfikowane zgodnie z przedstawionym wyżej opisem.


C++11 W PRAKTYCE: SYGNAŁY I SLOTY

Listing 1. Uproszczone definicje klas wchodzących w skład biblioteki kontrolek

// Reprezentuje abstrakcyjną kontrolkę // interfejsu użytkownika. // class Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); }; // Reprezentuje drzewiastą hierarchię // obiektów typu Control. // class ControlTree { public: bool OnKeyDown(int keyCode); bool OnKeyUp(int keyCode); }; // Reprezentuje bazową klasę dla wszystkiego // rodzaju kontrolek // przycisków. // class Button : public Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); };

Na Listingu 4 (patrz kolejna strona) przedstawiona jest przykładowa implementacja konstruktora klasy BrowseMainMenuState, która przedstawia, w jaki sposób mogłaby wyglądać rejestracja obserwatora w obie­ kcie klasy Button. W ten sposób udało się nam zaimplementować mechanizm komunikacji zwrotnej dla kontrolki reprezentującej przycisk. Zakładamy, że w sytuacji gdy obiekt stanu przechwyci zdarzenie będące wynikiem interakcji użytkownika z aplikacją, przekaże go do drzewa kontro-

// Reprezentuje abstrakcyjny stan gry. // class GameState { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); }; // Reprezentuje przykładowy stan gry. // class BrowseMainMenuState : public GameState { public: virtual bool OnKeyDown(int keyCode) { return m_ControlTree ->OnKeyDown(keyCode); } virtual bool OnKeyUp(int keyCode) { return m_ControlTree ->OnKeyUp(keyCode); } private: ControlTreePtr m_ControlTree; };

lek, aż trafi ono w końcu do kontrolki reprezentującej przycisk, gdzie zostanie obsłużone, i jeśli zajdzie taka potrzeba, to na obiekcie stanu wywołana będzie akcja zwrotna OnButtonActivated(), w której można umieścić implementację logiki reagującej na to zdarzenie. Myślisz Czytelniku, że na tym kończą się problemy? Też tak myślałem, kiedy pracując nad prototypem mojej biblioteki, zaimplementowałem opisany wyżej mechanizm. Niestety, rozwiązanie okazało się być dalekie od ideału...

Listing 3. Zmodyfikowane definicje klas BrowseMainMenuState oraz Button

class BrowseMainMenuState : public GameState , public ButtonObserver { public : virtual bool OnKeyDown(int keyCode) { return m_ControlTree->OnKeyDown(keyCode); } virtual bool OnKeyUp(int keyCode) { return m_ControlTree->OnKeyUp(keyCode); } private:

virtual void OnButtonActivated(); private: ControlTreePtr m_ControlTree ; }; class Button : public Control { public: virtual bool OnKeyDown(int keyCode); virtual bool OnKeyUp(int keyCode); public : void RegisterObserver(ButtonObserver *observer); };

/ www.programistamag.pl /

21


JĘZYKI PROGRAMOWANIA PROBLEMY Pierwszy, podstawowy problem, który napotkałem po zastosowaniu wzorca Obserwator w mojej bibliotece, to kwestia obsługi wielu kontrolek jednocześnie. Wyobraź sobie proszę, że obiekt reprezentujący stan przeglądania głównego menu gry (BrowseMainMenuState) musi komunikować się z zestawem kilku przycisków, np. GRA, OPCJE, POMOC. Jest to dość powszechny scenariusz. W tej sytuacji w konstruktorze klasy BrowseMainMenuState należałoby rejestrować obiekt stanu (this) jako obserwatora w kilku obiektach reprezentujących poszczególne przyciski. Jednakże podążając tą drogą, trafiamy na pewien koncepcyjny zgrzyt: w jaki sposób wewnątrz metody OnButtonActivated() dowiedzieć się, który przycisk został faktyczne aktywowany? Prostym rozwiązaniem tego problemu może być następująca modyfikacja sygnatury funkcji OnButtonActivated(): virtual void OnButtonActivated(const Button* activatedButton)=0; Przy takim podejściu zakładamy, że aktywowany przycisk wywołujący metodę OnButtonActivated() przekaże do niej wskaźnik na siebie samego. To poniekąd rozwiązuje sprawę, jednakże nadal musimy w jakiś sposób wyłuskać z przekazanego obiektu informację o tym, z kim mamy do czynienia. Tutaj pojawia się problem nadawania kontrolkom unikalnych identyfikatorów, co dodatkowo komplikuje sprawę. Na tym jednak problemy się nie kończą. Otóż kiedy przyjdzie nam oprogramować bardziej skomplikowaną logikę obsługi zdarzeń generowanych przez kontrolki, okazuje się, że kończy się to na napisaniu sporych rozmiarów konstrukcji switch, w której poszczególne sekcje case obsługują akcje przypisane do poszczególnych przycisków. Oczywiście, fragmenty kodu odpowiadające poszczególnym sekcjom można odpowiednio pozamykać w oddzielnych funkcjach, jednakże nie zmienia to faktu, że utrzymywanie tego rodzaju kodu w przypadku bardziej złożonych projektów staje się sporym narzutem.

ROZWIĄZANIE Moje pierwsze eksperymenty z prototypową implementacją biblioteki (czytaj: próba jej zastosowania do skonstruowania części interfejsu użytkownika w prawdziwej grze) skończyły się fiaskiem. Co prawda, wszystko działało, jednakże kod obsługujący logikę interfejsu użytkownika po stronie gry był nieakceptowalny (zbyt rozdmuchany, bardzo uciążliwy w utrzymywaniu). W tej sytuacji zacząłem się zastanawiać, jak - patrząc z punktu widzenia użytkownika tej biblioteki - powinien wyglądać ten kod. Listing 5 przedstawia wizję, która pojawiła się w mojej głowie. W komentarzach zawartych na listingu znajdują się wyjaśnienia opisujące działanie tego rozwiązania. Jeśli przeczytałeś uważnie wspomniane komentarze, to zgodzisz się zapewne ze mną, iż takie rozwiązanie byłoby o wiele bardziej przyjazne dla użytkownika w porównaniu do stosowania wzorca Obserwator. Sercem całego rozwią-

22

/ 1 . 2012 . (1)  /

Listing 4. Rejestracja obserwatora w obiekcie klasy Button

BrowseMainMenuState::BrowseMainMenuState() { // ... // Wyłuskaj obiekt reprezentujący // przycik z drzewa kontrolek. // Button* button = /* ... */; button->RegisterObserver(this); // }

zania jest mechanizm wiążący konkretne metody klasy reprezentującej stan ze zdarzeniami generowanymi przez kontrolki UI (zwróć uwagę na punkt 2 w komentarzach umieszczonych w ciele konstruktora klasy BrowseMainMenuState przedstawionego na Listingu 5). Jednakże tutaj pojawia się kluczowe pytanie: jak takie powiązanie zaimplementować?

SYGNAŁY I SLOTY (SIGNALS AND SLOTS) Odpowiedzią jest mechanizm sygnałów i slotów, interesująca metoda rozsyłania zdarzeń w aplikacjach, wykorzystywana przede wszystkim w interfejsach użytkownika. Mechanizm ten został w dużej mierze spopularyzowany przez Qt (http://qt.nokia.com/), jedną z najpopularniejszych, niezależnych od platformy bibliotek wspierających tworzenie graficznych interfejsów użytkownika. O sygnałach i slotach mówi się, że to wariant wzorca Obserwator, w którym potrzeba tworzenia powtarzalnego kodu sprowadzona jest do minimum. Jak działa ten mechanizm, najłatwiej przekonać się w praktyce. Na Listingu 6 przedstawiłem zestaw klas z naszego przykładu, zmodyfikowanych tak, aby korzystały z mechanizmu sygnałów i slotów. Czy dostrzegasz już elegancję mechanizmu sygnałów i slotów? Rozważmy najważniejsze elementy na wspomnianym listingu. Na początku, w klasie Button deklarujemy, że obiekty tejże klasy mogą emitować sygnał (zdarzenie) nazwane "Activated". Dzieje się to za pomocą umieszczenia makra SIGNAL w definicji tej klasy. W przykładowej implementacji metody Button::OnKeyDown pokazane jest, w jaki sposób można emitować sygnał. Robi się to za pomocą wyrażenia: EMIT(NazwaSygnału, ()); w naszym przypadku będzie to: EMIT(Activated, ());. Deklaracja klasy reprezentującej stan (BrowseMainMenuState) pozostaje bez zmian, zaś w jej konstruktorze łączymy sygnał Activated emitowany przez przycisk playButton z metodą OnPlayButtonActivated() wołaną dla instancji obiektu tejże klasy. I na tym koniec. Od tego momentu za każdym razem, kiedy przycisk "PLAY" zostanie aktywowany, wywołana będzie metoda BrowseMainMenuState::OnPlayButtonActivated() na obiekcie playButton. Prawda, że proste? O ile z punktu widzenia użytkownika korzystanie z mechanizmu sygnałów i slotów daje wrażenie prostoty i ele-


C++11 W PRAKTYCE: SYGNAŁY I SLOTY

Listing 5. Obsługa logiki interfejsu użytkownika

// Zauważ, że BrowseMainMenuState nie // musi implementować żadnych // dodatkowych interfejsów jak w przypadku // korzystania ze wzorca Obserwator. // class BrowseMainMenuState : public GameState { public : // Funkcje obsługujące zdarzenia aktywacji // poszczególnych klawiszy; nie muszą być // wirtualne. // void OnPlayButtonActivated(); void OnOptionsButtonActivated(); void OnHelpButtonActivated(); void OnExitButtonActivated();

Button * playButton = NULL; // 1. Wyłuskaj z drzewa kontrolek obiekt // reprezentujący przycisk "PLAY". // 2. Powiąż metodę BrowseMainMenuState:: // OnPlayButtonActivated() z aktywacją // przycisku // reprezentowanego przez obiekt // playButton. // 3. Wykonaj analogiczne akcje dla // pozostałych przycisków. }

private: ControlTreePtr m_ControlTree ; };

void BrowseMainMenuState ::OnPlayButtonActivated() { // Ta funkcja będzie wywołana za każdym // razem kiedy ktoś aktywuje przycisk // "PLAY". // Umieść tu logikę obsługującą to // zdarzenie. }

BrowseMainMenuState::BrowseMainMenuState() {

// Analogiczne implementacje pozostałych // funkcji obsługujących zdarzenia

gancji, o tyle z punktu widzenia jego implementacji w języku C++ (zgodnego ze standardem C++98), sprawa nie wygląda już tak różowo. Dla przykładu, Qt implementuje ten mechanizm za pomocą specjalnego rozszerzenia języka obsługiwanego przez tzw. kompilator meta-obiektów (ang. Meta-Object Compiler). W praktyce oznacza to, że aby skorzystać z mechanizmu sygnałów i slotów dostępnych w bibliotece Qt, Twój kod źródłowy musi być przed właściwą kompilacją przetworzony odpowiednim narzędziem. Istnie-

ją również implementacje tego mechanizmu obywające się bez dodatkowych faz kompilacji, chociażby Boost.Signals (http://www.boost.org/doc/html/signals.html), jednakże ich implementacja jest bardzo skomplikowana. Ja na potrzeby swojej biblioteki GUI opracowałem dość minimalistyczną implementację tego mechanizmu. Pomimo swojej prostoty implementacja ta zajmuje kilkaset linii kodu i w dość pokrętny sposób łączy zaawansowane techniki metaprogramowania opartego na szablonach oraz wymyślne makra.

Listing 6. Przykład wykorzystania mechanizmu sygnałów i slotów

class Button : public Control { SIGNAL(Activated, ()); public: virtual bool OnKeyDown(int keyCode); }; void { // // // // //

Button::OnKeyDown(int keyCode) Sprawdź czy należy aktywować przycisk. ... Jeśli warunki aktywacji zostały spełnione, emituj sygnał "Activated".

EMIT(Activated, ()); } class BrowseMainMenuState : public GameState { public: void OnPlayButtonActivated(); //

private: ControlTreePtr m_ControlTree; }; BrowseMainMenuState::BrowseMainMenuState() { Button* playButton = NULL; // Wyłuskaj z drzewa kontrolek obiekt // reprezentujący przycisk // "PLAY". // ... CONNECT(playButton, Activated, this, BrowseMainMenuState ::OnPlayButton Activated); // W analogiczny sposób połącz sygnał // "Activated" // emitowany przez pozostałe przyciski z // odpowiednimi // slotami. } / www.programistamag.pl /

23


JĘZYKI PROGRAMOWANIA Główny problem, z którym przychodzi się borykać programistom implementującymy mechanizm sygnałów i slotów, to brak wsparcia dla tzw. adapterów obiektów funkcyjnych w języku C++, czyli konstrukcji, która pozwoliłaby opakować dowolne wywołanie funkcji i traktować je jako pełnoprawny obiekt. Kiedy skończyłem implementować mechanizm sygnałów i slotów w C++98, zacząłem zastanawiać się, jak można by to samo zadanie zrealizować za pomocą C++11. Okazało się, że wykorzystując możliwości najnowszej wersji języka C++, implementacja opisanego wyżej mechanizmu staje się zdecydowanie łatwiejsza, niemalże banalna - przynajmniej w porównaniu z jej odpowiednikiem opartym na C++98. W dalszej części artykułu pokażę, jak można to zadanie zrealizować.

INTERMEZZO Zanim przejdziemy do finalnej części artykułu, chciałbym poruszyć kilka kwestii dotyczących C++11 w ujęciu praktycznym. Jak powszechnie wiadomo, standard ISO/ IEC 14882:2011 (znany powszechnie jako C++11) został opublikowany we wrześniu 2011. Jak to jednak w życiu bywa - ze wsparciem dla tego standardu ze strony kompilatorów jest różnie (sytuacja tutaj zmienia się de facto z dnia na dzień, więc podaruję sobie próby opisania, jakie fragmenty specyfikacji wspierają poszczególne kompilatory). Nie da się jednak ukryć, że do skompilowania kolejnych przykładów przedstawionych w niniejszym tekście będziesz potrzebował kompilatora (w końcu chcemy zajmować się C++11 w ujęciu praktycznym!). Ja osobiście opracowując źródła dla niniejszego artykułu, korzystałem z Microsoft Visual Studio C++ 2010 Express Edition. Jest to darmowe narzędzie i z marszu kompiluje sporą część kodu zgodnego ze standardem C++11. Używając go, nie powinieneś mieć żadnych problemów, z kompilacją kodów źródłowych przedstawionych na listingach umieszczonych w niniejszym tekście. Podejrzewam, że nie będziesz miał z tym również problemów korzystając z najświeższej wersji GCC bądź z kompilatora LLVM dołączonego do najnowszej wersji środowiska Xcode. Ogólnie dobrym sposobem przekonania się, czy kompilator wspiera C++11, jest próba zbudowania przykładowego programu, np. takiego jak przedstawiono na Listingu 7. Jeśli Twój kompilator bez problemu "przełknie" ten kod, to możesz śmiało kontynuować lekturę artykułu.

SYGNAŁY I SLOTY W C++11 Jak zaimplementować mechanizm sygnałów i slotów w C++11? Implementację tego mechanizmu (docelowo powinna ona znajdować się w pliku Signal.hpp) przedstawiłem na Listingu 8 (nast. strona). Rozważmy krok po kroku, co się tutaj dzieje. Aby wykonać zadanie, musimy zmierzyć się z implementacją trzech podstawowych elementów funkcjonalności. Pierwszy z nich to definicja sygnału (makro SIGNAL), drugi to emisja sygnału (makro EMIT), trzeci - to mechanizm łączenia sygnałów i slotów (rodzina makr CONNECT). Pierwszy element realizujemy za pomocą wstrzykiwa-

24

/ 1 . 2012 . (1)  /

nia fragmentu definicji klasy. Definicja makra SIGNAL rozpoczyna się od słowa kluczowego określającego zakres widoczności (public), po którym umieszczona jest definicja inline funkcji Add#signal#Listener(). Widzimy, że pre-procesor składa odpowiednio nazwę tej funkcji, wklejając w jej środek nazwę sygnału. Dla naszego przykładowego sygnału nazwanego "Activated" nazwa funkcji miałaby postać AddActivatedListener. Funkcja ta jako argument przyjmuje obiekt typu std::function<void##args>. std::function to szablon klasy reprezentującej tzw. polimorficzny adapter obiektów funkcyjnych. Szablon ten jest częścią biblioteki standardowej C++11 (aby z niego skorzystać, należy dołączyć nagłówek <functional>); pozwala on tworzyć obiekty podo­bne semantycznie i składniowo do wskaźników do funkcji, z tą różnicą, że mogą reprezentować wszystko, co może być w języku C++ wywołane (funkcje wolne, wskaźniki do funkcji, wskaźniki do metod, obiekty funcyjne itd.), zakładając oczywiście zgodność arguemntów. Dla przykładu definicja adaptera, zdolnego do reprezentowania dowolnego obiektu funkcyjnego, który zwraca wartość int, a przyjmuje dwa argumenty: int oraz referencję do std::string, wyglądałaby następująco: std::function<int(int, std::string)> fun(...); W miejscu trzech kropek można przekazać adres dowolnego obiektu funcyjnego, który ma identyczną sygnaturę jak nasz adapter. Wracając do naszej implementacji, analizując implementację funkcji Add##signal##Listener, możemy się przekonać, że omawiane adaptery rzeczywiście są pełnoprawnymi obiektami. W naszym przypadku po prostu zapamiętujemy adapter w kontenerze std::vector. Kontener ten jest wstrzykiwany jako prywatna składowa obiektu, za pośrednictwem makra SIGNAL. Jego nazwa (m_##signal##Listeners) wiąże się z tym, że Listing 7. "Witaj, Świecie!" w stylu C++11

#include #include #include #include

<algorithm> <iostream> <string> <vector>

using namespace std; int main() { vector<string> words; words.push_back("Hello"); words.push_back(", "); words.push_back("world"); words.push_back("!"); for_each(words.begin(), words.end(), [](string& word) { cout << word; }); return 0; }


C++11 W PRAKTYCE: SYGNAŁY I SLOTY

przechowuje on obiekty nasłuchujące (można też powiedzieć: funkcje zwrotne) dla konkretnego typu sygnału. Gdybyśmy chcieli użyć makra SIGNAL w naszym przykładzie z przyciskami, to moglibyśmy w definicji klasy Button umieścić definicję: SIGNAL(Activated, ()); W praktyce to by oznaczało, że klasa ta będzie emitować sygnał Activated, bez przekazywania dodatkowych argumentów. Warto w tym miejscu zwrócić uwagę, że typ wartości zwracanej dla wszystkich funkcji zwrotnych używanych w mechanizmie sygnałów i slotów jest domyślnie określony jako void. Przejdzmy teraz do definicji makra EMIT. Makro to pozwala wyemitować sygnał. Widząc jak zaimplemetowane zostało makro SIGNAL, łatwo się domyśleć, że emisja sygnału będzie polegać na wywołaniu funkcji zwrotnych przechowywanych w kontenerze przyporządkowanym dla konkrentego sygnału. W ten właśnie sposób zdefioniowane jest makro EMIT. Korzystjąc z pętli for, przeglądamy kontener i aktywujmey kolejno zapamiętane w nim obiekty funkcyjne. Warto w tym miejscu zwrócić uwagę na pewien szczegół, a mianowicie na konstrukcję "for each in" użytą w implementacji makra EMIT pokazanej na Listingu 7. Konstrukcja ta jest rozwiązaniem specyficznym dla środowiska Microsoft Visual Studio, które nie obsługuje póki co standardowej konstrukcji for pozwalającej iterować po elementach określonego zakresu. Jeśli korzystasz z innego kompilatora niż Visual Studio C++, to zamień tę kłoptliwą linię na: for(auto func : m_##signal##Listeners) \ Korzystanie z makra EMIT jest bardzo podobne do korzystania z makra SIGNAL, z tą różnicą, że zamiast przekazywania nazw typów podajemy konkretne argumenty. Gdybyśmy chcieli użyć makra EMIT w naszym przykładzie z przyciskami, to moglibyśmy gdzieś w implementacji wybranej metody Button umieścić wyrażenie: EMIT(Activated, ()); Gdybyśmy z jakichś przyczyn zażyczyli sobie, aby w trakcie emisji sygnału przekazywać jakiś argument (np. wartość całokowitą reprezentującą identyfikator kontrolki), to sygnał należałby zdefiniować następująco: SIGNAL(Activated, (int)); Emisja sygnał z kolei miałaby postać: EMIT(Activated, this->Id()); Zakładam tutaj, że Id() to metoda zdefioniwana gdzieś w bazowej klasie Control, zwracająca jej identyfikator. Ostatni brakujący element układanki to mechanizm łączenia sygnałów i slotów. W przedstawionej imple-

mentacji służy do tego rodzina makr CONNECT. Dlaczego rodzina? A no dlatego, że zakładamy obsługę funkcji zwrotnych o różnych liczbach argumentów. Stąd właśnie nazwy makr CONNECT1, CONNECT2, CONNECT3, itd. Każde z tych makr obsługuje funkcje zwrotne o liczbie argumentów odpowiadającej wartości dołączonej do nazwy makra. Same definicje poszczególnych makr są proste: na początku za pomocą asercji sprawdzamy, czy emiter nie jest przypadkiem pustym obiektem (zauważ, że w porównaniu używamy nowego słowa kluczowego nullptr, jako zamiennika dla starego, poczciwego NULL'a). Pozostaje jeszcze stworzenie odpowiedniego adaptera obiektu funkcyjnego. Na szczęście biblioteka standardowa C++11 oferuje niezwykle pożyteczny szablon std::bind, który w tym przypadku realizuje dokładnie to, czego oczekujemy: wiąże konkretny obiekt (obserwatora) ze slotem (tj. z metodą tego obiektu) i z argumentami. Ponieważ na etapie łączenia nie znamy jeszcze argumentów, trzeba użyć obiektów zastępczych, które są zdefiniowane w przestrzeni nazw std::placeholders. Do obiektów tych odnosimy się, pisząc: std::placeholders::_1, std::placeholders::_2, itd. Na Listingu 7 przedstawiłem implementację makr CONNECT1, CONNECT2 oraz CONNECT3. Kolejne makra z tej rodziny można definiować poprzez analogię, a najlepiej jest napisać sobie prosty program w ulubionym języku skryptowym (np. Perl, Python czy Ruby) który wygeneruje nam określoną liczbę definicji tego makra. Zadanie to pozostawiam do realizacji jako ćwiczenie dla dociekliwych Czytelników.

SYGNAŁY I SLOTY W DZIAŁANIU I tak oto mamy działający mechanizm sygnałów i slotów zaimplementowany w języku C++11 (zadziwiająco szybko poszło, nieprawdaż?). Dobrze by było przetestować go w praktyce. Umieszczenie przykładu związanego z rzeczywistą biblioteką kontrolek interfejsu użytkownika (nawet bardzo prostą) pod każdym względem przekracza ramy tego artykułu. Możemy się jednak pokusić o prosty przykład wykorzystujący zwykłą konsolę tekstową. Przykład taki przedstawiony jest na Listingu 8. Mamy tutaj definicje dwóch prostych klas emitujących sygnały: przycisk (Button) oraz suwaka (Gauge). Przycik emituje sygnał stanowiący powiadomienie o jego aktywacji. Suwak z kolei powiadamia o zmianie swojego ustawienia, przekazując przy tym liczbę zmiennoprzecinkową z zakresu 0.f-1.f określajacą wartość, na którą został ustawiony. Obydwa obiekty umieszczone są w klasie GameState, która przechwytuje sygnały emitowane przez wspomniane obiekty. Połączenie następuje w konstruktorze klasy GameState. W funkcji main tworzony jest obiekt stanu, a następnie wywoływane są odpowiednio funkcje Activate() i Set() na obydwu kontrolkach, w wyniku czego następuje emisja sygnałów. Na wyjściu programu powinny pojawić się komunikaty o przechwyceniu sygnałów, wypisane z funkcji zwrotnych GameState::OnPlayButtonActivated() oraz GameState::OnVolumeGaugeSet(). / www.programistamag.pl /

25


JĘZYKI PROGRAMOWANIA Listing 8. Implementacja mechanizmu sygnałów i slotów w C++11

#ifndef __SIGNAL_HPP_INCLUDED__ #define __SIGNAL_HPP_INCLUDED__

} #define CONNECT1(emitter, signal, \ observer, slot) \ { \ assert((emitter) != nullptr); \ \ (emitter)->Add##signal##Listener( \ std::bind((slot), (observer), \ std::placeholders::_1)); \ }

#include <cassert> #include <functional> #include <vector> #define SIGNAL(signal, args) \ public: \ inline void Add##signal##Listener( \ const std::function<void##args>& \ listener) \ { \ m_##signal##Listeners \ .push_back(listener); \ } \ \ private: \ std::vector< \ std::function<void##args>> \ m_##signal##Listeners;

#define CONNECT2(emitter, signal, \ observer, slot) \ { \ assert((emitter) != nullptr); \ \ (emitter)->Add##signal##Listener( \ std::bind((slot), (observer), \ std::placeholders::_1, \ std::placeholders::_2)); \ }

#define EMIT(signal, args) \ for each(auto func in\ m_##signal##Listeners) \ { \ func##args; \ }

#define CONNECT3(emitter, signal, \ observer, slot) \ { \ assert((emitter) != nullptr); \ \ (emitter)->Add##signal##Listener( \ std::bind((slot), (observer), \ std::placeholders::_1, \ std::placeholders::_2, \ std::placeholders::_3)); \ }

#define CONNECT0(emitter, signal, \ observer, slot) \ { \ assert((emitter) != nullptr); \ \ (emitter)->Add##signal##Listener( \ std::bind((slot), \ (observer))); \

#endif

PODSUMOWANIE Na tym kończy się pierwszy artykuł z cyklu "C++11 w Praktyce". Czytelnicy, którzy zadali sobie trud związany z jego przestudiowaniem, mieli okazję zapoznać się i zrozumieć ideę mechanizmu komunikacji, opartej na sygnałach i slotach oraz przekonać się, jak łatwo można zaimplementować ten mechanizm, korzystając z nowych udogodnień języka C++ opisanych w najnowszym standardzie tego języka. Mam nadzieję, że udało mi się udowodnić, że C++11 jest wart tego, aby inwestować swój czas w jego poznanie - chociażby po to, aby oszczędzić sobie zbędnej pracy.

Rafał Kocisz

Gorąco zachęcam Czytelników do stosowania mechanizmu sygnałów i slotów w swoich projektach - sam używam tej techniki niemalże na codzień i z własnej praktyki wiem, jak bardzo jest ona użyteczna. Zachęcem również do eksperymentowania z C++11. Chętnie zapoznam się z Waszymi uwagami i sugestiami dotyczącymi zarówno niniejszego tekstu, jak i idei samego cyklu. Wszystkich chętnych zapraszam do kontaktu pod adresem: rafal.kocisz@gmail.com.

rafal.kocisz@gmail.com

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

26

/ 1 . 2012 . (1)  /


C++11 W PRAKTYCE: SYGNAŁY I SLOTY

/ www.programistamag.pl /

27


PROGRAMOWANIE SYSTEMÓW OSADZONYCH Marek Sawerwain

Arduino – Pierwszy kontakt Arduino to bardzo popularna platforma oparta o mikrokontroler z rodziny AVR. Jest to znakomita propozycja dla tych, którzy chcieliby poznać podstawy techniki cyfrowej. W artykule zaprezentowano podstawowe wiadomości o Arduino i przykłady tworzenia programów dla tego typu urządzenia.

P

latforma Ardunio jest dziełem dwóch studentów Massimo Banzieho'iego i Davida Cuartiellesa z północnych Włoch (z miasta Ivera), którzy podczas przygotowań do pracy dyplomowej opracowali niewielką i nieskomplikowaną platformę sprzętową opartą o mikrokontroler firmy ATMEL Atmega8. Oznacza to, iż Arduino to tzw. urządzenie do zastosowań embedded (czyli tzw. urządzenia osadzane bądź wbudowane, stosowane w różnego rodzaju urządzeniach np.: jako programator pralki), czyli jest to układ z mikroprocesorem, pamięcią RAM, pamięcią FLASH odgrywającą rolę pamięci ROM oraz zestawem wyjść i wejść do komunikacji z innymi urządzeniami. Ponieważ Arduino jest wyposażone w port USB, to łatwo je podłączyć do tradycyjnego komputera, np. w celu zmiany oprogramowania lub współpracy z komputerem. W takim przypadku nie trzeba oddzielnego zasilania, do prostych projektów (np.: takich jak opisane w tym artykule) w zupełności wystarczy zasilanie dostępne przez złącze USB. Po wgraniu oprogramowania, urządzenie może być odłączone od komputera i zasilane z zewnętrznego zasilacza, a nawet z baterii. Bardzo szybko się okazało, że projekt Arduino jest również świetnym produktem. Powstało kilkanaście odmian Arduino, o których więcej informacji można odszukać w dalszej części tego artykułu (ramka pt. „Jakie Arduino na początek?”). Ogromną popularność Arduino zyskało poprzez fakt, iż jest to produkt oferowany na zasadach podobnych do Open Source. Przy czym w tym przypadku otwarty kod źródłowy to nie tylko programy czy biblioteki

Jakie Arduino na początek? Jest wiele różnych płytek Arduino, które obecnie można spotkać, m.in. są to Arduino Uno, Mega 2560, Mega, Duemilanove, Diecimila. Z punktu widzenia początkującego użytkownika należy wybierać tylko pomiędzy Arduino UNO R3 albo Mega 2560 R3, są to najnowsze wersje dostępne podczas pracy nad tym artykułem. Drugie urządzenie Mega 2560 oferuje więcej pamięci FLASH bo 256KB, oraz 8 kb RAM,

28

/ 1 . 2012 . (1)  /

kodu wykonywane przez mikrokontroler, ale także płyta (choć ze względu na wymiary to raczej płytka) drukowana, układ podłączeń, ogólnie znany zestaw komponentów elektronicznych, z jakich buduje się Arduino, a także parametry elektryczne poszczególnych złącz. Oznacza to, iż każdy może samodzielnie zmontować swoje własne urządzenie zgodne z Arduino lub po prostu kupić gotowe do pracy urządzenie. Otwartość specyfikacji spowodowała też, iż pojawiły się rozwiązania oparte o mikrokontrolery innych producentów, np.: Chipkit oparte o układy PIC32, zgodne z rozmieszczeniem złącz oferowanych przez oryginalne Arduino. Spowodowało to także iż zaczęły pojawiać się rozszerzenia, określane angielskim słowem Shield, które są nakładane na płytkę Arduino. Rozszerzenia oferują np.: możliwość wyświetlania danych poprzez wyświetlacze LCD, dają możliwość dostępu do sieci Ethernet czy zapisu na kartach SD. Ogromna liczba rozszerzeń oferuje dostęp do różnorakich czujników np. do mierzenia ciśnienia atmosferycznego, temperatury otoczenia, istnieją także bardziej zaawansowane rozszerzania jak akcelerometry, odbiorniki GPS, a nawet liczniki Geigera mierzące poziom promieniowania.

ŚRODOWISKO PRACY Do popularności Arduino przyczynia się nie tylko ogólnie dostępna specyfikacja i dostępność Arduino, ale także łatwość programowania. Arduino wykorzystuje kompilator GCC do kompilacji programów. Opracowano również uproszczone, a przez to bardzo przystępne środowisko

a także znacznie więcej wejść oraz wyjść do komunikacji z innymi urządzeniami. Możliwe jest również podłączenie większej ilości pamięci operacyjnej RAM. Naturalnie te większe zasoby pozwalają na budowę większych projektów. Jednakże, choć podstawowe UNO oferuje tylko 32kb FLASH oraz 1 kb RAM, to jednak jest tańsze i m.in. z tego względu warto je wybrać na początek. Pozostałe rodzaje Arduino np.: Nano, Mini, Pro czy też LilyPad, to płytki Arduino

do zastosowań specjalnych, np. : LilyPad można wszyć do ubrania, a wersje Pro pracują z napięciem 3.3V, co jest bardzo istotne, bowiem większość układów cyfrowych preferuje ten rodzaj napięcia. Dlatego, te rodzaje platformy Arduino są przeznaczone dla zaawansowanych użytkowników. Dodatkowo, nie zawsze można je programować tak prosto jak wymienione wcześniej urządzenia UNO czy MEGA.


ARDUINO – PIERWSZY KONTAKT

Rysunek 1. Środowisko Arduino do tworzenia programów dla platformy Arduino

pracy o nazwie Arduino (w dalszej części pakiet ten będzie nazywany Arduino IDE lub AIDE) i naturalnie jest to projekt Open Source. Pakiet jest dostępny dla trzech najpopularniejszych systemów operacyjnych takich jak MacOS, Linux oraz Windows. W przypadku Linux'a warto sprawdzić, czy używana dystrybucja oferuje dostęp do Arduino. Warto też upewnić się, jakiej wersji Arduino IDE chcemy używać, bowiem przykłady/programy dla wersji starszych niż 1.0 mogą nie kompilować się w nowszych odmianach AIDE. W tym artykule będziemy konsekwentnie stosować wersję 1.0. Rysunek 1 przedstawia typowe okno AIDE, jak widać, możliwości edycyjne pakietu Arduino IDE są raczej podstawowe, jednakże do nauki oraz tworzenia mniejszych programów są wystarczające. Podstawowym elementem jest wybór urządzenia, dla którego będziemy tworzyć nasze aplikacje. Realizujemy to poprzez wybór z menu Tools opcji Board i np.: wskazujemy Arduino Uno. Projekty w AIDE są określane mianem szkiców (Sketch). W skład szkicu zazwyczaj wchodzi tylko jeden plik o rozszerzeniu ino np.: o nazwie przyklad1.ino i nowe AIDE 1.0 wymaga, aby taki plik był umieszczony w katalogu o takiej samej nazwie jak plik (przy zakładaniu nowego szkicu katalog jest tworzony samodzielnie przez AIDE). Jeśli chcemy utworzyć dodatkowe pliki źródłowe, to powinny one być plikami nagłówkowymi. Nie jest to dobre rozwiązanie dla bardziej zaawansowanych programów, ale należy

pamiętać, iż Arduino to także platforma dla elektroników, którzy nie muszą być bardzo zaawansowanymi programistami, a nawet nie chcą tracić czasu na dodatkowe problemy z obsługą programu dla mikrokontrolera. Dlatego też po utworzeniu nowego szkicu, można przystąpić do wpisywania kod źródłowego oraz jego kompilacji i załadowania programu do urządzenia. Jednakże, po instalacji pakietu AIDE, w przypadku systemu Windows, trzeba jeszcze zadbać o sterowniki dla Arduino. Po podłączeniu urządzenia kablem USB (jest to tzw. popularny kabel do drukarek w przypadku oryginalnego Arduino) istnieje możliwość, iż system nie znajdzie sterownika, dlatego w menadżerze urządzeń trzeba odszukać urządzenie Arduino, następnie z menu kontekstowego wywołanego lewym przyciskiem należy wybrać opcję Update driver (lub Uaktualnienie sterownika) i wskazać katalog, gdzie znajduje się pakiet AIDE; zawiera on odpowiednie sterowniki. Po kilku chwilach w menadżerze zobaczymy poprawnie zainstalowane urządzenie. Trzeba jeszcze odczytać, na jakim porcie COM system Windows podłączył Arduino. Nie jest to błąd, bo choć porty COM w przypadku komputerów PC już dawno odeszły do lamusa, to Arduino poprzez USB komunikuje się z PC za pomocą starych dobrych COM'ów. W AIDE trzeba wskazać ten COM np.: może to być COM7 albo COM11, za pomocą opcji Serial Port z menu Tools. Jest to ważne, bowiem w następnych przykładach będziemy wykorzystywać port COM do odczytywania komunikatów nadawanych przez Arduino do komputera PC. / www.programistamag.pl /

29


PROGRAMOWANIE SYSTEMÓW OSADZONYCH W przypadku korzystania AIDE w systemie Linux wiele dystrybucji np.: Fedora zawiera gotowe pakiety, więc w menadżerze pakietów wystarczy wskazać, iż chcemy zainstalować AIDE, jednakże to może nie wystarczyć, bowiem w przypadku Fedory użytkownika, który używa AIDE należy dodać do dwóch grup o nazwach lock oraz dialout: ■■ usermod -a -G lock nazwa-użytkownika ■■ usermod -a -G dialout nazwa-użytkownika

DWA I PÓŁ PRZYKŁADU NA POCZĄTEK Nagłówek sugeruje dość dziwną treść, lecz za chwilę okaże się, iż jest całkiem zasadny. Po instalacji stero­wników do obsługi Arduino warto zrealizować proste przykłady. Pierwszy będzie polegał na wykorzystaniu faktu, iż wszystkie nowe płytki Arduino mają wbudowaną diodę świecącą (oznaczoną jako L13, zazwyczaj tego typu diody określane są skrótem LED), którą można sterować z poziomu własnych programów, więc napiszemy krótki program do „migania” diodą. W drugim przykładzie nasz program będzie przekazywał komunikaty poprzez port COM, do sterowania dodatkowymi diodami. Od razu należy dodać, iż tak proste przykłady nie potrzebują dodatkowego zasilnia, dlatego wystarczy iż Arduino jest podłączone za pomocą kabla USB. Listing 1 przedstawia pełny kod źródłowy naszego pierwszego przykładu. Podobny przykład znajduje się także w AIDE, jednak lepiej jest samodzielnie napisać bądź przepisać z listingu tego typu program, bo zawsze jest to bardziej pouczające. Autorzy AIDE postanowili, iż przygotują nie tylko samo narzędzie, ale także zestaw dodatkowych bibliotek, obsługujących różne standardy w urządzeniach typu embedded (spis podstawowych bibliotek przedstawia tabela pt. “Standardowe biblioteki dostępne w Arduino IDE”). Choć nasz program nie stosuje dodatkowych bibliotek, to korzysta z przygotowanej struktury aplikacji dla Arduino. Typowe programy zawierają zawsze dwie funkcje. Pierwsza z nich setup jest uruchamiana tylko na począ­ tku działania naszego programu i wykonuje czynności przygotowawcze. Główne zadanie jest realizowane przez funkcję loop. Funkcja loop jest wykonywana w pętli, więc po zakończeniu ostatniej linii kodu funkcji loop, Arduino rozpoczyna wykonywać funkcję loop od początku. Jak widać, nie ma funkcji odpowiedzialnej za zakończenie działania naszego programu. Wbrew pozorom nie jest to błąd, nasz trywialny program może zostać zatrzymany w dowolnym momencie (nawet dość brutalnie, wystarczy wyciągnąć wtyczkę z gniazdka USB ;-]) i nie trzeba wykonywać dodatkowych czynności. Znaczenie poszczególnych funkcji został opisane w komentarzu do kodu z Listingu 1, w ramach uzupełnienia można dodać iż funkcja pinMode pozwala na określenie, czy dany pin Arduino będzie reprezentował sygnał wyjściowy OUTPUT bądź też sygnał wejściowy INPUT. Funkcja digitalWrite ustala wartość sygnału logicznego na wyjściu na wysoki – HIGH, oraz niski – LOW. Stan wysoki powo-

30

/ 1 . 2012 . (1)  /

Listing 1. „Blinkacz”, prosty program dla Arduino, który włącza oraz wyłącza diodę świecącą w odstępach jednosekundowych

void setup() { // określenie iż cyfrowy port o numerze 13 // jest portem wyjściowym pinMode(13, OUTPUT); } void loop() { digitalWrite(13, HIGH); // włączenie LED delay(1000); // oczekiwanie jedną sekundę digitalWrite(13, LOW); // wyłączenie LED delay(1000); // oczekiwanie jedną sekundę }

duje świecenie się diody. Arduino posiada także możliwość odczytu sygnału analogowego, a dokładnie wartości napięcia, lecz o tym będziemy mówić innym razem. Uruchomienie tego prostego przykładu wymaga, aby w pierwszej kolejności przeprowadzić kompilację, którą można zrealizować za pomocą kombinacji klawiszy CTRL-R bądź poprzez wybór z menu opcji Sketch, a następnie opcji Verify / Compile. Po zakończonej i bezbłędnej kompilacji należy się upewnić, czy Arudino jest podłączone do komputera i za pomocą klawiszy CTRL-U załadować nasz program do urządzenia (załadowanie programu można także rozpocząć poprzez przycisk Upload (ma kształt strzałki w lewo) z paska roboczego, albo z menu FILE poprzez wybór opcji Upload). Po operacji załadowania programu do pamięci FLASH, Arduino natychmiast przystępuje do wykonywania wczytanego programu. Przykład możemy łatwo uzupełnić o komunikaty tekst­ owe przesyłane przez port szeregowy. W funkcji setup

Standardowe biblioteki dostępne w Arduino IDE Nazwa

Opis

EEPROM

odczyt oraz zapis do pamięci typu EEPROM

Ethernet

biblioteka do obsługi sieci Ethernet stosowana, jeśli dołączono rozszerzenie o nazwie Arduino Ethernet

Firmata

obsługa standardowego protokołu szeregowego

LiquidCrystal

obsługa wyświetlaczy typu LCD

SD

odczyt oraz zapis danych na kartach typu SD

Servo

biblioteka odpowiedzialna za kontrolowanie serwo-napędów

SPI

odpowiada za komunikację przez szynę typu Serial Peripheral Interface (SPI)

SoftwareSerial

transmisja szeregowa przez dwa dowolnie wybrane złącza Arduino

Stepper

biblioteka odpowiedzialna za kontrolowanie napędów krokowych

Wire

komunikacja za pomocą standardu TWI/ I2C do obierania oraz przesyłania danych z różnego rodzaju czujników


ARDUINO – PIERWSZY KONTAKT

dodajemy operację inicjalizacji obsługi portu szeregowego: Serial.begin( 9600 ); Wartość 9600 to szybkość w bitach przesyłania danych, czyli w przybliżeniu około 1000 znaków na sekundę. Niewiele, jeśli zaczniemy to porównywać np. w szybkością łącza internetowego, ale do wielu zadań, gdzie stosuje się urządzenia typu Arduino, jest to wystarczająca wielkość, zwłaszcza jeśli urządzenie jest zasilane z baterii. Przesłanie komunikatu tekstowego, np. do okna AIDE o nazwie „Serial Monitor” jest realizowane za pomocą metody println: Serial.println("tekst komunikatu"); Dodajmy także, iż jeśli pojawi się konieczność zakończenia komunikacji szeregowej, to wyłączenie komunikacji sprowadza się do następującej linii kodu: Serial.end(); Warto dodać, że Arduino MEGA posiada cztery porty do komunikacji szeregowej, w programach są one reprezentowane przez obiekty: Serial, Serial1, Serial2, Serial3.

PRZYKŁAD DRUGI W drugim przykładzie nadal będziemy wykorzystywać diody świecące, ale tym razem w liczbie sześć. Z tego powodu przed przystąpieniem do programowania trzeba przygotować odpowiedni schemat układu, który pomoże zbudować cały układ. W tym celu warto wykorzystać pro-

gram Fritzing, który jest edytorem obwodów dla Arduino. Jego ogromną zaletą jest to, iż oferuje bardzo dobrej jakości i dość dokładne graficzne reprezentacje poszczególnych płytek i elementów, co przydaje się osobom począ­ tkującym, które nie muszą, a nawet nie chcą znać się na czytaniu schematów urządzeń elektronicznych. Rysunek 2 przedstawia schemat podłączenia sześciu diód, nie jest to optymalny sposób podłączenia diod do Arduino, bowiem każda dioda wymaga oddzielnego złącza, lepszym rozwiązaniem jest zastosowanie tzw. rejestru przesuwnego. Jednakże na razie opisujemy najprostsze przykłady, więc nie jest to dla nas kluczowe zagadnienie. Do każdej diody musimy dodać rezystor o oporności 220 omów, może być też 330 omów, bo choć Arduino generuje mały prąd, to jednak zbyt duży dla jakiekolwiek diody LED. Podłączenie diody bezpośrednio do Arduino z pewnością spowoduje uszkodzenie podłączonej diody. Rysunek 2 pokazuje nam także, iż cały schemat możemy zmontować przy pomocą tzw. płytki stykowej (bądź prototypowej). Warto się w taką, a także zestaw kabelków do łącznia poszczególnych elementów elektroni­cznych, zaopatrzyć. Taka płytka znakomicie nadaje się na początek, bowiem nie wymaga lutowania elementów, a ponadto znacząco upraszcza tworzenie prototypów urządzeń i ich testowanie. Podsumowując, potrzebujemy sześć zwykłych diod LED oraz sześć rezystorów 220 omów, płytkę stykową, trochę kabelków, najlepiej w trzech kolorach, aby tak jak na rysunku rozróżniać poszczególne połączenia. Po montażu układu i podłączeniu poszczególnych połączeń do Arduino warto wszystko raz jeszcze sprawdzić dla pewności, a następnie można już przystąpić do tworzenia programu. Listing 2 zawiera pełny kod źródłowy programu.

Rysunek 2. Schemat obwodu dla drugiego przykładu z sześcioma diodami

/ www.programistamag.pl /

31


PROGRAMOWANIE SYSTEMÓW OSADZONYCH Listing 2. Program do sterownia sześcioma diodami

int lastKey = 0; void l13_signal(int d1, int d2) { digitalWrite(13, HIGH); delay(d1); digitalWrite(13, LOW); delay(d2); } void setup() { pinMode(13, OUTPUT); pinMode(7, OUTPUT); pinMode(6, OUTPUT); pinMode(5, OUTPUT); pinMode(4, OUTPUT); pinMode(3, OUTPUT); pinMode(2, OUTPUT); Serial.begin( 9600 ); } void loop() { if (Serial.available() > 0) { int readval=-1; readval = Serial.read(); if( readval == '0') { lastKey = 0; }

W przykładzie drugim, podobnie jak w pierwszym, będziemy zapalać i gasić poszczególne diody, począwszy od L13 po diody podłączone do złącz 7, 6, 5, 4, 3, 2. Funkcja setup jest podobna do poprzedniego przykładu. Ustalamy, które złącza są złączami wyjściowymi oraz określamy szybkość transmisji. Główne zadanie realizuje funkcja loop, choć mamy jeszcze jedną pomocniczą funkcje l13_ signal, które zapala i gasi diodę l13 w podanych odstępach czasu w milisekundach. Funkcję tę łatwo przerobić dla dowolnego złącza, ale to zadanie dla czytelnika. Pierwszy zadanie realizowane w funkcji loop polega na odczytaniu informacji z portu szeregowego. Dlatego, metodą available() sprawdzamy, czy są jakieś dane do odczytania. Jeśli tak, to odczytujemy jeden bajt, i w zależności, czy odczytano znak „0” bądź „1” ustalamy wartość zmiennej lastKey, wykorzystywanej do sterownia diodami. Wartość zmiennej lastKey jest wykorzystywana do sterownia kolejnością zapalania poszczególnych diod LED. Trzeba zwrócić uwagę na to, iż podczas sterownia diodami, nie sprawdzamy, jakie dane odczytujemy, jednak Arduino buforuje odczytane znaki, więc po zakończeniu sekwencji odczytamy umieszczone w buforze znaki i na-

32

/ 1 . 2012 . (1)  /

if(readval == '1') { lastKey = 1; } } if(lastKey == 0) { int i; l13_signal(100,100); for( i=7 ; i>1 ; i--) { digitalWrite(i, HIGH); delay(100); digitalWrite(i, LOW); delay(100); } } if(lastKey == 1) { int i; l13_signal(100,100); for( i=1 ; i<8 ; i++) { digitalWrite(i, HIGH); delay(100); digitalWrite(i, LOW); delay(100); } } }

stępna sekwencja zostanie zrealizowana zgodnie z podanym kodem. Sam znak sterujący, zero bądź jedynka, jest wysyłany za pomocą Serial Monitora, z poziomu Arduino IDE. Jeśli jednak Arduino jest podłączone przez kabel USB, to można przesłać informację za pomocą dowolnego programu.

BRAKUJĄCA POŁOWA PRZYKŁADU Po zaprogramowaniu urządzenia, program sterujący jest umieszczony w pamięci FLASH, a to oznacza, iż po wyłączeniu urządzenia i jego ponownym włączeniu nadal aktywny jest ten sam program. Inaczej mówiąc, urządzenie działa samodzielnie bez pomocy Arduino IDE. Daje nam to możliwość np. odczytu bądź przesyłania z komputera PC do Arduino dodatkowych danych. Naturalnie pojawia się pytanie, jak napisać taki program do komunikacji z Arduino. W tym celu wykorzystamy transmisję szeregową. Na początek najłatwiej będzie utworzyć prosty program do przesyłania zera bądź jedynki do naszego układu z diodami. Możemy wybrać dowolny język programowania, najlepiej. szczególnie na początek, wybrać taki język, gdzie istnieje gotowy pakiet do wygodnej obsługi komunikacji


ARDUINO – PIERWSZY KONTAKT

Listing 3. Prosta aplikacja w Pythonie do sterowania diodami

import serial import time

W Internecie:

class SixLEDCtrl(object): def __init__(self, port = '/dev/ttyACM0' ): self.serial = serial.Serial( port, 9600, timeout = 1 ) def fwd(self): self.serial.write('1') return True def bck(self): self.serial.write('0') return True slc = SixLEDCtrl() while True: slc.fwd() time.sleep( 1 ) slc.bck() time.sleep( 1 )

z portem szeregowym, np. Python oferuje pakiet o nazwie serial, a dokładnie o nazwie PySerial, do obsługi starych dobrych COM'ów. Jedynym problemem może być nazwa portu COM, ale tę możemy odczytać z poziomu AIDE. Jak widać z Listingu 3, obsługa portu COM jest bardzo łatwa, mamy klasę o nazwie SixLEDCtrl, która posiada dwie metody fwd i bck wysyłające odpowiednio zero oraz jeden. W głównej części programu po utworzeniu obiektu slc w pętli wywołujemy wspominane metody sekunda po sekundzie.

PODSUMOWANIE Niewątpliwie wielką zaletą Arduino jest fakt, iż łatwo przystąpić do realizacji pierwszych projektów, wystarczy bowiem znać język C/C++ lub Python i rozróżniać przysłowiowy rezystor od diody czy tranzystora, aby rozpocząć

Marek Sawerwain

■■ Główna strona projektu Arduino: http://www.arduino.cc ■■ ChipKit, platforma sprzętowa podobna do Arduino, ale oparta o układ Microchip z rodziny PIC32MX: http://www.chipkit.org ■■ Fritzing, edytor obwodów dla mikrokontrolera Arduino (choć nie tylko), zawiera bogatą bazę komponentów współpracujących z platformą Arudino: http://www.fritzing.org ■■ KTechlab, narzędzie do pełnej programowej symulacji systemu z mikrokontrolerem PIC16F: http://sourceforge.net/apps/mediawiki/ktechlab/ index.php?title=Main_Page ■■ MCU8051 IDE , pakiet do symulacji mikrokontrolera 8051: http://mcu8051ide.sourceforge.net ■■ Pakiet w języku Python do obsługi portu szeregowego: http://pyserial.sourceforge.net/pyserial.html

naukę bądź też tworzenie własnych nieskomplikowanych projektów. Przedstawione w artykule przykłady może zrealizować każdy bez względy na wiedzę o elektronice. Lista możliwości wykorzystania Arduino jest ogromna. Arduino można stosować do realizacji np. funkcji inteligentnego domu np. jako sterownik do kontroli grzejnika. Społeczność użytkowników Arduino przygotowała wiele różnych dodatkowych bibliotek, w tym do sterowania napędami, więc Arduino sprawdzi się także jako sterownik różnych pojazdów i napędów. Ze względu na cenę, choć ta nie jest naturalnie bardzo niska, Arduino to także dobra platforma do edukacji związanej z techniką cyfrową. W sieci Internet, a także na serwisie YouTube można odszukać wiele przykładów projektów gdzie zastosowano Arduino, a także inne popularne mikrokontrolery takie jak układy 8051. Toteż pozostaje tylko życzyć udanych łowów w Internecie.

redakcja@programistamag.pl

Autor, pracownik naukowy Uniwersytetu Zielonogórskiego, na co dzień zajmuje teorią kwantowych języków programowania ale także tworzeniem oprogramowania dla systemów Windows oraz Linux. Zainteresowania: teoria języków programowania oraz dobra literatura.

/ www.programistamag.pl /

33


TECHNOLOGIE FLASH/FLEX Paweł Murawski

Wprowadzenie do AMFPHP AMF (Action Message Format) jest protokołem, który pozwala przesyłać zserializowane obiekty w formacie binarnym (na przykład obiekty utworzone w Actionscript). Podstawową zaletą tego protokołu jest jego wydajność w stosunku do wywołań GET/POST - dane są dużo bardziej skompresowane, ponadto jest on natywnie wspierany przez Flasha. Jedną z implementacji pozwalających wykorzystać jego możliwości jest projekt AMFPHP, dzięki któremu można w prosty sposób połączyć część serwerową naszej aplikacji z częścią kliencką. W poniższym przykładzie opiszę, jak za jego pomocą można skomunikować Flexa z bazą danych MySQL.

N

a początek przygotujemy niezbędne narzędzia do pracy. Przede wszystkim należy pobrać AMFPHP (www.silexlabs.org/amfphp). Instalujemy go na naszym serwerze, w moim przypadku był to Apache będący częścią aplikacji Wamp Server (www.wampserver. com), która zapewni nam dodatkowo bazę danych MySQL oraz obsługę języka PHP. Potrzebujemy jeszcze IDE do programowania we Flexie, ja używam Flash Buildera (www.adobe.com/products/flash-builder.html), ale dla miłośników darmowych rozwiązań mogę polecić narzędzie FlashDevelop (www.flashdevelop.org).

MYSQL Zaczniemy od części bazodanowej. Utworzymy nową bazę MySQL o nazwie amf. Następnie za pomocą poniższych zapytań utworzymy w tej bazie prostą tabelę oraz zapełnimy ją danymi: CREATE TABLE IF NOT EXISTS `users` ( `id` int(11) NOT NULL AUTO_INCREMENT, `imie` varchar(32) NOT NULL, `wiek` int(11) NOT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 AUTO_ INCREMENT=3; INSERT INTO `users` (`id`, `imie`, `wiek`) VALUES (1, 'Adam Brzęczyński', 33), (2, 'Ewa Łąśćkiewicz', 31); Utworzenie prostej bazy danych

Będą to dane, które pobierzemy do naszej aplikacji Flexowej. Zestaw znaków najlepiej ustawic na utf8_general_ci (wspiera on polskie litery). Na tym etapie dobrze jest także ustawić użytkownika dla tej bazy z odpowie­ dnimi uprawnieniami.

PHP Instalacja AMFPHP polega na skopiowaniu folderu Amfphp do katalogu www naszego serwera. Następnie w katalogu Amfphp/Services należy utworzyć nową klasę PHP (pełny

34

/ 1 . 2012 . (1)  /

kod znajduje się w sekcji „download”‚ na stronie www. programistamag.pl) o nazwie AMFTest. Wewnątrz tej klasy tworzymy prywatny obiekt, który będzie przechowywał połączenie z bazą: private $connection; Następnie w kontruktorze tej klasy otwieramy połączenie z bazą danych MySQL: public function __construct() { $this->connection = new mysqli(HOST, USER, PASS, DB_NAME); $this->connection->query("SET NAMES utf8"); if (mysqli_errno($this->connection)) { throw new Exception("Wystapil blad podczas laczenia sie z baza danych: ". mysqli_error($this->connection)); } } Połączenie z bazą danych z poziomu PHP

Bardzo ważne jest wykonanie na bazie zapytania SET NAMES utf8 - dzięki temu jest duża szansa, że unikniemy problemu złego wyświetlania polskich znaków po stronie Flexa. Pozostało nam tylko napisać funkcję, która pobierze dane z tabeli users: /** * Pobranie listy rekordow z tabeli users. */ public function getUsers() { $sql = "SELECT id, imie, wiek FROM users"; $result = $this->connection->query($sql); if (mysqli_errno($this->connection)) { throw new Exception("Wystapil blad podczas Pobranie danych z bazy

cd. na kolejnej stronie


AMFPHP - PODSTAWY

Rysunek 1. Service Browser pobierania danych z ". "tabeli users: ". mysqli_error($this->connection)); } else { $users = array(); while ($object = $result->fetch_object()) { $users[] = $object; } } return $users; } Funkcja getUsers pobiera dane z tabeli users i zwraca je w postaci tablicy $users. Istotną kwestią, o której należy pamietać, jest odpowiednie komunikowanie o wszelkich błędach podczas pobierania danych. W powyższym przykładzie została do tego wykorzystana konstrukcja throw new Exception, która wyśle do Flexa informacje o powstałym błędzie MySQL (opis błędu zwracany jest z funkcji mysqli_error). Na obecnym etapie można przetestować działanie naszej funkcji za pomocą Service Browser (Rysunek 1). Jest to aplikacja dostarczana razem z AMFPHP, która pozwala wywoływać wszystkie metody zawarte w klasach w kata-

logu Services. Przy standardowej instalacji ścieżka dostępu do Service Browser to http://localhost/Amfphp. Jest to bardzo przydatne podczas testów, ale na środowisku produkcyjnym taka możliwość powinna zostać wyłączona.

FLEX Kolejnym krokiem będzie utworzenie nowego proje­ktu Flexowego o nazwie Amfphp (pełny kod znajduje się w sekcji „download”‚ na stronie www.programistamag. pl). W pliku Amfphp.mxml oprogramowujemy połączenie z AMFPHP, na przykład w funkcji wywoływanej bezpośre­ dnio po inicjalizacji: Utworzenie połączenia z AMFPHP

protected function initializeHandler(event:F lexEvent):void { var connector:RemoteObject = new RemoteObject("amfphp"); connector.endpoint = "http://localhost/ Amfphp/index.php"; connector.source = "AMFTest"; connector.addEventListener(ResultEvent. RESULT, resultHandler); connector.addEventListener(FaultEvent. FAULT, faultHandler); connector.getUsers(); } / www.programistamag.pl /

35


TECHNOLOGIE FLASH/FLEX

Rysunek 2. Widok z debuggera po poprawnym zwróceniu wyników z bazy danych protected function resultHandler(event:Resul tEvent):void { var users:ArrayCollection = new ArrayCollection(); for each (var item:Object in event.result) { users.addItem(item); } trace(users); } protected function faultHandler(event:FaultE vent):void { trace(event.fault.faultString); } Funkcje nasłuchujące

Paweł Murawski

Obiekt RemoteObject jest odpowiedzialny za komunikację z AMFPHP. Jego atrybut endpoint musi wskazywać na plik index.php, znajdujący się w katalogu Amfphp. Atrybut source musi być nazwą naszej klasy PHP. Po ustawieniu funkcji nasłuchujących można wywołać funkcję getUsers z klasy AMFTest. Dzięki temu, że klasa RemoteObject jest określona jako dynamic, notacja jest taka sama jak przy wywołaniu zwykłej metody, czyli po kropce. Aby poprawnie przetworzyć zwrócony wynik, należy jeszcze utworzyć dwie funkcje nasłuchujące (listing obok). Funkcja resultHandler odbierze dane zwrócone przez PHP i zapisze je do obiektu ArrayCollection. Dzięki temu możemy je od razu użyć na przykład jako Data Providera dla któregoś z Flexowych komponentów. W przypadku gdyby wystąpił błąd podczas pobierania danych z bazy będziemy mogli go obsłużyć w funkcji faultHandler. Na obecnym etapie najlepiej jest ustawić w naszym IDE do Flexa dwa breakpointy na funkcjach trace, dzięki czemu można bezpośrednio w debuggerze podejrzeć ich zawartość (Rysunek 2). Jeśli wszystko poszło dobrze, to powinniśmy zobaczyć w obiekcie users dane, które wprowadziliśmy do bazy danych na początku artykułu.

pawelmurek@tlen.pl

Aktualnie pracuje jako Flex Developer dla firmy Young Digital Planet. Jego zainteresowania zawodowe skupiają się na technologiach webowych i mobilnych, programowaniu gier oraz szeroko rozumianej inżynierii oprogramowania.

36

/ 1 . 2012 . (1)  /


TYTUŁ

/ www.programistamag.pl /

37


INŻYNIERIA OPROGRAMOWANIA Sławomir Sobótka

Domain Driven Design krok po kroku Część I: Podstawowe Building Blocks DDD

Domain Driven Design jest zestawem technik i koncepcji służących do projektowania złożonych modeli biznesowych i ich dosłownej implementacji. Niektórzy – tak jak Martin Fowler – preferują stosowanie DDD również w prostszych przypadkach z uwagi na jego elegancję i implikacje techniczne w aspektach testowalności i rozszerzalności. Techniki DDD zyskały swą popularność w roku 2003, wraz z publikacją książki Erica Evansa. Mimo upływu lat koncepcja DDD jest wciąż żywo rozwijana i wzbogacana o nowe techniki – do których sięgniemy w kolejnych odsłonach serii. W niniejszym artykule zostaną przedstawione podstawowe Building Blocks DDD wraz z przykładami ich implementacji w Javie z wykorzystaniem Spring i JPA.

A

rtykuł jest pierwszym z serii tekstów mających na celu szczegółowe przedstawienie kompletnego zestawu technik modelowania oraz nakreślenie kompletnej architektury aplikacji wspierającej DDD. Przed dalszą lekturą zachęcam do zapoznania się z artykułem stanowiącym wstęp do DDD „Domain Driven Design – Sposób na projektowanie złożonych modeli biznesowych” – link do tekstu znajduje się w ramce „w sieci”. Tekst ten zawiera opis kluczowych koncepcji DDD, których poznanie jest krytyczne dla zrozumienia intencji rozwiązań technicznych oraz podejść, które będą prezentowane w naszej serii „krok po kroku”. W kolejnych częściach będziemy poznawać techniki pokrywające większość kluczowych artefaktów i aktywności procesu wytwórczego, pozwalające na przeprowadzenie kompletnego projektu opartego o DDD: ■■ Część 2: Zaawansowane modelowanie DDD, konte­ ksty i architektura zdarzeniowa; ■■ Część 3: Szczegóły implementacji aplikacji wykorzystującej DDD na platformie Java (dwa podejścia: Spring i EJB 3.1 oraz JPA); ■■ Część 4: Skalowalne systemu w kontekście DDD architektura CqRS; ■■ Część 5: Kompleksowe testowanie aplikacji opartej o DDD; ■■ Część 6: Behavior Driven Development - Agile 2.0.

PROJEKT REFERENCYJNY Wszystkich tych czytelników, którzy chcieliby zapoznać się z kolejnymi zagadnieniami serii, zapraszam do strony projektu „DDD&CqRS Laven”, której adres znajduje się w ramce „w sieci”. Znajdziecie tam kompletną implementację projektu, zawierającą przykłady rozwiązań zawarte w niniejszym tekście, jak i kolejnych częściach serii. Motto projektu brzmi: „Więcej niż jedynie przykład, ale zdecydowanie nie jest to kolejny frameowork... zaczyn – coś, z czego robi się dobry chleb”.

38

/ 1 . 2012 . (1)  /

Laven to autorska koncepcja projektu, który zawiera rozwiązania niemal gotowe do stosowania w systemach produkcyjnych, jednak nie hermetyzuje ich w klasycznej formie frameworka. Z założenia wszelkie kastomizacje mogą być dokonywane na „żywym” kodzie, zamiast zmagania się z konfiguracją sztywnych struktur frameworka.

Założenia projektu referencyjnego: ■■ Prezentacja wszystkich Building Blocks DDD w niestrywializowany sposób; ■■ Prezentacja technik DDD (np. Bounded Context); ■■ Prezentacja rzeczywistych technik implementacji, gotowych do wdrożenia w kodzie produkcyjnym; ■■ Prezentacja pragmatycznego podejścia do implementacji CqRS; ■■ Dostarczenie rzetelnie wykonanego, wzorcowego kodu źródłowego; ■■ Przyjęto nieinwazyjną filozofię - ograniczenie wpływu technologii na kształt projektu; ■■ Opracowany styl architektoniczny jest przenośny na inne frameworki i platformy; Przykłady podejścia do testowania jednostkowego i akceptacyjnego - z wykorzystaniem Behavior Driven Development (JBehave, Selenium, model Agentów); Projekt jest całkowicie darmowy – zarówno kod, jak i dokumentacja. Tłem technicznym projektu jest Java. Wszystkie techniki zostały zilustrowane na dwóch stosach technologii. Pierwszym z nich jest Spring (w tym Spring MVC) i JPA. Drugi to Java EE 6 (w tym JSF 2, EJB 3.1, JPA). W tekstach artykułów będziemy posługiwać się pierwszym stosem. Warto podkreślić, że na poziomie koncepcyjnym będziemy niezależni od technologii. Duża część przykładów kodu jest niezależna od specyfiki platformy i powinna być zrozumiała dla każdego, kto posiada podstawową umiejętność czytania kodu w językach wywodzących się z C++.


DOMAIN DRIVEN DESIGN KROK PO KROKU

OPIS DOMENY Przykłady zostały osadzone w ramach bardzo uproszczonego systemu klasy ERP. Wybraliśmy kilka stosunkowo dobrze (w rozumieniu: intuicyjnie) znanych domen ERP i zaimplementowaliśmy ich namiastki. Każdy z nas posiada pewną intuicję odnośnie klientów, zamówień, produktów itp., tak więc nie będziemy tracić czasu na ich szczegółowe wyjaśnianie.

Bounded Context Techniką Bounded Context zajmiemy się szczegółowo w drugiej części naszej serii, natomiast na tym etapie zakładamy, że istnieją trzy moduły: ■■ Sprzedaż - obsługuje zamawianie produktów, obliczania rabatów, fakturowanie, analizę trendów sprzedaży. ■■ CRM - obsługuje zarządzanie relacjami z klientem. Klient w tym module jest modelowany jako inny artefakt (innego ograniczonego kontekstu) niż w module Sprzedaż. ■■ Magazyn – obsługuje przechowywanie, pakowanie i wysyłkę zamówień. Zakładamy, że wiedza domenowa odnośnie wymagań dla każdego z modułów znajduje się w umyśle innego Eksperta Domenowego. Eksperci ci niekoniecznie rozumieją się nawzajem, mimo używania słów, które brzmią tak samo, mają na myśli inne pojęcia. Zastrzegamy, że przedstawiony model jest mocno uproszczony na potrzeby edukacyjne. Jeżeli są Państwo zainteresowani tematyką modelowania omawianych domen, to odsyłamy do zasobów modeli i archetypów analitycznych.

MODYFIKACJA WARSTWOWEJ ARCHITEKTURY APLIKACJI Każdy z modułów naszego systemu będzie zaprojektowany wg tego samego stylu architektonicznego – w rozumieniu architektury aplikacji. W najprostszym ujęciu architektury warstwowej wyróżniamy 3 warstwy: prezentacji, logiki i dostępu do danych. Na marginesie: pamiętajmy, że warstwy (Layers) służą do porządkowania kodu i nie są tym co Poziomy (Tiers) porządkujące infrastrukturę techniczną (klienty, serwery, bazy danych). Co prawda DDD abstrahuje od prezentacji i persystencji, jednak my zajmiemy się tymi zagadnieniami w kolejnych częściach serii z uwagi na wpływ, jaki wywierają na wydajność i skalowanie. Modyfikacja wprowadzona przez DDD polega na rozwarstwieniu warstwy logiki na dwie wyspecjalizowane warstwy: logikę aplikacji oraz logikę biznesową.

Logika aplikacji Logika aplikacji jest cienką warstwą serwisów (rzadziej obiektów stanowych) odpowiadających między innymi za szczegóły techniczne takie jak bezpieczeństwo i transa­ kcje. Najważniejszą odpowiedzialnością tej warstwy jest

jednak modelowanie kroków Use Case lub User Story. Scenariusz kroku polega na orkiestracji obiektów domenowych z niższej warstwy. Serwisy aplikacyjne stanowią niejako API serwera, uruchamiane z klientów webowych, mobilnych lub publikowane jako WebServisy. Serwisy z tej warstwy są nazywane również Operation Script (w odróżnieniu od proceduralnego Transaction Script). Klasy z tej warstwy zwykle testujemy w podejściu end-to-end, ponieważ mnogość możliwych scenariuszy przejścia praktycznie uniemożliwia wysokie pokrycie testami jednostkowymi – stosunek włożonej pracy do poziomu redukcji ryzyka jest zwykle niekorzystny. Dodatkowym kosztem testów jednostkowych w tej warstwie jest konieczność zaślepiania (fake/stub/mock) dużej ilości zależności. Zagadnieniem testowania lub specyfikowania tej warstwy zajmiemy się w części 5 i 6.

Logika domenowa Warstwa logiki domenowej modeluje zachowania i reguły obiektów biznesowych. DDD skupia większość technik i uwagi na modelowaniu tej właśnie warstwy. Odpowiedzialność warstwy logiki biznesowej skupia się jedynie wokół modelu biznesu i nie powinna zawierać technikaliów zależnych platformy, serwera lub frameworka. Z technicznego punktu widzenia czyni to ją przenośną oraz, co ważne – testowalną poza ciężkim środowiskiem serwerowym. Natomiast z analitycznego punktu widzenia kod warstwy logiki domenowej może mieć dosłowne przełożenia na model analityczny – czyli zachowujemy podstawowe założenie DDD: Ubiquitous Language. Klasy z tej warstwy testujemy jednostkowo. Jest to relatywnie tanie podejście z uwagi na mniejszą ilość zależności. Dążymy do wysokiego pokrycia testami kodu z tej warstwy z uwagi na to, że modeluje on złożone odpowiedzialności biznesowe.

BUILDING BLOCKS DLA WARSTWY LOGIKI DOMENOWEJ Naszą podróż po krainie DDD odbędziemy w stylu Bottom-Up, rozpoczynając od strony technicznej, tak aby w pierwszej kolejności poznać standardowe „klocki” służące do modelowania domeny. Na bazie tej wiedzy, w części drugiej przejdziemy do zagadnień strategicznego modelowania. Czytelników wywodzących się ze świata Java EE czeka na wstępie mały szok poznawczy, który może przerodzić się w Dysonans Kognitywny. Buildng Blocks służące do modelowania Domeny to nie tylko Encje – do tego nie będą to encje anemiczne, czyli struktury danych. Building Blocks DDD są swego rodzaju językiem wzorców, które stanowią „rusztowanie mentalne” dla modelarza. Każdy ze standardowych klocków ma za zadanie uwypuklenie pewnej koncepcji, zgodnie z główną zasadą DDD: „make explicit what is implicit”. Czyli modelujmy jawnie i wprost złożone reguły i koncepcje biznesowe. Reguły i koncepcje, które w klasycznym podejściu znikają / www.programistamag.pl /

39


INŻYNIERIA OPROGRAMOWANIA Przykład Agregatu: Order

@Entity @Table(name = "Orders") @DomainAggregateRoot public class Order extends BaseAggregateRoot { public enum OrderStatus { DRAFT, SUBMITTED, ARCHIVED } @ManyToOne private Client client; /** * Sample of Value Object usage */ @Embedded private Money totalCost; /** * Sample of encapsulation - this structure is hidden */ @OneToMany(cascade = CascadeType.ALL, fetch = FetchType.EAGER) private List<OrderLine> items; private Timestamp submitDate; @Enumerated(EnumType.STRING) private OrderStatus status; /** * Sample of Policy usage (Strategy Design Pattern)<br> * Policy is injected by Factory/Repo but can be also changed in business * method */ // To be injected by Factory/Repository @Transient private RebatePolicy rebatePolicy; protected Order() { } /** * Meant to be used by factory<br> * Notice that Policy is set via setter because Policy need to be initialised * also in Repository */ Order(Client client, Money initialCost, OrderStatus initialStatus) { this.client = client; totalCost = initialCost; status = initialStatus; items = new ArrayList<OrderLine>(); } /**

40

/ 1 . 2012 . (1)  /

* Sample business method that: * <ul> * <li>hides internal state - OrderLine * <li>can veto - if order is not in DRAFT status * <li>not only modifies structure (list) but also performs logic * calculates total cost * </ul> */ public void addProduct(Product product, int quantity) { checkIfDraft(); OrderLine line = find(product); if (line == null) { items.add(new OrderLine(product, quantity, rebatePolicy)); } else { line.increaseQuantity(quantity, rebatePolicy); } recalculate(); } /** * Sample business method. Fires Domain Event */ public void submit() { checkIfDraft(); status = OrderStatus.SUBMITTED; submitDate = new Timestamp(System. currentTimeMillis()); eventPublisher.publish(new OrderSubmittedEvent(getEntityId())); } public void archive() { status = OrderStatus.ARCHIVED; } private void checkIfDraft() { if (status != OrderStatus.DRAFT) throw new OrderOperationException("Operation allowed only in DRAFT status", client.getEntityId(), getEntityId()); } private void recalculate() { totalCost = Money.ZERO; for (OrderLine line : items) { line.recalculate(rebatePolicy); totalCost = totalCost.add(line. getEffectiveCost()); }


DOMAIN DRIVEN DESIGN KROK PO KROKU

} private OrderLine find(Product product) { for (OrderLine line : items) { if (product.equals(line.getProduct())) return line; } return null; } /** * Sample encapsulation of unstable internal implementation - assumption: * this impl may vary in time. So we use projection of the internal state of * this Aggregate<br> * <br> * Projection hides internal structure using Value Objects. Projection is * also unmodifiable. * * @return */ public List<OrderedProduct> getOrderedProducts() { List<OrderedProduct> result = new ArrayList<OrderedProduct>(items.size()); for (OrderLine line : items) { result.add(new OrderedProduct(line. getProduct().getEntityId(), line. getProduct().getName(), line.getQuantity(), line.getEffectiveCost(), line. getRegularCost())); } return Collections. unmodifiableList(result); } /** * Sample access to the internal state (immutable Value Object) - <b>remember to allow such access * only if makes sense</b>, don't do that by default!<br> * <br> * Notice that there is no setter! * * @return */ public Money getTotalCost() { return totalCost; } }

głęboko w tysiącach linii kodu gigantycznych serwisów projektowanych zgodnie z najlepszym anty-wzorcem, czyli „Boską klasą”.

Agregaty Agregaty są główną jednostką modelowania w DDD. Technicznie jest to graf obiektów, natomiast koncepcyjnie spójna jednostka pracy/zmiany.

Implementując agregaty, zwracamy uwagę na ich hermetyczność – jeden z głównych wyznaczników kodu obiektowego. Najważniejszym zagadnieniem podczas projektowania Agregatu jest określenie jego granicy. Modelowanie zbyt dużych agregatów jest główną przyczyną niepowodzeń modeli opartych o DDD. Dodatkowo zbyt duże agregaty powodują implikacje wydajnościowe na poziomie Mapera Relacyjno-obiektowego oraz problemy związane z równoległym dostępem do danych. Strategiami określania granicy agregatu zajmiemy się w kolejnej części naszej serii. Zweryfikujemy wówczas nasz model, poddając pod dyskusję wielkość Agregatu Order (listing obok). Listing klasy Order ilustruje szereg opisanych poniżej technik DDD, które zostały zastosowane w celu literalnego oddania wiedzy Eksperta Domenowego, utrzymana Ubiquitous Language oraz przygotowania modelu na rozbudowę przy minimalnym impakcie na kod innych klas.

Enkapsulacja Agregatu Warto zwrócić uwagę na fakt, iż Agregat nie pozwala na modyfikację wszystkich swych pól przez settery. Tego typu modyfikacje mogą mieć owszem sens, ale tylko w niewielu przypadkach. Stan Agregatu zmieniany jest za pomocą metod biznesowych oddających słownictwo Eksperta Domenowego. Niektóre metody biznesowe mogą nie być dozwolone w pewnym stanie Agregatu (np. Order.submit ()) - dlatego rzucają wyjątki (błędy) domenowe. Poza tym nie wszystkie gettery mają sens – część pól jest szczegółem implementacyjnym. Zauważmy, że metoda naszego Aggregate: addProduct przyjmuje jako parametr encję Product i ukrywa istnienie lasy OrderLine, która jest szczegółem wewnętrznej implementacji. W tym konkretnym przykładzie zakładamy, że model Zamówienia jest niestabilny - oczekujemy zmian w najbliższej przyszłości. Załóżmy, że Ekspert domenowy nie jest pewien modelu pozycji na zamówieniu. Dlatego chcemy zmniejszyć obszar katastrofy wynikającej ze zmian Agregatu do niego samego. Dlatego nie ujawniamy poza Agregat Order modelu zawartego w klasie OrdelLine. Wprowadzamy ValueObject o nazwie OrderedProduct jako „adapter”, który niejako rzutuje wewnętrzny model Agregatu na świat zewnętrzny. Zauważmy, że metoda Order.getOrderedProducts dokonuje transformacji wewnętrznego (z założenia niestabilnego) modelu pozycji do „zewnętrznego interfejsu” OrderedProduct. Metoda zwraca niemodyfikowalną listę, ponieważ operacje na kolekcji będącej „projekcją” nie mają żadnego sensu.

Inne odpowiedzialności Agregatów W kolejnych częściach naszej serii zapoznamy się z zaawansowanymi odpowiedzialnościami Agregatów takimi jak generowanie Zdarzeń Biznesowych. Pociągnie to za sobą konieczność wstrzykiwania zależności do wnętrza Agregatów. / www.programistamag.pl /

41


INŻYNIERIA OPROGRAMOWANIA Przykład Value Object: Money

@SuppressWarnings("serial") @Embeddable public class Money implements Serializable { public static final Currency DEFAULT_ CURRENCY = Currency.getInstance("EUR"); public static final Money ZERO = new Money(BigDecimal.ZERO, DEFAULT_CURRENCY); private BigDecimal value; private String currencyCode; public Money(double value, Currency currency) { this(new BigDecimal(value), currency. getCurrencyCode()); } @Override public boolean equals(Object obj) { if (obj instanceof Money) { Money money = (Money) obj; return compatibleCurrency(money) && value.equals(money.value); } return false; } public Money multiplyBy(double multiplier) { return multiplyBy(new BigDecimal(multiplier)); } public Money add(Money money) { if (!compatibleCurrency(money)) { throw new IllegalArgumentException("Currency mismatch"); } return new Money(value.add(money.value), determineCurrencyCode(money)); } public String getCurrencyCode() { return currencyCode; } public boolean greaterThan(Money other) { return value.compareTo(other.value) > 0; } @Override public String toString() { return String.format("%0$.2f %s", value, getCurrency().getSymbol()); } }

42

/ 1 . 2012 . (1)  /

Agregat jako Maszyna Stanów Możemy rozszerzyć model Agregatu o wprowadzenie Wzorca Projektowego State Machine. Jest to silna technika służąca do modelowania problemów o następującej charakterystyce: ■■ obiekt może być w wielu stanach (w systemach biznesowych są zwykle związane z pewnymi statusami domeny); ■■ obiekt wykonuje operacje, których sposób wykonania zależy od aktualnego stanu; ■■ oczekujemy wprowadzenia w przyszłości nowych stanów - otwieramy nasz projekt na rozbudowę; ■■ nie spodziewamy się, wprowadzając wielu nowych operacji - ponieważ utrzymanie stanu może być kłopotliwe. Przykład uzasadnionego wprowadzenia Maszyny Stanów: w module Magazynowym mamy Agregat modelujący Paczkę. Paczka zawiera szereg metod biznesowych związanych z dodawaniem do niej zawartości, wysyłką i odbiorem. Zakładamy, że zakres odpowiedzialności tegoż Agregatu jest raczej stabilny i nie spodziewamy się wielu nowych metod. Paczka może istnieć w wielu stanach (zlecona, pakowana, wysłana, odebrana,...) i spodziewamy się, że zbiór możliwych stanów jest różny, w zależności od wdrożenia systemu u klientów.

Value Objects Value Objects są zwykle niedocenianym elementem modelu domenowego. Warto z nich korzystać, ponieważ mimo prostoty wnoszą dużą wartość do modelu – mianowicie zwiększają siłę wyrazu Ubiquotus Language, pomagając w enkapsulacji detali (listing obok). Listing klasy Money przedstawia przykłady Value Object, którego zadaniem jest opakowanie typów „technicznych” i nadanie im znaczenia (metod) biznesowego. Przykładowo bez stosowania VO technicznie możliwe jest pomnożenie 5zł przez 2zł, ale w wyniku otrzymamy bezsenso­ wną wartość 10zł w kwadracie. Zwróćmy uwagę, że klasa Money pozwala dodać Money, ale mnożenie jest możliwe przez wartość bezwymiarową. Jak rozumieć zwiększenie kwoty pieniędzy w przypadku wartości ujemnych? Decyduje o tym Ekspert Domenowy, a model oddaje te reguły w swych metodach. VO dodatkowo dokonują walidacji wartości, które są przez nie opakowywane. Przykładowo wartość prawdopodobieństwa spoza zakresu <0, 1> nie ma sensu. VO z technicznego punktu widzenia są immutable. Dlatego mogą być bezpiecznie zwracane z wnętrza agregatów jako wartości do odczytu. Na listingu klasy Order mieliśmy do czynienia z przykładem wykorzystania VO OrderedProduct jako nośnika danych z wnętrza Agregatu. Z punktu widzenia implementacji VO są pomocne w redukcji code smell o nazwie „primitive obsession”. Smell ten polega na posługiwaniu się zbyt niskim poziomem abstra­ kcji (typami technicznymi), co prowadzi do pojawiania się


DOMAIN DRIVEN DESIGN KROK PO KROKU

długich list parametrów i tak zwanych „Data clumps”. Przykładowo dodanie do siebie dwóch kwot, w dwóch walutach po kursach obowiązujących w danym przedziale dat może wyglądać w następujący sposób: add(BigDecimal, String, Date, Date, BigDecimal, String, Date, Date).

Fabryki Fabryki odgrywają w DDD rolę obiektów domenowych, ponieważ spoczywa na nich część logiki biznesowej. Ich zadaniem jest budowanie Agregatów na podstawie „półproduktów”. Fabryki dokonują również walidacji półproduktów, dzięki czemu zakładamy, że istniejące w pamięci Agregaty są w poprawnym stanie (listing poniżej). Przykład Fabryki

@DomainFactory public class OrderFactory { @Inject private RebatePolicyFactory rebatePolicyFactory; @Inject private InjectorHelper injector; public Order crateOrder(Client client) throws OrderCreationException { checkIfclientCanPerformPurchase(client); Order order = new Order(client, Money. ZERO, OrderStatus.DRAFT); injector.injectDependencies(order); RebatePolicy rebatePolicy = rebatePolicyFactory.createRebatePolicy(); order.setRebatePolicy(rebatePolicy); addGratis(order, client); return order; } private void checkIfclientCanPer formPurchase(Client client) throws OrderCreationException { if (client.getEntityStatus() != EntityStatus.ACTIVE) throw new OrderCreationException("Can not perform purchase, because of the Client status: " + client.getEntityStatus(), client. getEntityId()); } Listing klasy OrderFactory przedstawia przykładową Fabrykę, która tworzy zamówienie dla Klienta, sprawdzając uprzednio, czy klient ten może dokonywać zakupów. Fabryka bierze na siebie również odpowiedzialność wstrzykiwania zależności do Agregatu. W naszym przykładzie Agregat potrzebuje do pracy Polityki Rabatowania. Fabryka wylicza konkretny typ Polityki na podstawie reguł biznesowych biorących pod uwagę historię zamówień klienta. Dzięki takiemu podejście zmniejszamy coupling

Agregatu Order. Klasa Order posiada jedynie zależności do domenowych obiektów współpracujących i nie musi być zależna od obiektów technicznych. Zależności techniczne przechodzą na klasę Fabryki. W teorii couplingu mamy 3 poziomy zależności: create, contain, call. Fabryka bierze na siebie (odciążając Agregat) najbardziej szkodliwą zależność: create oraz w niektórych przypadkach contain. W rezultacie znacznie zwiększamy testability klasy Order. Zagadnienia zwiększania testowalności Agregatów przez wprowadzanie ich fabryk zostaną szerzej poruszone w jednej z kolejnych części poświęconej testowaniu automatycznemu, natomiast zagadnienia wstrzykiwania poruszmy w części poświęconej architekturze aplikacji.

Repozytoria Repozytoria są abstrakcją persystencji dla Agregatów. Repozytorium odpowiada za pobranie i utrwalenie konkretnego Agregatu. Repozytoria, w odróżnieniu od DAO, nie są obarczone dziesiątkami metod wyszukujących, tworzonych na potrzeby ekranów. Repozytorium może zawierać metody wyszukujące (OrderRepository.findUncorfirmed), ale tworzone jedynie na potrzeby logiki biznesowej. Natomiast wyszukiwania na potrzeby ekranów należą do dedykowanych serwisów wyszukujących, którymi zajmiemy się w części poświęconej architekturze aplikacji. @DomainRepository public interface OrderRepository { public Order save(Order order); public Order load(Long orderId); public List<Order>findUncorfirmed(Client client) } Listing interfejsu OrderRepository ilustruje przykładowy kontrakt Repozytorium. Przykładową implementację z wykorzystaniem JPA oraz opartą o idiom Generic Repository i klasę Bazowego Agregatu przedstawimy w części poświęconej szczegółom implementacji na platformie Java EE. Warto dodać, że w systemach budowanych jako wartość dodana ponad istniejącymi systemami może dojść do sytuacji, w której Repozytorium danego Agregatu pobiera jego części z kilku DAO (kilka systemów, baz, web serwisów) i dokonuje ich złożenia w nowy koncept biznesowy.

Serwisy Domenowe Paradygmat obiektowy nie jest adekwatny do każdego modelu. Zwykle część modelu domenowego to procedury mające na celu np. transformację jednych Agregatów w inne. Z tego powodu w DDD istnieje Building Block modelujący tego typu operacje – są to Serwisy Domenowe. W odróżnieniu od Serwisów Aplikacyjnych (o których za chwilę) Serwisy Domenowe operują na poziomie logiki biznesowej. / www.programistamag.pl /

43


INŻYNIERIA OPROGRAMOWANIA Przykład Serwisu Domenowego

@DomainService public class InvoicingService { @Inject private ProductRepository productRepository; public Invoice issuance(Order order, TaxPolicy taxPolicy){ //TODO refactor to InvoiceFactory Invoice invoice = new Invoice(order. getClient()); for (OrderedProduct orderedProduct : order.getOrderedProducts()){ Product product = productRepository. load(orderedProduct.getProductId()); Money net = orderedProduct. getEffectiveCost(); Tax tax = taxPolicy.calculateTax(product. getType(), net); InvoiceLine invoiceLine = new InvoiceLine(product, orderedProduct. getQuantity(), net, tax); invoice.addItem(invoiceLine); } return invoice; } } Listing klasy InvoicingService jest modelem „księgowego” w domenie zamówień. Jego zadaniem jest wygenerowanie Faktury na podstawie Zamówienia, biorąc pod uwagę Politykę Podatkową. Zwróćmy uwagę na fakt, iż nasz księgowy nie jest obiektem persystentnym. Mimo tego traktujemy go jako obiekt domenowy. Księgowy enkapsuluje reguły związane z fakturowaniem; część z reguł jest specyficzna dla wdrożenia w danym kraju, dlatego została wyniesiona do Polityk Poda­ tkowych. Warto zwrócić uwagę na decyzję projektową polegającą na stworzeniu komponentu Księgowego. Bardzo złym pomysłem byłoby obarczanie Agregatu Order metodą issueInvoice() i podobnymi, ponieważ z czasem zamówienie stałoby się „Boską klasą”. Księgowy operuje na „projekcji” z wnętrza Agregatu Order, czyli na VO OrderedProduct, dzięki czemu nie jest wrażliwy na zmiany w implementacji Agregatu.

Polityki Z technicznego puntu widzenia polityki są implementowane jako Strategy Design Pattern, wnosząc pierwiastek funkcyjności do języków obiektowych. Koncepcyjnie Polityki są „domknięciem” lub „dostrojeniem” modelu. Pozwalają na rozbudowę modelu bez modyfikacji corowych Agregatów. W dotychczasowych przykładach spotkaliśmy się z Polityką Rabatowania

44

/ 1 . 2012 . (1)  /

(RebatePolicy) używaną przez Agregat Order oraz Polityką Obliczania Podatku (TaxPolicy) używaną przez księgowego (InvoicingService). @DomainPolicy public interface TaxPolicy { /** * calculates tax per product type based on net value */ public Tax calculateTax(ProductType productType, Money net); Listing interfejsu TaxPolicy ilustruje kontrakt, jaki jest zawierany pomiędzy księgowym (InvoicingService) a implementacjami obliczania podatku dla różnych krajów. Z punktu widzenia modelarza Polityka jest kolejnym Building Blockiem, który wspiera mantrę „make explicit what is implicit”. Obliczanie podatku jest co prawda czynnością, ale tak istotną z puntu widzenia modelu, że znajduje w nim swoje miejsce jako „first class citizen”.

Pozostałe Building Blocks W kolejnych częściach naszej serii omówimy zastosowanie zaawansowanych „klocków” DDD takich jak: Zdarzenia Domenowe, Sagi i Specyfikacje.

MODELOWANIE SCENARIUSZY Po zapoznaniu się z Building Blocks warstwy logiki domenowej przejdziemy o jedno „piętro” wyżej, do warstwy logiki aplikacji. Jak już wspomniano we wstępie, warstwa ta modeluje kroki Use Case lub User Story. W niniejszym artykule przyjrzymy się implementacji Serwisu Aplikacyjnego w sposób poglądowy. Natomiast w kolejnych częściach serii skupimy się na implementacji (transakcje, bezpieczeństwo), testowaniu end-to-end (również BDD i JBehave) oraz podejdziemy nieco inaczej do implementacji tej warstwy (CommandHandlers i architektura CqRS). Listing klasy PurchaseApplicationService (obok) przedstawia typowy model kroków Use Case/User Story operujący na modelu domenowym: stworzenie zamówienia, dodanie produktu do zamówienia, zatwierdzenie zamówienia. Serwis Aplikacyjny: ■■ integruje wiele zależności (repozytoria, fabryki, serwisy pomocnicze); ■■ zapewnia transakcyjność i bezpieczeństwo (w tym wypadku poprzez adnotacje i AOP); ■■ integruje komponenty aplikacyjne (w tym wypadu pracujący w sesji, zalogowany użytkownik); ■■ orkiestruje obiekty domenowe. Przykładem orkiestracji wszystkich obiektów domenowych jest metoda approveOrder(id), która kolejno: ■■ pobiera Agregat Zamówienia z Repozytorium;


DOMAIN DRIVEN DESIGN KROK PO KROKU

Przykład Serwisu Aplikacyjnego

@ApplicationService public class PurchaseApplicationService { @Inject private OrderRepository orderRepository; @Inject private OrderFactory orderFactory; @Inject private ProductRepository productRepository; @Inject private InvoiceRepository invoiceRepository; @Inject private InvoicingService invoicingService; @Inject private SystemUser systemUser; @Inject private ApplicationEventPublisher eventPublisher; /** * Sample usage of factory and repository * * @throws OrderCreationException */ public void createNewOrder() throws OrderCreationException { Client client = loadClient(systemUser. getUserId()); Order order = orderFactory. crateOrder(client); orderRepository.persist(order); } /** * Sample call of the domain logic<br> * Sample publishing Application (not Domain) Event */ public void addProductToOrder(Long productId, Long orderId, int quantity) { Order order = orderRepository. load(orderId); Product product = productRepository. load(productId); // Domain logic order.addProduct(product, quantity); orderRepository.save(order); // if we want to Spy Clients:) eventPublisher.publish(new ProductAddedToOrderEvent(product. getEntityId(), systemUser.getUserId(), quantity)); } /** * Sample of the separation of domain logic in aggregate and domain logic in

* domain service * * @param orderId */ public void approveOrder(Long orderId) { Order order = orderRepository. load(orderId); Specification<Order> orderSpecification = generateSpecification(systemUser); if (!orderSpecification. isSatisfiedBy(order)) throw new OrderOperationException("Order does not meet specification", order.getEntityId()); // Domain logic order.submit(); // Domain service Invoice invoice = invoicingService.issuance(order, generateTaxPolicy(systemUser)); invoiceRepository.save(invoice); orderRepository.save(order); } /** * Assembling Spec contains Business Logic, therefore it may be moved to * domain Building Block OrderSpecificationFactory * * @param systemUser * @return */ @SuppressWarnings("unchecked") private Specification<Order> generateSpecific ation(SystemUser systemUser) { Specification<Order> specification = new Co njunctionSpecification<Order>(// new DestinationSpecification(Locale. CHINA).not(),// do not send to China new ItemsCountSpecification(100),// max 100 items new DebtorSpecification()// not debts or max 1000 => debtors can // buy for max 1000 .or(new TotalCostSpecification(new Money(1000.0)))); // vip can buy some nice stuff if (!isVip(systemUser)) { specification = specification.and(new RestrictedProductsSpecification()); } return specification; } }

/ www.programistamag.pl /

45


INŻYNIERIA OPROGRAMOWANIA

DDD Building Blocks ■■ ■■ ■■ ■■ ■■ ■■ ■■ ■■ ■■ ■■

Entity – identyfikowalne obiekty zawierające odpowiedzialność biznesową; Aggregate – hermetyczne grafy obiektów, z jedną encją będącą „korzeniem agregatu”, która stanowi API całości. Agregat jest główną jednostką logiki domenowej w DDD; Value Object – wrapper dla typów prostych, nadający im znaczenie biznesowe oraz wygodny interfejs; Service – specyficzne operacje, które nie pasują do żądnego agregatu; Policy – model wariacji operacji biznesowych, Strategy Design Pattern; Specification – model złożonych warunków biznesowych, wywodzi się z Composite Design pattern; Event – model wydarzeń biznesowych, może służyć do przetwarzania równoległego lub komunikacji pomiędzy Bounded Context; Saga – model złożonego procesu, który stan jest trwały oraz zależy od wielu zdarzeń; Factory – tworzy nowe instancje złożonych Agregatów, dbając o ich poprawność. Zwiększa testowalność, biorąc niejako na siebie operatory new; Repository – zarządza trwałością Agregatu/Encji.

W sieci ■■ http://domaindrivendesign.org oficjalna strona DDD ■■ http://bottega.com.pl/pdf/materialy/sdj-ddd.pdf wstępny artykuł poświęcony DDD ■■ http://code.google.com/p/ddd-cqrs-sample/ przykładowy projekt

■■ weryfikuje go przy pomocy Specyfikacji Domenowej; ■■ wykonuje za Zamówieniu operację biznesową submit(); ■■ zapisuje Agregat Zamówienia do Repozytorium; ■■ generuje Fakturę przy pomocy Księgowego, decydując o użytej przez niego Polityce Podatkowej na podstawie danych o Zalogowanym Użytkowniku; ■■ zapisuje Fakturę do Repozytorium.

Sławomir Sobótka

Uogólniając, mamy do czynienia ze stworzeniem/pobraniem „aktorów” biznesowych, wykonaniem przez nich (pomiędzy nimi) scenariusza, utrwaleniem stanu aktorów.

PODSUMOWANIE Niniejszy artykuł miał na celu zapoznanie czytelników z podstawowymi Building Blocks DDD oraz podstawowymi koncepcjami stojącymi za technikami modelowania DDD. Doświadczeni czytelnicy zapewne zauważyli, iż wszystkie wymienione techniki należą do dobrych praktyk projektowania obiektowego (SOLID, GRASP, RDD). Przykładowy model został zaprojektowany w nieco defensywnym, hermetycznym stylu, tak aby przyszłe modyfikacje nie naruszały zbyt drastycznie całej struktury, a miały jedynie lokalny impakt. W kolejnych częściach wprowadzimy zaawansowane Building Blocks oraz zaawansowane techniki strategi­ cznego modelowania, co skłoni nas do modyfikacji aktualnego „kształtu” modelu.

slawomir.sobotka@bottega.com.pl

Programujący architekt aplikacji specjalizujący się w technologiach Java i efektywnym wykorzystaniu zdobyczy inżynierii oprogramowania. Trener i doradca w firmie Bottega IT Solutions. W wolnych chwilach działa w community jako: prezes Stowarzyszenia Software Engineering Professionals Polska (http://ssepp.pl), publicysta w prasie branżowej i blogger (http://art-of-software.blogspot.com).

46

/ 1 . 2012 . (1)  /


TYTUŁ

/ www.programistamag.pl /

47


INŻYNIERIA OPROGRAMOWANIA Artur Machura

Praktyczne wykorzystanie metod zdefiniowanych w RUP na stanowisku Analityka Biznesowego Umiejętność praktycznego wykorzystania na stanowisku Analityka metod i narzędzi Inżynierii Oprogramowania wydaje się bardzo wartościowa. Bowiem efekty z tej pracy przesądzają najczęściej o rezultacie końcowym całego przedsięwzięcia. Co wynika zasadniczo z definiowania na tym etapie, zarówno celowościi jak i sposobu prowadzenia prac przez cały zespół wytwórczy.

P

owszechnie wiadomo, że proces produkcji oprogramowania jest czasochłonny. Im większe jest przedsięwzięcie, tym wykorzystanie praktyk Inżynierii Oprogramowania staje się trudniejsze. Nie tylko z powodu samej wiedzy na ich temat, ale przede wszystkim umiejętnemu ich wykorzystaniu w konkretnym projekcie, w którym są prowadzone poszczególne prace. Analiza Biznesowa jest właśnie jedną z tych szczególnych dyscyplin Inżynierii Oprogramowania, gdzie problemy z zastosowaniem praktyk Inżynierii Oprogramowania przekładają się nie tylko na same produkty analityczne, ale i całą resztę pracy projektantów, programistów, testerów. A więc projekt, implementację, testy, wdrożenie i oczywiście sam produkt. Którego jakość czy też funkcjonalność będzie szczegółowo obserwowana przez klienta – przyszłego użytkownika przez cały okres jego eksploatacji. Dlatego każdy powinien sobie odpowiedzieć samodzielnie na pytanie przez pryzmat własnej firmy, projektu, doświadczeń – czy warto posiadać wiedzę, jak praktycznie i sensownie wspierać warsztat metodyczno – narzędziowy Analityka Biznesowego praktykami Inżynierii Oprogramowania ?

WPŁYW CZYNNIKÓW EKONOMICZNYCH NA ZASTOSOWANIE PRAKTYK INŻYNIERII OPROGRAMOWANIA NA STANOWISKU PRACY ANALITYK BIZNESOWY Niewiele jest takich środowisk pracy, które byłyby niezależne od czynników ekonomicznych. To właśnie rynek i wszelkie aspekty z nim powiązane rzutują również i na zastosowanie praktyk Inżynierii Oprogramowania. I zapewne każdy z czytających zastanawia się, co dokładniej może wpływać na pracę Analityka Biznesowego. Bo odpowiedź na to pytanie mogłaby wyjaśnić – jakie techniki i metody wykorzystywać na stanowisku Analityka. Czy też jak je wykorzystywać oraz w którym kierunku rozwijać własne kompetencje. Międzynarodowa organizacja Analityków Biznesowych IIBA (International Institute of Business Analysis) sugerując się zapewne wieloma opiniami w tym zakresie, zdefiniowała tzw. obszary wiedzy i kompetencji, z którymi każdy pełniący obowiązki Analityka Biznesowego jest związany. Podręcznik BABOK (Business

48

/ 1 . 2012 . (1)  /

Analysis Body of Knowledge) dosyć precyzyjnie opisuje składowe tych obszarów. Celowo wymieniam te zaga­ dnienia, ze względu na poznanie wytycznych IIBA odnośnie kierunków rozwoju tej specjalizacji, jaką jest analiza biznesowa. Które, bo bliższym przyjrzeniu pokazują trud tej pracy w środowisku komercyjnym, z uwzględnieniem praktycznego wymiaru, jaki stawia rynek. Obszary tych kompetencji to: ■■ Zaplanowanie i monitorowanie wykonywanej pracy w ramach całego przedsięwzięcia, aby korzyści z analizy biznesowej były jak największe. Na co mają wpływ tak istotne zagadnienia jak: identyfikacja udziałowców przedsięwzięcia oraz ich wpływu na produkty anality­ czne, selekcja odpowiednich technik analizy biznesowej, szacowanie czasu i kosztu potrzebnej pracy do wykonania, zaplanowanie skutecznej metody komunikacji z udziałowcami, zaplanowanie sposobu zarządzania wymaganiami, określanie produktów analitycznych, definiowanie procesu realizacji prac analitycznych, sposób oceny postępu pracy. ■■ Skuteczne pozyskiwanie wymagań, aby zapewnić w realizowanym przedsięwzięciu ich kompletność, popra­wne znaczenie oraz stabilność. Gdzie wyróżnia się różne metody pozwalające na pozyskiwanie tych wymagań. Mianowicie: tzw. burze mózgów, analizowanie dokumentacji, spotkania zespołowe, analiza interfejsów systemowych, przeprowadzanie wywiadów, obserwacja udziałowców i przyszłych użytkowników systemu w środowisku wykonywanej przez nich pracy, tworzenie prototypów rozwiązania, przeprowadzanie warsztatów poświęconych wymaganiom, przeprowadzanie ankiet. ■■ Zarządzanie wymaganiami i zapewnienie odpowiedniej komunikacji, w rezultacie zastosowania wiedzy której analityk jest w stanie skutecznie zarządzać wymaganiami, zagadnieniami oraz ich ewentualnymi zmianami. Przyszli użytkownicy systemu i pozostali udziałowcy jednoznacznie identyfikują wymagania i istnieje pomiędzy nimi wspólne stanowisko – dotyczące przyszłej funkcjonalności wytwarzanego systemu.


PRAKTYCZNE WYKORZYSTANIE METOD ZDEFINIOWANYCH W RUP

■■ Przedsiębiorczości podczas analizowania, gdzie zwraca się uwagę również na same czynności wykonywane przez analityka, które są konieczne do identyfikacji potrzeb biznesowych, problemów, możliwości i ewentualnych rozwiązań. Formułowanie uzasadnienia koniecznych do poniesienia kosztów inwestycji, które są potrzebne w celu dostarczenia rozwiązania. ■■ Analizowanie wymagań to kolejny obszar kompetencji, w którym opisywane są zadania i techniki wykorzystywane przez analityka w celu definiowania rozwiązań spełniających oczekiwania. Wykonywane zadania obejmują takie zagadnienia jak: priorytetowanie wymagań, organizowanie wymagań, specyfikowanie i modelowanie wymagań, definiowania przypuszczeń i ograniczeń, weryfikowanie wymagań, walidowanie wymagań. ■■ Uczestniczenie i walidacja podczas budowy rozwiązania co ma na celu dopilnowanie i zapewnienie, że rozwiązanie zostanie poprawnie zaimplementowane i zostaną spełnione oczekiwania biznesu. Przekrój pojęć mieszczących się w tych w/w obszarach kompetencji wskazuje dosyć obiektywnie na oczekiwany kierunek rozwoju kompetencji oraz środowisko pracy, które wynikają właśnie z czynników ekonomicznych związanych z pracą wykonywaną na stanowisku analityka. W dalszej części artykułu scharakteryzowano wybrane i przykładowe przedsięwzięcie oraz skutki czynników ekonomicznych na przyjęty warsztat metodyczny i narzędziowy – zdefiniowany w metodyce RUP.

SCHARAKTERYZOWANIE PRACY I PRZEDSIĘWZIĘCIA, KTÓRA WPŁYNĘŁA NA WNIOSKI AUTORA Aktywne uczestnictwo w przedsięwzięciach informaty­ cznych, gdzie wykonuje się konkretne prace zaplanowane w harmonogramie – poza właściwymi efektami z tej pracy, może przynieść wiedzę o praktycznym zastosowaniu pewnych stosowanych technik Inżynierii Oprogramowania. Produkty, które wytwarza się na etapie Analizy Biznesowej – mogą również być postrzegane w ten sposób. A więc, wnioski z pracy wykonywanej przez osobę, która obejmowała obowiązki Analityka w konkretnym przedsięwzięciu informatycznym – mogą być niezmiernie wartościowe, bowiem nazywają pewne produkty – opisane w tym przypadku przez RUP (Rational Unified Process), z perspektywy praktycznego zastosowania. Podczas realizacji których obowiązywały w przedsięwzięciu priorytety: 1. Po pierwsze terminy wykonania zadań; 2. Po drugie jakość wykonanej pracy; 3. Po trzecie wpływ wykonanej pracy na losy projektu i możliwe sprzężenia zwrotne. W kolejnych punktach zostaną scharakteryzowane zajęcia wykonywane na stanowisku Analityka. Model wytwórczy, scharakteryzowany przez rysunek na następnej stronie – odzwierciedla ideę wytwarzania systemów wytwórczych, w ramach której produkty wytwarzane na

etapie analizy biznesowej są przez niniejszy artykuły opisywane. Przed rozpoczęciem opisywania prowadzonych prac, należałoby dokonać interpretacji organizacji pracy w wykorzystywanej metodyce RUP. Tylko właściwa interpretacja pozwoli w dalszej części artykułu zrozumieć problematykę poruszaną przez artykuł. Przede wszystkim zakłada się proces wytwórczy z grupy podejść ewolucyjnych w wytwarzaniu systemów informatycznych. Proces ten wyróżnia w tym podejściu iteracje i przyrosty systemu, podczas ewolucji prac. Nad całością prowadzonych prac czuwają fazy przedsięwzięcia. Gdzie wymienia się: ■■ ■■ ■■ ■■

Inicjację, Rozwinięcie, Tworzenie, Przejście.

Bardzo interesujące w tym podejściu jest zaangażowanie poszczególnych - skategoryzowanych przez dyscypliny inżynierii oprogramowania obszarów pracy podczas w/w faz. Gdzie koniecznie należy zrozumieć, że omawiana w tym przypadku analiza biznesowa jest realizowana przede wszystkim w dwóch początkowych fazach przedsięwzięcia, tj. inicjacji oraz rozwinięciu. Kolejne fazy zakładają wręcz znikome wykorzystanie prac anality­cznych dotyczących tzw. biznesu. Należy tu zauważyć – że podczas prac związanych z analizą biznesową identyfikuje i rozpatruje się m.in. wymagania dotyczące systemu. Samo projektowanie systemu, pomimo że rozpoczyna się w fazie inicjacji, to jest realizowane przede wszystkim w fazie kolejnej - rozwinięcia. Czas realizacji projektu, w którym uczestniczył autor niniejszego artykułu – rozciągnięty był na przestrzeni kilku lat. Organizacja przedsięwzięcia wyłączyła wykonawcę z etapu dotyczącego przygotowywania specyfikacji istotnych warunków zamówienia. Wspominam o tym z tego względu, że pewna podstawowa praca analityczna została wykonana przed rozpoczęciem prac przez wykonawcę. Jest to właśnie jeden z tych większych problemów, gdzie wykorzystanie tej metodyki może zostać z góry skazane na niepowodzenie ze względu na wyłączenie fazy inicjacji poza zasadniczy obszar prac wykonawcy, w rezultacie czego, wykonawca otrzymuje z góry zbiór wymagań systemowych do spełnienia. I co najistotniejsze, bez możliwości metodycznego określenia celów biznesowych. Prace wykonywane przez Analityka wykonawcy w omawianym artykule i harmonogramie można by było skategoryzować do następujących punktów: 1. Udział w spotkaniach z klientem, których celem jest omawianie i doprecyzowanie wymagań, 2. Przekładanie pozyskanych na spotkaniach z klientem informacji, na wiedzę i byty projektowe, 3. Realizowanie prac analitycznych i projektowych na podstawie pozyskanej wiedzy, / www.programistamag.pl /

49


INŻYNIERIA OPROGRAMOWANIA

Rysunek 1. Proces wytwórczy RUP

4. Przekazywanie i uzgadnianie produktów analitycznych z pozostałymi członkami zespołu, 5. Weryfikacja i konsultacje dotyczące efektów realizacji prac zarówno z członkami zespołu, jak i klientem. Niestety, wyżej wymienione obszary prac, z racji pewnej złożoności i sposobu organizacji pracy nie były wystarczająco kontrolowane przez zarząd projektu. Przyjęto taką politykę w projekcie, która polegała na zarządzaniu członkami zespołu, a nie bezpośrednimi efektami ich prac. W konsekwencji kolejne etapy cyklu wytwórczego nie opierały się bezpośrednio na dokumentacji specyfikacji analitycznej. Pewne tempo prac oraz brak zdefiniowanej roli w projekcie „Inżynier Procesu Wytwórczego” przyczyniło się m.in. do tego, że zrezygnowano w projekcie na dokładne stosowanie praktyk inżynierii oprogramowania zawartych w RUP, które pozwoliłyby na rozbijanie zaga­ dnień projektowych na pewne byty – którymi można by było zarządzać. W dalszej części opiszę produkty wytwarzane w rezultacie w/w prac oraz ich skuteczność w tak zorganizowanym projekcie.

Udział w spotkaniach z klientem, których celem jest omawianie i pozyskiwanie wymagań. Udział w spotkaniach z klientem miał na celu na opisywanym etapie wytwórczym: ■■ omawianie wymagań zawartych w SIWZ (Specyfikacji Istotnych Warunków Zamówienia), ■■ pozyskiwanie ewentualnych nowych wymagań, ■■ doprecyzowanie wymagań.

50

/ 1 . 2012 . (1)  /

Rezultatem pracy były przede wszystkim notatki ze spotkań. Dokumenty te stanowiły podstawę formalną do dalszych prac. Na tym etapie prac, niestety, nie zarządzało się pojęciem „wymaganie” w taki sposób aby: umożliwić na jego dokładną kategoryzacją, umieszczenie w modelu wymagań czy też udostępnić w bibliotece projektu pozostałym członkom projektu, bowiem nie istniała taka kultura organizacyjna projektu. Raczej operowano tymi pojęciami w taki sposób, aby uzyskać dobry kontakt z klientem biznesowym, któremu najczęściej obce jest inżynierskie środowisko pracy. Główny problemem opisywanego obszaru prac jest zapewnienie takiej organizacji pracy całego zespołu, aby powiązać oczekiwania dwóch odmiennych środowisk pracy: klienta i wykonawcy.

Przekładanie pozyskanych na spotkaniach z klientem informacji na wiedzę i byty projektowe Ustalono, że metodyka RUP po pewnej adaptacji pozwoli na właściwe wytwarzanie produktów w tym obszarze prac. Popularna technika przypadków użycia, niezmiernie wartościowa w celu dokumentowania oczekiwań klienta od systemu - nie spotkała się jednak z dużym uznaniem przez projektantów i programistów ze względu na pewne problemy, które można podsumować do punktów: ■■ Trudności w prezentowaniu przypadków użycia na takim poziomie abstrakcji, który z jednej strony byłby zrozumiały przez klienta, a z drugiej w sposób wręcz inżynierski opisywał użycie systemu informatycznego. Formułowanie przypadków obrazujących tzw. cel aktora, w złożonym


PRAKTYCZNE WYKORZYSTANIE METOD ZDEFINIOWANYCH W RUP

systemie spotkał się z pewnym trudem. Wymagał bowiem jeszcze długiej pracy (poza uzgodnieniami z klientem), która miałaby na celu doprecyzować i rozbić scenariusze na takie porcje informacji, które były efektywnie rozumiane i implementowane przez zespół programistów. ■■ Brak statycznej perspektywy funkcjonalności opisanej przez przypadki użycia, przyczynił się do scalenia modeli analitycznych dopiero na kolejnych etapach wytwórczych: projektowaniu i implementacji. W konsekwencji problemy tam zauważone skutkowały często: 1. ponowieniem prac nad samymi przypadkami użycia, które były zaakceptowane przez klienta, 2. angażowaniem projektantów i programistów do prac analitycznych (kosztem ich czasu przewidzianego do prac głównie związanych z projektowaniem i implementacją). Reasumując, praca dotycząca przekładania pozyskanych na spotkaniach z klientem informacji na wiedzę i byty projektowe powinna nie tylko być właściwa. Wymaga również wzajemnej świadomości pracy do wykonania przez obie strony: klienta i wykonawcy.

Przekazywanie i uzgadnianie produktów analitycznych z pozostałymi członkami zespołu Przekazywanie prac do pozostałych członków zespołu odbywało się poprzez bezpośrednią rozmowę, na potrzeby której organizowano często spotkania. Ponadto dyskutowano z wykorzystywaniem wszystkich dostępnych mediów komunikacji (telefon, e-mail, telekonferencje). Podczas rzeczywistych prac - członkowie zespołu byli skłonni do dyskusji bezpośrednich z pominięciem tzw. dokumentacji. Co nawiązuje zasadniczo do idei „manifestu zwinnego wytwarzania oprogramowania” oraz wpływa negatywnie na ideę wykorzystywania zunifikowanego języka modelowania UML – który z założenia pozwala na jednoznaczną komunikację osobom posiadającym różne kompetencje. Ponadto specyfikacja prac analitycznych, które były nastawione na środowisko współpracy z klientem, niekoniecznie dostarczała rezultatów pracy oczekiwanych przez projektantów i programistów. Specyfikacja przypadków użycia nastawiona na klienta, brak perspektywy staty­ cznej – na pewno nie zapewniała stabilnego źródła informacji, konsekwencją czego były często długie rozmowy, nie zawsze prowadzące do jednoznacznych wniosków. Zasadniczym problemem pracy związanej z przekazywaniem i uzgadnianiem produktów analitycznych do pozostałych członków zespołu stanowi głównie zagadnienie przyjęcia efektywnej metody komunikacji i pogodzenia mentalności osób cechujących się różnymi kompetencjami.

Weryfikacja i konsultacje dotyczące efektów realizacji prac zarówno z członkami zespołu, jak i klientem Model iteracyjno-przyrostowy wymaga częstych odbiorów prac zarówno wewnątrz zespołu wykonawcy, jak

i przez zamawiającego. Oznacza to, że weryfikacja prac była praktycznie na porządku codziennych obowiązków. Co w praktycznym wymiarze, gdzie klient podczas trwania projektu wykonuje również swoje główne prace (inne od uczestniczenia w projekcie) są obarczone pewnym wyzwaniem dla samego klienta. Można powiedzieć, że z chwilą rozpoczęcia projektu drastycznie zwiększa się dla niego liczba obowiązków. Model iteracyjno – przyrostowy, pomimo dobrych chęci i gwarancji zrozumienia z klientem na płaszczyźnie realizowanych prac projektowych – może wręcz utrudniać główną pracę klienta. Który to, z chwilą nie dysponowania wystarczającą ilością czasu, może wpływać destrukty­ wnie na postęp prac. Zadanie poświęcone weryfikacjom i konsultacjom dotyczącym efektów realizacji prac, zarówno z członkami zespołu, jak i klientem, przez pewną pożądaną i właściwą cykliczność – bez pewnych zabiegów dotyczących zmian organizacyjnych – może pogrążać zarówno pracę zasa­dniczą wykonywaną przez samego klienta, jak i realizowany projekt. Dlatego też należy zwrócić szczególną uwagę na przyjęte metody porozumiewania się podczas ewolucji systemu i towarzyszących temu cyklicznym spotkaniom.

ASPEKT HISTORYCZNY Podsumowanie wniosków z zastosowania technik obie­ ktowych na stanowisku analityka biznesowego, można byłoby skonfrontować w pierwszej kolejności ze sprawą fundamentalną. Otóż, jak można wnioskować np. z historii powstania zunifikowanego języka modelowania UML. Który oparty był o wybór najpopularniejszych trzech metodyk (OOAD – Grady Booch, OOSE – Ivar Jacobson, OMT - James Rumbaugh), spośród wielu współzawodniczących ze sobą (lata '80, ' 90). Można zauważyć, że metodyki te opierały się o spostrzeżenie, jak ważną rolę odgrywają w przedsięwzięciach m.in.: ■■ ■■ ■■ ■■ ■■

modelowanie modelowanie modelowanie modelowanie modelowanie

dziedziny przedmiotowej, aspektu użytkownika, aspektu projektowania, aspektu implementacji, cyklu życia oprogramowania.

Już w pierwszych bardziej złożonych projektach zauważano, że pominięcie w projekcie aspektu modelowania - może niekorzystnie wpłynąć na jego efekt końcowy. Przywołanie takich podstaw historycznych stojących za potrzebą modelowania powinno zwrócić uwagę na fakt, że bardzo wiele osób na świecie, praktycznie począwszy od pierwszych przedsięwzięć informatycznych, dostrzegało potrzebę modelowania. Okres rywalizacji różnych metodyk czy też wybór międzynarodowego standardu UML tym bardziej podkreśla znaczenie tego faktu. Innymi słowy, artykuł ten nie odkrywa, a raczej przypomina o pewnym fakcie. Otóż, inżynier oprogramowania może sam dochodzić do pewnych wniosków, albo poznawać te, które zostały już wcześniej zgromadzone i opisane – najczęściej wiele lat temu. / www.programistamag.pl /

51


INŻYNIERIA OPROGRAMOWANIA PODSUMOWANIE Po pierwsze już sama organizacja projektu rzutuje na odpowiedni sposób prowadzenia prac analitycznych, w tym wybrania określonej metody stosowania technik obiektowych. W innym przypadku nie tylko projekt nie osiąga założonych pozytywnych rezultatów, ale i zniesławione zostają techniki obiektowe, w tym te, które wspierają warsztat analityka biznesowego. Dużą wagę należałoby przywiązać tu do odpowiedzi na pytanie: ■■ jakie znaczenie w projekcie mają efekty z analizy biznesowej? ■■ czy postawiono taki wymóg jakościowy wobec prowadzonych pracach analitycznych, aby projekt dostarczał konkretnych jego składowych – obrazujących aspekt statyczny i dynamiczny ? ■■ czy zdefiniowane oczekiwania od produktów anality­ cznych, poza samym ich celem zastosowania, uwzględniają środowisko pracy i jego właściwości? Miejąc tu na uwadze takie zagadnienia jak: • proces wytwarzania oprogramowania i jego dojrzałość, • przyszłą konserwację oprogramowania będącego w użyciu, • ewentualną integrację oprogramowania, • sprawność i efektywność produkcji oprogramowania bez uszczerbku dla jakości. ■■ czy organizacja projektu umożliwi na taką realizację prac analitycznych, aby osiągnąć ich założony cel? Jak wynika ze scharakteryzowanego w niniejszym artykule przykładowego projektu, skutki zastosowania określonych technik obiektowych mogą być zarówno pozytywne, jak i negatywne. Co jest zasadniczo związane ze sprawami poruszonymi przez w/w pytania. Doświadczenia z projektu pokazują, że w konsekwencji nie podjęcia decyzji dotyczących w/w zagadnień, mogą powstać konkretne wyzwania – których pominięcie bezpośrednio może doprowadzić do klęski pracy. Wymienia się tu: ■■ organizacja projektu powinna zapewnić sprawne zarządzanie projektu zarówno przez zamawiającego, jak i wykonawcę, ■■ zarówno zamawiający, jak i wykonawca powinni być świadomi skutków podjętych decyzji podczas zbierania wymagań,

■■ wybór odpowiedniej metody komunikacji w projekcie powinien zapewnić odpowiedni poziom komunikacji osobom o różnych kompetencjach, jak i mentalności, ■■ organizacja przedsięwzięcia i przebiegu projektu powinna uwzględnić również i wszelkie aspekty poza projektowe osób i organizacji uczestniczących w przedsięwzięciu. Scharakteryzowana wcześniej praca i projekt na pewno powinna skłonić do zastanowienia i odpowiedzi na pytanie: czy w mojej firmie i projekcie chciałbym podjąć się działań zmierzających do zminimalizowania opisywanych w tym artykule problemów? W pierwszej kolejności scharakteryzuję w tym celu korzyści płynące z wprowadzenia określonych i uzasadnionych metod na stanowisku analityka, tj.: ■■ wykonawca poznaje cele biznesowe organizacji oraz potrzeby pracowników i innych udziałowców, ■■ wykonawca rozumie, jaka oczekiwana funkcjonalność systemu i wymagania systemowe byłyby w stanie umożliwić na osiąganie zdefiniowanych celów i potrzeb biznesowych, ■■ wykonawca jest świadomy sposobu przyszłego wykorzystania systemu i ewentualnych zmian organizacyjnych z chwilą rozpoczęcia jego eksploatacji, ■■ klient poznał proces wytwórczy realizowanego przedsięwzięcia na tyle, aby świadomie współpracować z wykonawcą i rozumieć konsekwencje współpracy i następstw podejmowanych decyzji, ■■ klient jest pewny, że zarejestrowane przez wykonawcę wymagania są zgodne z jego oczekiwaniami, ■■ współpraca klienta z wykonawcą przekłada się bezpośrednio na pierwszą inżynierską postać systemu, zapoznanie klienta z efektami tej pracy zmniejsza ryzyko ewentualnego niezrozumienia pomiędzy pierwszym etapem dyskusji na temat wymagań a projektem samego systemu, ■■ przyjęta metoda komunikacji nie budzi żadnych zastrzeżeń w kontekście jednoznacznego zrozumienia pomiędzy wszystkimi członkami zespołu, efektywność komunikacji potwierdza jej skuteczność, ■■ problem dysponowania odpowiednią ilością czasu pomiędzy osobami wyznaczonymi do przeglądów kolejnych postaci systemu rozwiązuje precyzyjna ewolucja systemu, co w rezultacie minimalizuje czas i trud związany z akceptacją.

Bliższe szczegóły dotyczące usług analitycznych i szkoleń na stronie Portalu www.uml.com.pl

Artur Machura, właściciel portalu: www.uml.com.pl

artur.machura@uml.com.pl kom. 501-176-256

Portal Inżynierów Oprogramowania www.uml.com.pl, istnieje od 2006 roku. Misją portalu jest dyskusja i promowanie praktyk Inżynierii Oprogramowania. Skupia wielotysięczną grupę zawodową – kierowników projektów i produktów, analityków biznesowych i systemowych, projektantów, programistów, testerów i innych pozostałych związanych z produkcją oprogramowania.

52

/ 1 . 2012 . (1)  /


PRAKTYCZNE WYKORZYSTANIE METOD ZDEFINIOWANYCH W RUP

tieto.pl

Oczekujesz na zwrot w karierze? Teraz Twoja kolej na sukces. Dzięki Tieto, jeden z największych operatorów telefonii komórkowej w Europie zwiększyłpoziom swoich usług o 25% w zaledwie sześć miesięcy. Pracując w Tieto, masz realny wpływ na życie ludzi. Również Twoje życie. Jesteś gotów stawiać wyzwania i stawiać czoła wyzwaniom?

Dowiedz się więcej na tieto.pl/kariera Tieto zatrudnia około 18 000 ekspertów w blisko 30 krajach na całym świecie. Ponad 1200 osób to specjaliści pracujący w Polsce. Pasja, z którą świadczymy naszym klientom usługi- IT, R&D i doradcze- przynosi realne zmiany w branżach takich, jak: telekomunikacyjna, finansowa, energetyczna czy motoryzacyjna.

Knowledge. Passion. Results.


KLUB LIDERA - IT Michał Bartyzel, Mariusz Sieraczkiewicz

Ewolucyjna architektura

Jak zorganizować proces rozwoju architektury? Kłopot z architekturą oprogramowania polega na tym, że na poziomie sformułowania jest to chwytne hasło, lecz gdy przejdziemy do konkretów, to właściwie nie wiadomo, o czym warto w tym temacie mówić. O wzorcach? Jeśli tak, to na jakim poziomie szczegółowości? Czy o szczegółach technologicznych? Czy wspominać o procesie tworzenia architektury?

P

onieważ to, czym jest architektura, bywa różnie rozumiane, zaczynamy mieć wobec architektury pewne oczekiwania, co do tego, co powinna nam dać wiedza na jej temat. W zależności od organizacji, skali projektów, technologii, występujących problemów oczekiwania mogą mieć różne odcienie. Skoncentrujmy się na tych, które występują najczęściej, żeby nie powiedzieć zawsze, niezależnie od szczegółowych niuansów.

ŻEBY BYŁO SZYBKO W wielu organizacjach praca nad oprogramowaniem jest raczej kontynuowana, niż rozpoczynana od nowa. Zaszłości architektoniczne nie są dziełem tego konkretnego zespołu, lecz spadkiem, którego nie można się zrzec. Dodatkowo, w przypadku dużych systemów zespół stopniowo przejmuje za nie odpowiedzialność, stopniowo poznaje, z czym ma do czynienia. Musi upłynąć nieco czasu – kilka lat – aby ludzie zdobyli globalny ogląd na architekturę oraz zaczęli generować pomysły na zmiany. W szybkozmiennej rzeczywistości, przy napiętych terminach, gdy oprogramowanie musi dostarczać coraz to nowe funkcjonalności, zadania związane z architekturą mimowolnie schodzą na dalszy plan. Stąd oczekujemy, że wiedza na temat rozwiązań architektonicznych pomoże szybko zaradzić naglącym problemom. W ten sposób traktujemy prace nad architekturą jako coś, co powinno dziać się w międzyczasie, gdzieś na boku, a właściwie to nie wiadomo kiedy i gdzie. Architektura oprogramowania zaczyna jawić się jako problem, którego rozwiązanie dotąd można odraczać, dopóki nie objawi się jakimiś przykrymi konsekwencjami. Nie wszystkie problemy, w które zaangażowana jest architektura, da się rozwiązać szybko. Skoro doprowadzenie systemu do stanu, w jakim się znajduje, zajęło wiele wysiłku i czasu, to poprawienie błędów również musi kosztować. Może nie jest to ten sam koszt, lecz z pewnością jakiś będzie.

EWOLUCYJNIE, NIE REWOLUCYJNIE Oczekujemy, że poprawa architektury będzie stopniowo postępować. Mało kto może sobie pozwolić na nagłą przerwę w projekcie i beztroskie zagłębienie się w przyjemnej

54

/ 1 . 2012 . (1)  /

refaktoryzacji. Stąd architektura powinna ewoluować. Co to znaczy? Naszym zdaniem oznacza to, że architektura oprogramowania dojrzewa wraz z dojrzewaniem funkcjonalności systemu. Bywa, że wymagające funkcjonalności oparte są na rozwiązaniach architektonicznych, które z trudnością są w stanie im sprostać. Oczywiście zdarza się również przerost formy nad treścią i architektury-potwory, które są zbyt rozbudowane jak na potrzeby systemu. Ciekawe, że formułując to oczekiwanie, zapominamy, że wspomniana ewolucja wymaga również pewnego nakładu prac.

DUŻO MAGICZNYCH SZTUCZEK Oczekujemy, że rozwiązania architektoniczne zadziałają na zasadzie magicznych sztuczek. Zastosujemy jakiś sprytny trik, który rozwiąże większość problemów. Właściwie to najlepiej, żeby zbyt wiele nie zmieniać, tylko lekko zmodyfikować tu i tam i już wszystko będzie dobrze. Niestety jeśli chodzi o architekturę, to repertuar magii jest dość ograniczony. Nie ma eliksirów i zaklęć, które bezkosztowo zaradzą wszelkim problemom. Powyższe oczekiwania można sprowadzić do wspólnego mianownika. Rysuje się z nich obrazek, w którym niemal bez wysiłku dokładamy tu i ówdzie parę drobiazgów i system zaczyna działać jak należy. Od czasu do czasu rzeczywiście tak bywa, lecz jest to raczej kwestia wyją­ tkowego zbiegu okoliczności niż reguły. Jeśli architektura jest wadliwa, to nie da się jej poprawić poprzez dołożenie nowych rzeczy. Zazwyczaj trzeba ją zmienić. Nie wywrócić do góry nogami, lecz zmodyfikować, czasem nieznaczenie, ale jednak.

OBAWY W odniesieniu do architektury pojawia się również kilka obaw, które utrudniają nam jej rozwijanie.

Jak się do tego zabrać Bardzo często nie wiadomo od czego właściwie zacząć poprawianie architektury. Wszystkie osoby pracujące nad kodem wiedzą dokładnie, co jest z nim nie tak, widzą poszczególne problemy i najczęściej wiedzą, jak je rozwiązać. Nie wiedzą jednak, jak się do tego zabrać z perspektywy


EWOLUCYJNA ARCHITEKTURA

całości systemu. Pytania, które sobie zazwyczaj zadajemy, to: ■■ Czym się zająć w pierwszej kolejności? ■■ Jak wybrać między alternatywnymi rozwiązaniami problemu? ■■ Kto to powinien zająć się poprawianiem architektury? ■■ Jak zorganizować prace? ■■ Jak się upewnić, że niczego nie popsułem? ■■ Czy mam wystarczającą wiedzę, aby przeanalizować wpływ zmian? Można zaryzykować stwierdzenie, że jedną z przyczyn wskazanych niewiadomych jest brak globalnego spojrzenia na architekturę systemu. Drugą przyczyną jest brak wspólnego rozumienia architektury. Może się to wydawać zaskakujące, ale gdy przeprowadzamy wywiad z grupą doświadczonych programistów lub architektów, to bardzo często każda z osób ma swoją własną interpretację architektury systemu, który tworzy, oraz swoją własną wizję, w jaki sposób ta architektura powinna się rozwijać.

Nie wiem, czy mi wolno Inna obawa, z powodu której nikt nie dotyka architektury, wynika z tego, że właściwie to nie wiadomo, czy wolno się tym zająć. W związku z tym pojawiają się następujące pytania: ■■ Czy to jest właściwy moment, żeby się tym zająć? ■■ Ile czasu zajmą zmiany? ■■ Czy mogę to zrobić samodzielnie, czy powinienem zapytać o zgodę? ■■ Kogo powinienem zapytać o zgodę? ■■ Na co będą miały wpływ moje zmiany i kogo o nich poinformować? ■■ Czy mam wystarczającą wiedzę, żeby się tym zająć? ■■ Jak przekonać moich zwierzchników, że warto poprawić architekturę? Pytanie „Czy wolno mi zmodyfikować architekturę?” wybrzmiewa silniej w organizacjach, które są podwykonawcami dla zachodnich partnerów. Przejmują one odpowiedzialność za część systemu, a praktycznie wcale jej nie przejmują. Nie mogą znacząco zmodyfikować kodu, bez zgody zleceniodawcy. Kłopot jest poważny, ponieważ programista, który na co dzień pracuje z kodem i boryka się z konkretnymi problemami, nie może zmienić architektury, bez pozwolenia kogoś, kto kodu tak dobrze nie zna lub pracował nad nim lata temu.

Czy to jest moja sprawa? Programista stykający się z konkretnym problemem projektowym w kodzie zadaje sobie zazwyczaj pytanie: Czy to jest moja sprawa? Czy ja powinienem się tym zająć? Mimo powszechnego mówienia o współwłasności kodu, praktyczne implementacje tej zasady pozostawiają wiele do życzenia. Programista stający w obliczu zmiany w architekturze oprogramowania nie ma poczucia, że to jest jego

odpowiedzialność i że to właśnie on powinien się tym zająć. Siłą rzeczy nasuwa się oczywista przyczyna tego typu wątpliwości. Programista rozliczany jest za efekt w postaci nowej funkcjonalności lub naprawionego błędu. Jeśli przy okazji uda mu się poprawić architekturę, to chwała mu za to – wdzięczni będą co najwyżej koledzy borykający się z tym problemem. Jeśli jednak coś popsuje albo opóźni się z zadaniem, to spotka się z reprymendą. Summa summarum – angażowanie się w ulepszanie architektury jest nieopłacalne. Co bardziej zdeterminowani przemycają prace nad poprawą kodu w postaci odpowiedniej ilości czasu doliczonego do estymacji zadania. Druga przyczyna związana z brakiem poczucia odpowiedzialności za poprawę architektury leży w tym, że taka odpowiedzialność nigdy nie była zdefiniowana. W zespole są wskazani liderzy, testerzy, programiści przydzieleni do komponentów, wiadomo jak zgłaszać błędy i kontaktować się z klientem, jak budować i wdrażać wersję systemu. Nie ma jednak zdefiniowanego procesu rozwoju architektury. Skoro brak jest nazwanego procesu, to również brak wydzielonych odpowiedzialności. Skutek jest taki, że czas potrzebny na poprawki w architekturze jest upychany gdzieś między zadania albo wręcz wydzierany z zasobów projektu.

MODELE ORGANIZACYJNE Trywialny model organizacyjny dedykowany rozwojowi architektury polega na tym..., że go nie ma. Kod źródłowy został kiedyś tam napisany, a teraz kilka lub kilkadziesiąt osób go rozwija. W najgorszym przypadku, a takie też bywają, architektura zainicjowana na samym początku ostaje się w niezmienionej formie. Co jakiś czas tylko pojawia się poważny problem, który przy odrobinie szczęścia udaje się załatać. Taki stan rzeczy skutkuje bardzo silnym specjalizowaniem się programistów. Konkretne osoby stają się właściwie niezbędne do wykonania określonych prac, gdyż tylko one potrafią odnaleźć się w gąszczu workaroundów. W niektórych zespołach spośród najbardziej doświadczonych programistów wyłania się (samoczynnie albo z nadania) lider, który dba o rozwój systemu jako spójnej całości. Do pewnego momentu (zespół ok. 12 osób) tacy liderzy są jeszcze w stanie ogarnąć cały system, potem potrzeba już nieco specjalizacji.

Większe struktury Im większy zakres projektów, tym większą tendencję do rozbudowy mają struktury organizacyjne. Dotyczy to również osób czuwających nad architekturą systemów. Zanim wnikniemy w temat głębiej, kilka słów o tym, kim jest architekt i co należy do jego odpowiedzialności. Jeśli chodzi o konkretne obowiązki, rozumienie tego stanowiska jest różne w różnych organizacjach. Postarajmy więc tę różnorodność uporządkować, aby rzucić na nią nieco światła. Najbardziej ogólny podział definiowany przez samych architektów (osoby mające w nazwie stanowiska słowo „architekt” lub „architect”) to podział na: ■■ Architekta korporacyjnego (ang. Enterprise Architect); / www.programistamag.pl /

55


KLUB LIDERA - IT ■■ Architekta aplikacji (ang. Software Architect). Architekt korporacyjny zajmuje się całością infrastruktury programowej w całej organizacji. Zastanawia się nad tym, jak zorganizować współpracę poszczególnych systemów ze sobą, aby wspierały działania organizacji. Wśród zadań architekta korporacyjnego można wymienić: ■■ przygotowanie mapy systemów dla organizacji; ■■ opracowanie infrastruktury programowej organizacji; ■■ oszacow. kosztu usunięcia/dodania systemu do/z infrastruktury; ■■ opracowanie polityki wdrożeń systemów. Architekt korporacyjny musi bardzo dokładnie znać specyfikę danej organizacji. Jego rozwiązania są zazwyczaj „szyte na miarę” konkretnych potrzeb. Architekt aplikacji zajmuje się wewnętrzną strukturą pojedynczego systemu. Typowe zadania architekta aplikacji to: ■■ dobranie technologii do wymagań funkcjonalnych; ■■ zaprojektowanie sposoby działania, przechowywania i prezentacji danych w systemie; ■■ opracowanie sposobu egzekwowania zasad bezpieczeństwa przez system; Można się spotkać z opiniami, że „tak naprawdę” architektem jest architekt korporacyjny, natomiast architekt aplikacji bywa z pejoratywnym wydźwiękiem nazywanym projektantem (ang. desingner). Jest to oczywista nieprawda – nie ma czegoś takiego jak „prawdziwy architekt”. Obowiązki architekta zależą przede wszystkim od potrzeb organizacji. Wraz ze zwiększaniem się skali działania może pojawić się stanowisko Głównego architekta (ang. Chief Enterprise Architect). Jest on zwierzchnikiem architektów korporacyjnych i dba o to, aby wdrażanie konkretnych rozwiązań dotyczących infrastruktury programowej odpowiadało procesom biznesowym w organizacji i przynosiło możliwie dużo korzyści. W przypadku dużych systemów składających się z podsystemów, pojawia się Architekt funkcjonalny (ang. Functio­nal Software Architect). Łączy on w sobie odpowiedzialności Analityka biznesowego, Analityka funkcjonalnego i menadżera. Typowe zadania architekta funkcjonalnego to: ■■ p procesu biznesowego i zdefiniowanie funkcjonalności, do których powinni mieć dostęp użytkownicy; ■■ określenie, w których miejscach/systemach powinny zostać zaimplementowane określone funkcjonalności. Pojedynczym systemem zajmuje się już wspomniany wcześniej architekt aplikacji. Czasem nazywany jest również Architektem systemowym, Architektem technicznym (ang. Technical architect) albo Architektem domenowym (ang. Domain Architect). Niektóre typologie definiują zu-

56

/ 1 . 2012 . (1)  /

pełnie różną odpowiedzialność dla wymienionych ról, my bazujemy na podobieństwach w zakresie obowiązków architektów, z którymi mieliśmy przyjemność współpracować.

STANOWISKO ARCHITEKT Wydzielone stanowisko architekta prowadzi do sytuacji, w której jedna osoba przygotowuje strukturę systemu, natomiast inna ma mu ją zakodować. Powoduje to, że odpowiedzialność myślenia o architekturze ucieka od programistów w stronę architektów. Może za bardzo zasugerowaliśmy się metaforami z zakresu budownictwa, gdzie architekt opracowuje koncept, inżynier projekt, a robotnicy po prostu robią swoje. W przypadku systemów informatycznych to nie działa. Jest istotna różnica między budownictwem a oprogramowaniem. W przypadku budownictwa sposób korzystania z budynku, jego przeznaczenie, użytkownicy, kształt nie zmieniają się w takim zakresie i tak często jak w przypadku systemów informatycznych. Ta zmienność powoduje, że jeśli programiści tworzący kod wyrugowani są z tworzenia architektury, to architektura prawie na pewno będzie nieodpowiednia. Architekci działający w takim układzie nazywani są PowerPoint architects [LarmanVodde]. Być może są w stanie opracować wstępny zrąb systemu, lecz gdy dalej wciąż nie będą dotykać kodu, ich projekty staną się wytworami czystej fantazji, w żaden sposób nie związanymi z rzeczywistością kodu.

Lider architektury Za architekturę systemu powinien czuć się odpowiedzialny każdy, kto pracuje nad kodem. Tworzenie stanowiska Architekt sprzyja sztucznemu oddzielaniu tworzenia kodu od troski o jego poprawną strukturę oraz zniechęca programistów do kreatywnego myślenia. Zamiast wprowadzania stanowisk proponuje się [Leffingwell, LarmanVodde, Ambler] role osób troszczących się o architekturę. Naszym zdaniem odpowiednią nazwą dla tej roli jest Lider architektury, gdyż nazwa ta nie sugeruje, kto jest odpowiedzialny za architekturę, lecz kto przewodzi jej rozwojowi. Do zadań lidera architektury należy: ■■ zbieranie informacji o problemach w architekturze, z jakimi spotykają się programiści, oraz pomysłów na ich rozwiązanie; ■■ edukowanie programistów w zakresie architektury; ■■ przeprowadzanie przeglądów kodu (ang. code review); ■■ inicjowanie zmian w architekturze; ■■ opracowywanie dokumentacji HLD i wytycznych pracy dla zespołu. W przypadku dużych systemów każdy podsystem może mieć swojego lidera. W tym przypadku ważną działalnością Grupy liderów architektury jest dbanie o regularny przepływ informacji, podejmowanie decyzji dotyczących systemu jako całości, a następnie propagowanie zmian w swoich obszarach odpowiedzialności. Najbardziej efektywnym sposobem wymiany wiedzy w tej grupie są regularne spotkania.


EWOLUCYJNA ARCHITEKTURA

Stworzenie takiej „żyjącej” grupy liderów architektury skutkuje następującymi korzyściami: ■■ istnieje spójne rozumienie architektury systemu jako całości; ■■ istnieje jednoznaczna wizja rozwoju architektury; ■■ utrzymywana jest aktualna (i minimalistyczna) dokumentacja HLD; ■■ wytworzona wiedza pozostaje w organizacji.

Stanowisko może mieć znaczenie Z pewnych powodów w organizacjach, w których rozpoczyna się proces myślenia o rozwoju architektury, programiści sami dążą do powstania stanowiska Architekta. Wśród tych powodów można wymienić: ■■ możliwość awansu na inne stanowisko; ■■ stanowisko architekta jest uznawane przez programistów za bardziej prestiżowe niż stanowisko programisty; ■■ zwiększenie atrakcyjności własnego CV. Opisany problem można zaadresować następująco: W jaki sposób nieformalną rolę lidera architektury powiązać z prestiżem dla programisty? Rozwiązanie tej kwestii wymaga pewnych zabiegów na poziomie organizacji. Jako

przykładowe rozwiązania można podać: ■■ Wprowadzenie listów gratulacyjnych za konkretne osiągnięcia; ■■ Wprowadzenie systemu certyfikacji wewnętrznej; ■■ Wprowadzenie kultury zdobywania sprawności na zasadzie Black Belt Factory.

PROCES EWOLUCJI ARCHITEKTURY Na początku artykułu wspominaliśmy, że jeśli jakiś proces nie został nazwany w organizacji, to prawdopodobnie nie będzie funkcjonował. Właściwie mamy do czynienia z dwoma procesami. Jeden definiuje sposób zarządzania ewolucją architektury, drugi określa sposób pracy zespołu projektowego. Odpowiedzialnością za oba procesy obarczeni są oczywiście liderzy architektury.

Zarządzanie ewolucją architektury Proces ten (Rysunek 1) oparty jest na metodzie Kanban [Leffingwell]. Proces składa się z czterech podstawowych etapów: 1. 2. 3. 4.

Kolekcjonowanie pomysłów (ang. epic) w Koszyku; Definiowanie Rejestru (ang. backlog); Analizy pozycji Rejestru; Implementacji przeanalizowanych pozycji rejestru.

Rysunek 1. Zarządzanie ewolucją architektury

/ www.programistamag.pl /

57


KLUB LIDERA - IT

Rysunek 2. Proces pracy zespołu Faza kolekcjonowania to po prostu zbieranie wszystkich potrzeb i pomysłów związanych ze zmianą architektury. Najlepszym źródłem tych pomysłów są programiści, gdyż to właśnie oni pracują z kodem na co dzień i znają go najlepiej. Pomysły mają formę luźnych zdań, np.: ■■ Zamienić Hibernate na myBatis; ■■ Wprowadzić loadbalancing; ■■ Usunąć nieużywane funkcjonalności. Każdy taki pomysł musi zostać oceniony przez liderów architektury, pod następującymi kątami: ■■ Jaka wartość biznesowa zostanie dostarczona klientowi? ■■ Jak duży jest wpływ na poprawę architektury? ■■ Jaki jest koszt wprowadzenia tego pomysłu? Po pozytywnym przejściu oceny pomysł musi zostać podzielony na pozycje w Rejestrze. Na podstawie wspomnianej oceny każda pozycja otrzymuje swój priorytet, a sam Rejestr jest zawsze uporządkowany od najwyższego (najważniejszego) do najniższego priorytetu. Pozycje ze szczytu rejestru trafiają do określonych osób z zespołu do analizy. Działania podejmowanie na tym etapie to: ■■ doprecyzowanie wymagań technicznych związanych z daną pozycją; ■■ określenie alternatyw do autorskiej implementacji; ■■ określenie wpływu na poszczególne komponenty systemu; ■■ określenie zasobów niezbędnych do zaimplementowania danej pozycji; ■■ przeprowadzenie próbnej implementacji nietypowych pomysłów (ang. proof of concept); ■■ opracowanie założeń i zarysu projektu niezbędnych do zaimplementowania danej pozycji.

58

/ 1 . 2012 . (1)  /

Na każdym z wymienionych etapów pomysł może zostać zarzucony. Jeśli tak się nie stanie, to ostatecznie trafia on do realizacji.

Praca zespołu Proces (Rysunek 1) definiujący pracę zespołu [Leffingwell, Ambler] oparty jest na iteracyjnym modelu wytwarzania oprogramowania w duchu Agile. Każde wydanie rozpoczyna się Iteracją „0” inicjującą, w trakcie której następuje wstępne modelowanie systemu. W przeciwieństwie do tradycyjnych podejść, nie chodzi tu to kompleksowy projekt architektury „od początku do końca”, lecz o wybranie najodpowiedniejszego pomysłu na architekturę dla danego wydania. Efektem Iteracji „0” są: ■■ wspólne (zespołowe) rozumienie architektury systemu; ■■ szkice docelowej architektury; ■■ rozpoznanie potencjalnych problemów i zagadnień do opracowania. Nie chodzi zatem o szczegółowy dokument, lecz szczegółowy pomysł na poradzenie sobie z konkretnym zaga­ dnieniem. Po zakończeniu iteracji dodających żądane funkcjonalności następuje dodatkowa krótsza Iteracja zakańczająca (ang. Hardening Iteration), której celem jest zredukowanie długu technicznego występującego w kodzie. Działania podejmowane w tej iteracji to: ■■ implementowanie pozycji z Rejestru (dot. architektury), dla których nie było miejsca w standardowych iteracjach; ■■ poprawianie czytelności kodu; ■■ doprowadzenie projektu do stanu, w którym spełnia on przyjęte normy jakości.


TYTUŁ

Dodatkowa krótka iteracja po wydaniu wersji nosi nazwę Iteracji innowacji (ang. Hackathon Iteration). W trakcie tej iteracji każdy członek zespołu może eksplorować dowolną technologię pod kątem użyteczności dla organizacji. Iteracja innowacji spełnia następujące ważne zadania: ■■ stanowi odetchnięcie dla zespołu po poprzednich iteracjach; ■■ jest źródłem nowych pomysłów do wspomnianego Rejestru; ■■ pomaga programistom być „na czasie” z technologiami, co poprawia motywację.

WDRAŻANIE Opisane procesy wydają się nieskomplikowane, jednak mogą nastręczać pewne trudności. Samo zagadnienie wdrażania jest tematem na oddzielny artykuł, dlatego ograniczymy się tutaj do kilku wskazówek.

Konsekwencja

Procesy adaptacyjne narzucają niewielką ilość prostych zasad, natomiast wymagają rygorystycznego ich przestrzegania. W rzeczywistości projektu, w obliczu nieubłaganie nadchodzących terminów, istnieje duża pokusa zrezygnowania z jednej rozmowy, spotkania, zasady postępowania. Potem można zrezygnować z następnej, potem z kolejnej, aż po pewnym czasie wracamy znów do punktu wyjścia. Pamiętajmy, że to, jak w przyszłości będzie funkcjonował zespół, zależy od tego, co robimy dziś.

Michał Bartyzel, Mariusz Sieraczkiewicz

Literatura:

■■ Agile Software Requirements, D. Leffingwell, ■■ Practices for Scaling Lean & Agile Development, C. Larman, B. Vodde, ■■ Patterns of Agile Practice Adoption, Amr Elssamadisy, ■■ http://agilemodeling.com, ■■ Materiały szkoleniowe BNS IT.

Zmiana ma swoją dynamikę Nigdy nie jest tak, że wprowadzając jakąś zmianę w działaniu zespołu, otrzymujemy od razu stan pożądany. Pomiędzy stanem wyjściowym a docelowym występuje okres adaptacji, w trakcie którego mamy poczucie, że dzieje się gorzej. Jak długo będzie trwał ten etap? Nie sposób stwierdzić arbitralnie, gdyż jest zbyt zależne od kontekstu. Dopiero po przejściu okresu adaptacji, następuje ukonstytuowanie się nowych zasad działania.

Katalizatory zmiany Skuteczne wdrożenie nowych procesów wymaga nieustannej troski. W organizacji potrzebni są ludzie, którzy zachwycą się nowymi ideami i staną się katalizatorami zmiany. Jeśli nikt nie będzie się troszczył o utrzymanie powziętych postanowień, zwłaszcza w okresie adaptacji, to pierwsze sukcesy szybko zostaną zapomniane, a pozyty­ wne zmiany znikną bez echa wskutek postępującego rozmywania odpowiedzialności.

m.bartyzel@bnsit.pl, m.sieraczkiewicz@bnsit.pl

Trenerzy i konsultanci w firmie BNS IT. Badają i rozwijają metody psychologii programowania, 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. reklama

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

59


E–LEARNING

DevCastZone

– nowy wymiar e-edukacji dla programistów E-learning systematycznie staje się coraz bardziej popularną formą zdobywania wiedzy i kwalifikacji. Aż 80% osób korzystających z e-learningu określa go jako bardzo dobrą formę edukacji. Jest on niezwykle popularny wśród studentów i absolwentów uczelni wyższych, ale – jak się również okazuje – zaczyna wkraczać do firm.

E

-learning jest sposobem szkolenia niewymagającym bezpośredniego kontaktu z wykładowcą czy trenerem. Utożsamiany jest głównie z internetową formą nauki, a jednym z najbardziej popularnych obszarów zdalnego kształcenia jest branża IT. Programiści, specjaliści IT bardzo chętnie sięgają po e-szkolenia, gdyż w pewien sposób łączą się one z ich zainteresowaniami i ukierunkowaniem pracy. Wychodząc naprzeciw oczekiwaniom początkujących, jak i doświadczonych programistów, powstała platforma DevCastZone, której patronem medialnym jest magazyn Programista. Platforma w elastyczny sposób łączy ze sobą zalety szkoleń e-learningowych i tradycyjnych szkoleń stacjonarnych.

NOWA ODSŁONA E-LEARNINGU Większość czytelników mogłaby założyć, że nie ma już informacji czy zagadnienia, które nie zostało opracowane i umieszczone w Internecie. Zapewne coś w tym jest, ale czy w dobie nieustannego pośpiechu, braku czasu i nawału obowiązków nie lepiej byłoby otrzymać interesującą nas treść szybko i w skonsolidowanej formie? Z tego właśnie założenia zrodził się pomysł skondensowanych szkoleń dla programistów w postaci devcastów – szkoleń e-learningowych, które łączą zalety kształcenia zdalnego i tradycyjnego. Devcast to wykład wideo prowadzony przez trenera połączony z częścią praktyczną (screencastem). Uzupełnieniem są dodatkowe materiały elektroniczne, tj. ćwiczenia z rozwiązaniami, skrypt i prezentacja. Uczestnicy devcastów mają również możliwość sprawdzenia swojej wiedzy, rozwiązując test i uzyskując certyfikat ukończenia e-szkolenia.

ZAPOTRZEBOWANIE NA WIEDZĘ Wykfalifikowany personel firmy jest jednym z głównym gwarantów sukcesu przedsiębiorstwa. W czasach kiedy technologia i standardy z nią związane zmieniają się każdego dnia, konieczne jest utrzymanie ciągłego rozwoju kadry pracowniczej. Zapotrzebowanie na szkolenia dla programistów jest ogromne. Na typowe szkolenia stacjonarne kosztujące średnio 3,000zł za uczestnika pozwolić sobie mogą jedynie duże firmy i korporacje. Firmy z sektora MŚP zmuszone są do poszukiwania innych rozwiązań i chętnie korzystają ze szkoleń dofinansowanych, których koszt oscyluje w granicach 100200zł. DevCastZone staje się atrakcyjnym odpowiednikiem szkoleń tradycyjnych, eliminując jednocześnie ich wady, w tym najbardziej uciążliwą – wysoką cenę. Devcasty dorównują poziomem przekazywanej wiedzy szkoleniom stacjonarnym, oferują atrakcyjne i kompleksowe tematy, których próżno szukać w dotychczasowej ofercie e-learningowej.

60

/ 1 . 2012 . (1)  /

KORZYŚCI Przewaga e-learningu na tradycyjną formą kształcenia staje się w dzisiejszych czasach coraz bardziej wyraźna. Zwiększa się wykorzystanie urządzeń z mobilnym dostępem do Internetu, co pozwala na dostęp do treści szkoleniowych o każdej porze i w dowolnym miejscu. Znikają problemy związane z logistyką, wyżywieniem, wyszukaniem ośrodka i noclegu, nie ma też konieczności całkowitego odrywania się od codziennych zajęć czy porzucania projektów na trzy lub więcej dni. Zdarza się, że w przypadku tradycyjnej formy edukacji trudno jest zebrać grupę do zorganizowania szkolenia o specjalistycznej tematyce. Jest to również interesujące rozwiązanie dla korporacji chcących szybko i skutecznie przeszkolić dużą grupę nowych pracowników.

DEVCASTY Przed stworzeniem platformy DevCastZone zostało przeprowadzone badanie mające na celu określenie kierunku, w którym serwis powinien zmierzać. Zrodziły się tematy i chara­kter e-szkoleń, ale również elementy i aspekty, które powinny być ich składowymi. Niemal 65% ankietowanych stwierdziło, że taka forma szkoleń programistycznych może być efektywna i chętnie wzięłoby w nich udział. Szkolenia przygotowane są przez doświadczonych programistów, trenerów oraz specjalistów IT. Zasada działania Dev­ CastZone jest niezwykle prosta. Należy wybrać interesujący nas devcast i uzyskać do niego dostęp. W trakcie trwania dev­castu wyświetlane jest nagranie trenera, który podo­ bnie jak w przypadku szkoleń tradycyjnych prowadzi wykład wzbogacony o prezentację. Po omówieniu zagadnienia teoretycznego następuje przedstawienie ćwiczeń i przykładów w środowisku programistycznym. Dodatkowo dostępne są materiały (prezentacja, skrypt, ćwiczenia), które służą zunifikowaniu zdobytej wiedzy. Każdy devcast podzielony jest na moduły szkoleniowe, dzięki czemu nie ma konieczności kupowania treści, która jest znana lub po prostu nas nie interesuje.

ROZWIĄZANIE DLA DUŻYCH FIRM Duże firmy decydują się na kształcenie w tradycyjny sposób, gdyż nie mają alternatyw w postaci dobrej jakości szkoleń e-learningowych. DevCastZone dostarcza mechanizm ułatwiający organizowanie e-szkoleń dużym grupom pracowniczym. Wystarczy przy rejestracji wybrać konto o charakterze biznesowym. Pozwoli to na kupowanie wielu devcastów i zapraszanie do udziału w nich swoich pracowników.


NOWY WYMIAR E-EDUUAAAI DLA PROGRAMISTÓW

Włąąz ssę! Deeeasss o nnowaaaane e-szzooenna dda ppoooammssów, a w nnnh: wwwładd eneeów w oomme wwdeo waaszzaaa ppzzzładd w oomme ssseennassów maaeeeałł szzooennowe do pobbanna ppezennaaae ćwwwzenna z ozwwązannamm eeapamm h ossąąanna tesss sppawdzaaąąe wwedzę seeeeeeeaaa ęeeeeeeeaaa WWdeo e-szzooenna Deeeasssone Dowoone mmeesse, pooa zas

TTeśśś ze szzooeń ssaaaonaannnh

Nowe eehnoooooe

Możżżwość powwóózeń

NNewweeee oszz

Weeeeeeaaaa wwedzz

PPzeszzooenne nowwwh złonnów zespołu

Dobbeeanne eeemennów szzooeń

sappaszamm na www.deeeasszone..om Uwaaa! DDa wszzsssssh zzzeennnów PPoooammsss deeeass

Wppowadzenne do

eehnoooooo FFex

w ppomoooonee enne 23,20zł! PPzz zaauppe podaa od: programista_3105 wod ważnn do 31.05.2012

www.deeeaazone..om

www..aaebooo..om/deeeasszone / www.programistamag.pl /

61


IT BOYZ Estetyka informatyka

IT Boyz. Praca wre

62

/ 1 . 2012 . (1)  /


FORMULARZ ZAMÓWIENIA

Prenumerata »» 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.

FORMY PRENUMERATY

Magazyn Programista jest wydawany: »» w wersji drukowanej, »» wersji elektronicznej.

Zamówienie można złożyć poprzez formularz na: http://programistamag.pl/infosite/register lub wysyłając skan poniższego formularza na adres: prenumerata@programistamag.pl

RODZAJ PRENUMERATY

CZAS TRWANIA PRENUMERATY

CENA BRUTTO

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 adres wysyłki

Imię i nazwisko:

ZAMAWIAM PRENUMERATĘ OD NUMERU:

ZAMAWIAM (ZAZNACZ POLE)

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



Issuu converts static files into: digital portfolios, online yearbooks, online catalogs, digital photo albums and more. Sign up and create your flipbook.