Programista 06 2014 iNTERnet

background image

6

/

2014

(

25

)

www

programistamag

pl

Cena 22.90 zł (w tym VAT 8%)

Index: 285358

BUDOWA OPROGRAMOWANIA DO ANALIZY I PRZETWARZANIA TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH

Swift od Apple

.NET Micro Framework

Gerrit Code Review

Nowoczesny język

programowania

dla systemów iOS

oraz OS X

Programowanie

firmware dla

urządzenia STM32F4

Discovery

Jak poprawić

efektywność przy

przeglądach kodów

źródłowych

Prototyp gry sieciowej

w Unity 3D

background image

DOMENY | E-MAIL | HOSTING | STRONY WWW

| SERWERY

* Ceny bez VAT (23%). 1&1 Serwer o12A-32 w cenie 349,90 zł/mies. w umowie na 24 miesiące. 1&1 Serwer L2 w cenie 79,90 zł/mies. w umowie na 24 miesiące.

Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin dostępne na www.1and1.pl

1and1.pl

22 116 27 77

SPRAWDŹ TEŻ
1&1 SERWER L2

OKAZJA!

AMD Opteron

TM

6338P

12 rdzeni

2,3 GHz

32 GB lub 64 GB pamięci

2 x 2 TB HDD (software

RAID) lub 2 x 4 TB HDD
(hardware RAID)

Pełne wsparcie dla 64-bit

Parallels

®

Plesk

w wersji polskiej

Opcja: 2 x Intel

®

S3500

240 GB SSD za 60 zł/mies.*

NOWOŚĆ!

Serwer dedykowany o12A-32: najnowszy procesor AMD Opteron™ 6338P,
dysk 2 x 2 TB software RAID oraz w 100% serwerowe podzespoły.

210 x 297 + 5 mm

LEPIEJ, PEWNIEJ

SZYBCIEJ!

Z AMD OPTERON

TM

6338P

349,

90

zł/mies.*

już od

79,

90

zł/mies.*

już od

TELEFON

PORADA

SPECJALISTY

MIESIĄC

30 DNI

NA PRÓBĘ

MAPPL1406S1P_210x297+5_KB_39L300.indd 1

02.06.14 10:25

background image

SPIS TREŚCI

/

EDYTORIAL

BIBLIOTEKI I NARZĘDZIA

Podstawy WPF, część 4 – MVVM................................................................................................

Wojciech Sura, Damian Jarosch

4

Projektowanie komponentów wizualnych. Część 1: Wstęp...................................................

Wojciech Sura

8

Automatyzacja za pomocą GruntJS............................................................................................

Kacper Cyran

12

JĘZYKI PROGRAMOWANIA

Swift: rewolucja czy ewolucja?.....................................................................................................

Rafał Kocisz

16

PROGRAMOWANIE GRAFIKI

Przetwarzanie geometrii przy pomocy Transform Feedback OpenGL 4.3..........................

Piotr Sydow

22

Budowa oprogramowania do analizy trójwymiarowych obrazów medycznych................

Michał Chlebiej

26

PROGRAMOWANIE GIER

Unity3D – prototyp gry sieciowej................................................................................................

Marek Sawerwain

32

Przewodnik po MonoGame, część 2: komponenty gry...........................................................

Jacek Matulewski

38

TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ

Gerrit Code Review........................................................................................................................

Wojciech Frącz

42

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

.NET Micro Framework. Programowanie firmware dla urządzenia STM32F4 Discovery.....

Dawid Borycki

48

RECENZJA

MegiTeam – hosting od programistów dla programistów.....................................................

Rafał Kocisz

58

WYWIAD

Polacy górą! Relacja z finałów Hello World Open 2014.........................................................

60

PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

IBM Mainframe..............................................................................................................................

62

LABORATORIUM BOTTEGA

Brakujący element Agile Część 5: Spotkania.............................................................................

Paweł Badeński

66

STREFA CTF

Zdobyć flagę… CONFidence DS CTF 2014 – web200.............................................................

Krzysztof "vnd" Katowicz-Kowalewski

70

KLUB LIDERA IT

Jak całkowicie odmienić sposób programowania, używając refaktoryzacji (część 10).....

Mariusz Sieraczkiewicz

74

KLUB DOBREJ KSIĄŻKI

Agile. Szybciej, łatwiej, dokładniej...............................................................................................

Rafał Kocisz

78

Co ma sauna do startupów?

Daleko, daleko od naszego rodzimego kraju, za jednym morzem i zapewne parunastoma
lasami, egzystuje sobie Espoo – miasto położone w Finlandii, nieopodal stolicy. Cóż w nim
takiego szczególnego? Na pewno duże nagromadzenie biurowców należących do korporacji

dobrze znanych dla większości Polaków, ale ten tekst skupi się wyłącznie na jednej saunie.

Sauna ta jest o tyle wyjątkowa, że nie „rozgrzewa” ludzi, lecz powstające w szybkim

tempie mikroprzedsiębiorstwa. W jaki sposób? Otóż w campusie Aalto University stoi duży

murowany budynek podpisany „Startup Sauna”, otwarty dla wszystkich w dni robocze od
8:00 do 17:00.

W środku znajduje się duża hala z tak zwanym „open space” – każdy może po pro-

stu przyjść, zająć wolny segment biurowy i pracować wraz ze swoją drużyną. Organiza-
cja w żaden sposób nie wymaga rejestracji i nie nakłada na użytkowników żadnych opłat.

Koszty związane z utrzymaniem budynku pokrywają wspólnie uniwersytet i fińska agencja
rządowa Tekes. Co ciekawe, była to całkowicie oddolna inicjatywa studentów.

To w Finlandii. A w Polsce? Może wystarczy tylko spróbować? ;)

Magazyn Programista istnieje również dzięki Tobie, drogi czytelniku. Cały czas jeste-

śmy ciekawi Twojej opinii:

http://fb.me/ProgramistaMagazyn

Z poważaniem, Redakcja

Wydawca/ Redaktor naczelny:

Anna Adamczyk

annaadamczyk@programistamag.pl

Redaktor prowadzący:

Łukasz Łopuszański

lukaszlopuszanski@programistamag.pl

Korekta:

Tomasz Łopuszański

Kierownik produkcji:

Krzysztof Kopciowski

bok@keylight.com.pl

DTP:

Krzysztof Kopciowski

Dział reklamy:

reklama@programistamag.pl

tel. +48 663 220 102

tel. +48 604 312 716

Prenumerata:

prenumerata@programistamag.pl

Współpraca:

Michał Bartyzel

Mariusz Sieraczkiewicz

Michał Leszczyński

Marek Sawerwain

Łukasz Mazur

Rafał Kułaga

Sławomir Sobótka

Michał Mac

Gynvael Coldwind

Bartosz Chrabski

Adres wydawcy:

Dereniowa 4/47

02-776 Warszawa

Druk:

Drukarnia Kontakt

ul. Gospodarcza 5a

05-092 Łomianki

Nakład: 5000 egz.

Redakcja zastrzega sobie prawo do skrótów i opracowań

tekstów oraz do zmiany planów wydawniczych, tj. zmian

w zapowiadanych tematach artykułów i terminach

publikacji, a także nakładzie i objętości czasopisma.
O ile nie zaznaczono inaczej, wszelkie prawa do

materiałów i znaków towarowych/firmowych

zamieszczanych na łamach magazynu Programista są

zastrzeżone. Kopiowanie i rozpowszechnianie ich bez

zezwolenia jest Zabronione.
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.

Magazyn Programista wydawany jest

przez Dom Wydawniczy Anna Adamczyk

Zamów prenumeratę magazynu Programista

przez formularz na stronie:

http://programistamag.pl/typy-prenumeraty/

lub zrealizuj ją na podstawie faktury Pro-

forma. W spawie faktur Pro-Forma prosimy

kontktować się z nami drogą mailową:

redakcja@programistamag.pl

.

Prenumerata realizowana jest także

przez RUCH S.A. Zamówienia można

składać bezpośrednio na stronie:

www.prenumerata.ruch.com.pl

Pytania prosimy kierować na adres e-mail:

prenumerata@ruch.com.pl

lub kontaktując

się telefonicznie z numerem: 801 800 803

lub 22 717 59 59, godz.: 7:00 – 18:00 (koszt

połączenia wg taryfy operatora).

background image

4

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

Wojciech Sura, Damian Jarosch

WZORZEC?

Każdy, kto z programowaniem ma już trochę do czynienia, wie o istnieniu
czegoś takiego, jak wzorce projektowe. Wzorzec projektowy nie jest gotową
biblioteką czy listą instrukcji mówiących o tym, w jaki sposób krok po kroku
zaimplementować fragment kodu źródłowego. Jest on raczej zbiorem wska-
zówek, które znajdują zastosowanie w określonej klasie problemów. „Jeżeli
masz pewien specyficzny problem”, mówi taki wzorzec, „mam pewien spraw-
dzony pomysł na to, w jaki sposób możesz podejść do jego rozwiązania”.

Wśród wzorców możemy wyróżnić kilka rodzajów, pomiędzy którymi

znajdują się wzorce strukturalne, behawioralne i architektoniczne. Pierwsze
z nich mówią o tym, w jaki sposób wiązać ze sobą zależne od siebie struktury
danych. Wzorce behawioralne pomagają zaprojektować interakcję pomiędzy
różnymi elementami systemu informatycznego. Ostatnie zaś – architektonicz-
ne – mówią o tym, jak projektować architekturę samej aplikacji. MVVM należy
właśnie do trzeciej kategorii wzorców.

Współcześnie bardzo rzadko pisze się programy całkowicie od zera – prze-

ważnie korzystamy z jakiegoś frameworka, który dostarcza nam pewien zbiór
gotowych rozwiązań, na przykład do wyświetlenia interface'u użytkownika,
współpracy z systemem operacyjnym i tak dalej. Duża część z nich – być może
nawet przeważająca większość - jest neutralna na poziomie architektury apli-
kacji. Chcę przez to powiedzieć, że taki „neutralny” framework pozostawia
programiście całkowitą dowolność w zakresie wyboru wzorca architektonicz-
nego (lub zrezygnowania z takowego i zaprojektowania architektury aplikacji
od zera).

WPF w tym kontekście różni się nieco od innych frameworków. Z jednej

strony nie narzuca on bezpośrednio konkretnego wzorca architektonicznego,
jak na przykład ASP.NET MVC: nic nie stoi na przeszkodzie, by w aplikacji WPF
postawić na formatce przycisk, ustawić mu handler zdarzenia Click i oprogra-
mować w nim logikę aplikacji (co oczywiście samo w sobie jest dosyć marnym
pomysłem). Z drugiej strony jednak WPF bardzo wyraźnie faworyzuje MVVM:
zawiera bowiem szereg mechanizmów, które zostały przygotowane specjal-
nie pod ten właśnie wzorzec. I jeśli mu się nie podporządkujemy od początku,
w pewnym momencie okaże się, że zaimplementowanie niektórych skądinąd
prostych rozwiązań będzie bardzo trudne do zrealizowania.

HISTORIA

W 2004 roku Martin Fowler – specjalista z zakresu architektury oprogramowa-
nia i analizy obiektowej – opublikował na swoim blogu artykuł o wzorcu, który
nazwał Presentation Model (PM). Martin pisze na swoim blogu:

GUI składają się zwykle z widgetów, które wyświetlają stan pojedynczego

ekranu GUI. Jednak pozostawianie tego stanu w widgetach powoduje, że trudniej

jest go w danym momencie pobrać, gdyż wiąże się to z manipulowaniem API tych
widgetów, a ponadto zachęca do implementowania logiki
prezentacji w klasie
widoku
.

Aby zapobiec takim sytuacjom, Martin zaproponował wprowadzenie po-

działu architektury aplikacji na trzy części: przechowujący wyświetlane dane
model, widok, którego zadaniem jest prezentacja tych danych (widokiem jest
na przykład okno), oraz Presentation Model, który pobiera dane z modelu,
przygotowuje je do wyświetlenia, a następnie przekazuje widokowi.

Skrót MVVM ujrzał światło dzienne niedługo później. W 2005 roku John

Grossman, będący obecnie jednym z architektów WPF i Silverlighta opubliko-
wał na blogu wpis o wzorcu Model-View-ViewModel. Wzorzec ów u podstaw
był bardzo podobny do pomysłu Martina Fowlera: oba opierają się o dostar-
czenie klasy będącej abstrakcją widoku, przechowującej jego aktualny stan
oraz zawierającej odpowiednią logikę. Różnica polega jednak na tym, że o ile
Presentation Model jest wzorcem uogólnionym – można go zastosować w
dowolnym programie, niezależnie od języka czy platformy – to MVVM został
przygotowany specjalnie dla WPFa: zakłada on bowiem jawnie korzystanie z
niektórych elementów tego frameworka.

ZASADA TRÓJPODZIAŁU

MVVM zakłada podział architektury aplikacji na trzy części.

Pierwszą z nich jest model, którego zadaniem jest tylko i wyłącznie prze-

chowywanie prezentowanych później użytkownikowi danych. Modelem
może być więc na przykład lista, słownik albo nawet zwykła, bardziej lub
mniej skomplikowana klasa przygotowana przez programistę.

Podczas projektowania modelu należy mieć na uwadze, iż jest on jedynie

kontenerem. Jeśli więc znajdzie się w nim jakakolwiek logika, powinna ona
dotyczyć tylko i wyłącznie przechowywania danych: na przykład metody za-
pewniające spójność danych, albo zapisujące lub odczytujące te dane z dys-
ku. W myśl wzorca MVVM model nie powinien zawierać żadnej logiki, która
przechowywane dane w jakikolwiek sposób przetwarza.

Drugim elementem jest widok. Ten z kolei odpowiedzialny jest za zapre-

zentowanie danych użytkownikowi. Widokiem może być więc na przykład
okno wraz z zestawem odpowiednich kontrolek.

Pozostał nam jeszcze ostatni fragment układanki – viewmodel. Nazwa,

choć może mało finezyjna, tłumaczy jednak doskonale, jakie jest jego zada-
nie: wiąże on model z widokiem, dostarczając temu ostatniemu danych z mo-
delu w postaci, która będzie mogła zostać zaprezentowana użytkownikowi.

MVVM zakłada bardzo ścisłe zasady enkapsulacji pomiędzy tymi trzema

warstwami.

Model nie jest świadom istnienia żadnego z pozostałych elementów. Jego

zadaniem jest tylko przechowywanie danych i do tego się ogranicza.

Podstawy WPF, część 4 – MVVM

Windows Presentation Foundation wynosi projektowanie interface'u użytkownika
na zupełnie nowy poziom. Dzieje się tak w dużej mierze za przyczyną języka XAML,
który bez żadnych dodatkowych bibliotek czy rozszerzeń pozwala na opisanie wy-
glądu okien i stron, przygotowanie tematów graficznych dla aplikacji, wprowadze-
nie animacji, stylowanie elementów wyświetlanych w listach, czy wręcz zaprojek-
towanie własnych kontrolek praktycznie od zera. A to wszystko deklaratywnie, bez
napisania pojedynczej linijki kodu. Kiedy jednak mamy już przygotowany interface
użytkownika, trzeba zakasać rękawy i zacząć go oprogramowywać. W tej części
podstaw WPFa chcemy więc opowiedzieć o wzorcu, który jest w tym frameworku
promowany w szczególny sposób: o MVVMie.

background image

5

/ www.programistamag.pl /

PODSTAWY WPF, CZĘŚĆ 4 – MVVM

Viewmodel jest świadomy istnienia modelu, który może zostać mu prze-

kazany bezpośrednio lub poprzez pewien interface. Przygotowanie abstrakcji
modelu daje tę korzyść, że można go w dowolnym momencie wymienić na
zupełnie inną klasę, a viewmodel wciąż poradzi sobie z pobraniem z niego
danych.

Nieco inaczej jest ze współpracą viewmodelu z widokiem – choć ten

pierwszy wyprowadza dane w sposób łatwy do skonsumowania przez wi-
dok, na tym kończy się jego rola – nie współpracuje z widokiem w sposób
bezpośredni.

Viewmodel spełnia też jeszcze jedną istotną rolę: przetwarza lub zleca in-

nym klasom przetwarzanie danych. Jest on więc miejscem, w którym znajduje
się logika danego fragmentu aplikacji.

Wreszcie widok – przeważnie jest nim okno (Window) lub strona (Page)

zaprojektowana w XAMLu, która wykorzystuje mechanizm bindingów, by po-
brać dane z viewmodelu i zaprezentować je w kontrolkach. Widok nigdy nie
przetwarza danych. Gdy zajdzie potrzeba ich modyfikacji (na przykład użyt-
kownik zadecyduje o dodaniu, usunięciu lub edycji elementu), widok ograni-
cza się tylko do poinformowania viewmodelu o zaistniałej sytuacji, i jego rola
na tym się kończy.

PLUSY I MINUSY

MVVM wyróżnia się pewną szczególną cechą. Mówi się o nim, że o ile trudne
rzeczy robi się w nim łatwo, to z łatwymi jest dokładnie na odwrót – robi się je
stosunkowo trudno. Jest tak dlatego, że niezależnie od klasy problemu MVVM
wymusza implementację trzech wymienionych wcześniej warstw: modelu,
viewmodelu i widoku. Jeśli aplikacja jest nieskomplikowana, spowoduje to
niepotrzebnie duży narzut kodu źródłowego.

Wzorzec ten ma jednak dużo zalet, których nie sposób przeoczyć. Po

pierwsze, wszystkie trzy warstwy można bardzo łatwo wymieniać. Istnieje
potrzeba przekazania innego modelu? Żaden problem – wystarczy tylko, że
będzie on implementował odpowiedni interface. Chcemy wyświetlić dane
w inny sposób? Nic trudnego – wystarczy tylko przygotować nowy widok,
który będzie umiał współpracować z viewmodelem. Tego ostatniego wymie-
nia się rzadko, ale gdyby zaszła taka potrzeba, i to można stosunkowo łatwo
zrealizować.

Z prostoty wymiany warstw wynika jeszcze jedna istotna zaleta: aplikacje

napisane zgodnie ze wzorcem MVVM można bardzo łatwo testować jednost-
kowo. Zauważmy bowiem, że viewmodel nie jest powiązany w żaden sposób
z widokiem – on dostarcza mu tylko dane do wyświetlenia. Wystarczy więc
dostarczyć viewmodelowi spreparowany model, zamiast widoku - odpowied-
ni mock i już możemy pisać testy.

Separacja warstwy logiki i prezentacji umożliwia też uniezależnienie pracy

nad interface użytkownika i logiką aplikacji. Wystarczy bowiem, że programi-
sta dogada się z projektantem co do interface'u viewmodelu i od tego mo-
mentu obaj mogą pracować całkowicie niezależnie.

TO NIE WZORZEC, TO STYL ŻYCIA

No, może nie tyle styl życia, co bardziej sposób myślenia. MVVM wyma-
ga bowiem od programisty zmiany podejścia do implementacji pewnych
rozwiązań.

Popatrzmy na prosty przykład: viewmodel podczas realizacji zleconych

mu przez użytkownika zadań dochodzi do momentu, w którym musi zapytać
o coś użytkownika przy pomocy okna dialogowego. Nic prostszego, wystar-
czy wywołać metodę

MessageBox.Show, prawda?

Niestety – jeśli zdecydowaliśmy się na MVVM, takie działanie będzie stano-

wiło złamanie zasad wzorca. Dzieje się tak dlatego, że do viewmodelu wpro-
wadziliśmy w tan sposób fragment pewnego specyficznego widoku. Gdyby-
śmy teraz chcieli przenieść aplikację na przykład do środowiska mobilnego, to
będziemy mieli kłopot, ponieważ pytania zadawane są tam użytkownikowi w
inny sposób. Sama wymiana widoku wówczas nie wystarczy – trzeba będzie
przepisać viewmodel, a właśnie przed tym chce nas uchronić MVVM.

Problem ten – w innych modelach właściwie nie występujący – można

rozwiązać w kilku krokach:
1. Wyprowadzić w widoku metodę, która wyświetli komunikat (w aplikacji

desktopowej wywoła po prostu

MessageBox.Show)

2. Metodę tę opublikować poprzez interface
3. Interface ten przekazać viewmodelowi

Teraz w każdej chwili, gdy viewmodelowi zachce się powiadomić o czymś
użytkownika, skorzysta z interface'u (abstrakcji widoku), któremu zleci zreali-
zowanie tego zadania. MVVM nie zostanie tu złamany, ponieważ viewmodel
nie będzie powiązany z konkretnym widokiem. Jeśli będziemy teraz chcieli
zamienić widok na inny, wystarczy zadbać o to, aby zaimplementował odpo-
wiedni interface.

OD TEORII DO PRAKTYKI

Spróbujmy napisać teraz prostą aplikację WPF, wykorzystując opisany wcześniej
wzorzec MVVM. Aplikacja będzie służyła do sprawdzania, do jakiej sieci należy
podany przez użytkownika numer telefonu komórkowego – w tym celu program
połączy się z serwisem internetowym i za pośrednictwem prostego API pobierze
odpowiednią informację. Do tego celu użyjemy klasy

HttpWebClient, wykonu-

jąc asynchroniczne zapytanie przy pomocy async oraz await. Aplikację napiszemy
od zera, bez użycia żadnego frameworka - pozwoli nam to w pełni zrozumieć ideę
MVVM oraz poznać poszczególne elementy, które ją realizują.

Pierwszym krokiem jest dodanie do solucji nowej, pustej aplikacji WPF.

Aplikacja taka składa się z dwóch plików XAML i odpowiadających im plików
z kodem źródłowym.

Plik XAML to kod opisujący interface okna lub strony oparty na XML. W

pliku tym możemy deklaratywnie budować interface użytkownika podobnie
do tego, jak buduje się stronę WWW w plikach (X)HTML. Natomiast powiązany
z nim plik .xaml.cs jest tzw. „code behind” pliku .xaml, czyli miejscem, w któ-
rym dostarczamy kod źródłowy pracujący z zaprojektowanym interfacem. Plik
.xaml.cs jest częścią widoku.

Plik App.xaml (oraz App.xaml.cs) będzie dla nas punktem wejścia, w któ-

rym powołamy do życia klasy viewmodeli, oraz serwisów, które będą realizo-
wały funkcjonalność aplikacji.

WPF pozwala instancjonować obiekty deklaratywnie – poprzez wstawienie

ich w postaci odpowiednich znaczników do kodu XAML. Możemy wykorzystać
to do utworzenia viewmodeli w pliku App.xaml. Dzięki takiemu podejściu za-
pewnimy sobie Intellisense do edytora XAML oraz dostarczymy dane, które bę-
dziemy mogli zobaczyć w czasie projektowania aplikacji (design-time).

<

Application.Resources

>

<

viewmodel

:

MainWindowViewModel

x

:

Key

="MainWindowViewModel"/>

<

viewmodel

:

HistoryWindowViewModel

x

:

Key

="HistoryWindowViewModel"/>

</

Application.Resources

>

Jak odróżnić dane design-time od rzeczywistych danych, które będą używane
w czasie pracy programu? Otóż wyposażymy nasze viewmodele w dwa kon-
struktory. Jeden, bezparametrowy, zostanie użyty przez Visual Studio podczas
tworzenia edytora widoku XAML. Drugi zaś, z dodatkowymi parametrami,
wykorzystamy w trakcie pracy naszej aplikacji. Nadpisanie

Application.

Resources następuje w pliku App.xaml.cs w metodzie OnStartup:

protected override void

OnStartup(

StartupEventArgs

e)

{

base

.OnStartup(e);

var

checkPhoneHistoryService =

new

CheckPhoneHistoryService

();

var

checkPhoneService =

new

CheckPhoneService

(checkPhoneHistoryService);

Resources[

"MainWindowViewModel"

] =

new

MainWindowViewModel

(checkPhoneService,checkPhoneHistoryService);

Resources[

"HistoryWindowViewModel"

] =

new

HistoryWindowViewModel

(checkPhoneHistoryService);

}

background image

6

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

Nasza aplikacja posiada dwa widoki (będą nimi okna). Datacontextem dla

widoków będą odpowiednie viewmodele, zaś za pośrednictwem mechani-
zmu „dziedziczenia” wartości dependency properties, będą do nich miały do-
stęp również kontrolki WPF umieszczone w oknach.

Najbardziej popularnym – i również najwygodniejszym – sposobem prze-

kazywania danych z viewmodelu do widoku jest wiązanie właściwości. Na
przykład właściwość

Text kontrolki TextBlock lub TextBox można przywią-

zać do pewnej własności viewmodelu. Ponieważ jednak viewmodel rzadko
kiedy zawiera dependency properties, trzeba zastosować inne rozwiązanie,
które pozwoli na prawidłowe przekazywanie informacji o zmianach wartości
własności. W tym celu implementujemy w viewmodelu interface

INoti-

fyPropertyChanged, a każdą zmianę wartości ręcznie raportujemy należą-
cym do tego interface'u zdarzeniem

PropertyChanged.

Jedną z kluczowych zmian w stosunku do klasycznego modelu progra-

mowania jest to, że logiki aplikacji nie wiążemy z interface użytkownika przy
pomocy zdarzeń – staramy się zminimalizować ilość kodu znajdującego się
w code-behind widoków. Zamiast zdarzeń korzystamy natomiast z mechani-
zmu komend (Commands). Wśród innych ma to też tę dodatkową zaletę, że
w łatwy sposób możemy poinformować widok, czy komenda w określonych
okolicznościach może zostać wykonana, czy nie. Ponadto wywoływanym ko-
mendom można też z widoku przekazać dodatkowy parametr.

Za obsługę komend odpowiedzialne są klasy implementujące interface

ICommand, który posiada dwie metody. Pierwsza odpowiedzialna jest za wy-
konanie akcji, zaś druga zwraca wartość

true lub false w zależności od tego

czy dana akcja może, czy też nie może zostać wykonana.

<

Button

Content

="Sprawdź"

Command

="{

Binding

CheckNumberCommand

}"></

Button

>

Aby móc skorzystać z intefejsu ICommand, należy ręcznie zaimplemen-
tować go w odpowiedniej klasie, a później utworzyć jej instancję. Do-
brym miejscem na utworzenie instancji klas komend jest konstruktor klasy
MainWindowsViewModel.

Podczas instancjonowania klas reprezentujących komendy przekazujemy

do nich akcje (metody) odpowiedzialne za faktyczne wykonanie komendy
oraz za sprawdzenie, czy w danym momecie możliwe jest jej wykonanie. Jak
widzimy, w samej klasie implementującej

ICommand nie posiadamy żadnej

logiki, a jedynie wykonujemy akcję, którą przekaże nam Viewmodel.

Przykładowo, po powiązaniu własności

Command przycisku „Sprawdź” z

odpowiednią komendą udostępnioną przez viewmodel, przycisk ów będzie
dostępny tylko wtedy, gdy aplikacja nie będzie zajęta sprawdzaniem po-
przedniego wywołania, oraz gdy podany przez użytkownika numer będzie
prawidłowy (czyli będzie składał się z dziewięciu cyfr).

Analogicznie postąpimy w przypadku przycisku pokazującego historię.

Przycisk nie będzie dostępny, jeśli historia jest pusta.

private bool

CanShowHistory()

{

return

_checkPhoneHistoryService.History.Count > 0;

}

private void

ShowHistory()

{

_navigationService.NavigateToHistory();

}

Drugim aspektem, w którym wyraźnie widać zastosowanie wzorca MVVM,
jest wyświetlanie okien dialogowych. Zastosowaliśmy tutaj sposób opisany
na początku artykułu: widok implementuje funkcjonalność, która pozwoli na
wyświetlanie takich okien, ale to viewmodel będzie decydował o tym, kiedy
okno dialogowe ma się pojawić. Podejście to gwarantuje nam odseparowanie
części odpowiedzialnej za faktyczne wyświetlanie okien od logiki aplikacji. W
podobny sposób zaimplementujemy nawigację do okna z historią. Również
i tu pojawia się abstrakcja w postaci interfejsu umożliwiającego nawigację.

Dużą zaletą takiego rozwiązania jest łatwość zmiany sposobu zachowania

się aplikacji: jest tak dlatego, że zarówno widok, jak i viewmodel w żaden spo-
sób nie determinują sposobu nawigacji ani też wyglądu okna dialogowego.

Równie dobrze zamiast klasy

MessageBox możemy zastosować np. zwykłe

okno z własnym szablonem czy stylem.

Jak widzimy, całkiem niewielkim nakładem pracy osiągnęliśmy dość dużą

funkcjonalność, którą w łatwy sposób możemy później testować.

MVVM LIGHT TOOLKIT

Na podstawie poprzedniego przykładu łatwo zauważyć, że istnieją pewne ty-
powe scenariusze, dla których istnieją równie typowe rozwiązania. Aby nie mar-
nować czasu i energii na powielanie takich schematów, z pomocą przychodzą
nam wszelkiego rodzaju frameworki czy też toolkity realizujące wzorzec MVVM.

Do najbardziej znanych należą:

» MVVM Light Toolkit, którego autorem jest Laurent Bugnion. Zawiera on

zestaw klas bazowych, snippetów oraz szablonów projektów. Bardzo lekki
framework, dający pełną kontrolę nad kodem.

» Caliburn Micro – rozbudowany framework MVVM posiadający wiele go-

towych mechanizmów. Bardzo łatwy i intuicyjny za sprawą podejścia „co-
nvention over configuration”

» MVVMCross – stosunkowo młody framework wspomagający tworzenie

aplikacji MVVM dla urządzeń mobilnych oraz aplikacji desktopowych.
Jego główną zaletą jest w pełni przenoszalny kod viemodelu pomiędzy
różnymi platformami.

DA SIĘ SZYBCIEJ?

Spróbujmy teraz ponownie zaimplementować nasz przykładowy program zgod-
nie ze wzorcem MVVM, ale tym razem przy użyciu MVVM Light Toolkit. Po zainsta-
lowaniu tego toolkita w zestawie szablonów dla projektów Windows pojawią się
nowe pozycje. Jeśli utworzymy projekt przy pomocy jednego z tych szablonów,
otrzymamy gotowy szkielet aplikacji implementującej wzorzec MVVM.

Pierwszym elementem, na który powinniśmy zwrócić uwagę, jest plik View-

ModelLocator.cs. Następuje tam bowiem zarejestrowanie w kontenerze zależno-
ści wszystkich klas viewmodelu oraz wykorzystywanych w nich klas serwisów.

Kontener zależności (IoC container) to mechanizm pozwalający rejestro-

wać interface'y oraz klasy, które je implementują. Daje to nam możliwość po-
bierania instancji klas wprost z kontenera na podstawie zadanego interface'u.
Kontener sam zajmuje się ich instancjonowaniem oraz dba o rozwiązanie
zależności. Spójrzmy na poniższy kod. Na początku rejestrujemy wszystkie
serwisy metodą generyczną Register. Polega to na „mapowaniu” interfejsów
na implementujące je klasy. Następnie w taki sam sposób rejestrujemy klasy
viewmodeli. Jednak w tym przypadku klasy te posiadają pewne zależności:
poprzez parametry konstruktorów przekazywane są potrzebne do prawidło-
wego działania serwisy. Ponieważ wcześniej zarejestrowaliśmy je w kontene-
rze, podczas pobierania naszego viewmodelu, kontener będzie „wiedział”, co
dokładnie powinien przekazać do konstruktorów viewmodeli.

Oprócz łatwego zarządzania zależnościami użycie kontenera IoC oraz

praca na interfejsach daje nam wiele korzyści podczas pisania testów jed-
nostkowych. Zamiast przekazywać pełną implementację serwisów, możemy
utworzyć mocki, czyli uproszczone klasy symulujące pożądane działanie wy-
magane przez scenariusz testu.

static

ViewModelLocator()

{

ServiceLocator

.SetLocatorProvider(() =>

SimpleIoc

.Default);

if

(

ViewModelBase

.IsInDesignModeStatic)

{

SimpleIoc

.Default.Register<

ICheckPhoneHistoryService

, Design.

DesignCheckPhoneHistoryService

>();

}

else

{

SimpleIoc

.Default.Register<

ICheckPhoneHistoryService

,

CheckPhoneHistoryService

>();

}

SimpleIoc

.Default.Register<

ICheckPhoneService

,

CheckPhoneService

>();

}

background image

7

/ www.programistamag.pl /

PODSTAWY WPF, CZĘŚĆ 4 – MVVM

Takie rozwiązanie zapewni nam łatwe zarządzanie zależnościami: jeśli na

przykład zdecydujemy się na użycie jakiegoś serwisu w naszym viewmodelu, wy-
starczy, że przekażemy go jako interface przez parametr konstruktora oraz wcze-
śniej zarejestrujemy odpowiednią klasę w kontenerze zależności. Framework sam
zadba o to, aby przy tworzeniu instancji klasy ViewModel „wstrzyknąć” odpowied-
nie zależności. Warto zauważyć, że metoda

Register rejestrująca klasę posiada

dodatkowy parametr

createInstanceImmediately, który umożliwia natych-

miastowe utworzenie instancji klasy zaraz po jej zarejestrowaniu.

Oprócz tego otrzymujemy również łatwą możliwość sprawdzenia, czy ak-

tualnie jesteśmy w trybie „design” – czyli czy nasza aplikacja wyświetlana jest
w Visual Studio lub Blendzie. Jeśli tak jest, to możemy zarejestrować zupełnie
inny zestaw serwisów – dostarczając tym samym przykładowe dane pomocne
przy procesie tworzenia szablonów.

Drugim ważnym elementem są same klasy viewmodeli. Dziedziczą one po

klasie

ViewModelBase, co zapewnia nam kilka usprawnień, pomagających w

implementacji wzorca MVVM. Oprócz obowiązkowej implementacji interface'u
INotifyPropertyChanged otrzymujemy na przykład właściwość Messen-
gerInstance, która umożliwia komunikację pomiędzy klasami viewmodeli.

Dodatkowo, oprócz wspomnianej klasy bazowej

ViewModelBase, dosta-

jemy też gotowe klasy implementującą interface

ICommand: RelayCommand

oraz

RelayCommand<T>.

TESTOWANIE

Korzystanie ze wzorca MVVM znacząco ułatwia tworzenie testów jednostko-
wych. Dzięki temu, że viewmodel nie jest zależny od UI, jego testowanie staje
się stosunkowo proste.

Po utworzeniu projektu testowego należy dodać referencję do assembly,

który przechowuje viewmodele. Przeważnie trzyma się je w osobnej biblio-
tece, jednak w naszym przypadku, dla zachowania prostoty implementacji,
zestawem tym jest aplikacja.

Na początek musimy zadbać o to, aby wszystkie potrzebne obiekty serwiso-

we zostały dostarczone w postaci mocków; w projekcie wykorzystującym MVVM
Light Toolkit wszystkie zależności przekazujemy przez parametry konstruktora.

Testowanie komend sprowadza się do przetestowania metod

Executed

oraz

CanExecute. W pierwszym przypadku sprawdzamy, czy serwis odpo-

wiedzialny za przetworzenie danych zwrócił odpowiedni obiekt. Dzięki temu,
że komunikaty wyświetlane są z wnętrza viewmodelu przy pomocy przekaza-
nego mu wcześniej interface'u, możemy nawet sprawdzić, czy w określonych
okolicznościach wyświetlone zostało właściwe okno dialogowe.

Przetestowanie

CanExecuted sprowadza się do sprawdzenia spodziewa-

nego rezultatu na podstawie danego scenariusza testowego.

CO JESZCZE?

MVVM Light Toolkit – jak sama nazwa wskazuje - jest stosunkowo lekkim to-
olkitem. Oprócz wymienionych pomocnych elementów posiada on również
wsparcie dla języka XAML w postaci obiektu o nazwie

EventToCommand.

Jeśli kontrolka posiada zdarzenie, które chcielibyśmy obsłużyć w viewmode-
lu, jako własność

Command wystaczy ustawić wspomniany wcześniej obiekt

(zwany też behaviorem).

Pozostałe elementy, które otrzymujemy wraz z toolkitem:

» Obsługa powiadomień: mechanizm zaimplementowany w postaci odpo-

wiednich klas, służący do komunikacji pomiędzy viewmodelami.

» DispatcherHelper – statyczna klasa ułatwiająca dostęp do wątku UI, po-

mocna zwłaszcza podczas pracy z wywoływaniami metod typu async/await

» Blendable – jest to wsparcie dla aplikacji Blend. Umożliwia łatwe projek-

towanie interfejsu użytkownika, drag'n'drop właściwości z viewmodelu,
oraz pracę na żywo z danymi design-time.

NA KONIEC

W cyklu artykułów opisaliśmy pewien zbiór podstaw programowania z pomo-
cą Windows Presentation Foundation. Mamy nadzieję, że ułatwią one począt-
kującemu programiście zaznajomienie się z tym frameworkiem. Zachęcamy
też, aby poświęcić trochę czasu na praktyczne przećwiczenie opisanych w
poprzednich artykułach zagadnień – w końcu sama wiedza nie wystarczy, aby
stać się doświadczonym programistą.

Kody źródłowe wszystkich przykładowych programów dostępne są do

ściągnięcia ze strony internetowej magazynu Programista.

Damian Jarosch

Programista C# z ośmioletnim stażem. Pasjonat technologii mobilnych, w szczególności
Windows Phone oraz Xamarin (Monotouch i Monodroid). Aktywny freelancer. Programista
w PGS Software.

W sieci

P Wzorce projektowe:

http://pl.wikipedia.org/wiki/Wzorzec_projektowy_(informatyka)

P MVVM w aplikacjach WPF:

http://msdn.microsoft.com/en-us/magazine/dd419663.aspx

P Presentation Model – prekursor MVVM:

http://martinfowler.com/eaaDev/PresentationModel.html

P Opis MVVM:

http://blogs.msdn.com/b/johngossman/archive/2005/10/08/478683.aspx

Wojciech Sura

wojciechsura@gmail.com

Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.

background image

8

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

Wojciech Sura

OD ZERA? DLACZEGO?

Pierwszym pytaniem, które zawsze warto zadać sobie przed zabraniem się
za pisanie nowego komponentu, jest: „czy naprawdę potrzebuję napisać tę
kontrolkę od zera?”. Współczesne internetowe repozytoria toną w ogromnej
ilości kontrolek – od bardzo prostych, które na przykład zmieniają tylko wy-
gląd standardowego ich odpowiednika (przycisku, pola wyboru itp.), aż do
bardzo zaawansowanych, oferujących szeroką funkcjonalność i możliwości
dostosowania wyglądu oraz zachowania. Warto mieć na uwadze, że bardzo
dużo wysokiej jakości kontrolek jest darmowych, a czasem wręcz udostęp-
nionych wraz ze źródłem na zasadach open source. Dlatego też przed zaka-
saniem rękawów warto sprawdzić najpierw, czy ktoś nie wykonał już pracy,
której właśnie zamierzamy się podjąć.

Czasem zdarza się jednak, że skorzystanie z gotowego rozwiązania nie

wchodzi w grę. Powodów może być dużo – zacznijmy choćby od budżetu:
większość dużych firm specjalizujących się w pisaniu komponentów ceni się
proporcjonalnie do jakości oferowanych przez nie produktów. Oznacza to, że
ceny dużych pakietów kontrolek zaczynają się zwykle w okolicach $1000, a
nierzadko za tę cenę otrzymujemy tylko licencję jednostanowiskową. Dru-
gim aspektem jest brak kontroli nad źródłem. Przypuśćmy na przykład, że
zajdzie potrzeba rozszerzenia takiego komponentu o funkcjonalność, której
na chwilę obecną nie oferuje. Możemy spróbować zwrócić się do producenta
lub autora z prośbą o jej dodanie (z różnymi rezultatami) albo wykonać re-
verse-engineering kodu kontrolki i spróbować wstrzyknąć do niej tę funkcjo-
nalność samodzielnie (również z różnymi rezultatami). Piszę to bez przekąsu
– przeanalizowanie kodu rzędu kilku lub kilkunastu tysięcy linii, a następnie
rozszerzenie go tak, by nie zaburzyć filozofii przyświecającej pierwotnemu
twórcy kontrolki, wcale nie jest prostym zadaniem.

Aspekt kontroli nad kodem źródłowym ma jeszcze inne oblicza. Na przy-

kład kontrolkę możemy zaprojektować tak, by nadać jej wstępnie konkretny
kierunek rozwoju, aby przygotować ją do współpracy z innymi elementami
pisanego przez nas systemu lub wyposażyć w mechanizmy, które uproszczą
późniejsze rozszerzanie jej możliwości. Kontrolki komercyjne przeważnie są
uniwersalne, co ma oczywiście swoje zalety, ale w niektórych przypadkach
może przysporzyć nam też kłopotów.

Innym powodem, dla którego możemy zastanowić się nad napisaniem

własnego komponentu od zera, może być również fakt, iż jest on na tyle spe-
cyficzny, że nikt do tej pory jeszcze takiego nie napisał (a jeśli nawet napi-
sał, to nie udostępnił). Zdarzyło mi się na przykład projektować komponenty
wyświetlające dane pozyskiwane z prototypowego urządzenia medycznego.

W tym przypadku nie było co liczyć na to, że znajdziemy gdzieś w Internecie
gotowe rozwiązanie.

CASE STUDY

Jako case study mogę zaprezentować kilka kontrolek, które wyszły spod mo-
jej ręki, naświetlając jednocześnie okoliczności ich powstania.

SpkToolbar

Rysunek 1. SpkToolbar

Okoliczności powstania tej kontrolki nie były zbyt skomplikowane. W roku

2007 Microsoft wprowadził do swoich produktów wstążkę (Ribbon). Kompo-
nent ten spotkał się z bardzo burzliwym przyjęciem – jedni od razu go po-
kochali, inni - znienawidzili. Czas i statystyki pokazały jednak, że w ogólnym
rozrachunku wstążka się przyjęła, a Microsoft zaczął wprowadzać ją do coraz
większej liczby swoich aplikacji, by w końcu stała się standardowym interfa-
cem systemu operacyjnego w Windows 8.

Wówczas programy pisałem jeszcze w Delphi, gdzie przez dłuższy czas

wstążka nie była dostępna, a gdy wreszcie pojawiła się, to cieszyć nią mogli
się tylko użytkownicy płatnych wersji środowiska. Ponieważ przez długi czas
nie pojawił się żaden darmowy odpowiednik tego komponentu, usiadłem i
zabrałem się za napisanie takiego od zera samodzielnie. Jak można się łatwo
domyślić, nie udało mi się odtworzyć całej funkcjonalności – zaimplemento-
wałem tylko przyciski – ale równocześnie przygotowałem API kontrolki tak,
by w łatwy sposób można było dodawać kolejne jej składniki. Zadbałem też
o design-time support (edytory dla IDE) oraz o współpracę z resztą VCLa (na
przykład możliwość przywiązywania akcji do przycisków).

Jako ciekawostkę dodam, że gdy zakończyłem przygodę z Delphi, zdecy-

dowałem się przekazać kody źródłowe kontrolki środowisku programistów
Lazarusa. Tam znaleźli się ochotnicy, którzy popracowali trochę nad kodem,
by kontrolka stała się w pełni obsługiwana przez to IDE, a obecnie znajduje się
w oficjalnym repozytorium Lazarus Code and Component Repository, więc
jeśli ktoś jest zainteresowany obejrzeniem źródeł, może swobodnie pobrać
je z Internetu.

Projektowanie komponentów wizu-

alnych. Część 1: Wstęp

Przeważająca większość frameworków udostępnia pewien domyślny zestaw kontro-
lek wizualnych, przy pomocy których możemy zbudować interface naszej aplikacji.
Wśród nich znajdziemy takie komponenty, jak przycisk, pole wyboru, pole opcji, lista
itd. W przypadku przeciętnych aplikacji biznesowych, w których interakcja z użytkow-
nikiem sprowadza się do wprowadzania lub edycji danych, jest to zestaw w zupełności
wystarczający. Sytuacja komplikuje się jednak nieco, gdy nasza aplikacja potrzebuje
edytora lub podglądu jakiegoś specyficznego rodzaju danych. Z pomocą przychodzą
wtedy internetowe repozytoria, w których możemy odnaleźć brakującą kontrolkę.
Istnieje również inna opcja – możemy spróbować napisać taką kontrolkę samodzielnie.
W nadchodzącej serii artykułów postaram się przybliżyć czytelnikom ten temat.

background image

9

/ www.programistamag.pl /

PROJEKTOWANIE KOMPONENTÓW WIZUALNYCH. CZĘŚĆ 1: WSTĘP

ProCalc

Rysunek 2. ProCalc

Drugim komponentem, który zaprojektowałem od zera, jest renderer wykre-
sów funkcji, który wykorzystywany jest w moim programie – kalkulatorze.
Przypuszczam, że takich komponentów – zarówno płatnych, jak i darmo-
wych – udałoby się trochę w Internecie znaleźć, jednak w tym konkretnym
przypadku nie wchodziły one w grę, ponieważ potrzebowałem bardzo ściśle
zintegrować go z wewnętrznymi mechanizmami kalkulatora. Poza tym zajmo-
wałem się wtedy dosyć intensywnie DirectX-em i miałem ochotę poekspery-
mentować z tym API: cała funkcjonalność rysowania wykresu oddelegowana
jest do natywnej biblioteki DLL, która wykresy rysuje przy pomocy Direct2D.
Przypuszczam, że teraz nie zdecydowałbym się już na takie rozwiązanie, ale
wtedy wydawało się to całkiem dobrym pomysłem.

Jeśli ktoś ma ochotę obejrzeć komponent w akcji, może poszukać w Inter-

necie programu ProCalc, jest on bezpłatnie dostępny na mojej stronie inter-
netowej, a także na Softpedii.

Virtual treeview

Rysunek 3. VirtualTreeView

Programiści Delphi znają z pewnością komponent VirtualTreeView autor-

stwa SoftGems. Jest on tak dobrze napisany i tak funkcjonalny, że CodeGear
wykorzystał go podczas pisania niektórych wersji swojego IDE – możemy go
zobaczyć na przykład w Borland Developer Studio 2005.

VirtualTreeView, poza ogromnymi możliwościami dostosowania tego, w jaki

sposób wyświetlane są elementy drzewa, posiada pewną kluczową funkcjo-
nalność: jest wirtualne. Oznacza to, że nie przechowuje ono prezentowanych
danych, tylko na bieżąco odpytuje o to, jaką ikonę i jaki tekst powinien mieć wy-
świetlany w danym momencie element. Pozwala to na olbrzymią oszczędność
pamięci oraz eleganckie odseparowanie danych od ich prezentacji.

Spośród standardowych kontrolek udostępnianych przez Win32 API

w trybie wirtualnym może pracować ListView (używany np. do wyświetlania
plików w Eksploratorze). Niestety, Microsoft nie zdecydował się na udostęp-
nienie takiego trybu w komponencie TreeView, co zainspirowało mnie do
zaimplementowania odpowiednika VirtualTreeView dla Windows Forms. Wy-
daje mi się, że cel został osięgnięty – oprogramowałem wszystkie najważniej-
sze funkcje pozwalające na normalne korzystanie z drzewa, zaś kod źródłowy
udostępniłem w serwisie CodePlex (słowo kluczowe: Spk.Controls)

ProTranscriber

Rysunek 4. ProTranscriber

ProTranscriber jest programem, który nie ujrzał jeszcze światła dziennego
– wciąż jest w fazie beta. Zamarzyło mi się kiedyś odzyskać kilka fortepiano-
wych akordów z pewnej piosenki, więc napisałem program, który znacznie
mi w tym pomógł. Na zrzucie ekranu widać dwa moje komponenty: wykres
widma dźwięku oraz klawiaturę wraz z wykresem częstotliwości.

Podgląd widma dźwięku jest takim rodzajem komponentu, który można zna-

leźć w Internecie; nie odnalazłem jednak żadnego darmowego, którego jakość by
mnie usatysfakcjonowała. Cóż; kiedy pisze się programy hobbystycznie, przeważnie
każda cena większa od 0 PLN jest argumentem wykluczającym użycie danej kon-
trolki. Napisałem więc własny, a było to o tyle ciekawe doświadczenie, że niosło ze
sobą dosyć duże wyzwanie, o którym napiszę w jednym z następnych artykułów.

Klawiatura wraz z dopasowanym do niej wykresem częstotliwości jest

z kolei komponentem zbyt specyficznym, by udało mi się znaleźć go w posta-
ci gotowej do użycia.

NodeLab

Rysunek 5. NodeLab

background image

10

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

Zaprojektowania od zera wymagał również komponent w prywatnym

projekcie, nad którym pracuję teraz – jest ściśle dopasowany do architektury
programu, i w tym przypadku po prostu nie miałem innego wyjścia.

KIEDY NIE PISAĆ WŁASNYCH

KONTROLEK?

W dwóch słowach: kiedy odpowiednia kontrolka już istnieje. Napisałem już
o internetowych repozytoriach, ale jest jeszcze jeden aspekt tego kryterium:
kontrolki standardowe. W sieci bez przerwy znajduję niewielkie programiki
narzędziowe (na przykład konwertujące .flv do .avi albo pozwalające na pod-
gląd stylów muzycznych do keyboardów Yamahy), które wyposażone są w
radosny, świecący w oczy interface użytkownika. Każdy przycisk, pole wybo-
ru, opcji, elementy menu renderowane są ręcznie, a nierzadko zdarza się też,
że program wyposażony jest wręcz w mechanizm skórek, które pozwalają na
globalną zmianę wizualnego tematu aplikacji. Podobnie jest również w przy-
padku aplikacji dostarczanych z urządzeniami, a więc narzędzi do kart siecio-
wych, tunerów TV czy drukarek.

Jestem zdecydowanym przeciwnikiem takich rozwiązań. Trzeba zawsze

mieć na uwadze, że człowiek ma bardzo dużą tendencję do przyzwyczajania się
do różnych rozwiązań. Kształt standardowych kontrolek zostaje zakodowany w
pamięci, dzięki czemu potrzeba bardzo mało czasu, żeby odnaleźć się w oknie
dialogowym, które korzysta z systemowego mechanizmu wyświetlania. Kiedy
jednak zastosujemy alternatywny wygląd kontrolek, użytkownik będzie musiał
najpierw poświęcić trochę czasu na zorientowanie się w samym wyglądzie apli-
kacji. W efekcie spowoduje to, że czas realizacji zadania wydłuży się o walkę z in-
terfacem użytkownika, a to jest jeden z kluczowych czynników, które decydują
o ogólnym wrażeniu, jakie na użytkowniku zrobi nasza aplikacja.

Warto też popatrzeć na historię różnych aplikacji. Zacznijmy od systemu

operacyjnego. Windows w swoich pierwszych wersjach dawał możliwość do-
stosowania wyglądu standardowych kontrolek. Na początku można było to
zrobić przy pomocy osobnych aplikacji, a w Windows XP funkcjonalność ta
została już wbudowana w system operacyjny. W praktyce jednak rzadko kiedy
widziałem, by ktoś skorzystał z tej funkcji - przeważająca większość znanych
mi użytkowników korzystała z domyślnego, niebieskiego lub szarego tematu
albo wręcz wracała do tradycyjnego wyglądu okien. Microsoft musiał mieć po-
dobne spostrzeżenia, bo w późniejszych wersjach systemu zrezygnowano już
z możliwości decydowania o wyglądzie elementów interface'u użytkownika.

Dosyć podobnie rzecz ma się na przykład z odtwarzaczami muzyki. Kiedyś

królował nieśmiertelny Winamp, którego wygląd można było dosyć dowolnie
zmieniać. Teraz najczęściej widuję ascetycznego FooBara, VLC Playera, który
korzysta ze standardowego schematu systemowego, albo Windows Media
Playera w swoim standardowym wyglądzie.

Oczywiście istnieje też wiele aplikacji, które wyłamują się ze schematu i

korzystają z własnego zestawu kontrolek (częściowo lub całkowicie). Na przy-
kład Google Chrome ma interface znacząco różniący się od standardowego,
systemowego; podobnie jest też na przykład z aplikacjami z pakietu Office.
Pamiętajmy jednak, że w przypadku takich aplikacji nad ich wyglądem pra-
cuje sztab specjalistów, którzy wiedzą, jak zaprojektować kontrolki, by były
estetyczne i jednocześnie nie wchodziły w drogę użytkownikowi, który będzie
z nich korzystał.

KOPIUJ, WKLEJ

Sytuacja, w której mamy pod ręką grafika i specjalistę od UX, jest sytuacją ide-
alną. Dostaniemy wtedy gotowy design odpowiednich kontrolek i pozosta-
nie nam tylko ich zaimplementowanie. Niestety jednak w życiu bywa różnie
i często zdarza się, że to programista jest osobą, która będzie musiała zadbać
o wygląd aplikacji. Dzieje się tak najczęściej w przypadku aplikacji pisanych
hobbystycznie (co, niestety, często kończy się powstaniem potworków, o któ-
rych wspominałem wcześniej).

Moja rada w takim przypadku jest bardzo prosta: przyjrzyjmy się już istnie-

jącym aplikacjom i elementom interface'u użytkownika i kopiujmy pomysły bez

skrępowania! Oczywiście w miarę możliwości: tak, aby nie popełnić plagiatu
albo nie złamać jakiegoś patentu. Na szczęście w większości przypadków spra-
wa rozbija się o detale, a te są zbyt małe, by można było się o nie procesować.

Dlaczego podchodzę do sprawy w taki sposób? To proste: jeśli nawet je-

stem dobrym programistą, to grafik ze mnie żaden. Jednocześnie wiem, że
nad wyglądem większości dużych aplikacji pracowało grono specjalistów –
powielając ich pomysły, mam możliwość zaprojektowania estetycznych kon-
trolek, samemu nie mając na ten temat zbyt dużej wiedzy. W jednym z następ-
nych artykułów postaram się zaprezentować kilka prostych pomysłów, które
można wykorzystać, by uatrakcyjnić projektowane przez siebie kontrolki.

RODZAJE KOMPONENTÓW

Na poziomie architektury komponenty wizualne możemy podzielić na dwie
kategorie: komponenty kompozytowe i – z braku lepszego określenia – nie-
standardowe (custom).

Komponenty kompozytowe stanowią złożenie (kompozycję) kilku innych

komponentów. Przykładowo załóżmy, że w naszej aplikacji bardzo często ko-
rzystamy z pól do wpisywania nazwy użytkownika i hasła. Aby zapewnić jed-
nolity interface użytkownika (a jednocześnie ułatwić sobie pracę), możemy
zbudować komponent kompozytowy zawierający dwa pola tekstowe i dwie
etykiety, którego będziemy mogli potem użyć w wielu innych miejscach.

Komponowanie kontrolek ma swoje wady i zalety. Do tych ostatnich na-

leży z pewnością fakt, że w ten sposób ujednolicamy interface użytkownika
– komponent użyty w każdym miejscu aplikacji będzie wyglądał tak samo.
Ponadto, jeśli zdecydujemy się na to, aby ten wygląd zmienić, odpowiednią
zmianę wystarczy zrobić tylko w jednym miejscu. Drugim plusem jest moż-
liwość zaszycia w takiej kontrolce dodatkowej funkcjonalności, na przykład
wstępnej walidacji nazwy użytkownika lub długości oraz jakości hasła. Mo-
żemy też zadbać o to, aby komunikacja pomiędzy komponentem a innymi
elementami formatki była odcięta od jego zawartości. Mam na myśli to, że
nazwę użytkownika i hasło możemy wyprowadzić poprzez własności takiego
komponentu – jego użytkownicy nie muszą przejmować się tym, że są one
wewnętrznie transportowane do pól tekstowych. Dzięki temu, gdy później
zajdzie taka potrzeba, pole tekstowe możemy na przykład wymienić na listę
rozwijaną z opcją edycji – i znów zmianę będzie wystarczyło wprowadzić tylko
w jednym miejscu.

Wadą kontrolek kompozytowych jest to, że jesteśmy w dużym stopniu ogra-

niczeni, jeśli chodzi o ich funkcjonalność. Jest tak dlatego, że możemy korzystać
tylko z już istniejących kontrolek: nierealne jest na przykład przygotowanie
komponentu kompozytowego wyświetlającego widmo dźwięku albo wykres
funkcji (choć w niektórych frameworkach nie jest to do końca prawda, ale o tym
za chwilę). Drugim problemem jest to, że z perspektywy użytkownika mamy
niewielki wpływ na to, w jaki sposób komponent kompozytowy działa we-
wnątrz. Komponent taki nie jest bowiem traktowany przez framework na osob-
nych zasadach – dla niego jest to po prostu kilka kontrolek ułożonych na innej,
będącej kontenerem. Jeśli więc na przykład twórca komponentu nie zadba o to,
aby wewnętrzne kontrolki miały ustawiony właściwy tab-order (czyli kolejność
zmiany fokusu podczas przełączania klawiszem tab), to jesteśmy już na niego
skazani. Problem ten dotyczy wszystkich tych aspektów, które wynikają ze spo-
sobu, w jaki kontrolki komunikują się z frameworkiem.

Kontrolki niestandardowe z kolei to te, które są projektowane przez pro-

gramistę od zera. W dużym skrócie dostaje on pewien prostokątny obszar, po
którym może rysować, oraz jest informowany o pewnym zbiorze zdarzeń, któ-
re zachodzą w obrębie tego obszaru (na przykład ruchy i kliknięcia myszy, wci-
skanie przycisków na klawiaturze w momencie, gdy kontrolka ma fokus itp.),
i to wszystko: całą interakcję musi on opracować praktycznie od zera. Przygo-
towanie kontrolki niestandardowej wymaga znacznie więcej pracy i uwagi od
programisty, ale ma tę zaletę, że ma on całkowitą dowolność w zakresie tego,
w jaki sposób kontrolka taka będzie funkcjonowała. Przeważająca większość
artykułów z tej serii omawiać będzie tematykę programowania kontrolek nie-
standardowych. Na marginesie: wszystkie kontrolki zaprezentowane na zrzu-
tach ekranu są kontrolkami niestandardowymi.

background image

11

/ www.programistamag.pl /

PROJEKTOWANIE KOMPONENTÓW WIZUALNYCH. CZĘŚĆ 1: WSTĘP

Istnieją takie frameworki, które w pewnym stopniu pozwalają połączyć

cechy kontrolek kompozytowych i niestandardowych. Polega to na tym, że
w zestawie standardowych kontrolek istnieją takie, które z założenia służą do
tego, by składać z nich bardziej zaawansowane komponenty – na przykład w
bibliotece Windows Presentation Foundation znajdziemy takie kontrolki, jak
Border (ramka) czy Path (ścieżka). Korzystanie z takiej metody projektowa-
nia pociąga niestety za sobą wszystkie wady kontrolek kompozytowych – na
przykład może się zdarzyć, że po umieszczeniu obrazka na przycisku będzie
on funkcjonował jako pełnoprawny komponent i będzie otrzymywał fokus
(co oczywiście jest okolicznością niepożądaną). Z drugiej strony jednak po-
zwala na stosunkowo precyzyjne zaprojektowanie wyglądu kontrolki w bar-
dzo wygodny i niewymagający dużego wysiłku sposób (w przypadku WPFa
sprowadza się to do opisania kontrolki w opartym na XMLu języku XAML).

JAK PROJEKTOWAĆ KONTROLKI?

Zanim zabierzemy się za pisanie kodu, dobrze jest zastanowić się wcześniej,
jak dana kontrolka będzie wyglądać oraz w jaki sposób będzie prowadzona
interakcja z użytkownikiem. Istnieje kilka obszarów, na które należy zwrócić
uwagę w sposób szczególny.

Wygoda użytkowania

Z kontrolkami jest podobnie jak z rzeczywistymi przedmiotami: oczekujemy
od nich, aby były funkcjonalne i wygodne w użytkowaniu – do tego nie trzeba
chyba nikogo przekonywać. Istotnym aspektem jest tu jednak również kwestia
zachowywania pewnych standardów. Na przykład, jeśli w zakresie funkcjonal-
ności naszej kontrolki przewidujemy możliwość zaznaczania elementów, to o ile
nie koliduje to ze specyfiką wyświetlanych danych, dobrze jest udostępnić użyt-
kownikowi zwykłe zaznaczenie prostokątne oraz uwzględnić przyciski Ctrl (do-
dawanie i usuwanie z zaznaczenia) oraz Shift (zaznaczenie zakresu elementów).
Zastosowanie innego mechanizmu może doprowadzić do tego, że użytkownik
po prostu nie będzie wiedział, jak naszej kontrolki użyć.

Wygoda użytkowania odnosi się również do programisty, który będzie ko-

rzystał z tej kontrolki. Warto więc pomyśleć podczas projektowania o tym, aby
API kontrolki było funkcjonalne, elastyczne i czytelne.

Wydajność

Nieresponsywne programy są irytujące, a jedną z przyczyn nieresponsywnych
programów są nieresponsywne kontrolki. Dobrze napisana kontrolka powin-
na realizować wszystkie zadania natychmiast; czasami wystarczy oddelego-
wać długie zadanie do osobnego wątku, ale zdarza się też, że trzeba uciec się
do bardziej wyrafinowanych rozwiązań.

Estetyka

Można dosyć bezpiecznie oszacować, że sam wygląd (odcięty od funkcjo-
nalności, niezawodności i tak dalej) programu składa się na 80% pierwszego
wrażenia użytkownika. Stare przysłowie mówi, że nie da się zrobić drugiego

pierwszego wrażenia, więc warto dołożyć starań, aby wypadło ono jak naj-
korzystniej. Poza tym jest bardzo prawdopodobne, że nasze programy będą
użytkowane przez kogoś na co dzień – dlaczego nie umilić mu pracy estetycz-
nym interfacem?

Niezawodność

Myślę, że i ten punkt nie wymaga szerokiego komentarza, a jest szczególnie
ważny, gdy naszej kontrolki będą używały osoby trzecie. Jest tak dlatego, że
komponent jest osobnym, zamkniętym fragmentem aplikacji. Jeśli pojawią
się w nim błędy, to zwykle jedyną opcją jest kontakt z autorem, ponieważ na-
wet wówczas, gdy dostępny jest kod źródłowy, jego analiza, diagnoza proble-
mu i późniejsze naprawianie znacząco utrudnia pracę użytkownikowi kompo-
nentu (o ile w ogóle się za to zabierze).

WYMAGANIA WSTĘPNE

Co trzeba wiedzieć, aby zabrać się za pisanie własnych kontrolek? Po pierwsze,
konieczna jest już pewna wiedza na temat frameworka, w którym pracujemy:
nie jest to na pewno zajęcie dla osób rozpoczynających dopiero przygodę z
programowaniem. W niniejszym cyklu będziemy korzystać z Windows Forms,
czyli w praktyce – Win32 API. Nie jest to być może framework ostatnio bardzo
popularny, ale świetnie nadaje się do nauki pisania własnych komponentów,
ponieważ jest dosyć prosty, stosunkowo wydajny i udostępnia wszystko, cze-
go potrzebujemy do pracy. Poza tym dużo pomysłów i algorytmów, które
będę prezentował, jest całkowicie niezależnych od konkretnego framewor-
ka, więc łatwo będzie je przenieść do innych środowisk albo nawet języków
programowania.

Doświadczenie podpowiada mi, że warto odświeżyć trochę swoją wiedzę

z matematyki – głównie z geometrii w układzie współrzędnych. Samo ryso-
wanie kontrolek nie wymaga tej wiedzy zbyt wiele, ale w momencie, gdy za-
czniemy oprogramowywać interakcję z użytkownikiem, okaże się, że będzie
ona bardzo przydatna.

Bardzo przydaje się również odrobina zmysłu artystycznego – choćby w

kwestii doboru kolorów oraz stosowania różnych efektów wizualnych. Ale i
bez niego można się obyć – postaram się zaprezentować w późniejszych ar-
tykułach kilka sztuczek, które niewielkim kosztem pozwolą osiągnąć całkiem
efektowne rezultaty.

Nie ukrywam, że bardzo zalecaną cechą jest też cierpliwość: miejmy cały

czas na uwadze, że każdy pojedynczy piksel musimy narysować sami; każdą
pojedynczą interakcję z użytkownikiem musimy zaprogramować sami; każdy
element API kontrolki musimy napisać sami! Nie raz zdarzało mi się pracować
pod powiększeniem x8 (chwała temu, kto wymyślił skróty Win+Num plus, Wi-
n+Num minus), żeby wyśledzić „wyciekające” piksele.

CO DALEJ?

Tym sposobem zakończyliśmy wprowadzenie do projektowania kontrolek wi-
zualnych. W następnej części spróbujemy napisać dwa proste komponenty:
jeden kompozytowy, zaś drugi – niestandardowy.

Wojciech Sura

wojciechsura@gmail.com

Programuje od przeszło dziesięciu lat w Delphi, C++ i C#, prowadząc również prywatne
projekty. Obecnie pracuje w polskiej firmie PGS Software S.A., zajmującej się tworzeniem
oprogramowania i aplikacji mobilnych dla klientów z całego świata.

background image

12

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

Kacper Cyran

L

ata mijają, a znaczenie JavaScriptu ciągle rośnie. Społeczność wzbogaciła
się o wiele open-sourcowych narzędzi, dzięki którym praca front-end de-
veloperów stała się przyjemniejsza oraz bardziej efektywna. Jednym z nich

jest

Grunt, który zautomatyzuje za Ciebie powtarzalne zadania. Używasz kompi-

latorów CSS, łączysz obrazki w sprite'y, by przyspieszyć ładowanie strony? Mini-
fikujesz, łączysz i kompilujesz pliki JS? A może chcesz na bieżąco oglądać swoją
prace w przeglądarce? Jeśli tak, to czytaj dalej. Ten artykuł jest dla Ciebie!

CZYM JEST GRUNT?

Projekt powstał w oparciu o platformę node.js oraz package manager npm.
Grunt to dedykowany głównie developerom front-endu „JavaScript Task Run-
ner”, czyli manager zadań dostępny z wiersza poleceń. Idea projektu jest bar-
dzo prosta, pomaga zautomatyzować czynności cyklicznie powtarzane, tak
zwane „taski”. W przypadku front-endu może to być kompresja CSS i JS, testy
jednostkowe czy kompilacja CoffeScript.

DLACZEGO WARTO UŻYWAĆ GRUNTA?

Czasy, w których królował jQuery i CSS bez kompilacji, minęły bezpowrotnie,
proces wytwarzania nawet prostych stron internetowych jest skomplikowany
i wymaga wielu narzędzi wspomagających.

Podstawowy powód to prosta i bezproblemowa automatyzacja wszyst-

kich nudnych procesów tworzenia oprogramowania. Dodatkową zaletą jest
uniezależnienie się od platformy, na której pracuje developer, czy różnych
wersji oprogramowania. Dzięki npm i Gruntowi wszyscy developerzy pracują
w jednolitym i spójnym środowisku niezależnym od platformy i środowiska
programistycznego.

Dodatkowo instalacja całego managera jest bardzo prosta i szybka.
Główne korzyści:

» efektywność – pełna automatyzacja pozwala w dłuższym rozrachunku

zaoszczędzić całe godziny, dni i tygodnie pracy, wprowadzanie zmian nie
będzie tak kosztowne, a developer zajmuje się tylko tym, co lubi;

» konsekwencja – wykonania tej samej rzeczy – proces budowania projektu

będzie zawsze taki sam, bez względu na pogodę, humor czy stan psycho-
fizyczny developera;

» społeczność – cała paleta pomocnych tasków dostępna na licencjach

open-source, ponad 1900 pluginów do Grunta;

» elastyczność – większość pluginów ma na tyle elastyczną konfigurację, że

możemy bez problemu dostosować je do swojego projektu;

» jakość – oszczędzając czas, można skupić się w większej mierze na jakości

wytwarzanego oprogramowania.

PIERWSZA INSTALACJA

I KONFIGURACJA

Wskazówka dla osób, które do tej pory nie miały do czynienia z node.js i npm,
instalacja jest bardzo prosta:

W przypadku systemu Ubuntu:

sudo apt-get install npm

Użytkownicy Windowsa cały pakiet mogą ściągnąć ze strony:

http://nodejs.org/download

.

Po instalacji inicjalizujemy pakiet npm, który wykona za nas wstępną konfigu-
rację modułów potrzebnych do działania managera.

W katalogu głównym projektu tworzymy plik package.json, w którym za-

pisujemy zależności, przechowuje on wszystkie dane na temat wymaganych
modułów, ale również metadanych naszego projektu takich, jak: nazwa, wersja,
opis, autor, lub repozytoria. Więcej na

https://npmjs.org/doc/json.html

.

{

"

name

"

:

"

Grunt test - SMSAPI.pl

"

,

"

version

"

:

"

0.0.1

"

,

"

description

"

:

"

Testowy plik package.json

"

,

"

author

"

:

"

Kacper Cyran - SMSAPI.pl

"

,

"

license

"

:

"

MIT

"

,

"

devDependencies

"

:

{

}

}

W przypadku kiedy dostaliśmy gotowy plik package.json, wpisujemy po-

lecenie:

npm install.

Kolejnym krokiem jest instalacja i dodanie pakietu Grunta do listy wyma-

ganych moduów:

npm install grunt-cli --save-dev.

Komenda

--save-dev automatycznie dopisuje Grunta do zależności

w pliku package.json.

{

...

"

devDependencies

"

:

{

"

grunt-cli

"

:

"

~0.1.9

"

,

}

...

}

Tworzymy dodatkowy plik grunt, aby ułatwić sobie pracę nad projektem.

#!

/bin/sh

.

/node_modules/

.

bin/grunt

"$@"

Notatki

* w IDE, w którym pracujemy, zalecam dodać do ignorowanych folder
./node_modules ze względu na dużą ilość plików ściąganych przez npm.
* npm pozwala tworzyć globalne instancje Grunt poleceniem

npm install

-g grunt-cli, ale nie zalecam tej praktyki ze względu na niekompatybil-
ność różnych wersji pakietów pomiędzy projektami.
* jeżeli korzystamy z jakiegoś systemu kontroli wersji, to zalecam dodać kata-
log /node_modules do pliku .gitignore.

Konfiguracja Grunta

Kolejnym etapem jest już konfiguracja Grunta. Tworzymy plik Gruntfile.js:

module

.

exports

=

function

(

grunt

)

{

// tutaj będzie miała miejsce rejestracja i konfiguracja zadań

};

UglifyJS

Biblioteka UglifyJS jest dobrze znana programistom JavaScriptu, pozwala łą-
czyć, kompresować i filtrować pliki .js, wszystkie te zadania są powtarzalne.

Zadanie wykonywane z linii poleceń wygląda mniej więcej tak:

// uglifyjs [input files] [options]

uglify src/file1.js src/file2.js -o desc/output.min.js

Automatyzacja za pomocą GruntJS

Grunt to task manager stworzony głównie z myślą o front-endzie. Masz dość ręcznego
składania projektu? Grunt wykona za Ciebie wszystkie nudne i powtarzalne zadania.

background image

13

/ www.programistamag.pl /

AUTOMATYZACJA ZA POMOCĄ GRUNTJS

Każda zmiana w dowolnym pliku wymaga ponownego wpisania parame-

trów uruchomienia skryptu.

Kiedy chcemy zminimalizować inny katalog i zapisać plik wynikowy w inne

miejsce, ponownie musimy zmienić parametry w command line, żeby wykonać
polecenie:

uglify vendors/bootstrap.js -o desc/vendors.min.js.

Wykonywanie tych czynności jest męczące, bardzo łatwo można zapo-

mnieć o którymś z wymaganych plików, zmienić kolejność czy zapisać zmiany
do innego pliku.

Poza tym, kiedy pracujemy w grupie, taka praktyka jest praktycznie nie-

możliwa. Dlatego lepiej jest użyć gotowego skonfigurowanego skryptu, który
zrobi to za nas.

Instalacja pluginu dla UglifyJS

Pierwszym zadaniem, jakie dodamy, będzie właśnie konfiguracja UglifyJS.
Teoretycznie możemy sami napisać odpowiedni plugin w node.js, jednak
npm pozwala nam skorzystać z tysięcy gotowych konfigurowalnych i prze-
testowanych rozwiązań. Wystarczy wejść na stronę

https://npmjs.org

i wy-

szukać interesujące nas pakiety. Do biblioteki UglifyJS możemy użyć pluginu
grunt-contrib-uglify. Na stronie projektu znajduje się dobry opis konfiguracji
i przykłady użycia.

Najpierw instalujemy bibliotekę:

npm install grunt-contrib-uglify --save-dev

po instalacji musimy włączyć plugin w pliku Gruntfile.js i przechodzimy do
konfiguracji zadań:

grunt.loadNpmTasks('grunt-contrib-uglify');

W naszym przypadku plik Gruntfile.js będzie wyglądał następująco:

module

.

exports

=

function

(

grunt

)

{

// załadowanie pluginu "uglify"

grunt

.

loadNpmTasks

(

'grunt-contrib-uglify'

)

;

// inicjalizacja konfiguracji

grunt

.

initConfig

(

{

// załadowanie metadanych z pliku package.json

// może być pomocne przy bardziej skomplikowanych zadaniach

pkg

:

grunt

.

file

.

readJSON

(

'package.json'

),

// ustawienie konfiguracji dla pluginu "uglify"

uglify

:

{

// pierwszy zdefiniowany scope i konfiguracja

my_target

:

{

files

:

{

'desc/output.min.js'

:

[

'src/file1.js'

,

'src/file2.js'

]

}

}

,

other_scope

:

{

files

:

{

'desc/vendors.min.js'

:

[

'vendors/bootstrap.js'

]

}

}

}

}

)

;

// rejestracja domyślnego zadania

grunt

.

registerTask

(

'default'

,

[

'uglify'

])

;

// rejestracja dodatkowa do minimalizacji js

grunt

.

registerTask

(

'js'

,

[

'uglify:my_target'

])

;

};

Mamy pierwsze skonfigurowane i działające zadania

my_target oraz

other_scope.

Zadaniem

my_target jest wczytanie z plików zawartości z src/file1.js

i

src/file2.js, następnie złączenie ich do jednego pliku desc/output.min.js,

oraz skompresowanie.

Uruchomienie zadań

Uruchomienie zadań można wykonać na kilka sposobów w konsoli w katalo-
gu z plikiem

Gruntfile.js.

Wykonanie domyślnego zadania:

default (grunt.registerTask('default', ['uglify']);),
może być zapisane w postaci:

./grunt default, lub ./grunt.

Uruchomienie naszego zdefiniowanego zadania:

grunt.registerTask('js', ['uglify:my_target']);
./grunt js

Można również uruchomić task bez definiowania poleceniem

registerTask.

Polecenie

./grunt uglify:my_target będzie działało identycznie jak

./grunt js, natomiast polecenie ./grunt uglify będzie działało tak jak
./grunt default.

W tym momencie struktura naszego katalogu będzie wyglądała

następująco:

/package.js - paczka zależności npm
/Grunfile.js - konfiguracja grunta
/node_modules - zainstalowane moduły
/src/

/file1.js
/file2.js

/vendors

/bootstrap.js

/desc/

/vendors.min.js
/output.min.js

/grunt

Grunt watch

W tym momencie mamy pewien powtarzalny zestaw reguł, dzięki któremu
nie musimy już myśleć co i jak skompresować. W dalszym ciągu jednak nie po-
zbyliśmy się ciągłego przełączania się pomiędzy konsolą a naszym edytorem
w celu wykonania zdefiniowanych zadań. Czy jest na to sposób i czy Grunt
może nam w tym pomóc? Zdecydowanie tak, znowu mamy już gotowe roz-
wiązanie, które możemy dostosować do swoich potrzeb.

Rozwiązaniem tym jest plugin grunt-contrib-watch, którego zadaniem jest

śledzenie zmian, jakie są zapisywane w plikach na dysku, i uruchamianie ściśle
określonych zadań. Na bazie naszego poprzedniego przykładu chcemy zmie-
niać skrypty znajdujące się w katalogu src/ i po zapisaniu mieć gotowy zmini-
malizowany plik desc/output.min.js, jednocześnie nie potrzebujemy sprawdzać
zewnętrznych bibliotek znajdujących się w katalogu vendors/.

Podobnie jak poprzednio instalujemy i włączamy zadanie:

npm install grunt-contrib-watch --save-dev

Plik Gruntfile.js

// ...

grunt

.

loadNpmTasks

(

'grunt-contrib-watch'

)

;

// ...

Następnie przechodzimy do skonfigurowania pluginu, przedstawiona konfi-
guracja jest minimalna, sam plugin ma wiele ciekawych ustawień. Wydaję mi
się, że jedną z bardziej interesujących jest opcja

livereload, która pozwala

przy pomocy odpowiednich pluginów automatycznie przeładować zawartość
strony. Jest to bardzo dobra opcja, kiedy pracujemy na dwóch monitorach, dzię-
ki temu bez odrywania rąk z klawiatury widzimy zmiany na stronie od razu po
przegenerowaniu JavaScript'u lub przekompilowaniu less'a do CSS'a.

background image

14

/ 6

. 2014 . (25) /

BIBLIOTEKI I NARZĘDZIA

// ...

grunt

.

initConfig

(

{

// ...

watch

:

{

scripts

:

{

files

:

'js/*.js'

,

tasks

:

[

'uglify:my_target'

],

}

,

css

:

{

files

:

'less/*.less'

,

tasks

:

[

'less'

],

options

:

{

livereload

:

1337

}

}

,

}

,

// ...

}

)

;

// ...

Jak widać z konfiguracji: śledzimy wszystkie zmiany w katalogu js/ w plikach
z rozszerzeniem .js, po zmianie odpalany jest task kompresujący tylko i wyłącznie
nasze pliki, nie dotykając plików z zewnętrznych bibliotek. Oddzielnie śledzone są
pliki z rozszerzeniem .less, pod które podpięty jest całkiem inny task.

System plików

Bardzo ważną cechą zadań Grunta są pewne reguły działające na systemie pli-
ków. W pierwszym przykładzie z uglify podawaliśmy ścieżki do konkretnych
plików JS, które mają być kompresowane, tzw. static mappings. Ale co w przy-
padku kiedy plików JS lub less w danym katalogu jest dużo? Wypisanie każdego
z osobna jest również czasochłonne i nieefektywne. I tutaj ponownie pomaga
nam

Grunt, który potrafi przeprowadzać automatyczne operacje na strukturze

plików (*dynamic mappings*). Potrafi zwrócić tablice wszystkich plików w da-
nym katalogu, albo tylko te, które pasują do pełnego wzorca. Przykład:

/src/*.js - zwróci wszystkie pliki znajdujące się w katalogu src/

z rozszerzeniem .js

/src/{x,y,z}.js - zwróci pliki o nazwach x.js, y.js, z.js

znajdujące się w katalogu src/

Szablony

Czasem potrzebujemy ustawić pewne opcje dynamicznie, np. ścieżkę docelo-
wą CSS'a, która zmienia się w zależności od numeru wersji produktu.

W tym celu możemy skorzystać z szablonów, a raczej systemu dynamicz-

nych zmiennych pobieranych z ustawień Grunta. W tym celu można użyć spe-
cjalnych tagów

<% %>. Przykład:

grunt

.

initConfig

(

{

concat

:

{

sample

:

{

src

:

[

'baz/*.js'

],

dest

:

'build/<%= bar %>.js'

,

// 'build/bcd.js'

}

,

}

,

foo

:

'c'

,

bar

:

'b<%= foo %>d'

// 'bcd'

}

)

;

Syndrom dużego projektu

Z natury programiści lubią małe funkcje, małe pliki i prostą logikę. Co zrobić,
kiedy Gruntfile.js urośnie nam do 1500 linijek ?

Można rozbić ustawienia na wiele mniejszych plików, w tym przykładzie

użyłem pluginu load-grunt-configs, który wczytuje dodatkową konfigurację
ze wskazanych plików:

var

options

=

{

config

:

{

src

:

[

"

tasks/*.js

"

,

...]

}

};

var

configs

=

require

(

'load-grunt-configs'

)(

grunt

,

options

)

;

grunt

.

initConfig

(

configs

)

;

W naszym przykładzie stworzyliśmy katalog tasks/, a w nim pliki

uglify.js,

watch.js i less.js. Przykład pliku watch.js:

module

.

exports

.

tasks

=

{

watch

:

{

scripts

:

{

files

:

'js/*.js'

,

// ścieżka jakie pliki mają być śledzone

tasks

:

[

'uglify:my_target'

],

// zadanie jakie ma być wykonane

po każdej zmianie

}

,

css

:

{

files

:

'less/*.less'

,

tasks

:

[

'less'

],

options

:

{

livereload

:

1337

}

}

}

}

POLECANE PLUGINY

Lista przydatnych pluginów, dzięki którym możemy zaoszczędzić sporo czasu:

» grunt-contrib-watch – śledzenie zmian na plikach i uruchamianie odpo-

wiednich tasków po zmianie zawartości

» grunt-contrib-uglify – minimalizacja plików JavaScriptowych

» grunt-contrib-concat – łączenie wielu plików w jeden

» grunt-responsive-images – tworzenie wielu wersji obrazka dopasowa-

nych do różnych rozdzielczości

» grunt-spritesmith – zapisuje wszystkie zdjęcia w jednym pliku i generuje

CSS (less, Sass) z odpowiednimi parametrami position-background

» grunt-bower-task – package manager dla stron i aplikacji internetowych

» grunt-contrib-copy – kopiowanie plików

» grunt-contrib-coffee – kompiluje pliki CoffeeScript do JavaScriptu

» grunt-contrib-jshint – pomaga znaleźć potencjalne problemy w JavaScript

» grunt-contrib-imagemin – kompresja obrazków

» grunt-contrib-qunit – testy jednostkowe JavaScript

» grunt-contrib-less, grunt-contrib-sass, grunt-contrib-stylus – Preprocesory CSS:

I wiele innych pluginów, które pomogą w codziennych zadaniach.

PODSUMOWANIE

Pomimo że poznanie, zrozumienie i wdrożenie Grunta wymaga czasu i pew-
nych kosztów, to korzyści z jego używania są dużo większe niż w przypadku
ręcznego dbania o jakość tworzonych rozwiązań. Jeżeli uda się przebrnąć przez
początek, to kolejny dzień w pracy rozpoczniesz od polecenia grunt watch,
a każdy kolejny projekt rozpocznie się od instalacji i skonfigurowania Grunta.

Kacper Cyran

k.cyran@smsapi.pl

Absolwent Wydziału Elektroniki, Automatyki i Informatyki na Politechnice Śląskiej
w Gliwicach. Od 2008 tworzy i rozwija aplikacje internetowe na stanowisku pro-
gramisty. Aktualnie pracuje w firmie ComVision, w której odpowiedzialny jest za
rozwój platformy SMSAPI.

background image
background image

16

/ 6

. 2014 . (25) /

JĘZYKI PROGRAMOWANIA

Rafał Kocisz

SZCZYPTA HISTORII

O tym, jak dużą niespodzianką jest Swift, szczególnie dla doświadczonych
programistów wytwarzających oprogramowanie dedykowane systemom
pod znaku jabłuszka, świadczyć może to, że przez ostatnie dwadzieścia lat
korzystali oni tylko i wyłącznie z języka Objective-C. Język ten, zaprojektowa-
ny w 1983 roku przez Brad'a Cox'a oraz Tom'a Love'a, został wykorzystany do
zaprogramowania systemu operacyjnego NeXTstep, którego następcami są
OS X oraz iOS.

Póki co nic nie wskazywało na jakiekolwiek zmiany w tej materii. Co wię-

cej, język ten przeszedł stosunkowo niedawno dość poważny lifting (Objec-
tive-C 2.0), co mogło jedynie utwierdzić jego użytkowników w przekonaniu
o jego monopolistycznej pozycji w swoim segmencie rynku. A tutaj na po-
czątku czerwca 2014 roku - taka niespodzianka.

SPOJRZENIE Z LOTU PTAKA

Czym więc jest Swift? Apple w następujący sposób podsumowuje ten język:

Swift to innowacyjny, nowy język programowania dla Cocoa oraz Cocoa To-

uch. Cechuje się dużym poziomem interakcji, a pisanie w nim programów jest
przyjemnością. Składnia języka jest bardzo zwięzła, a jednocześnie pełna wyrazu.
Aplikacje pisane w tym języku działają szybko jak błyskawica. Swift jest z miej-
sca gotowy do użycia w Twoim kolejnym projekcie pod iOS lub OS X, bądź przy
rozszerzaniu istniejącej aplikacji, bo kod napisany w tym języku działa ramię w
ramię z Objective-C.

W tym krótkim opisie podkreślone są cechy tego języka, przyjrzyjmy im

się bliżej:

» Innowacyjność. Pod tym hasłem kryje się zestaw nowoczesnych mechani-

zmów języka takich jak: domknięcia leksykalne (ang. closures), krotki (ang.
tuples), typy generyczne (ang. generics), klasy oraz elementy zapożyczone
z języków funkcjonalnych, np. map czy filter. W tym ujęciu Swift wpisuje
się w kanon współczesnych, interpretowanych języków programowania
takiej klasy jak Python, Ruby, Groovy.

» Interakcja. Pod tym hasłem kryje się interaktywna powłoka języka (tzw.

REPL), która umożliwia programiście interakcję z kodem w czasie działania
aplikacji. REPL nie jest w zasadzie niczym nowym, programiści korzystają-
cy z innych języków interpretowanych używają tego mechanizmu od lat,
jednakże w uniwersum Apple jest zdecydowanie powiew świeżości. Poza
tym programiści iOS oraz OS X w ramach nowej wersji sztandarowego IDE
marki Apple (Xcode) mają do dyspozycji interaktywne narzędzie Playgro-
unds (Rysunek 1), które pozwala między innymi wizualizować działanie
budowanych algorytmów, tworzyć w locie testy jednostkowe oraz łatwo
eksperymentować z nowymi API.

» Wydajność. Apple na każdym kroku podkreśla, że Swift pod kątem wydaj-

ności nie ustępuje językowi Objective-C, przede wszystkim dzięki temu, iż
jest on w locie przekształcany do zoptymalizowanego kodu natywnego.
Faktem pozostaje, że komentarze pojawiające się ze strony programistów
eksperymentujących z językiem, w kontekście wydajności aplikacji pisa-
nych w Swift, nie są aż tak optymistyczne jak chciałoby Apple...

» Dostępność. Swift'a może zacząć z miejsca używać każdy, kto ma konto

developerskie Apple. W takim wypadku wystarczy pobrać i zainstalować
sobie paczkę ze środowiskiem Xcode 6 Beta. Oprócz tego Apple oferuje
całkiem pokaźny zestaw materiałów do nauki tego języka.

Rysunek 1. Narzędzie Playgrounds w akcji

PRZEGLĄD MOŻLIWOŚCI JĘZYKA

Rzućmy okiem na Swift od strony praktycznej. Na początek oczywiście szybkie
spojrzenie na program typu Hello, World! (patrz: Listing 1).

Listing 1. Program typu Hello, World! w Swift

println

(

"Hello, World!"

)

Pierwsze wnioski, które nasuwają się po analizie tego krótkiego programu, są
następujące:

» aplikację typu Hello, World! w języku Swift da się napisać w jednej linii!

» Swift nie wymaga stosowania średników jako separatorów instrukcji,

» w Swift korzystamy z nawiasów okrągłych przy wywoływaniu funkcji, nie-

jako w opozycji do nawiasów kwadratowych używanych przy wywoływa-
niu metod w Objective-C.

Programiści korzystający ze Swift będą się musieli pożegnać z rozróżnieniem
pomiędzy plikami nagłówkowymi (.h) a źródłowymi (.m). Programy pisane w
nowym języku Apple umieszczane są w pojedynczych plikach tekstowych
opatrzonych rozszerzeniem.swift.

Swift: rewolucja czy ewolucja?

Na tegorocznym WWDC Apple zafundowało wszystkim swoim developerom sporą
niespodziankę w postaci... nowego języka programowania! Swift, bo o tym właśnie
języku tu mowa, to dla wielu programistów duży znak zapytania. W niniejszym ar-
tykule postaram się przybliżyć czytelnikowi ten temat oraz udzielić odpowiedzi na
pytanie postawione w tytule: czy Swift należy postrzegać w kategoriach rewolucji,
czy po prostu mamy do czynienia z nieuchronną ewolucją...

background image
background image

18

/ 6

. 2014 . (25) /

JĘZYKI PROGRAMOWANIA

Szkolenia realizowane przez Akademię EITCA:

http://cg.eitca.pl

http://bi.eitca.pl

http://kc.eitca.pl

http://is.eitca.pl

Nie będzie też znaków plus (+) oraz minus (-), służących do rozróżniania

zwykłych metod od metod statycznych w Objective-C. Składnia definicji klasy
została w Swift bardzo uproszczona. Na Listingu 2 przedstawione są dwie pro-
ste definicje klas:

Shape oraz Square prezentujące takie mechanizmy języka

jak konstruktory, dziedziczenie czy przeciążanie funkcji wirtualnych.

Listing 2. Proste definicje klas w Swift

class

Shape

{

var

numberOfSides:

Int

=

0

var

name:

String

init

(

name:

String

)

{

self

.

name

=

name

}

func

toString

()

->

String

{

return

"A shape with \(numberOfSides) sides."

}

}

class

Square

:

Shape

{

var

sideLength:

Double

init

(

sideLength:

Double

,

name:

String

)

{

self

.

sideLength

=

sideLength

super

.

init

(

name:

name

)

numberOfSides

=

4

}

func

area

()

->

Double

{

return

sideLength

*

sideLength

}

override func

toString

()

->

String

{

return

"A square with sides of length \(sideLength)."

}

}

let test

=

Square

(

sideLength:

5.2,

name:

"my test square"

)

test

.

area

()

test

.

toString

()

Bardzo miłym akcentem jest wsparcie dla mechanizmu właściwości (ang. pro-
perties
) powiązanych z akcesorami (

get/set), znanego z takich języków pro-

gramowania jak C# czy AcionScript 3.0. Na Listingu 2a pokazana jest prosta
definicja klasy korzystająca z tego udogodnienia.

Listing 2. Przykład użycia mechanizmu właściwości w Swift

class

EquilateralTriangle

:

Shape

{

var

sideLength:

Double

=

0.0

init

(

sideLength:

Double

,

name:

String

)

{

self

.

sideLength

=

sideLength

super

.

init

(

name:

name

)

numberOfSides

=

3

}

var

perimeter:

Double

{

get

{

return

3.0

*

sideLength

}

set

{

sideLength

=

newValue

/

3.0

}

}

override func

toString

()

->

String

{

return

"An equilateral triagle with "

"sides of length \(sideLength)."

}

}

Dużą zmianą w stosunku do Objective-C jest niewątpliwie wprowadzenie me-
chanizmu typów uogólnionych (ang. generics). Dla przykładu, klasa

NSArray

z Objective-C może w zasadzie przechowywać obiekty dowolnego typu (przy
czym wszystkie one muszą dziedziczyć po protokole

NSObject). Rozwiązanie

to, pomimo stwarzania pozoru dużej elastyczności, w praktyce jest bardzo mało
odporne na błędy, przede wszystkim ze względu na fakt, iż kompilator nie jest w
stanie wykryć pewnych niezgodności, które objawiają się dopiero w czasie wy-
konania programu. Typy uogólnione w Swift, czyli mechanizm zbliżony do sza-
blonów języka C++, pozwalają tworzyć definicje klas parametryzowane typami.
W tym układzie niezgodność typów jest wykrywana już na etapie kompilacji.

Listing 3 pokazuje przykład użycia typów uogólnionych w Swift.

Listing 3. Typy uogólnione w Swift

struct IntPair

{

let

a:

Int

!

let

b:

Int

!

init

(

a:

Int

,

b:

Int

)

{

self

.

a

=

a

self

.

b

=

b

}

func

equal

()

->

Bool

{

return

a

==

b

}

}

let intPair

=

IntPair

(

a:

5,

b:

10)

intPair

.

a

// 5

intPair

.

b

// 10

intPair

.

equal

()

// false

struct Pair

<

T:

Equatable

>

{

let

a:

T

!

let

b:

T

!

init

(

a:

T

,

b:

T

)

{

self

.

a

=

a

self

.

b

=

b

}

func

equal

()

->

Bool

{

return

a

==

b

}

}

let pair

=

Pair

(

a:

5,

b:

10)

pair

.

a

// 5

pair

.

b

// 10

pair

.

equal

()

// false

let floatPair

=

Pair

(

a:

3.14159,

b:

2.0)

floatPair

.

a

// 3.14159

floatPair

.

b

// 2.0

floatPair

.

equal

()

// false

Skoro już mówimy o typach, trzeba jasno powiedzieć, że Swift jest językiem
silnie typowanym, co można uznać za duży krok do przodu w dziedzinie od-
porności na błędy. Ceną za silne typowanie jest zazwyczaj bardziej skompli-
kowana składnia (patrz: język C++). Projektanci Swift podjęli próbę zmierzenia
się z tym problemem, wprowadzając do swojego języka mechanizm inferencji
typów, czyli technikę stosowaną w językach statycznie typizowanych, która
zwalnia programistę z obowiązku specyfikowania typów, przerzucając obo-
wiązek ich identyfikacji na kompilator. Przykład zastosowania tego mechani-
zmu w Swift pokazany jest na Listingach 4a oraz 4b.

Listing 4a. Tworzenie instancji obiektu w Objective-C

Person

*

me

=

[[Person alloc] initWithName

:

@"Rafal Kocisz"

];

[me sayHello];

Listing 4b. Inferencja typów w Swift

var me

=

Person

(

name:

"Rafal Kocisz"

)

me

.

sayHello

()

background image

Szkolenia realizowane przez Akademię EITCA:

http://cg.eitca.pl

http://bi.eitca.pl

http://kc.eitca.pl

http://is.eitca.pl

background image

20

/ 6

. 2014 . (25) /

JĘZYKI PROGRAMOWANIA

Listingi te pokazują dwa przypadki tworzenia instancji obiektu klasy

Per-

son; jeden w Objective-C, zaś drugi w Swift. Jak widać, w drugim przypadku
kompilator sam jest w stanie wydedukować typ obiektu, co w rezultacie skut-
kuje znacznym uproszczeniem składni.

Dużo pozytywnych zmian pojawia się w Swift w związku z obsługą napi-

sów. W nowym języku od Apple napisy są pełnoprawnymi obiektami, można
je łatwo porównywać za pomocą operatora == czy łączyć dzięki zastosowaniu
operatorów + oraz +=. W Swift nie istnieje również rozróżnienie pomiędzy napi-
sami zmiennymi (ang. mutable) oraz niezmiennymi (ang. immutable), które wy-
stępowało w Objective-C. Niejeden programista ucieszy się też z faktu, że napi-
sy dostępne w ramach języka Swift domyślnie obsługują pełny zestaw znaków
Unicode! Co ciekawe, znaków tego rodzaju można używać również w nazwach
identyfikatorów. Biorąc pod uwagę to, jak niewygodna jest obsługa napisów w
języku Objective-C, opisane wyżej zmiany będą zapewne bardzo miłe progra-
mistom wytwarzającym aplikacje pod system iOS oraz OS X.

Warto też wspomnieć o znacznym usprawnieniu instrukcji

switch, któ-

ra w przypadku Swift'a - w odróżnieniu od Objective-C - obsługuje zarówno
napisy, jak i założone obiekty. Poza tym, instrukcja

switch w nowym języku

od Apple domyślnie wykonuje skok do kolejnej instrukcji tuż po wykonaniu
kodu umieszczonego w dopasowanej sekcji

case. W tym układzie słowo klu-

czowe

break (którego używanie wiąże się niejednokrotnie z powstawaniem

trudnych do wyśledzenia błędów) przestaje być potrzebne. Dla tych, którzy
mimo wszystko chcieliby mieć możliwość wywołania kilku sekcji

case pod

rząd, Swift oferuje słowo kluczowe

fallthrough, za pomocą którego można

tego rodzaju efekt uzyskać. Na Listingu 5 przedstawiony jest przykład użycia
instrukcji

switch w języku Swift.

Listing 5. Przykład użycia instrukcji switch w języku Swift

let vegetable

=

"red pepper"

switch

vegetable

{

case

"celery"

:

let vegetableComment

=

"Add some raisins and make ants on a log."

case

"cucumber"

,

"watercress"

:

let vegetableComment

=

"That would make a good tea sandwich."

case

let x where x

.

hasSuffix

(

"pepper"

):

let vegetableComment

=

"Is it a spicy \(x)?"

default

:

let vegetableComment

=

"Everything tastes good in soup."

}

Koniec końców, autorom Swift należy się duża pochwała za dodanie do tego
języka elementów programowania funkcjonalnego. Przede wszystkim, w
Swift funkcje są pełnoprawnymi obiektami, które można przekazywać jako
parametry, przechowywać w kontenerach itd. Swift oferuje również domknię-
cia leksykalne (ang. closures), mechanizm podobny nieco do bloków (ang.
blocks) z Objective-C 2.0, jednakże bardziej potężny: porównywalny z wyra-
żeniami lambda rodem z języka C#. Listing 6 zawiera kilka przykładów użycia
opisanych wyżej mechanizmów w kodzie Swift (między innymi: funkcja, która
zwraca funkcję; funkcja, która przyjmuje inną funkcję jako parametr, a także
praktyczne zastosowanie domknięcia leksykalnego).

Listing 6. Elementy programowania funkcjonalnego w języku Swift

func

makeIncrementer

()

->

(

Int

->

Int

)

{

func

addOne

(

number:

Int

)

->

Int

{

return

1

+

number

}

return

addOne

}

var increment

=

makeIncrementer

()

increment

(7)

func

hasAnyMatches

(

list:

Int

[],

condition:

Int

->

Bool

)

->

Bool

{

for

item

in

list

{

if

condition

(

item

)

{

return

true

}

}

return

false

}

func

lessThanTen

(

number:

Int

)

->

Bool

{

return

number

<

10

}

var numbers

=

[20,

19,

7,

12]

hasAnyMatches

(

numbers

,

lessThanTen

)

numbers

.

map

(

{

(

number:

Int

)

->

Int

in

let result

=

3

*

number

return

result

})

Na tym będziemy kończyć nasz pobieżny przegląd możliwości języka. Celowo
użyłem tutaj słowa „pobieżny”, jako że moim zamiarem było przede wszystkim
przybliżenie czytelnikowi istoty języka Swift; pod takim też kątem starałem się
dobrać prezentowane przykłady. W dalszej części artykułu postaram się odpo-
wiedzieć na kilka pytań, które mogły pojawić się w Twojej głowie w związku
z pojawieniem się nowego języka do Apple.

QUO VADIS OBJECTIVE-C?

Póki co, drogi przyjacielu, nigdzie się nie wybieram! ;) - taką odpowiedź usłysze-
libyśmy zapewne od języka Objective-C, gdyby umiał on mówić. Objective-C
mówić oczywiście nie potrafi, jednakże umiejętność tę posiadły rzesze pro-
gramistów, którzy na co dzień korzystają z tego języka. Wyobrażasz sobie ich
reakcję w sytuacji, gdyby miał on z dnia na dzień zniknąć, wyparować? Taki
eksperyment myślowy polecam wszystkim tym, którzy prowadzą zagorzałe
dyskusje na temat najbliższej przyszłości Objective-C.

Trochę trudniej jest odpowiedzieć na inne pytanie: „którego języka w

tym układzie warto się uczyć?“. Generalnie, wydaje się, że opracowując Swift,
Apple chciał z jednej strony obniżyć barierę wejścia dla tych, którzy dopiero
uczą się programować, zaś z drugiej strony - zrobić ukłon w kierunku młodszej
generacji programistów wychowanych na takich językach jak Python, Ruby
czy C#. Dla tych programistów przesiadka na Swift będzie czymś zupełnie
naturalnym.

Co z kolei ze starszą generacją programistów (do której sam się już nieste-

ty w pewnym stopniu zaliczam ;))? Dla tych ludzi, zakorzenionych w językach
pokroju C oraz C++, Swift będzie prawdopodobnie nieco mniej atrakcyjny.
Osobom tego pokroju zapewne łatwiej byłoby nadal używać Objective-C,
który jest przecież nadzbiorem klasycznego C.

Trudno przewidywać dalsze ruchy Apple w zakresie wsparcia dla języków

programowania; podejrzewam, że przed nami jest dość ciekawy okres przej-
ściowy, czas, w którym Apple będzie starać się przesunąć środek ciężkości
zainteresowania programistów w kierunku Swift'a. Jednakże jestem głęboko
przekonany, że Objective-C jeszcze długo pozostanie z nami.

REWOLUCJA CZY EWOLUCJA?

Biorąc pod uwagę to wszystko, co opisałem powyżej, moja odpowiedź na to
pytanie może być tylko jedna: zdecydowanie ewolucja!

No bo zastanówmy się: w dziedzinie języków programowania Swift prze-

cież żadną rewolucję nie jest. Jest to oczywiście wysokiej klasy nowoczesny
język programowania, ale nie wprowadza nic takiego, co można by uznać za

background image

21

/ www.programistamag.pl /

SWIFT: REWOLUCJA CZY EWOLUCJA?

reklama

jakąś rewelację. Gdyby Apple zdecydował się wprowadzić np. własny dialekt
Lispu jako swój nowy, oficjalny język programowania, to być może byłbym
skłonny uznać taki ruch za rewolucyjny... Swoją drogą, szkoda, że się na to nie
zdecydowali... Trudno mówić również o rewolucji w kontekście jakichś gwał-
townych zmian (Objective-C póki co pozostaje z nami).

W mojej opinii Swift'a należy postrzegać jako nowe, fajne narzędzie,

które dostaliśmy w prezencie od firmy Apple, i podchodzić to tego tak, jak
należy podchodzić do narzędzia: to znaczy w sposób pragmatyczny, a nie
emocjonalny.

Bardzo spodobał mi się pewien komentarz na jednym z forów, na którym

rozgorzała dyskusja dotycząca przyszłości Swift'a oraz Objective-C. Autor
komentarza zauważył, że w gruncie rzeczy kwestia języka, z którego w da-

nym momencie korzystamy, jest w dużym stopniu drugorzędna. Prawdziwa
trudność (a zarazem sztuka) związana z programowaniem leży w umiejętno-
ści konstruowania algorytmów, projektowania interfejsów, klas itd. Z kolei
w przypadku programowania systemów iOS oraz OS X znacznie ważniejszym
(i trudniejszym) zadaniem w stosunku do opanowania języka jest poznanie
oraz zrozumienie olbrzymiej biblioteki klas dostępnych w ramach framewor-
ków Cocoa oraz Cocoa Touch.

W tym ujęciu, tym, którzy pytają: czego warto się dziś uczyć, odpowiem tak:

uczcie się programować aplikacje pod systemy iOS oraz OS X, zgłębiajcie API
Cocoa oraz Cocoa Touch. A czy będziecie używać do tego celu języka Objec-
tive-C, czy może Swift'a, wydaje mi się naprawdę kwestią drugorzędną. Wy-
bierzcie sobie po prostu ten język, który lepiej Wam pasuje! :)

Rafał Kocisz

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 Koordyna-
tor Projektu w firmie BLStream.

background image

22

/ 6

. 2014 . (25) /

PROGRAMOWANIE GRAFIKI

Piotr Sydow

W

poniższym artykule zobaczymy, w jaki sposób zbudowany jest
opisywany mechanizm na przykładzie procesora geometrii, oraz
przedstawimy sposób, w jaki można wykorzystać jego zalety, nie

tylko do zadań związanych z tworzeniem trójwymiarowych obrazów, ale rów-
nież w celu przeprowadzania obliczeń czy symulacji.

Nazwa Transform Feedback (TFBK) została zarezerwowana w OpenGL dla

części funkcjonalności potoku graficznego odpowiedzialnego za ponowne
wykorzystanie przetwarzanej geometrii. W dużym skrócie: mechanizm ten
polega na ciągłym zapisywaniu aktualne przetwarzanych prymitywów gra-
ficznych (punkt, trójkąt etc.) do zewnętrznego bufora, o formacie identycz-
nym do bufora wejściowego. Po przetworzeniu wszystkich danych z bufora
wejściowego następuje zamiana buforów: wejściowego z wyjściowym oraz
wysłana komenda wymuszająca kolejny przebieg przetwarzania. W ten spo-
sób dane, raz wysłane do pamięci VRAM karty graficznej, będą ciągle zastępo-
wane nowymi bez konieczności ingerencji głównego procesora w zawartość
pamięci VRAM.

Mechanizm TFBK zalicza się do statycznych elementów architektury

OpenGL. Jego funkcjonowanie jest kontrolowane tylko przy użyciu zmien-
nych maszyny stanu, natomiast nie ogranicza to w żaden sposób jego możli-
wości. OpenGL posiada dużo ustawień, które pozwalają dostosować sposób
jego działania do wymagań programisty. Sposób jego wykorzystania w dużej
mierze zależy od aktualnej konfiguracji potoku przetwarzającego. W danym
momencie tylko jeden procesor geometrii może przesyłać dane poprzez
magistralę TFBK do bufora zewnętrznego. Natomiast tylko ostatni jest bez-
pośrednio z nią połączony. Oznacza to, że jeśli w potoku graficznym znajduje
się tylko procesor wierzchołków VS, będzie on bezpośrednio połączony z bu-
forem zewnętrznym mechanizmu TFBK. Jeżeli natomiast włączony zostanie
dodatkowo procesor geometrii Geometry Shader, to będzie bezpośrednio
połączony z buforem zewnętrznym, a bezpośrednie powiązanie Vertex Sha-
der z mechanizmem TFBK przestanie istnieć. Gdy uruchomimy dodatkowo
procesor Tesselation Shader, w dalszym ciągu bezpośrednio z mechanizmem
TFBK będzie połączony procesor Geometry Shader. Jest to spowodowa-
ne kolejnością, w jakiej występują poszczególne procesory w architekturze
OpenGL (Rysunek 1).

Włączenie funkcji TFBK nie jest jednoznaczne z przekierowaniem strumie-

nia danych tylko do bufora zewnętrznego. Efekt działania takiego algorytmu
można wyświetlić na ekranie, jeśli nie zostanie to zablokowane poprzez wy-
wołanie polecenia:

glEnable

(

GL_RASTERIZER_DISCARD

);

Przetwarzanie geometrii przy pomo-

cy mechanizmu Transform Feedback

OpenGL 4.3

Architektura OpenGL od początku swojego istnienia ulega ciągłym modyfikacjom.
Wraz z kolejnymi wersjami standardu pojawiają się nowe możliwości przetwarzania
zarówno geometrii, jak i obrazu. Siłę standardu można zawdzięczyć wielu elementom
architektury. Jednak bardzo istotny wpływ na jego popularność miało zunifikowanie
formatu danych wyjściowych oraz danych wejściowych dla każdego etapu przetwarza-
nia. Pozwoliło to na projektowanie algorytmów, w których wynik poprzednich obliczeń
służy jako źródło dla następnych. Rekurencyjny charakter architektury OpenGL jest do-
stępny zarówno po stronie procesora obrazu Fragment Shader (FS), jak i procesorów
geometrii: Vertex Shader (VS), Tesselation Shader (TS) oraz Geometry Shader (GS).

Powoduje ono całkowite zablokowanie przesyłania geometrii do mechani-
zmu odpowiedzialnego za rasteryzację geometrii. W efekcie program Frag-
ment Shader nie jest uruchamiany.

glDisable

(

GL_RASTERIZER_DISCARD

);

Wywołanie powyższego polecenia przywraca normalny przepływ danych z
części przetwarzającej geometrię do elementów potoku graficznego odpo-
wiedzialnych za przetwarzanie obrazu.

Do poprawnego funkcjonowania mechanizmu TFBK niezbędne jest zdefi-

niowanie zmiennych oznaczonych kwalifikatorem

out wewnątrz ostatniego

programu przetwarzających geometrię. Pozostałe programy również wymaga-
ją występowania takich zmiennych (poprawna komunikacja w potoku), jednak
z perspektywy działania TFBK nie są one istotne. Dowiązanie magistrali TFBK
do wyjścia Shadera następuje poprzez wywołanie polecenia, którego jednym
z parametrów jest tablica z nazwami zmiennych

varying. Tylko dane okre-

ślonych zmiennych zostaną przekierowane do bufora Transform Feedback
Buffer. Zmienne, które zostały przekierowane tylko do bufora TFBK, mogą być
rozpoznane przez sterownik OpenGL jako nieaktywne. W związku z tym należy
każdorazowo po wywołaniu poniższego polecenia uruchomić proces łączenia
programu cieniującego przy pomocy procedury

glLinkProgram(program).

void

glTransformFeedbackVaryings

(

GLuint

program,

GLsizei

count,

const

GLchar

*

const

* varying,

GLenum

bufferMode

);

Bardzo istotnym elementem jest określenie ostatniego parametru

buffer-

Mode. Jego wartość zależy od sposobu, w jaki przygotowane są dane wejściowe
do programu cieniującego geometrię VS. Oczywiście jest to wymagane tylko w
przypadku, gdy program będzie działał na zasadzie symulacji. W innym przy-
padku nie ma konieczności dokładnego dopasowania buforów wejściowego
z wyjściowym. Możliwe są dwie wartości zaprezentowane w poniższej tabeli.

GL_SEPARATE_ATTRIBS

Każdy atrybut wysyłany jest do osobne-
go bufora TFBK

GL_INTERLEAVED_ATTRIBS Wszystkie atrybuty zapisywane są w

jednym buforze TFBK

Tabela 1. Możliwe wartości parametru bufferMode

background image

23

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

Drugim istotnym elementem uruchamiania TFBK, po ustaleniu zmien-

nych, jakie mają zostać zapisywane, jest określenie miejsca, do którego należy
je wysłać. Podany poniżej przykład prezentuje trzy możliwe konfiguracje (Li-
sting 1). Przez wybranie polecenia: a) uzyskujemy możliwość zapisania tylko
jednego atrybutu dla parametru

bufferMode = GL_SEPARATE_ATTRIBS,

natomiast dla drugiego parametru

GL_INTERLEAVED_ATTRIBS ilość atry-

butów jest ograniczona przez możliwości karty graficznej, sterownika oraz
wersji biblioteki OpenGL.

W każdym z trzech przypadków schemat postępowania jest podobny.

W pierwszej kolejności należy uzyskać dostęp do zasobu karty graficznej
poprzez wygenerowanie bufora. Następnie utworzyć dowiązanie uzyskane-
go zasobu do punktu dowiązań

GL_TARNSFORM_FEEDBACK_BUFFER. Osta-

tecznie rezerwujemy wymagany obszar pamięci VRAM (Listing 1). Wybierając
procedurę wiązania zasobu z miejscem na zasób TFBK, należy kierować się
wymaganiami danego algorytmu. Wszystkie trzy procedury działają podob-
nie, natomiast różnią się przede wszystkim elastycznością konfiguracji. Naj-
więcej możliwości oferuje procedura c), dzięki której otrzymujemy możliwość
dowiązania jednego bufora do wszystkich punktów dowiązania TFBK.

Listing 1. Przygotowanie bufora VBO oraz dowiązanie do punktu
dowiązań TFBK

GLuint

buffer;

glGenBuffers

(1, &buffer);

a. glBindBuffer

(

GL_TRANSFORM_FEEDBACK_BUFFER

, buffer);

b. glBindBufferBase

(

GL_TRANSFORM_FEEDBACK_BUFFER

, index, buffer);

c. glBindBufferRange

(

GL_TRANSFORM_FEEDBACK_BUFFER

, index,

buffer, offset, size);

glBufferData

(

GL_TRANSFORM_FEEDBACK_BUFFER

, size,

NULL

,

GL_DYNAMIC_COPY

);

Rysunek 2. Schemat przedstawiający wycinek kontekstu graficznego, odpowie-

dzialnego za przechowywanie informacji o konfiguracji mechanizmu Transform

FeedBack – buforach danych VBO dowiązanych do TFBK

W ogólności, dostępny jest tylko jeden bieżący punkt dowiązania TFBK (Rysu-
nek 2). Oznacza to, że w danym momencie możemy mieć dostęp, z poziomu

Rysunek 1. Schemat przepływu danych z zaznaczeniem kierunków ich przepływu, dla podstawowo skonfigurowanego potoku graficznego (VS, GS, FS). Na schema-

cie zostały przedstawione możliwe scenariusze przepływu danych dla pojedynczego przebiegu PASS N

background image

24

/ 6

. 2014 . (25) /

PROGRAMOWANIE GRAFIKI

aplikacji, do ogólnego punktu dowiązania. W celu uzyskania dostępu do bu-
forów dowiązanych w punktach od 1 do N, należy każdorazowo wykorzystać
procedurę b) lub c) przedstawioną na Listingu 1. Jej użycie spowoduje dowią-
zanie bufora do konkretnego punktu oraz do ogólnego punktu dowiązania.
Należy wziąć ten fakt pod uwagę, gdyż nie ma fizycznej możliwości odniesie-
nia się do konkretnych punktów dowiązania przy pomocy funkcji

glBuffer-

Data() oraz glCopyBufferSubData().

Proces przechwytywania danych rozpoczyna się po uruchomieniu pro-

cedury rysującej, pod warunkiem, że występuje ona pomiędzy poleceniami
kontrolującymi pracę mechanizmu TFBK (Listing 2).

Przechwytywanie można dowolnie zatrzymywać oraz wznawiać, nie nale-

ży jednak w trakcie jego działania zmieniać dowiązania buforów.

Listing 2. Funkcje kontrolujące pracę mechanizmu TFBK

void

glBeginTransformFeedback

(

GLenum

primitiveMode);

void

glResumeTransformFeedback

();

void

glPauseTransformFeedback

();

void

glEndTransformFeedback

();

Parametr

primitiveMode określa typ prymitywu graficznego, dozwolone

są trzy podstawowe kształty graficzne. Każdemu z nich odpowiada grupa
bardziej szczegółowych prymitywów, które powinny pojawić się na wejściu
programu przetwarzającego wierzchołki (Tabela 2).

GL_POINTS

GL_POINTS

GL_LINES

GL_LINES,

GL_LINE_STRIP,

GL_LINE_LOOP

GL_TRIANGLES

GL_TRIANGLES,

GL_TRIANGLE_STRIP,

GL_TRIANGLE_FAN

Tabela 2. Wartości parametru primitiveMode

Mechanizm TFBK może być przede wszystkim stosowany w animacjach, któ-
re wymagają ciągłego obliczania nowej pozycji czy orientacji na podstawie
poprzednich danych. Innym ciekawym zastosowaniem tego rozwiązania jest
próba symulowania cząsteczek bądź ciał sprężystych. Na potrzeby artykułu
powstała aplikacja, która prezentuje możliwości TFBK. Symulowana tkanina
jest gęstą siatką wierzchołków powiązanych z sobą wiązaniami sprężystymi.
Każdy z wierzchołków posiada pozycję, przypisaną masę oraz prędkość. Po-
zycja oraz prędkość są na bieżąco obliczane oraz poprzez TFBK zapisywane w
buforze zewnętrznym. Po zamianie buforów odczytywane są nowe wartości
pozycji oraz prędkości. Na ich podstawie liczone są nowe dane z uwzględnie-
niem sił działających na każdy z wierzchołków. Algorytm składa się z dwóch
odrębnych przebiegów cieniujących. Pierwszy wykonuje obliczenia symu-
lowanego obiektu, natomiast drugi służy tylko do wyświetlenia aktualnego
stanu symulacji.

Algorytm do poprawnego działania wymaga wiedzy o sąsiadujących

wierzchołkach (Rysunek 3). Aby uzyskać te dane, należy utworzyć mapę po-
łączeń, w postaci listy cztero-elementowych wektorów. Każdy element z listy
odpowiada każdemu symulowanemu wierzchołkowi, natomiast poszczegól-
ne wartości wektora określają wierzchołki, znajdujące się w relacji sąsiadu-
jącej, vec4(lewo, góra, prawo, dół). Wierzchołki, dla których nie są obliczane
przesunięcia, ze względu na siłę, są wprawiane w ruch, który wprowadza
cały układ w stan niestabilności (-1,-1,-1,-1). Na każdy z wierzchołków działa
siła związana z grawitacją oraz masą wierzchołka. Dodatkowo występuje siła
sprężystości, która wynika z niestabilności stanu, w jakim znajdują się cztery
sąsiednie wierzchołki. W symulacji występuje również siła kompensująca stan
niestabilności, która ogranicza stan niestabilny układu.

Rysunek 3. Geometria siatki ciała sprężystego oraz relacja sąsiedztwa między

wierzchołkami

Listing 3. Vertex Shader (przebieg symulacji). Deklaracja zmiennych,
danych wejściowych do programu przetwarzającego wierzchołki

// akutalna pozycja oraz masa wierzchołka

layout

(location =

0

)

in

vec4

point_position_mass;

// aktualna prędkość wierzchołka

layout

(location =

1

)

in

vec3

point_velocity;

// sąsiedztwo wierzchołka

layout

(location =

2

)

in

ivec4

point_connections;

// aktualne pozycje wszystkich wierzchołków.

uniform

samplerBuffer

tex_point_position_mass;

Listing 4. Vertex Shader (przebieg symulacji). Deklaracja zmien-
nych, danych wyjściowych, zapisywanych do bufora zewnętrznego
przy pomocy mechanizmu TFBK

out

vec4

tfbk_point_position_mass;

// nowa pozycja wierzchołka

out

vec3

tfbk_point_velocity;

// nowa prędkość wierzchołka

Listing 5. Vertex Shader (przebieg symulacji). Deklaracja, definicja
zmiennych danych sterujących pracą symulowanego układu

uniform

float

t =

0.07f

;

// stała czasowa t

uniform

float

angle;

// wartość kąta animacji

// kierunek wektora grawitacji

uniform

vec3

gravity_dir =

vec3

(

0.0f

,

1.0f

,

0.0f

);

uniform

float

gravity_scale =

0.08f

;

// wartość grawitacji

uniform

float

spring_k =

7.1f

;

// współczynnik sprężystości

uniform

float

spring_att =

2.8f

;

// współczynnik tłumienia

uniform

float

spring_rest =

0.88f

;

// współczynnik spoczynku

Symulację można kontrolować poprzez zmianę wartości zmiennych progra-
mu Vertex Shader (Listing 5). Wartości są na bieżąco wysyłane do programu
cieniującego wierzchołki. Umożliwiają zmianę siły grawitacji, właściwości
sprężyste układu oraz masę cząstek.

Listing 6. Vertex Shader (przebieg symulacji). Główna funkcja
realizująca symulację fizyczną

void

main(

void

)

{

// aktualna pozycja punktu

vec3

p = point_position_mass.xyz;

// aktualna masa punktu

float

m = point_position_mass.w;

// aktualna prędkość punktu

vec3

u = point_velocity;

background image

25

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

// wektor grawitacji

vec3

g = gravity_dir * gravity_scale;

// siła związana z masą oraz grawitacją kompensowana tłumieniem

vec3

F = g * m - spring_att * u;

bool

fixed_node =

true

;

for

(

int

i =

0

; i <

4

; i++){

if

(point_connections[i] != -

1

){

// pozycja sąsiedniego wierzchołka

vec3

q =

texelFetch

(tex_point_position_mass, point_

connections[i]).xyz;

// odległość między wierzchołkami

vec3

d = q - p;

float

x =

length

(d);

// wypadkowa sił działających na wierzchołek związana z

// niestabilnością układu sprężystego.

F += -spring_k * (spring_rest - x) *

normalize

(d);

fixed_node =

false

;

}

}

if

(fixed_node){

F =

vec3

(

0.0f

);

p.x +=

cos

(angle) *

0.007f

;

}

// przyspieszenie związane z wypadkową siłą działającą na masę punktu

vec3

a = F / m;

// całkowite przesunięcie związane z siłą oraz masą

vec3

s = u * t +

0.5f

* a * t * t;

// nowa prędkość wierzchołka

vec3

new_v = u + a * t;

// nowa pozycja wierzchołka

vec3

new_p = p + s;

// wysłanie nowej pozycji oraz masy wierzchołka do TFBK

tfbk_point_position_mass =

vec4

(new_p, m);

// wysłanie nowej prędkości wierzchołka do TFBK

tfbk_point_velocity = new_v;

}

Listing 7. Fragment Shader (przebieg symulacji). Program cienio-
wania fragmentu

#version

430

core

out

vec4

color;

void

main(

void

)

{

color =

vec4

(

1.0f

,

1.0f

,

1.0f

,

1.0f

);

}

Program cieniujący fragmenty nie wpływa na przebieg symulacji (Listing 7),
musi natomiast występować w celu poprawnego działania potoku graficzne-
go. Jego działanie nie jest widoczne na ekranie, ponieważ zostało to wcześniej
zablokowane przy pomocy zmiennej stanu:

GL_RASTERIZER_DISCARD.

Piotr Sydow

piotr.sydow.gm@gmail.com

Absolwent Informatyki na wydziale Elektroniki, Telekomunikacji i Informatyki Politechniki
Gdańskiej. Zawodowo skoncentrowany na architekturze, programowaniu silników graficz-
nych oraz GPU. W wolnym czasie tworzy oprogramowanie open-source oraz urządzenia i
systemy elektroniczne. W GLSL programuje od 5 lat.

Karol Sobiesiak i Piotr Sydow są w trakcie prac
nad książką „Shadery: Zaawansowane pro-
gramowanie w GLSL
”. Poruszone w niej te-
maty będą związane z programowaniem karty
graficznej oraz grafiką 3d. W ciekawy sposób
zostaną przedstawione nowoczesne techniki
wizualizacji oraz programowania GPU zarów-
no od strony praktycznej, jak i teoretycznej,
opierając się na najnowszym standardzie trój-
wymiarowej grafiki OpenGL 4.3 (4.4).

Rysunek 4. Ilustracja efektu działania algorytmu na siatkę ciała sprężystego.

Źródło:

https://github.com/kalist/gl4.3_transform_feedback_springs_1

Do poprawnego uruchomienia aplikacji mogą być wymagane biblioteki:

» QT 5.2.0,

» OpenGL 4.3,

» GLEW

Na podstawie: [1] Graham Sellers, „OpenGL Superbible: Comprehensive Tuto-
rial and Reference”, Addison Wesley, 6th edition (31 July 2013).

reklama

background image

26

/ 6

. 2014 . (25) /

PROGRAMOWANIE GRAFIKI

Michał Chlebiej

POCZĄTKI

Mój pierwszy obraz medyczny, z którym się zetknąłem około 2000 r., był po-
jedynczym przekrojem danych tomografii komputerowej głowy. Był to plik o
wielkości pół megabajta, o którym koledzy powiedzieli mi tylko, że rozmiar
danych to 512×512, 2 bajty na piksel i zaczynają sie po '0x7fe0' (później do-
wiedziałem się, czym jest format DICOM i jak dużo informacji może on za-
wierać oprócz samych intensywności pikseli). Napisałem prosty program w
środowisku C++ Builder, który znalazł magiczną frazę, przeczytał plik do koń-
ca i umieścił w tablicy, a następnie jakoś wyświetlił obraz. Wyświetlenie pole-
gało na przeskalowaniu zakresu danych (0-4096) na skalę 256 intensywności
i zobrazowaniu danych w skali szarości. To był wielki sukces – przekrój głowy
prawdziwego człowieka wylądował na moim ekranie. Kilka dni później do-
tarł do mnie zestaw kolejnych warstw poprzecznych z tego samego badania.
Eksploracja przestrzeni trójwymiarowej zakończyła się na dodaniu suwaka
pod wyświetlanym obrazkiem, który odsłaniał przed użytkownikiem kolejne

struktury anatomiczne mózgowia. Z takimi obrazami do niedawna pracowało
większość lekarzy – mimo iż informacja zawarta w danych jest przestrzenna, z
jakiegoś powodu lekarze woleli oglądać jedynie płaskie projekcje. Jedyne pa-
rametry, którymi manipulowali, były to tzw. „okno i poziom”, które przekładały
się na progowanie wartości minimalnej i maksymalnej, do których rozciągana
była paleta 256 odcieni szarości. Trójwymiar nie był aż tak kuszący, bo badania
w polskich szpitalach wykonywane były na starszych modelach tomografów
przeprowadzających badanie wolno, przy okazji serwując dużą dawkę szko-
dliwego promieniowania RTG. Radiolodzy wykonywali badania głowy bardzo
wybiórczo, wykonując jedynie kilkanaście projekcji poprzecznych na całą
głowę. Z takich danych zbudowanie trójwymiarowej bryły nie było zbytnio
ciekawe ani sensowne. Niedługo później dostałem możliwość pracy z danymi
pochodzącymi z ośrodków badawczych z Niemiec. Nowoczesne tomografy
dostarczyły obraz głowy i miednicy tego samego pacjenta zawierający ponad
setkę przekrojów poprzecznych. Taka ilość nie tylko zachęcała, ale wręcz zmu-
szała do przejścia do pracy w przestrzeni trójwymiarowej.

Budowa oprogramowania do anali-

zy i przetwarzania trójwymiarowych

obrazów medycznych

Obrazy medyczne stanowią niewyczerpalne źródło inspiracji naukowych. Artykuł
ten jest próbą krótkiej opowieści o mojej niekończącej się przygodzie z takimi
obrazami oraz o tym, jak próbowałem i wciąż próbuję zamknąć różne pomysły i
idee w jednym dedykowanym oprogramowaniu. Oprogramowanie to wykorzysta-
ne zostało w projekcie „Interactive fusion system of multiple 3D data as a surgical
preoperative strategy and educational tool“ tworzonym wspólnie z mgr Andrzejem
Rutkowskim oraz naukowcami z Wydziału Nauk Medycznych Uniwersytetu War-
mińsko-Mazurskiego, które otrzymało główną nagrodę na targach wynalazczości w
Brukseli w 2013 roku.

Rysunek 1. Pierwsza wersja platformy

background image

27

/ www.programistamag.pl /

BUDOWA OPROGRAMOWANIA DO ANALIZY TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH

PIERWSZA PLATFORMA

Z tymi niespotykanymi w Polsce danymi zacząłem pracować na stażu w
Niemczech, gdzie miałem przygotować materiał do pracy magisterskiej.
Cel wydawał się mnie wtedy przerastać – wydobyć dane 3D szczęki i talerza
kości biodrowej, a następnie przy pomocy przygotowanych narzędzi prze-
prowadzić wirtualny zabieg zaplanowania operacji przeszczepu kostnego
fragmentu kości z miednicy do żuchwy. Wybór narzędzi, na jakie zostałem
wtedy nakierowany, towarzyszy mi do dziś. Język programowania C++. Inter-
fejs użytkownika – miał być szybki w implementacji oraz dostępny na różne
platformy – wybór padł na bibliotekę QT. Trójwymiar – tu były spore wahania
– czy wybrać czysty OpenGL, czy może wykorzystać nakładkę, ze znacznie
rozszerzoną funkcjonalnością wizualizacyjną. Wybór padł na bibliotekę VTK,
którą w prosty sposób można było zintegrować z GUI biblioteki QT. Wybór
okazał się świetny – dynamiczne interfejsy budowało się bez użycia kreato-
rów GUI niezwykle szybko, a pomysły w trójwymiarze VTK realizował za po-
mocą prostego i czytelnego kodu.

Przygotowane i przetestowane oprogramowanie działało poprawnie (Ry-

sunek 1), ale zostało zbudowane bez gruntownego przemyślenia i solidnego
zaprojektowania. W trakcie pisania oprogramowania okazało się, że szybko
wyszliśmy od dwuwymiarowych danych źródłowych i potrzebne są dane trój-
wymiarowe dwojakiego rodzaju: oryginalne wolumeny 3D oraz powierzch-
nie reprezentujące wyodrębnione obiekty. Wolumeny 3D należało przecho-
wywać w tablicach jednowymiarowych i wyświetlać za pomocą renderingu
wolumetrycznego, natomiast powierzchniowe siatki trójkątne z posegmen-
towanych danych uzyskiwane były za pomocą algorytmu Marching Cubes i
po wygładzeniu wyświetlane. W pierwszej wersji stworzone zostały 3 okna 3D
– po jednym dla każdego rodzaju zadania – rednering powierzchniowy i edy-
cja danych siatkowych, dopasowywanie siatek oraz rendering wokseli (trój-
wymiarowych pikseli). Interakcja odbywała się za pomocą mało wygodnych
suwaków w globalnym (nie związanym z ekranem układem współrzędnych).
Poza tym program pisany był „prawie obiektowo", niby były obiekty, ale i tak
każda próba rozszerzenia funkcjonalności kodu kończyła się pisaniem czegoś
od zera (np. kolejnego okna 3D do innej funkcjonalności). Funkcjonalność na

tamten czas zamykała się w: wczytaniu danych w formacie DICOM (własny re-
ader formatu DICOM i interpretacji serii), oczyszczeniu danych wolumetrycz-
nych (filtracja medianą ważoną), segmentacji – siermiężny rozrost obszaru z
progami globalnymi, wygenerowania powierzchni, oraz ich postprocessingu
(dzięki VTK), dopasowaniu powierzchni – algorytm Levenberg-Marquardt z
popularnego Numerical Recipes in C i szereg drobnych narzędzi pozwalają-
cych na przeniesienie wykonanych operacji geometrycznych z danych po-
wierzchniowych na dane wolumetryczne. Przykładowe elementy pokazane
na Rysunku 1.

Niestety, źle zaprojektowane (a właściwie wcale nie projektowane, tylko

pisane „na hura") oprogramowanie musiało szybko zakończyć swój żywot.
Trzeba było je napisać od początku zgodnie z góry ustalonymi i przemyśla-
nymi zasadami.

PLATFORMA, WERSJA 2.0

W 2003 roku projekt został gruntownie przebudowany. Wiadomo było już, że:
dane wejściowe i wyjściowe mogą być zarówno siatkowe, jak i wolumetrycz-
ne, danych wczytanych do aplikacji może być dużo zarówno jednego, jak i
drugiego rodzaju, operacje będą mogły być wykonywane na dowolnej kom-
binacji tychże danych, interakcja wzajemna mogłaby się odbywać, równocze-
śnie używając widoków 2D (wzajemnie prostopadłych przekrojów danych
wolumetrycznych), jak i danych wolumetrycznych oraz zanurzonych w nich
siatkach trójkątnych, GUI musi umożliwiać dowolną rozbudowę (wiele, wiele
nowych zakładek), program musi być budowany w pełni obiektowo, projekt
musi być przystosowany do modyfikacji.

Tego ostatniego punktu oczywiście nie dało się zapewnić, ale trzeba było

przemyśleć jak najwięcej już na etapie projektowania. Znałem wtedy jeden
z projektów, który miał podobne cele i był tworzony przez grupę programi-
stów. Niestety zaplanowano aplikację tak bardzo ogólnie i tak bardzo rozsze-
rzalnie, że program w pierwszej wersji stanowił już ogromną i złożoną platfor-
mę, nie dając praktycznie żadnej uchwytnej funkcjonalności. Należało zatem
zachować prostotę, szczególnie biorąc pod uwagę fakt, że rozwijany będzie
tylko przez jedną osobę.

Rysunek 2. Druga – aktualna wersja platformy

background image

28

/ 6

. 2014 . (25) /

PROGRAMOWANIE GRAFIKI

Wybór narzędzi się praktycznie nie zmienił, a jedynie uaktualniły się wer-

sje bibliotek. Po przebudowie wykonanej ponad 10 lat temu aplikacja trzyma
się dzielnie, przyjmując wszystkie nowe pomysły bez większego ingerowa-
nia w rdzeń platformy. Dane zarówno wolumenów, jak i siatek trzymane są
w obiektach przechowujących zarówno ich indywidualne cechy, jak również
zależności z innymi obiektami danych. Dane trzymane są w listach (osobne li-
sty dla siatek i wolumenów), ale za pomocą bardzo prostego, dobudowanego
później mechanizmu można je agregować w grupy i operować na grupach
obiektów (filtracje, przekształcenia geometryczne itp.).

MODYFIKACJE

Prawdziwym testem architektury projektu jest jego modyfikowanie i rozwój
– nowe podejście do danych, albo wręcz nowy typ danych. Nowe typy da-
nych pojawiały się kilkakrotnie i to one stanowiły duże wyzwania. Na począ-
tek przyszły dane RGB – dane, w których kolor nie był opisywany jedynie za
pomocą intensywności reprezentowanej przez dwa bajty, tym razem doszły
dwie nowe składowe. Dane z projektu Visible Human Project (VHP) zawierały
oprócz pełnych skanów CT i MRI także serię fotografii o wysokiej rozdziel-
czości – skany poprzeczne wykonane zostały na podstawie prawdziwych
widoków poprzecznych zamrożonych w bryle lodu zwłok. Kolejne warstwy
anatomii były fizycznie zestrugiwane. Rozwiązanie tymczasowe zastosowane
dla takich danych okazało się być trwałym. W obiekcie RawData opisującym
dane wolumetryczne (za pomocą parametrów odczytanych z plików DICOM)
znajdowała się od zawsze tablica danych typu unsigned short. W wersji „kolo-
rowej" dodane zostały dwie tablice typu unsigned char odpowiadające skła-
dowym G i B. I tak, podstawowe funkcjonalności zawsze działały na takich da-
nych – przekształcając jedynie dane z kanału R. Natomiast jeśli była potrzeba
wykorzystania dodatkowej informacji, filtr mógł być napisany specjalnie dla
pozostałych kanałów. Można powiedzieć, że jest to typowa zmiana „na chwil-
kę", ale o dziwo trzyma się dzielnie i, co ważniejsze, spełnia w pełni swoje za-
danie. Można w tej chwili wczytać jako dane RawData sekwencję dowolnych
plików *.jpg , które zostaną przypisane do kolejnych warstw wolumenu i tak
też przechowywane. Rysunek 3 prezentuje dwa wzajemnie prostopadłe prze-
kroje danych RGB z projektu VHP, na które zostały nałożone interaktywne pola
do oglądania dopasowanych danych CT, MRI-T2 oraz MRI-T2.

CT (ang. Computed Tomography) – urządzenie do obrazowania wolume-
trycznego wykorzystujące promieniowanie RTG. Im tkanka ma większą
gęstość, tym silniej absorbuje promieniowanie. Wartości osłabienia pro-
mieniowania przekładają się na wartości intensywności końcowego obra-
zu. CT wykorzystywane najczęściej jest do obrazowania struktur kostnych.

MRI (ang. Magnetic Resonance Imaging) – urządzenie wykorzystujące

zjawisko magnetycznego rezonansu jądrowego atomów wodoru. MRI
wykorzystywane jest głównie do obrazowania tkanek miękkich – struktur
zawierających dużo wody.

Zarówno CT, jak i MRI po podaniu środka kontrastującego umożliwiają

obrazowanie naczyń krwionośnych.

Rysunek 3. Dane RGB

Innym typem danych, z jakim przyszło mi się zmierzyć, a który zupełnie

nie został przewidziany podczas projektowania odnowionej wersji oprogra-
mowania, były siatki wolumetryczne, czyli trójwymiarowe tablice wokseli
(elementów przestrzeni odpowiadających pikselom w grafice 2D). Celem mo-
jej pracy było wtedy przygotowywanie danych do symulacji fizycznych przy
pomocy metody modelowania FEM (ang. Finite Element Method). Dane z mo-
jej aplikacji miały być dostarczane do pakietu Marc Mentat, który na wejściu
przyjmował bardzo rzadkie (ang. decimated mesh) siatki trójkątne zamknię-
tych siatek (bez dziur). Siatki te następnie były przez oprogramowanie symu-
lacyjne przetwarzane na siatki wolumetryczne – wnętrze wypełniane było
ostrosłupami. Po tym kroku użytkownik oznaczał części siatek, przypisywał
parametry materiałowe, ustalał warunki brzegowe oraz parametry startowe
(np. działające siły) symulacji. Problem pojawił się, gdy chciałem umieścić w
takim modelu obiekt niestandardowy. Chodziło o symulację wzrostu nowo-
tworu o charakterystyce sferycznej. Pomysł był taki, aby umieścić wewnątrz
siatki małą kulkę i pompować ją (zwiększając ciśnienie w jej wnętrzu) podczas
symulacji. Efekt został osiągnięty poprzez wygenerowanie danych wstępnych
w pakiecie Marc Mentat, eksport danych w formacie NASTRAN, zaimporto-
wanie danych do mojej aplikacji, umieszczenie wewnątrz siatki trójkątnej
odpowiadającej powierzchni bardzo małej sfery, a następnie połączeniu
wszystkich trójkątów ze środkiem sfery (tworząc ostrosłupy), tak by uzyskać
obiekt wolumetryczny. Efekt został wyeksportowany w formacie NASTRAN i
zaimportowany do pakietu symulacyjnego. Trik prosty, ale bardzo skuteczny
(por. Rysunek 4, górne obrazy). Ciekawostką było dostosowanie tego pomy-
słu do prostej symulacji wszczepienia implantu w kobiecą pierś. Tym razem
zamiast kuli, wykorzystana została bardzo zdeformowana elipsoida – spłasz-
czony dysk, którego środek został przemieszczony tak, aby podczas symulacji
„pompowania" uzyskał on kształt prawdziwego implantu. Wirtualna operacja
dała ciekawe efekty (por. Rysunek 4, dolne obrazy).

Rysunek 4. Wszczepianie wirtualnego obiektu do symulacji wzrostu nowotworu

(górne obrazy) oraz implantów (dolne obrazy)

Kolejnym ciekawym wyzwaniem wymagającym nowego podejścia do da-
nych była praca z sekwencjami danych 2D. Jeśli są to dane zmienne w cza-
sie (np. obraz ultrasonograficzny), dane takie nazywa się często danymi 2.5
wymiarowymi. 3D zarezerwowane jest dla trzech wymiarów przestrzeni, na-
tomiast czas w literaturze przedmiotu zasłużył jedynie na połówkę wymiaru.
Aby obsłużyć takie dane, wystarczyło potraktować je jako kolejne warstwy
zbioru wolumetrycznego. Wymiar odpowiadał wymiarowi czasowemu, nato-
miast odległość pomiędzy kolejnymi warstwami stanowiła krok czasowy wy-
rażany w sekundach. Oprócz danych medycznych to samo podejście zostało
wykorzystane w dwóch nie medycznych projektach – stabilizacja animacji,
oraz rekonstrukcja głębi ostrości na podstawie częściowo ostrych zdjęć. W
pierwszym projekcie wczytywano serię fotografii RGB, wykonanych ręcz-
nie dookoła obiektu, a następnie przy pomocy dopasowywania kolejnych
warstw do siebie uzyskiwany był płynny ruch, który docelowo posłużył do

background image

29

/ www.programistamag.pl /

BUDOWA OPROGRAMOWANIA DO ANALIZY TRÓJWYMIAROWYCH OBRAZÓW MEDYCZNYCH

rekonstrukcji 3D obiektu (Rysunek 5). Drugi projekt, również fotograficzny,
pracował z serią zdjęć (tym razem wykonanych z użyciem statywu) z bardzo
płytką głębią ostrości, w których ostrość ustawiana była ręcznie na różne od-
ległości od obiektywu. Efektem było zdjęcie ze znacznie większą głębią ostro-
ści, mapa głębokości (podobna do tej z sensorów Kinect firmy Microsoft) oraz
prosta rekonstrukcja 3D.

Kolejnym wyzwaniem były dane 4D – tym razem czwarty wymiar ozna-

czał czas (w porównaniu z wspomnianym wcześniej 2.5D, 4D wydaje się być
bardziej intuicyjne) Tym razem chodziło o dane pochodzące z ultrasonografu
potrafiącego pozyskać kilkanaście klatek trójwymiarowych obrazujących cykl
pracy bijącego serca. Takie dane stanowiły spore wyzwanie. Po pierwsze, pro-
blemem był standard DICOM, w którym zostały zapisane dane. Standard do
chwili obecnej tak naprawdę wspiera jedynie zapis danych 2D. Każdy przekrój
zapisywany jest w osobnym pliku, a interpretacja wolumetryczna pozostaje
do opracowania przez programistę. Mimo iż istnieje tzw. DICOMDIR, który
pozwala na odzyskanie opisu, jakie pliki należą do jakiej rekonstrukcji, to w
wielu przypadkach są to dane bezużyteczne – nie pozwalają na prawidłową
rekonstrukcję. W wielu programach do przetwarzania i wizualizacji danych
medycznych 3D zrezygnowano z interpretowania zawartości DICOMDIR.
Dodatkowo, wszystko, czego nie można zapisać w standardzie DICOM, pro-
ducenci sprzętu umieszczają w tzw. tagach prywatnych. Stanowi to niestety
duże pole do nadużyć, dane zapisywane są tak, że informacje niezbędne do
przygotowania przestrzennej rekonstrukcji umieszczane są w tych nieopisa-
nych nigdzie tagach. W przypadku danych 4D sytuacja jest jeszcze ciekawsza,
dane umieszczane są zazwyczaj w jednym pliku, gdzie informacje obrazowe,
które odczyta większość przeglądarek DICOM, pokazuje jeden zrzut ekranu
prezentujący wizualizowany obiekt, natomiast prawdziwe dane obrazowe
zawarte są w sekcji prywatnej. W przypadku urządzenia, z którym pracowa-
łem, udało mi się wydobyć potrzebne dane: rozmiar klatki 3D, ich ilość oraz
rozmiar woksela i krok czasowy. Pozostało tylko odpowiednio umieścić i

obsłużyć takie dane w mojej aplikacji. Pomysł okazał się bardzo prosty i sku-
teczny – każda klatka 3D została zapisana jako osobny zbiór RawData. Klasa
bazowa została rozszerzona o możliwość reprezentowania klatki czasowej z
danej serii. W projekcie tym dane czasowe poszczególnych klatek zostały do-
pasowane geometrycznie do klatki zerowej, wykorzystując nieliniowy model
przekształcenia FFD (ang. Free Form Deformation). Następnie wydobyta geo-
metria lewej komory serca została poddana pozyskanemu polu deformacji
czasowej, na podstawie czego zrekonstruowany został ruch w postaci zmie-
niającej kształt siatki trójkątnej. Wynik prezentowany był zarówno w wersji
wolumetrycznej, jak i siatki powierzchniowej (Rysunek 6). Zmieniająca się w
czasie siatka była reprezentowana również jako zestaw siatek odpowiadają-
cych chwilom czasowym. Ze względu na fakt łatwej interpolacji deformacji
pomiędzy klatkami, generowany był wynik dla dwukrotnie większej ilości kla-
tek, co pozwoliło uzyskać znacznie płynniejszą animację. Animacja polegała
na cyklicznym pokazywaniu kolejnych siatek 3D.

Bardzo interesujące wyzwanie pojawiło się przed platformą, gdy zaczęliśmy

wspólnie z doktorantem mgr Andrzejem Rutkowskim pracować z technologią
stereowizji. Aplikacja działała w 3D, wykorzystując bibliotekę VTK, która z kolei
pracowała w oparciu o OpenGL wspierający tryby wyświetlania stereo (wy-
świetlanie osobnych obrazów w dwóch viewportach). Podczas prac wstępnych
pojawiły się dwa problemy – pierwszy techniczny, drugi koncepcyjny. Pierwszy
dotyczył przełączenia aplikacji w tryb stereo – polegającym na generowaniu
odpowiedniego obrazu dla lewego i prawego oka. Mimo moich usilnych prób
jedynym trybem, który chciał działać, był tryb anaglyph, wymagający użycia
okularów dwukolorowych i dających słabą jakość obrazu. Wykorzystywany
przeze mnie monitor wspierał technologię polaryzacyjną, dla której obraz po-
winien być generowany – dla lewego oka na lewej połówce obrazu, dla prawe-
go na prawej. Takiego trybu jak się okazało nie wspierała moja karta graficz-
na. Wymagany był tryb „quad buffer” wspierany w kartach NVIDIA Quadro, ja
natomiast miałem na pokładzie swojego peceta kartę NVIDIA GeForce, która

Rysunek 5. Stabilizacja animacji przy wykorzystaniu dopasowywania danych i rekonstrukcji 3D. Rysunek zawiera nałożone klatki ruchu przed i po stabilizacji

Rysunek 6. Rekonstrukcja pracy serca na podstawie echokardiografii 4D

background image

30

/ 6

. 2014 . (25) /

PROGRAMOWANIE GRAFIKI

nie była przez „quad buffer” obsługiwana. Po chwili zabawy z trybem anaglyph
doszedłem do wniosku, że i tak nie chciałbym używać wizualizacji stereo w
taki sposób. Interfejs użytkownika, który pozostał dwuwymiarowy, mieszał
się z głębią obrazu 3D. Powstała nowa koncepcja – wygenerowania specjalne-
go okna dla monitora 3D bez GUI. Główne okno aplikacji, na której pracował
użytkownik, pozostało bez zmian i było wyświetlane na monitorze podstawo-
wym. Drugie okno w postaci pełnoekranowej wyświetlane było na monitorze
3D. Aplikacja po przełączeniu w tryb stereo generowała obraz 3D dla lewego
oka (pokazywany na głównym monitorze), a w tle w drugim buforze genero-
wała obraz dla oka prawego, który nigdy nie był pokazywany użytkownikowi
na ekranie głównym. Obie wersje były grabowane (tj. kopiowane z aktualnie
nieaktywnego bufora ramki w technice podwójnego buforowania) i przy wy-
korzystaniu kodu OpenGL teksturowały odpowiednią połówkę okna monitora
3D. Zalet takiego rozwiązania było kilka. Korzystanie z niego było bardzo wy-
godne – prezentacja na monitorze 3D dedykowana była dla odbiorców, nato-
miast operator aplikacji widział pełne GUI. Generowany obraz stereo nie zależał
zbytnio od posiadanej karty graficznej – musiała być jedynie „przyzwoita". Co
ciekawe, bardzo proste okazało się wykorzystanie rzutnika stereo składające-
go się z pary bardzo jasnych rzutników (technologia polaryzacyjna). Komputer
musiał posiadać 2 karty graficzne, z których jedna generowała obraz dla moni-
tora operatora, natomiast druga obsługiwała dwumonitorowy ekran, na który
rozciągane było okno stereo. Dodatkowo wprowadziliśmy możliwość sterowa-
nia interakcją 3D za pomocą sensora Kinect. Wówczas elementy interaktywne-
go menu dla stojącego przed ekranem użytkownika generowane były jako ele-
menty 2D wyświetlane na oknie stereo. Dodatkowe informacje nie zaśmiecały
obsługi okna głównego, co stanowiło duży atut. Dla zwiększenia poczucia głębi
aplikacja dawała możliwość wykorzystania skyboxa z nałożonymi teksturami
imitującymi dowolne otoczenie.

Obrazowanie medyczne 3D można podzielić na metody renderingu obję-
tościowego lub inaczej wolumetrycznego (ang. volume rendering) oraz po-
wierzchniowego (ang. surface rendering). Pierwsza metoda do niedawna wy-
magała specjalnego sprzętu dedykowanego (np. karta VolumePro), jednakże
rozwój technologii i algorytmów pozwala obecnie cieszyć się tym typem
obrazowania przy wykorzystaniu typowych kart graficznych. Rendering wolu-
metryczny pozwala na wizualizację wnętrza danych bez dodatkowej obróbki,
wystarczy zdefiniować np. funkcję przezroczystości i metodę renderingu (np.
w bibliotece VTK). Z kolei rendering powierzchniowy wymaga przetworzenia
wstępnego (filtrowania, segmentacji/wyboru progu i wygenerowania siatki
– np. trójkątnej – reprezentującej powierzchnię danej struktury). Dzisiejsze
karty graficzne umożliwiają zaawansowane cieniowanie i teksturowanie ta-
kich siatek, co pozwala uzyskiwać bardzo atrakcyjne obrazy wynikowe. Pod-
czas pracy ze zbiorami medycznymi 3D wykorzystuje się miksowanie obu
technik, gdzie obiekty siatkowe zanurza się w wolumetrycznej „chmurze”.

Pobieżnie opisane w niniejszym artykule problemy dotyczyły w głównej mie-
rze nowych danych, z którymi spotykałem się podczas pracy nad rozwojem
aplikacji. Modyfikacji i ciekawych problemów programistycznych było bardzo
wiele, jednakże jak do tej pory nie natrafiłem na temat, który spowodowałby
konieczność tworzenia zupełnie nowej aplikacji. Aplikacja wchodzi obecnie w
fazę współpracy z oprogramowaniem rozszerzonej rzeczywistości (Oculus Rift),
nadal wspierając nowe pomysły i problemy dotyczące filtrowania, segmentacji,
wizualizacji i analizy danych medycznych (i nie tylko) 2D, 2.5D, 3D oraz 4D.

Rysunek 7. Wizualizacja stereo połączona wraz z menu interakcji

W sieci

P

http://www.nlm.nih.gov/research/visible/visible_human.html

VHP

P

http://www.vtk.org/

– strona darmowej platformy VTK

P

http://qt.digia.com/

– strona biblioteki QT

P

http://en.wikipedia.org/wiki/DICOM

– opis formatu DICOM

P

http://en.wikipedia.org/wiki/Volume_rendering

– rendering przestrzenny

P

https://www.youtube.com/user/vizemlab

– przykładowe projekty

Michał Chlebiej

Dr informatyki i mgr fizyki komputerowej, zajmujący się na co dzień obrazowaniem medycznym
od strony programistycznej na Wydziale Matematyki i Informatyki Uniwersytetu Mikołaja Koper-
nika w Toruniu. Przygodę z programowaniem zaczął od komputera Timex 2048. Szczególną więź
czuje z komputerami Amiga, które odegrały znaczącą rolę w rozwoju grafiki komputerowej.

background image
background image

32

/ 6

. 2014 . (25) /

PROGRAMOWANIE GIER

Marek Sawerwain

KOMUNIKATOR SIECIOWY

Pierwszy przykład, jaki zrealizujemy, będzie nieskomplikowanym komunikato-
rem sieciowym, za pomocą którego przesyłane będą komunikaty tekstowe do
wszystkich osób podłączonych do głównego serwera naszego komunikatora.

Unity3D pozwala w dość prosty sposób zrealizować taki projekt i, co war-

to podkreślić, nie ma potrzeby sięgania do podstawowych elementów pro-
gramowania sieciowego, nie będziemy np. posługiwać się gniazdkami TCP/IP
czy też implementować procedur opartych o wątki, gdzie należałoby odbie-
rać oraz przetwarzać otrzymane dane.

Wykorzystywać natomiast będziemy wysokopoziomowe rozwiązania

oraz gotowe komponenty, jakie oferuje Unity3D. Dlatego, aby rozpocząć
pracę nad pierwszym przykładem, wystarczy zainstalować darmową wersję
Unity3D (obecnie jest to wersja 4.5.1).

Po instalacji należy utworzyć nowy projekt, nie musimy importować żad-

nych dodatkowych zasobów, bowiem wykorzystamy tylko dostępne w Uni-
ty3D elementy do budowy interfejsu użytkownika. Należy utworzyć też nową
scenę i zapisać np. pod nazwą

SceneZero.

Tworzymy tylko dwa elementy: jest to kamera (która zazwyczaj jest już

utworzona) oraz pusty obiekt typu

GameObject (należy z menu Game Object

wybrać opcję Create Empty albo posłużyć się skrótem klawiszowym Ctrl-Shift-N).
Warto zmienić nazwę nowo utworzonego obiektu np. na

LogicInGame. Zgod-

nie ze swoją nazwą ten skrypt pełni główną rolę w naszym zadaniu.

Do obiektu

LogicInGame należy podłączyć dwa dodatkowe kompo-

nenty. Pierwszy z nich to skrypt, np. o nazwie

NetworkCode. Będziemy pisać

ten skrypt w języku C#, ale może to też być JavaScript lub Boo. Skrypt ten na
początku może być pusty, tj. utworzony przez środowisko Unity3D, i od razu
podłączony do naszego obiektu głównego.

Drugim komponentem, jaki dodamy do głównego obiektu, jest kompo-

nent

Network View. To za pomocą tego obiektu będą przesyłane informacje

pomiędzy serwerem a podłączonymi graczami. Aktualnie nie musimy zmie-
niać żadnych własności w komponencie

Network View, choć warto spraw-

dzić własność

State Synchronization. Własność ta przyjmuje trzy stany:

pierwszy

Off, oznaczający, iż nie będą przesyłane żadne informacje o stanie

obiektu. Naturalnie, nas nie interesuje taka sytuacja, bowiem chcemy, aby in-
formacje były przesyłane. Aby tak się stało, należy wybrać stan

Unreliable,

oznaczający, iż zawsze będzie przesyłany komplet informacji. Trzecia możli-
wość to

Reliable Delta Compressed. Jest to najlepszy wybór, bowiem

oznacza większą oszczędność w przesyłaniu danych, gdyż przesyłane są tylko
te informacje, które zmieniły swoją wartość (np. jeśli obiekt pozostaje w tym
samym miejscu, to informacje o pozycji obiektu nie zostaną przesłane).

Główne zadanie do zrealizowania to napisanie skryptu, który będzie obsługiwał

przesyłanie komunikatów. Pełny kod źródłowy tego skryptu przedstawia Listing 1.

Zaczynamy od deklaracji trzech podstawowych zmiennych. Pierwsza ze

zmiennych reprezentuje adres IP serwera, druga numer portu, a trzecia mak-
symalną liczbę połączeń, jaka będzie obsługiwana przez nasz serwer:

public

string

connectionIP =

"127.0.0.1"

;

public

int

connectionPort = 25001;

public

int

maxConnections = 15;

Rysunek 1.Projekt przekazywania komunikatów tekstowych za pomocą Unity3D

Potrzebna będzie jeszcze zmienna

messages, w której przechowywać bę-

dziemy wszystkie odebrane wiadomości. Wykorzystamy dostępny typ

Ar-

rayList, który uprości operację przeglądania, odczytywania listy otrzy-
manych wiadomości czy dodawanie do istniejącej listy nowej wiadomości.
Zdefiniujemy też pomocniczą zmienną

scrollView oraz zmienną typu

string o nazwie message, gdzie będziemy przechowywać wiadomość tek-
stową, którą mamy zamiar przesłać do pozostałych klientów podłączonych
do serwera. Powyższe trzy zmienne deklarujemy w następujący sposób:

private

static

ArrayList

messages =

new

ArrayList

();

private

Vector2 scrollView = Vector2.zero;

private

string

message;

W skrypcie mamy też kilka metod niezbędnych, aby zrealizować nasze za-
danie. W metodzie

Start, uruchamianej w momencie kreacji obiektu głów-

nego w środowisku gry, wykonujemy tylko jedną czynność: do zmiennej
message wpisujemy pusty ciąg znaków. Najważniejsze zadania wykonu-
je metoda

OnGUI, w której to będziemy wyświetlać podstawowe menu. W

menu użytkownik będzie decydował, czy nasza aplikacja ma zostać urucho-
miona jako serwer, czy też ma pełnić rolę klienta. Istotną rolę pełni metoda
ReceiveMessage. Przed nagłówkiem tej metody umieszczono dodatkowy
atrybut

RPC, oznaczający, iż metoda ta może zostać wywołana zdalnie po-

przeć sieć. Wywołanie może zostać zlecone przez serwer, ale także przez każ-
dego z klientów podłączonych do serwera. Możliwość zdalnego wywołania
metody bardzo upraszcza zadania, jakie napotyka się w grach pracujących w
środowisku sieciowym.

Listing 1. Skrypt do obsługi procesu przesyłania komunikatów tekstowych

using

UnityEngine;

using

System.Collections;

public

class

NetworkCode

: MonoBehaviour {

public

string

connectionIP =

"127.0.0.1"

;

public

int

connectionPort = 25001;

public

int

maxConnections = 15;

Unity3D – prototyp gry sieciowej

Na popularność środowiska Unity3D składa się kilka elementów, ale z pewnością
jednym z nich jest dość przystępne API, które upraszcza typowe zadania, jakie na-
potyka się, tworząc aplikacje oparte o grafikę 3D. To nie wszystko; interfejs pro-
gramistyczny do obsługi komunikacji w sieci również został uproszczony. Autorzy
Unity3D przygotowali bowiem bardzo przystępny zestaw klas, który pozwala sto-
sunkowo szybko zrealizować prototyp gry sieciowej, np. typu FPS.

background image

33

/ www.programistamag.pl /

UNITY3D – PROTOTYP GRY SIECIOWEJ

private

static

ArrayList

messages =

new

ArrayList

();

private

Vector2 scrollView = Vector2.zero;

private

string

message;

void

Start () {

message =

""

;

}

void

OnGUI() {

if

(Network.peerType == NetworkPeerType.Disconnected) {

GUI.Label(

new

Rect(10, 10, 200, 20),

"Status: Disconnected"

);

if

(GUI.Button(

new

Rect(10, 30, 120, 20),

"Client Connect"

)) {

Network.Connect(connectionIP, connectionPort);

}

if

(GUI.Button(

new

Rect(10, 50, 120, 20),

"Initialize Server"

)) {

Network.InitializeServer(maxConnections, connectionPort,

false

);

}

}

if

(Network.peerType == NetworkPeerType.Server) {

GUI.Label(

new

Rect(10, 10, 200, 20),

"Run as server"

);

}

if

(Network.peerType == NetworkPeerType.Client) {

GUI.Label(

new

Rect(10, 10, 300, 20),

"Status: Connected as

Client"

);

if

(GUI.Button(

new

Rect(10, 30, 120, 20),

"Disconnect"

)) {

Network.Disconnect(200);

}

message = GUI.TextField(

new

Rect(0, 100, 150, 25), message);

if

(GUI.Button(

new

Rect(150, 200, 50, 25),

"Send"

)) {

networkView.RPC(

"ReceiveMessage"

, RPCMode.All, message);

message =

""

;

}

}

GUILayout.BeginArea(

new

Rect(0 , 120 , 400 , 200));

scrollView = GUILayout.BeginScrollView(scrollView);

foreach

(

string

c

in

messages) {

GUILayout.Label(c);

}

GUILayout.EndArea();

GUILayout.EndScrollView();

}

[RPC]

private

void

ReciveMessage(

string

sentMess) {

messages.Add(sentMess);

}

}

PRZESYŁANIE KOMUNIKATÓW

Wszystkie zadania związane z komunikatami również realizujemy za pomo-
cą kodu w

OnGUI. W metodzie tej można wyróżnić cztery główne fragmenty.

Pierwszy fragment odnosi się do instrukcji warunkowej o postaci:

if

(Network.peerType == NetworkPeerType.Disconnected)

{ }

Wykrywa ona sytuację, jaka pojawia się po uruchomieniu aplikacji. Tuż po
starcie nie wiadomo bowiem jeszcze, czy uruchomiony program będzie pełnił
rolę serwera, czy też klienta. Ogólnie program w tym momencie nie jest pod-
łączony do sieci, co jak widać powyżej łatwo sprawdzić, odczytując wartość
pola

peerType. Możliwe jest też skorzystanie z pól isClient, isServer.

Dlatego, korzystając z podstawowych elementów GUI, tworzymy dwa

przyciski odpowiadające za podłączenie do uruchomionego serwera oraz za
utworzenie serwera. Dzięki Unity3D uruchomienie serwera sprowadza się do
jednej linii kodu:

Network.InitializeServer(maxConnections, connectionPort,

false

);

gdzie pierwszy argument to maksymalna liczba połączeń do serwera, drugi
parametr to numer portu, na którym będzie nasłuchiwał serwer, oraz trzeci

parametr, który dotyczy NAT, tj. czy ma być stosowana translacja adresów sie-
ciowych (NAT - Network Address Translation).

Natomiast jako klient podajemy adres serwera oraz numer portu:

Network.Connect(connectionIP, connectionPort);

Kolejne dwie instrukcje sprawdzają, czy działamy jako serwer

Network.

peerType == NetworkPeerType.Server. Jeśli tak, to w kodzie nie mu-
simy wykonywać żadnych dodatkowych czynności, ograniczamy się do wy-
świetlenia tekstu za pomocą

GUI.Label.

Jeśli jednak program jest klientem

Network.peerType == Network-

PeerType.Client, to, podobnie jak w przypadku serwera, pokazujemy
etykietę tekstową z odpowiednią informacją. Dodatkowo dodajemy przycisk
do rozłączenia. Czynność odłączenia klienta ponownie można zrealizować za
pomocą jednej linii kodu:

Network.Disconnect(200);

Parametr metody

Disconnect to wartość w milisekundach, tzw. timeout,

czyli czasu, jaki został przydzielony do poinformowania serwera (oraz pozo-
stałych klientów), że określony klient zostanie odłączony od serwera.

Kolejny element w obsłudze klienta to umieszczenie pola tekstowego:

message = GUI.TextField(

new

Rect(0, 100, 150, 25), message);

Wykorzystujemy zmienną

message, aby podać wartość, jaka ma zostać wy-

świetlona w polu, oraz do tej samej zmiennej zostanie wpisany ciąg znakowy
podany przez użytkownika. Jednak najważniejsza czynność to przesłanie ko-
munikatu, ale to także można zrealizować za pomocą jednej linii kodu:

networkView.RPC(

"ReceiveMessage"

, RPCMode.All, message);

Oznacza ona, iż klient zleca zdalne wykonanie metody o nazwie

Receive-

Message serwerowi oraz innym klientom, którzy są podłączeni do tego sa-
mego serwera (opisuje to stała

RPCMode.All). Treść wiadomości to oczywi-

ście zmienna

message.

Pozostała część kodu w metodzie

OnGUI jest odpowiedzialna za wyświe-

tlenie otrzymanych komunikatów przechowywanych w zmiennej

messages.

Łatwo to zrobić za pomocą

foreach, co znajduje potwierdzenie na kodzie

źródłowym.

W kodzie obsługującym przesyłanie komunikatów pozostała już tylko jed-

na metoda

ReceiveMessage, której zadaniem jest odbiór komunikatów. Z

tego powodu posiada parametr

sentMess, który zawierać będzie komunikat

wpisany przez użytkownika. Jej kod to tylko jedna linia, bowiem otrzymaną
wartość tekstową umieszczamy w zmiennej

messages:

messages.Add(sentMess);

Ostatni element związany z naszym pierwszym przykładem to jego testo-
wanie. Ponieważ nie można uruchomić dwóch kopii środowiska Unity3D,
to należy zbudować wersję binarną naszego przykładu. Wykonamy to za
pomocą opcji Build z menu File (Rysunek 2). Należy jednak wykonać dwie
dodatkowe czynności. Po pierwsze, naszą scenę o nazwie

SceneZero na-

leży dodać do listy

Scenes in Build za pomocą przycisku Add current.

Następnie należy zaznaczyć opcję

Run In Background w opcjach, ja-

kie ukrywają się pod przyciskiem

Player Settings. Opcja Run In

Background oznacza, że jeśli aplikacja nie będzie aktywna (np. przesło-
nięta przez okno innej aplikacji), tj. umieszczona w tle, to nadal będzie
aktywnie przetwarzać wszystkie zdarzenia. Jest to kluczowy element w
przypadku serwera, jeśli będzie testować aplikację na jednym kompute-
rze. Warto też „schować” okno wyboru rozdzielczości, jakie Unity3D po-
kazuje w momencie uruchomienia aplikacji, oraz wyłączyć tryb pełnego
ekranu i uruchamiać aplikację w oknie.

background image

34

/ 6

. 2014 . (25) /

PROGRAMOWANIE GIER

Rysunek 2. Budowa aplikacji binarnej oraz parametry aplikacji

SIEĆ I GRA W STYLU FPS

Drugi przykład pozwoli lepiej poznać podstawowe możliwości sieciowe w
przypadku gier FPS, ale i też zademonstrować problemy z synchronizacją, na
jakie napotykamy, tworząc tego typu oprogramowanie.

Analogiczne jak poprzednio, należy utworzyć nowy projekt oraz przynaj-

mniej jedną scenę. W przykładzie umieścimy tylko dwa główne obiekty: jeden
o nazwie

Arena, który będzie zawierać wszystkie elementy naszego obszaru,

gdzie toczy się nasza gra. Może to być obszar w postaci obiektu

terrain, a

także inne obiekty 3D, z których będziemy budować obszar naszej gry, np.
światła. Drugim istotnym obiektem będzie

SpawnPoint, każdy gracz, który

podłączy się do naszej gry, będzie ją rozpoczynał w punkcie o tej nazwie. Do
tego obiektu podobnie jak poprzednio podłączono skrypt z klasą

Network-

Logic. Podłączona zostanie także kamera, choć nie jest ona potrzebna. Po
zalogowaniu się do gry kamera podłączona do punktu

SpawnPoint zostanie

wyłączona, a włączymy kamerę gracza.

Jak widać, nie został wymieniony obiekt gracza, ponieważ będzie on dyna-

micznie tworzony po utworzeniu bądź zalogowaniu się graczy na serwer. Nale-
ży jednak utworzyć taki obiekt, może to być zwykły sześcian, bądź też kapsuła.
Można też wykorzystać gotowe rozwiązania z pakietu Character Controller.
Choć jak się okaże będzie on też niestety źródłem pewnych problemów (ale
uda się je przezwyciężyć za pomocą jednej czy też góra dwóch linii kodu).

Utworzony obiekt reprezentujący postać gracza (w naszym przypadku

będzie to obiekt o nazwie

FPSPlayer) należy zamienić na tzw. obiekt pre-

fab, np. poprzez proste przeciągnięcie obiektu z okna Hierarchii do okna
Project. Do obiektu

prefab należy też dodać komponent o nazwie Network

View. Należy też upewnić się, czy własność Observed zawiera odwołanie do
typu

FPSPlayer.

KLIENT I SERWER

Pozostałe czynności związane z naszym prototypem gry w stylu FPS są reali-
zowane przez skrypt

NetworkLogic podłączony do obiektu SpawnPoint.

Początkowo postępujemy podobnie jak poprzednio, tworzymy dwa przyciski,
gdzie podłączamy się do gry jako klient, oraz tworzony jest serwer. Fragment
kodu odpowiedzialny za nasze proste menu został umieszczony w metodzie
OnGUI, i jest identyczny jak w naszym komunikatorze z pierwszego przykła-
du, choć, co naturalne, usunięto kod odnoszący się do wyświetlania otrzyma-
nych komunikatów tekstowych.

Mamy jednak kilka dodatkowych metod. Pierwsza z nich:

OnServerIni-

tialized, jest wywoływana w momencie, gdy utworzony został serwer. Jeśli
tak się stało, to należy utworzyć obiekt gracza, który będzie funkcjonował na
serwerze. Druga z metod –

OnConnectedToServer – zgodnie ze swoją na-

zwą zostanie uruchomiona, gdy jako klient uzyskamy połączenie z serwerem.
W takim przypadku również należy utworzy obiekt gracza. W obu przypad-

kach wywoływana jest metoda

CreatePlayer, która utworzy obiekt gracza

na serwerze oraz na wszystkich podłączonych do serwera klientach.

Metoda

CreatePlayer (jej kod jest umieszczony na Listingu 2) wykonu-

je tworzenie obiektu gracza za pomocą

Network.Instantiate:

var g = (GameObject)Network.Instantiate(PlayerPrefab, transform.

position, transform.rotation, groupID);

gdzie pierwszy obiekt to typ reprezentujący gracza, czyli obiekt prefab utwo-
rzony wcześniej. Skrypt

NetworkLogic posiada publiczne pole Player Pre-

fab wymagające inicjalizacji obiektem FSPPlayer. Tę czynność trzeba wyko-
nać, tworząc obiekt

SpawnPoint i podłączając skrypt NetworkLogic. Jednak

sprowadza się to tylko do przesunięcia myszą obiektu

FSPPlayer z okna Pro-

ject do pola

Player Prefab w oknie Inspector, co widać także na Rysunku 3.

Rysunek 3. Projekt naszego prototypu FPS i właściwości obiektu SpawnPoint

Druga czynność wykonywana w metodzie

CreatePlayer to przełączenie

kamery z punktu

SpawnPoint do lokalnej kamery gracza, co polega na od-

szukaniu komponentu kamery w obiekcie

FPSPlayer:

var

obj = g.GetComponentInChildren<Camera>();

A następnie włączamy kamerę gracza i wyłączamy kamerę z punktu

SpawnPoint:

obj.camera.enabled =

true

;

camera.enabled =

false

;

Nie trzeba odszukiwać kamery z punktu

SpawnPoint, ponieważ nasz skrypt

NetworkLogic przynależy do obiektu SpawnPoint i wobec tego ma bezpo-
średni dostęp do obiektu kamery.

Do omówienia pozostała jeszcze trzecia metoda o nazwie

OnPlayer-

Disconnected. Jej wywołanie następuje w przypadku, gdy jeden z podłą-
czonych do serwera graczy odłączy się od serwera. Należy obiekt gracza usu-
nąć z gry za pomocą metody

DestroyPlayerObject:

Network.DestroyPlayerObjects( player );

Można w tym momencie uruchomić nasz prototyp. Podobnie jak poprzednio
budujemy wersję binarną, uruchamiamy zewnętrzny program w trybie ser-
wera. Klientem może być Unity3D, ale natychmiast zaobserwujemy dziwne
zachowanie się graczy. Wygląda to bowiem tak, iż każdy gracz steruje innymi
graczami. Aby to naprawić, należy przejść do skryptu FPSInputController.js,
jaki znajduje się w pakiecie Character Controller. W metodzie

Update należy

na samym początku dopisać dwie linie kodu (albo jedną, jeśli lubimy umiesz-
czać słowo kluczowe

return w tej samej linii co słowo if):

if( !networkView.isMine )

return;

background image

35

/ www.programistamag.pl /

UNITY3D – PROTOTYP GRY SIECIOWEJ

Powyższa linia kodu zapewnia nas, że informacje o wciśniętych klawiszach,

które są przetwarzane przez pozostałe skrypty obiektu

FPSPlayer, będą

przesyłane tylko do właściciela obiektu

networkView. Inaczej mówiąc, lokal-

ny klient (a także serwer) nie będzie przekazywał informacji np. o spacji, któ-
ra oznacza skok gracza, do pozostałych obiektów graczy. Bez tej linii każdy z
obiektów graczy otrzymywał informacje o wciśniętych klawiszach, a następnie
poprzez sieć otrzymywano informacje o przesunięciu się obiektów, co powo-
dowało dziwne zachowanie się wszystkich obiektów znajdujących się w grze.
Choć z drugiej strony wiadomo już, że można w ten sposób na odległość ste-
rować zachowaniem się innych obiektów, co może się przydać w przyszłości.

Niestety, nie eliminuje to opóźnienia sieci i tzw. „drgania” obiektów gra-

czy, gdy porusza się gracz lokalny oraz gracze sieciowi. Efekt ten zobaczymy,
nawet jeśli serwer i klient jest uruchomiony na tej samej maszynie. Aby to
poprawić, trzeba wprowadzić dodatkowe mechanizmy, które przedstawimy
w następnym punkcie.

Listing 2. Skrypt do obsługi sieci w prototypie gry typu FPS

using

UnityEngine;

using

System.Collections;

public

class

NetworkLogic

: MonoBehaviour {

public

GameObject PlayerPrefab;

public

string

connectionIP =

"127.0.0.1"

;

public

int

connectionPort = 25001;

public

int

maxConnections = 15;

public

bool

playerConnected;

private

int

groupID = 1;

void

Start () {

playerConnected =

false

;

}

void

CreateNetworkPlayer() {

playerConnected =

true

;

var

g = (GameObject)Network.Instantiate(PlayerPrefab,

transform.position, transform.rotation, groupID);

var

obj = g.GetComponentInChildren<Camera>();

obj.camera.enabled =

true

;

camera.enabled =

false

;

}

void

OnDisconnectedFromServer() {

playerConnected =

false

;

}

void

OnPlayerDisconnected(NetworkPlayer player) {

Network.DestroyPlayerObjects( player );

}

void

OnConnectedToServer() {

CreateNetworkPlayer();

}

void

OnServerInitialized() {

CreateNetworkPlayer();

}

void

OnGUI () {

if

(Network.peerType == NetworkPeerType.Disconnected) {

GUI.Label(

new

Rect(10, 10, 200, 20),

"Status: Disconnected"

);

if

(GUI.Button(

new

Rect(10, 30, 120, 20),

"Connect as

client"

)) {

Network.Connect(connectionIP, connectionPort);

}

if

(GUI.Button(

new

Rect(10, 50, 120, 20),

"Initialize

Server"

)) {

Network.InitializeServer(maxConnections, connectionPort,

false

);

}

}

if

(Network.peerType == NetworkPeerType.Server) {

GUI.Label(

new

Rect(10, 10, 200, 20),

"Status: Run as

server"

);

}

if

(Network.peerType == NetworkPeerType.Client) {

GUI.Label(

new

Rect(10, 10, 300, 20),

"Status: Connected as

Client"

);

playerConnected =

true

;

if

(GUI.Button(

new

Rect(10, 30, 120, 20),

"Disconnect"

)) {

Network.Disconnect(200);

}

}

}

}

Nowości w Unity3D

Aktualna wersja w momencie tworzenia tego artykułu nosi numer 4.5.1,
jednak wkrótce zostanie wydana kolejna odsłona linii „4”. A niedługo po
tym pokaże się kolejna odsłona środowiska Unity3D z numerem „5”.

Jedną z bolączek wydania „4” i starszych wersji był system GUI, a dokład-

nie sposób jego budowy. Nie można było budować GUI za pomocą myszy,
jak to jest w wielu narzędziach, do budowy interfejsu użytkownika np. w śro-
dowiskach komercyjnych typu RAD Studio czy Visual Studio, czy też darmo-
wym programie Sharp Develop. Kolejne wydanie w linii „4” ma przynieść po-
prawę obsługi GUI i umożliwić łatwiejsze tworzenie interfejsu graficznego.

Więcej nowości zaoferuje jednak wydanie „5”. Oprócz poprawy jakości gra-

fiki, dzięki wprowadzeniu tzw. shaderów fizycznych, uwzględniających lepszy
model oświetlenia oraz materiałów, duże zmiany czekają także w systemie
dźwięku, który został gruntownie zmodernizowany. Poprawki dotknęły też
narzędzia do grafiki 2D, które zostały ostatnio wprowadzone w linii „4”. Samo
Unity3D stanie się też narzędziem 64-bitowym (choć wersja 32-bitowa nadal
ma być wspierana), pojawi się również wsparcie dla WebGL oraz dla systemu
SpeedTree (biblioteka dostarczająca realistyczne modele trawy, krzewów oraz
drzew). Obsługa SpeedTree dostępna będzie również w wersji darmowej.

INTERPOLACJA POZYCJI GRACZA

Podane w tym miejscu rozwiązanie odnosi się do idei zastosowanej już jakiś
czas temu w silniku gier Source. Za pomocą interpolacji (dokładnie interpo-
lacji liniowej w przypadku pozycji graczy) będziemy stosować poprawki do
pozycji gracza zdalnego. W ten sposób ruchy graczy zdalnych będą znacznie
płynniejsze, choć nadal mogą zdarzać się „szarpnięcia”.

Nasz przykład możemy oprzeć o poprzednie rozwiązanie. Nadal gracze

pojawiają się w punkcie

SpawnPoint, i wykorzystujemy do tego metodę

CreatePlayer, jednak należy wprowadzić dodatkowy element w postaci
dodawania nowego komponentu w zależności od tego, czy gracz dołączył
do gry bezpośrednio na serwerze, czy też jest jest to gracz zdalny. Wystarczy
dodać jeden parametr do metody

CreatePlayer np. o nazwie playerType

i w kodzie metody w zależności od wartości tego parametru dołączać odpo-
wiedni komponent:

if

(playerType == 0)

g.AddComponent(

"NetworkPlayerLogicServer"

);

if

(playerType == 1)

g.AddComponent(

"NetworkPlayerLogicRemote"

);

Inne zmiany w skrypcie

NetworkLogic nie są potrzebne, ale należy natu-

ralnie utworzyć dwa dodatkowe skrypty odpowiedzialne za synchronizacje
pozycji graczy. Pierwszy z dodatkowych skryptów

NetworkPlayerLogic-

Server jest odpowiedzialny za przesyłanie stanu gracza znajdującego się na
serwerze. W klasie

NetworkPlayerLogicServer znajduje się tylko jedna

metoda o nazwie

OnSerializeNetworkView, której zadaniem jest odbie-

ranie informacji sieciowych.

Rysunek 4. Testy prototypu FPS wraz z interpolacją pozycji graczy zdalnych

background image

36

/ 6

. 2014 . (25) /

PROGRAMOWANIE GIER

Implementacja tej metody może przedstawiać się następująco:

void

OnSerializeNetworkView( BitStream stream, NetworkMessageInfo

info ) {

Vector3 position = Vector3.zero;

Quaternion rotation = Quaternion.identity;

if

( stream.isWriting ) {

// cześć I

}

else

{

// cześć II

}

}

Część pierwsza zawiera następujący kod:

position = transform.position;

rotation = transform.rotation;

stream.Serialize(

ref

position );

stream.Serialize(

ref

rotation );

Polega on na odczytaniu informacji o położeniu obiektu oraz wartości obrotu
i przekazania tych wartości do strumienia, bowiem, zgodnie z warunkiem w
instrukcji warunkowej, strumień danych znajduje się w trybie do zapisu.

Część druga wykonuje czynność odwrotną; odczytuje dane ze strumienia

i zapisuje je do obiektu:

stream.Serialize(

ref

position );

stream.Serialize(

ref

rotation );

transform.position = position;

transform.rotation = rotation;

Takie rozwiązanie jest wystarczające dla kodu serwera, ponieważ gracz grają-
cy na serwerze nie doświadcza opóźnienia związanego z obsługą sieci, dlate-
go wystarczy proste przenoszenie danych.

Dla graczy zdalnych należy jednak przygotować większe rozwiązanie:

jest to skrypt o nazwie

NetworkPlayerLogicRemote. Bazuje ono, jak już

powiedziano, na pomyśle z silnika Source, bardzo podobne rozwiązania mo-
żemy odszukać na stronie unifycommunity czy też na forum Unity3D. Całość
kodu znajduje się na Listingu 3.

Początek to deklaracja dodatkowych zmiennych, z których najważniej-

sza to

InterpolationBackTime. Zmienna ta określa częstość stosowania

poprawki za pomocą interpolacji w trakcie jednej sekundy toczącej się gry.
Istotna jest też struktura

playerState, która zawiera pozycję oraz obrót gra-

cza, a także czas otrzymania informacji. Zestaw tych danych będzie przecho-
wywany w tablicy

stateBuffer. W metodzie Start dodatkowo tworzymy

wspomnianą tablicę

stateBuffer.

Dane otrzymujemy naturalnie z metody

OnSerializeNetworkView,

zapis danych jest identyczny jak w poprzednim skrypcie dla gracza serwe-
rowego. Jednak, w procesie odczytu danych, zamiast do obiektu, do którego
podłączony jest skrypt, dane są zapisywane do tablicy

stateBuffer. Są one

zawsze przesuwane do góry o jedną pozycję, a ostatnio odczytana wartość
jest umieszczana pod indeksem zerowym tablicy

stateBuffer.

Realizacja interpolacji jest wykonywana w metodzie

Update. W metodzie

tej sprawdzamy dodatkowo, czy komponent

NetworkView przynależy do na-

szego obiektu, bowiem jeśli tak jest, to nie jest potrzebna interpolacja. Spraw-
dzamy też, czy odebrane zostały jakieś dane, odczytując wartość zmiennej
stateCount. Następnie, odejmujemy od aktualnego czasu sieciowego stałą
wartość określoną w zmiennej

InterpolationBackTime. Należy się upew-

nić, czy czas w pierwszej próbce znajdującej się w tablicy

stateBuffer pod

indeksem zero jest większy niż czas interpolacji. Jeśli tak jest, to za pomocą
pętli

for szukamy w tablicy stateBuffer stanu, który dotarł wcześniej niż

wyznaczony czas interpolacji (zmienna

interpolationTime).

Gdy odpowiedni stan zostanie odszukany, to oblicza się różnicę w czasie

pomiędzy odszukanym elementem (indeks i bezpośrednio wskazuje na ten
element), dodatkowo jest on kopiowany do zmiennej

startState, a ele-

mentem wcześniejszym (indeks i-1, lub zerowym, jeśli i-1 byłoby wartością
ujemną) zapamiętanym jako zmienna

targetState. Należy upewnić się,

czy obliczona różnica w czasie jest większa niż

0.0001 (jest to związane z

dokładnością wartości czasowych, jakie otrzymujemy). Następnie obliczamy
czas trwania całej procedury interpolacji (zmienna

t). Po tych czynnościach

możemy wykonać interpolację pomiędzy dwoma wskazanymi stanami, dla
pozycji oraz obrotu:

transform.position = Vector3.Lerp( startState.Position,

targetState.Position, t );

transform.rotation = Quaternion.Slerp(startState.Rotation,

targetState.Rotation, t);

W przypadku pozycji stosujemy funkcję

Lerp, natomiast dla obrotu Slerp,

która jest dopasowana do kwaternionów, jakie stosuje Unity3D do reprezen-
tacji obrotów, i jest to tzw. interpolacja sferyczna, stanowiąca odpowiednik
interpolacji liniowej, jaką uzyskuje się za pomocą

Lerp.

W kodzie z Listingu 3 mamy jeszcze ekstrapolację, która polega na pró-

bie przewidzenia nowej pozycji gracza na podstawie stanów zero oraz jeden.
Można ten fragment usunąć i zastąpić go kopiowaniem danych ze stanu
zerowego.

Listing 3. Skrypt do obsługi interpolacji pozycji oraz wartości obro-
tu obiektu gracza w prototypie gry typu FPS

using

UnityEngine;

using

System.Collections;

public

class

NetworkPlayerLogicRemote

: MonoBehaviour {

public

float

InterpolationBackTime = 0.1f;

private

playerState

[] stateBuffer =

null

;

private

int

stateCount;

private

struct

playerState

{

public

Vector3 Position;

public

Quaternion Rotation;

public

double

Timestamp;

public

playerState( Vector3 pos, Quaternion rot,

double

time

) {

this

.Position = pos;

this

.Rotation = rot;

this

.Timestamp = time;

}

}

void

Start() {

stateBuffer =

new

playerState

[ 25 ];

stateCount = 0;

}

void

Update() {

if

( networkView.isMine )

return

;

if

( stateCount == 0 )

return

;

double

currentTime = Network.time;

double

interpolationTime = currentTime - InterpolationBackTime;

if

( stateBuffer[ 0 ].Timestamp > interpolationTime ) {

for

(

int

i = 0; i < stateCount; i++ ) {

if

( stateBuffer[ i ].Timestamp <= interpolationTime || i ==

stateCount - 1 ) {

playerState

startState = stateBuffer[ i ];

playerState

targetState = stateBuffer[ Mathf.Max( i - 1,

0 ) ];

double

length = targetState.Timestamp - startState.

Timestamp;

float

t = 0f;

if

( length > 0.0001 ) {

t = (

float

)( ( interpolationTime -

startState.Timestamp ) / length );

}

transform.position = Vector3.Lerp( startState.Position,

targetState.Position, t );

transform.rotation = Quaternion.Slerp(startState.

Rotation, targetState.Rotation, t);

return

;

}

}

}

else

{

double

extrapolationLength = (interpolationTime -

stateBuffer[0].Timestamp);

if

(extrapolationLength < 1 && stateCount > 1 ) {

background image

37

/ www.programistamag.pl /

UNITY3D – PROTOTYP GRY SIECIOWEJ

transform.position = stateBuffer[0].Position +

(((stateBuffer[0].Position - stateBuffer[1].Position) /

((

float

)stateBuffer[0].Timestamp - (

float

)stateBuffer[1].

Timestamp) ) * (

float

)extrapolationLength);

transform.rotation = stateBuffer[0].Rotation;

}

}

}

void

OnSerializeNetworkView( BitStream stream,

NetworkMessageInfo info ) {

if

( stream.isWriting ) {

Vector3 position = transform.position;

Quaternion rotation = transform.rotation;

stream.Serialize(

ref

position );

stream.Serialize(

ref

rotation );

}

else

{

Vector3 position = Vector3.zero;

Quaternion rotation = Quaternion.identity;

stream.Serialize(

ref

position );

stream.Serialize(

ref

rotation );

for

(

int

k=stateBuffer.Length-1;k>0;k--) {

stateBuffer[k] = stateBuffer[k-1];

}

stateBuffer[0] =

new

playerState

( position, rotation, info.

timestamp) ;

stateCount = Mathf.Min(stateCount + 1, stateBuffer.Length);

}

}

}

W sieci:

P Główna strona środowiska Unity3D:

http://www.unity3d.com/

P Skrypt do synchronizacji oraz interpolacji obiektów poprzez sieć z por-

talu unifycommunity:

http://wiki.unity3d.com/index.php/NetworkView_Position_Sync

P Książka pt. „Unity Multiplayer Games ”, autorstwa Alana R. Stagnera, w

całości poświęcona zagadnieniu tworzenia gier sieciowych w Unity3D:

http://www.packtpub.com/unity-multiplayer-games/book

Marek Sawerwain

redakcja@programistamag.pl

Autor, pracownik naukowy Uniwersytetu Zielonogórskiego, na co dzień zajmuje się 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.

PODSUMOWANIE

To nie koniec możliwości, jakie można i trzeba zastosować, aby poprawić ja-
kość gry w sieci. Kolejnym elementem, jaki należy przedstawić, jest tzw. syn-
chronizacja autorytatywna, oferująca kolejny sposób przekazywania danych
odnoszących się do poszczególnych graczy. W uproszczeniu polega to na
tym, iż dane o sterowaniu, np. wciśnięte klawisze, przesyłane są do serwera,
a dopiero serwer zleca klientowi realizację odpowiedniego ruchu. Naturalnie,
przydałby się lepszy przykład omawiający problemy obsługi sieci w Unity3D,
niekoniecznie musi to być gra typu FPS, ale np. wyścigi samochodowe. I po-
lepszeniem obsługi graczy sieciowych oraz wyścigami samochodowymi bę-
dziemy zajmować się w następnej cześć naszego artykułu.

reklama

background image

38

/ 6

. 2014 . (25) /

PROGRAMOWANIE GIER

Jacek Matulewski

W

iele elementów gry można zamknąć w komponentach. W zasadzie
każdy większy fragment kodu, który jest wielokrotnie wykonywa-
ny, może być umieszczony w komponencie. Komponenty mogą

być „niewizualne”; dziedziczą wówczas z klasy

GameComponent. Przykładem

takiego komponentu może być obiekt kontrolujący zmieniany dynamicznie
podkład muzyczny lub komponent sterujący dialogiem postaci w grze RPG.
Takie komponenty posiadają metodę

Update, która po zarejestrowaniu kom-

ponentu jest automatycznie wywoływana z taką samą częstością, jak metoda
Update klasy gry Game1. Komponenty mogą także być wyposażone w meto-

Draw. Powinny wówczas dziedziczyć po DrawableGameComponent.

W tym artykule przedstawię przykład komponentu „wizualnego”, którym

będzie zwykły prostopadłościan. Wybór tej bryły nie jest przypadkowy. Na pro-
stopadłościanie wygodnie będzie nam testować oświetlenie i teksturowanie.

Stwórzmy nowy projekt gry: uruchommy Visual Studio z MonoGame,

przyciśnijmy klawisze Ctrl+Shift+N, w oknie New Project przejdźmy do kate-
gorii MonoGame, zaznaczmy projekt MonoGame Windows OpenGL Project,
wpiszmy nazwę MojaDrugaGraMonoGame i kliknijmy OK. Jeżeli chcemy prze-
łączyć grę do trybu

Reach, możemy w konstruktorze Game1 umieścić pole-

cenie

GraphicsDevice.GraphicsProfile = GraphicsProfile.Reach;.

Następnie zajmijmy się przygotowaniem efektu. W tym celu w klasie

Game1 zdefiniujmy pole o nazwie efekt typu BasicEffect i w metodzie
Game1.Initialize zainicjujmy je, wpisując kod z Listingu 1.

Listing 1. Inicjacja efektu

protected

override

void

Initialize()

{

efekt =

new

BasicEffect

(graphics.GraphicsDevice);

efekt.VertexColorEnabled =

true

;

efekt.Projection =

Matrix

.CreatePerspective(

2.0f * graphics.GraphicsDevice.Viewport.AspectRatio,

2.0f,

1.0f,

10.0f);

efekt.View =

Matrix

.CreateLookAt(

new

Vector3

(0, 0, 2.5f),

new

Vector3

(0, 0, 0),

new

Vector3

(0, 1, 0));

efekt.World =

Matrix

.Identity;

base

.Initialize();

}

Następnie dodajmy do projektu komponent gry o nazwie

Prostopadlo-

scian. W XNA służył do tego odpowiedni szablon, jednak w MonoGame go
nie ma. Dlatego dodamy do projektu zwykłą klasę, a następnie przekształcimy
ją w komponent. Z menu Project wybieramy polecenie Add Class…, w oknie
Add New Item zaznaczmy pozycję Class, a w polu Name wpisujemy Prostopa-
dloscian.cs
i klikamy przycisk Add. Po utworzeniu nowej klasy uzupełnijmy
przestrzenie nazw w jej pliku o te związane z MonoGame (Listing 2). Wskazu-
jemy też jej klasę bazową, tj.

DrawableGameComponent. W klasie Prosto-

padloscian definiujemy trzy pola. Jedno z nich to efekt typu BasicEffect.
Nie używamy ogólniejszej klasy

Effect, bo w dalszej części używać będzie-

my zdefiniowanej w

BasicEffect macierzy świata do określenia pozycji

prostopadłościanu na scenie. Pozostałe pola to referencja typu

Graphics-

Device, która jest argumentem niemal każdej ważnej metody MonoGame,
oraz bufor werteksów.

Listing 2. Klasa komponentu

using

System;

using

System.Collections.Generic;

using

System.Linq;

using

System.Text;

using

Microsoft.Xna.Framework;

using

Microsoft.Xna.Framework.Graphics;

using

Microsoft.Xna.Framework.Input;

namespace

MojaDrugaGraMonoGame

{

class

Prostopadloscian

:

DrawableGameComponent

{

GraphicsDevice

gd;

BasicEffect

efekt;

VertexBuffer

buforWerteksow;

}

}

W nowej klasie definiujemy także konstruktor przyjmujący sześć argumentów
(Listing 3). Jego argumenty

dx, dy i dz określają szerokość, wysokość i głębo-

kość prostopadłościanu. Aby wygodniej określać współrzędne wierzchołków,
dzielimy w konstruktorze trzy liczby przez dwa. W konstruktorze inicjujemy
także pole

gd i klonujemy efekt. Dzięki sklonowaniu komponent będzie dys-

ponował własnym, niezależnym od gry efektem. Następnie tworzymy lokalną
tablicę ośmiu punktów, które wykorzystywać będziemy do budowania wer-
teksów (zob. Rysunek 1). W konstruktorze zdefiniujmy także zmienne typu
Color określające trzy kolory dla trzech par powierzchni prostopadłościanu.

Przewodnik po MonoGame, część 2:

komponenty gry

Budowanie gry, nawet stosunkowo prostej, to spore wyzwanie dla programisty,
szczególnie jeżeli działa w pojedynkę, a liczba linii kodu ciągle rośnie. Bardzo łatwo
zgubić się w zawiłościach skomplikowanej logiki gry, obsługi poszczególnych jej
trybów czy interakcji graczy w grze wieloosobowej. Każdy programista wie, że aby
uniknąć utraty orientacji we własnym projekcie, powinien podzielić kod na klasy,
które będą realizować autonomiczne zadania i które łatwiej jest testować. Z klas
można budować większe całości bez konieczności kontrolowania niezliczonej liczby
zmiennych. To elementarz programowania obiektowego. W MonoGame poza kla-
sami mamy także do dyspozycji tzw. komponenty gry. Są to specjalne klasy, które
są odświeżane i rysowane automatycznie.

background image

39

/ www.programistamag.pl /

PRZEWODNIK PO MONOGAME, CZĘŚĆ 2: KOMPONENTY GRY

Listing 3. Konstruktor komponentu

public

Prostopadloscian(

Game

game,

BasicEffect

efekt,

float

dx,

float

dy,

float

dz,

Color

? kolor)

:

base

(game)

{

dx /= 2;

dy /= 2;

dz /= 2;

gd = game.GraphicsDevice;

this

.efekt = (

BasicEffect

)efekt.Clone();

Vector3

[] punkty =

new

Vector3

[8]{

new

Vector3

(-dx, -dy, dz),

new

Vector3

(dx, -dy, dz),

new

Vector3

(dx, dy, dz),

new

Vector3

(-dx, dy, dz),

new

Vector3

(-dx, -dy, -dz),

new

Vector3

(dx, -dy, -dz),

new

Vector3

(dx, dy, -dz),

new

Vector3

(-dx, dy, -dz)

};

Color

kolor1 = kolor ??

Color

.Cyan;

Color

kolor2 = kolor ??

Color

.Magenta;

Color

kolor3 = kolor ??

Color

.Yellow;

}

Rysunek 1. Punkty, z których zbudowany będzie prostopadłościan

Teraz najbardziej żmudna część kodu konstruktora – definiowanie tablicy wertek-
sów (Listing 4). Musimy je zdefiniować w taki sposób, aby każda ściana była wy-
świetlana jako ciąg złożony z dwóch trójkątów, co nieco ograniczy liczbę werteksów
(zob. ramkę o buforze indeksów). Następnie całą tę tablicę kopiujemy do karty gra-
ficznej, korzystając z bufora werteksów. Kolejność werteksów jest ważna – powinny
być tak ułożone, żeby wszystkie trójkąty ustawione były przodem na zewnątrz bryły
(zob. omówienie nawijania w pierwszej części kursu – Programista 4/2014).

Uwaga o buforze indeksów

Większe oszczędności moglibyśmy uzyskać, korzystając z bufora indek-
sów. Moglibyśmy dzięki niemu uniknąć trójkrotnego powtórzenia wer-
teksów w każdym wierzchołku prostopadłościanu. Ale to pod warunkiem,
że te trzy werteksy byłyby rzeczywiście identyczne. Niestety w prostopa-
dłościanie mogą się one różnić kolorem, a w przyszłości także normalną
i współrzędnymi teksturowania. Nie chcę przez to powiedzieć, że bufor
indeksów jest mało przydatny – dobrze poznamy jego zalety przy okazji
budowania gładkich powierzchni w kolejnych odcinkach kursu.

Listing 4. Fragment konstruktora odpowiedzialny za definiowanie
werteksów

VertexPositionColor

[] werteksy =

new

VertexPositionColor

[24]

{

//przednia sciana

new

VertexPositionColor

(punkty[3], kolor1),

new

VertexPositionColor

(punkty[2], kolor1),

new

VertexPositionColor

(punkty[0], kolor1),

new

VertexPositionColor

(punkty[1], kolor1),

//tylnia sciana

new

VertexPositionColor

(punkty[7], kolor1),

new

VertexPositionColor

(punkty[4], kolor1),

new

VertexPositionColor

(punkty[6], kolor1),

new

VertexPositionColor

(punkty[5], kolor1),

//gorna sciana

new

VertexPositionColor

(punkty[3], kolor2),

new

VertexPositionColor

(punkty[7], kolor2),

new

VertexPositionColor

(punkty[2], kolor2),

new

VertexPositionColor

(punkty[6], kolor2),

//dolna sciana

new

VertexPositionColor

(punkty[0], kolor2),

new

VertexPositionColor

(punkty[1], kolor2),

new

VertexPositionColor

(punkty[4], kolor2),

new

VertexPositionColor

(punkty[5], kolor2),

//lewa sciana

new

VertexPositionColor

(punkty[3], kolor3),

new

VertexPositionColor

(punkty[0], kolor3),

new

VertexPositionColor

(punkty[7], kolor3),

new

VertexPositionColor

(punkty[4], kolor3),

//prawa sciana

new

VertexPositionColor

(punkty[1], kolor3),

new

VertexPositionColor

(punkty[2], kolor3),

new

VertexPositionColor

(punkty[5], kolor3),

new

VertexPositionColor

(punkty[6], kolor3)

};

Dzięki interfejsowi

IVertexType formalnie rzecz ujmując, możliwe

jest przygotowanie sparametryzowanej wersji klasy

Prostopadlos-

cian<VertexType> : GameComponent where VertexType :

IVertexType. Jednak ponieważ inicjujemy werteksy wewnątrz tego kom-

ponentu, nadając im położenie i kolor, konieczne jest użycie konkretnego
typu werteksu. To niestety oznacza, że chcąc dodać do werteksu nowe
atrybuty, będziemy musieli zmienić jego typ.

Klasa

DrawableGameComponent, której użyliśmy jako klasy bazowej kompo-

nentu

Prostopadloscian, dziedziczy po klasie GameComponent. Ta klasa

implementuje interfejs

IUpdateable, który wymusza obecność metody Up-

date. A ponieważ jest ona zdefiniowana już w klasie GameComponent, nie
ma w zasadzie konieczności nadpisywania jej w klasie potomnej, tj. w klasie
naszego komponentu. Podobnie jest z klasą

DrawableGameComponent, któ-

ra rozszerza klasę

GameComponent, implementując jednocześnie interfejs

IDrawable, co zmuszą ją do posiadania metody Draw. Nam metoda Draw
będzie jednak potrzebna do wyświetlenia zdefiniowanych przed chwilą wer-
teksów. Dlatego nadpisujemy ją w klasie

Prostopadloscian (Listing 5).

Listing 5. Nadpisana metoda Draw komponentu

public

override

void

Draw(

GameTime

gameTime)

{

gd.SetVertexBuffer(buforWerteksow);

foreach

(

EffectPass

pass

in

efekt.CurrentTechnique.Passes)

{

pass.Apply();

for

(

int

i = 0; i < 6; ++i)

gd.DrawPrimitives(

PrimitiveType

.TriangleStrip, 4 * i, 2);

}

base

.Draw(gameTime);

}

To dobry moment, aby sprawdzić, jak działa nasz komponent. W tym celu mu-
simy utworzyć instancję klasy

Prostopadloscian i zarejestrować ją w klasie

gry, czyli dodać ją do listy komponentów w kolekcji

Game1.Components.

Wracamy zatem do edycji klasy

Game1 (plik Game1.cs), definiujemy w niej

pole

prostopadloscian typu Prostopadloscian, inicjujemy je w meto-

dzie

Initialize, tworząc obiekt tej klasy, i dodajemy go do listy komponen-

tów gry (Listing 6). Ostatni argument w konstruktorze komponentu ustalamy
jako równy

null. To oznacza, że użyjemy predefiniowanych kolorów, które

ułatwią nam dostrzeżenie głębi pomimo braku oświetlenia. Oczywiście pustą
wartość

null możemy zastąpić np. przez Color.White. Wówczas utworzy-

my biały prostopadłościan.

background image

40

/ 6

. 2014 . (25) /

PROGRAMOWANIE GIER

Listing 6. Tworzenie i rejestrowanie komponentu w klasie gry

protected

override

void

Initialize()

{

efekt =

new

BasicEffect

(graphics.GraphicsDevice);

...

efekt.World =

Matrix

.Identity;

prostopadloscian =

new

Prostopadloscian

(

this

, efekt, 1.5f, 1.0f,

2.0f,

null

);

this

.Components.Add(prostopadloscian);

base

.Initialize();

}

Po uruchomieniu gry, efekt, jaki zobaczymy na ekranie, będzie jeszcze niezbyt
widowiskowy. Ponieważ macierz świata jest jednostkowa, prostopadłościan
jest ustawiony do nas jedną ścianą (Rysunek 2). Warto byłoby zatem umożli-
wić dowolne ustawienie prostopadłościanu na scenie. To wymaga dostępu do
macierzy świata efektu sklonowanego w komponencie. Najprostsze i chyba
najbardziej eleganckie będzie zdefiniowanie w klasie

Prostopadloscian

własności udostępniającej tę macierz (Listing 7).

Listing 7. Zdefiniowana w komponencie własność udostępniająca
macierz świata

public

Matrix

MacierzSwiata

{

get

{

return

efekt.World;

}

set

{

efekt.World =

value

;

}

}

Rysunek 2. Prostopadłościan w domyślnym ustawieniu na scenie

Wykorzystując tę własność, możemy sterować orientacją prostopadłościanu
np. za pomocą klawiszy. Wystarczy do metody

Game1.Update dodać polece-

nia widoczne na Listingu 8. Oczywiście, jeżeli z jakiegoś powodu udostępnia-
nie macierzy świata nam nie odpowiada, możemy sprawdzić stan klawiatury
w klasie komponentu, w jego metodzie

Prostopadloscian.Update, i z jej

poziomu modyfikować obiekt

efekt.World (Rysunek 3).

Listing 8. Sterowanie orientacją komponentu. Metoda Update klasy
Game1

protected

override

void

Update(

GameTime

gameTime)

{

if

(

GamePad

.GetState(

PlayerIndex

.One).Buttons.Back ==

ButtonState

.Pressed ||

Keyboard

.GetState().IsKeyDown(

Keys

.Escape))

Exit();

float

katObrotu = 0.01f;

KeyboardState

stanKlawiatury =

Keyboard

.GetState();

if

(stanKlawiatury.IsKeyDown(

Keys

.LeftShift) ||

stanKlawiatury.IsKeyDown(

Keys

.RightShift))

katObrotu *= 10;

if

(stanKlawiatury.IsKeyDown(

Keys

.Left))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationY(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Right))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationY(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Up))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationX(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Down))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationX(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.OemPeriod))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationZ(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.OemComma))

prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationZ(-katObrotu);

base

.Update(gameTime);

}

Rysunek 3. Kolorowanie ścian kompensuje brak oświetlenia

Powielenie prostopadłościanu na scenie byłoby dowodem na dobrą izolację
klasy komponentu i jej prawidłowe działanie. Zdefiniujmy wobec tego w kla-
sie

Game1 drugą referencję typu Prostopadloscian i zainicjujmy ją w meto-

dzie

Game1.Initialize. Do obracania drugiej bryły przeznaczmy klawisze

W, S, A, D (Listing 9). Po uruchomieniu zobaczymy dwa prostopadłościany,
którymi możemy niezależnie obracać (Rysunek 4).

Rysunek 4. Dwa niezależne komponenty

Wyjaśnienia wymagają operacje przesunięcia, jakim poddajemy macierz
świata. Dzięki nim prostopadłościany obracane są nie według wspólnego
środka (środka układu sceny), a wokół własnych środków (zob. artykuł „Macie-
rze w grafice 3D” w poprzednim numerze).

Stworzony w tym krótkim artykule komponent prostopadłościanu będzie

naszym modelem w kolejnych częściach kursu. W następnej części przetestu-
jemy na nim oświetlenie i teksturowanie. Będziemy go także używać podczas
testów silnika fizyki, który dodamy do projektu.

background image

41

/ www.programistamag.pl /

PRZEWODNIK PO MONOGAME, CZĘŚĆ 2: KOMPONENTY GRY

Listing 9. Tworzenie i kontrola orientacji drugiego komponentu

public

class

Game1

:

Game

{

GraphicsDeviceManager

graphics;

SpriteBatch

spriteBatch;

BasicEffect

efekt;

Prostopadloscian

prostopadloscian, prostopadloscian2;

float

rozsuniecie = 2f;

...

protected

override

void

Initialize()

{

efekt =

new

BasicEffect

(graphics.GraphicsDevice);

...

efekt.World =

Matrix

.Identity;

prostopadloscian =

new

Prostopadloscian

(

this

, efekt, 1.5f, 1.0f, 2.0f,

null

);

prostopadloscian.MacierzSwiata *=

Matrix

.CreateScale(0.75f) *

Matrix

.CreateTranslation(

new

Vector3

(-rozsuniecie/2, 0, 0));

this

.Components.Add(prostopadloscian);

prostopadloscian2 =

new

Prostopadloscian

(

this

, efekt, 1.5f, 1.0f, 2.0f,

null

);

prostopadloscian2.MacierzSwiata *=

Matrix

.CreateScale(0.75f) *

Matrix

.CreateTranslation(

new

Vector3

(rozsuniecie/2, 0, 0));

this

.Components.Add(prostopadloscian2);

base

.Initialize();

}

...

protected

override

void

Update(

GameTime

gameTime)

{

...

float

katObrotu = 0.01f;

KeyboardState

stanKlawiatury =

Keyboard

.GetState();

if

(stanKlawiatury.IsKeyDown(

Keys

.LeftShift) ||

stanKlawiatury.IsKeyDown(

Keys

.RightShift)) katObrotu *= 10;

if

(stanKlawiatury.GetPressedKeys().Length>0)

{

prostopadloscian.MacierzSwiata *=

Matrix

.CreateTranslation(

new

Vector3

(rozsuniecie/2, 0, 0));

prostopadloscian2.MacierzSwiata *=

Matrix

.CreateTranslation(

new

Vector3

(-rozsuniecie/2, 0, 0));

}

if

(stanKlawiatury.IsKeyDown(

Keys

.Left)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationY(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Right)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationY(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Up)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationX(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Down)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationX(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.OemPeriod)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationZ(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.OemComma)) prostopadloscian.MacierzSwiata *=

Matrix

.CreateRotationZ(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.A)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationY(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.D)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationY(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.W)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationX(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.S)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationX(-katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.Q)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationZ(katObrotu);

if

(stanKlawiatury.IsKeyDown(

Keys

.E)) prostopadloscian2.MacierzSwiata *=

Matrix

.CreateRotationZ(-katObrotu);

if

(stanKlawiatury.GetPressedKeys().Length > 0)

{

prostopadloscian.MacierzSwiata *=

Matrix

.CreateTranslation(

new

Vector3

(-rozsuniecie / 2, 0, 0));

prostopadloscian2.MacierzSwiata *=

Matrix

.CreateTranslation(

new

Vector3

(rozsuniecie / 2, 0, 0));

}

base

.Update(gameTime);

}

...

}

Jacek Matulewski

Fizyk zajmujący się na co dzień optyką kwantową i układami nieuporządkowanymi na Wydziale
Fizyki, Astronomii i Informatyki Stosowanej UMK w Toruniu. Od 1998 r. interesuje się programo-
waniem dla systemu Windows, w szczególności platformą .NET i językiem C#. Autor serii książek
poświęconych programowaniu. Większość ukazała się nakładem wydawnictwa Helion. Wierny
użytkownik kupionego w połowie lat osiemdziesiątych "komputera osobistego" ZX Spectrum 48k.

ZADANIE

Przygotuj komponent o nazwie

GameScene dziedziczą-

cy z

DrawableGameComponent, który na wzór klasy gry

Game wyposażony będzie w listę komponentów Compo-
nents i który ułatwi zarządzanie „ekranami” w grze. Kom-
ponent-pojemnik powinien wywoływać metodę

Update

każdego z komponentów w swojej metodzie

Update,

a metodę

Draw – w swojej metodzie Draw. W efekcie

komponenty zarejestrowane w instancji

GameScene

będą odświeżane, jeżeli własność

Enabled instancji Ga-

meScene będzie ustawiona na true, a rysowane, jeżeli
równa prawdziwości będzie własność

Visible. Ponadto

zdefiniuj menedżera ekranów, który nie będzie kompo-
nentem i odpowiadać będzie tylko za zmianę aktywnego
ekranu, tzn. będzie rejestrował lub usuwał instancje klasy
GameScene w liście komponentów gry.

Do komponentu

GameScene dodaj zdarzenie, które

pozwoli wykonać kod zdefiniowany w metodzie zdarze-
niowej w pętli efektu. Do metody zdarzeniowej prześlij
efekt, aktualną instancję

GraphicDevice i instancję Ef-

fectPass z bieżącej iteracji pętli. Dodaj również zdarzenia
Updated i Drawed pozwalające na wykonanie dowolnego
kodu po odświeżeniu i narysowaniu komponentów sceny.

Podobnie, jak w przypadku pierwszej części, nagrodą za

szybkie rozwiązanie zadania będzie 3 miesięczna prenume-
rata elektroniczna dla pierwszych dwóch osób, które prześlą
kod źródłowy na adres redakcja@programistamag.pl

background image

42

/ 6

. 2014 . (25) /

TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ

Wojciech Frącz

WPROWADZENIE

Uzyskanie wysokiej jakości kodu źródłowego jest obecnie jednym z najważ-
niejszych wyzwań inżynierii oprogramowania. Czytelny i zrozumiały kod po-
zwala na szybkie wykrycie i naprawienie błędów oraz umożliwia łatwe i bez-
pieczne wprowadzanie modyfikacji. Jak więc możemy zapewnić, by koszty
utrzymania systemu nie rosły wraz z jego rozbudową?

Poza programowaniem sterowanym testami (TDD), które pomaga w za-

pewnieniu niezawodności kodu, bardzo pomocną praktyką okazują się prze-
glądy kodu źródłowego. Oprócz weryfikacji poprawności działania tworzone-
go oprogramowania można zakładać, że kod, który został sprawdzony, jest
także czytelny i zrozumiały. To bezpośrednio wpływa na koszty jego utrzyma-
nia, zarządzania oraz pozwala na ograniczenie ryzyka wynikającego z ewen-
tualnej utraty kluczowych programistów.

Jak wykonywać przeglądy kodu, aby proces nie utrudniał i nie spowalniał

codziennej pracy? W tym artykule przedstawiono wprowadzenie do jednego
z najpopularniejszych obecnie narzędzi wspierających tę praktykę – Gerrit.

W artykule przeglądy kodu są nazywane także jako code review lub w skrócie

CR. Mianem reviewera lub recenzenta nazywana jest osoba wykonująca przegląd
kodu ze względu na brak odpowiedniego słowa na tę rolę w języku polskim. Ser-
wer ciągłej integracji czasem opisywany jest jako serwer CI (Continuous Integration).

CZY POTRZEBNE NAM JEST KOLEJNE

NARZĘDZIE?

Nieodłącznym narzędziem przy procesie przeprowadzania przeglądów kodu w
zespole programistów jest system kontroli wersji. Pozwala on na śledzenie zmian
tak, aby żaden nowy element nie umknął uwadze osób sprawdzających kod.

SVN

Jeszcze kilka lat temu dominującym systemem kontroli wersji był SVN. W naj-
prostszym podejściu kod wgrywało się do trunka, gdzie był budowany przez
serwer ciągłej integracji. W przypadku porażki w gałęzi repozytorium znajdo-
wał się niestabilny kod. Każdy, kto pobrał go w nieodpowiednim momencie,
musiał czekać na jego naprawienie, co wstrzymywało jego pracę. Gdy w koń-
cu kolejna poprawka do danej zmiany została zweryfikowana przez serwer CI,
mógł on zostać sprawdzony przy code review. Ewentualne uwagi były nano-
szone znów w kolejnym commicie. Historia zmian bardzo często wyglądała
tak jak na Rysunku 1.

Rysunek 1. Historia zmian zaśmiecona kolejnymi poprawkami

Alternatywą do tego podejścia, oprócz pracy na branchach SVN, może

być przesyłanie między autorem kodu a reviewerem patchy ze zmianami, bez
wgrywania ich do repozytorium przed ich zaakceptowaniem. Nie ma wątpli-
wości, że takie rozwiązanie jest dalekie od wygodnego.

Git

Dzięki możliwości tworzenia lekkich topic branches w Git możliwa stała się pra-
ca na osobnych gałęziach kodu w zależności od realizowanego zadania. To roz-
wiązało problem niestabilnego kodu w trunk (tutaj: master). Niestety, nadal po
zakończonym zadaniu historia zmian wyglądała podobnie – wiele commitów z
poprawkami do poprawek przy nieudanych weryfikacjach przez CI lub przy CR.

Git teoretycznie pozwala na zmianę wykonanego już commita za pomocą ko-

mendy

git commit --amend. Każda taka zmiana wymusza potem jednak prze-

słanie jej na odległy serwer komendą

git push --force, co z kolei jest proble-

matyczne dla innych osób, posiadających przestarzałą, niezmodyfikowaną historię.
Technika ta więc niesie ze sobą kilka trudności w pracy w większych zespołach.

Problem w dużej mierze rozwiązuje popularna ostatnio technika pull re-

quest – czyli żądania wprowadzenia zmian do danej gałęzi kodu. Jest stoso-
wana między innymi w serwisach GitHub oraz BitBucket. Po odrzuceniu pull
request przez recenzenta można przygotować następny, uwzględniający
przekazane uwagi. Zapobiega zaśmiecaniu historii zmian – do kodu zostaje
wdrożony wyłącznie ostatni pull request, zawierający zaakceptowany kod.
Reviewer jednak nie ma możliwości porównania zmian między jednym pull
request a drugim, co skutkuje sprawdzaniem tego samego kodu po kilka razy.
Nie wiadomo też, kto odpowiedzialny jest za przejrzenie dołączanego kodu.

CO NA TO GERRIT?

Systemy kontroli wersji nie były tworzone z myślą o code review – przynajmniej
nie jako ich główna funkcjonalność. Dlatego z pomocą przychodzą nam narzę-
dzia ułatwiające przeprowadzanie tego procesu. Jednym z nich jest Gerrit.

Gerrit jest aplikacją on-line opakowującą repozytoria Git (zob. Rysunek 2).

Komunikacja z nią jest oparta o protokół Git, dlatego na maszynach develo-
perów nie jest konieczne żadne dodatkowe oprogramowanie. Jego zadaniem
jest przede wszystkim ułatwienie wykonywania przeglądów i zapewnienie,
że każdy nowy fragment kodu został sprawdzony przed wdrożeniem go do
wersji produkcyjnej systemu. Oprócz tego wzbogaca on repozytoria Git o
kontrolę dostępu przy wykonywaniu operacji takich jak fetch, pull czy push.

Rysunek 2. Ogólny schemat działania przy pracy z Gerritem

Gerrit Code Review

Przeglądy kodu źródłowego są popularną techniką umożliwiającą zapewnienie
wysokiej jakości kodu źródłowego. Czy warto ją stosować? Co dają nam przeglądy
kodu i jak je wykonywać, by były one efektywne? W tym artykule opisano podejście
do tej praktyki prezentowane przez Gerrita – coraz popularniejszej aplikacji uła-
twiającej przeprowadzanie przeglądów kodu źródłowego.

background image

43

/ www.programistamag.pl /

GERRIT CODE REVIEW

Proces

Praca z repozytorium pod kontrolą Gerrita zaczyna się tak samo jak w przy-
padku Git – należy pobrać najnowsze zmiany z gałęzi master komendą

git

clone lub git pull (oczywiście, Gerrit wspiera pracę nad kilkoma projekta-
mi i kilkoma branchami – zakładamy tutaj najprostszy przypadek). Stan repo-
zytorium przedstawiony jest na Rysunku 3 – wykonana została operacja #1.

W ramach realizacji przydzielonego zadania został stworzony commit (#2).

Wszystko wydaje się być w porządku, więc przesyłany on jest do Gerrita (#3) –
do specjalnej gałęzi refs/for/master. Jest to miejsce, w którym wykonane zmia-
ny czekają na zatwierdzenie przez reviewera (zwane dalej poczekalnią) i na
dołączenie do gałęzi master repozytorium. Każda gałąź kodu posiada swoją
poczekalnię o nazwie refs/for/nazwa_gałęzi.

Rysunek 3. Wykonywanie i poprawianie zmian w kodzie przy użyciu Gerrit

Change-Id vs. Commit-Id

Gerrit przy przesyłaniu nowego commita sprawdza, czy jest on już mu zna-
ny, i jeśli nie – uznaje go za nową zmianę (Change) w projekcie. Zostaje jej

przypisany indywidualny numer (Change number), pod którym będzie ona
dostępna w aplikacji, aby można było wykonać przegląd kodu.

W jaki sposób Gerrit rozpoznaje, czy dany commit przesyłany do repo-

zytorium jest nowy? W Git commity rozpoznawane są po 40-znakowych ha-
shach, zwanych także jako Commit Id. Przy pracy z Gerritem każdy commit
opatrzony jest dodatkowo innym hashem, zwanym Change-Id. Jest on gene-
rowany za pomocą automatycznego hooka przy wykonywaniu operacji

git

commit (#2). Change-Id zapisywany jest w opisie danego commita, w stopce.
Jeżeli więc jako opis podamy

[BUG-456] Fixed bug, to „prawdziwy” opis

tego commita może wyglądać następująco:

[BUG-456] Fixed bug

Change-Id: I671047556b6ddecbc76f99b0af5a342fbe20c0a3

Analizując ten hash, Gerrit jest w stanie stwierdzić, czy przyporządkować
przesyłanej zmianie nowy numer, czy też użyć już istniejącego.

Wielokrotne poprawki

Dlaczego Gerrit nie może po prostu używać Commit Id do rozpoznawania
zmian? Załóżmy, że wgrana przez nas zmiana nie została zatwierdzona przy
przeglądzie kodu, lub serwer ciągłej integracji zgłosił, że nie wszystkie testy
wykonały się poprawnie. Należy poprawić kod, ale nie chcemy wykonywać
kolejnego commita tylko w tym celu.

Przy pracy z Gerritem możemy wprowadzić pożądane poprawki oraz bez

przeszkód zmienić istniejący commit komendą

git commit --amend (#4).

Należy przy tym pamiętać, aby w jego opisie nie zmienić przyporządkowa-
nego Change-Id. Właśnie to pozwoli Gerritowi przy ponownym przesyłaniu
poprawionego commita (#5) na jego poprawne rozpoznanie i przypisanie
do tej samej zmiany (Commit Id zmienia się przy wykonywaniu

git commit

--amend, ponieważ Git uznaje go jako zupełnie inny commit).

reklama

background image

44

/ 6

. 2014 . (25) /

TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ

Przy przesyłaniu zmodyfikowanych commitów, zachowując ich Change-Id,

Gerrit tworzy historię modyfikacji kodu danej zmiany. Każda z nich nazywana
jest kolejnym patchsetem. Po zatwierdzeniu danej zmiany może ona zostać
wdrożona do docelowej gałęzi kodu (#6). Tylko ostatni patchset jest dołącza-
ny do historii commitów w gałęzi repozytorium Git, dzięki czemu historia nie
jest zaśmiecana poprawkami.

Praca nad większymi zadaniami

Czasem zadanie do wykonania nie jest na tyle proste i małe, by wszystkie
potrzebne modyfikacje zawrzeć w jednym commicie. W dodatku zmiany nie
powinny być zbyt duże, aby mogły być dokładnie przeglądnięte.

Gerrit umożliwia stworzenie topic-branchy w poczekalni. Kilka powiąza-

nych ze sobą zmian z różnymi Change-Id może być powiązane w pracę nad
jednym zadaniem dzięki możliwości ustawienia ich tematu (topic). Kod należy
przesłać do gałęzi refs/for/master/TOPIC, co oznacza, że zmiany czekają tam na
dołączenie do gałęzi master repozytorium, a praca w nich wykonana dotyczy
tematu TOPIC. Gerrit umożliwi wtedy wyświetlenie tych zmian jedna po dru-
giej w interfejsie użytkownika.

Jeżeli nasza praca składa się z kilku commitów, a reviewer ma uwagi do

pierwszej z nich – co możemy zrobić? Czy trzeba wykonać kolejną zmianę w
odpowiedzi na te uwagi? Oczywiście, że nie. Git umożliwia modyfikację do-
wolnego commita – nie tylko ostatniego. W tym przypadku zamiast komendy
git commit --amend należy skorzystać z git rebase --interactive,
która pozwoli na edycję dowolnego commita z historii zmian.

Wykonywanie przeglądów

Po przesłaniu zmiany do Gerrita, autor kodu powinien ustawić dla niej, kto
ma wykonać przegląd kodu. Reviewer zostanie powiadomiony wiadomością
e-mail o kolejnej zmianie czekającej na jego sprawdzenie.

Przegląd można wykonywać w przeglądarce internetowej na przejrzy-

stym ekranie podzielonym na dwie części – po lewej kod przed zmianami,
a po prawej - kod po zmianach (zob. Rysunek 4). Jeżeli zostaną zauważone
jakieś defekty lub niezrozumiałe fragmenty kodu, recenzent może dodać ko-
mentarz tekstowy do wybranej linii poprzez jej podwójne kliknięcie. Uwagi
można także dodawać do wybranego fragmentu kodu, zaznaczając go przed
dwuklikiem, oraz do całego pliku, używając odpowiedniego przycisku na sa-
mej górze ekranu.

Przeglądy kodu za pomocą Gerrita można wykonywać także w IDE (np.

Eclipse), sprawdzając od razu, czy kod działa tak jak powinien.

Po wprowadzeniu wszystkich uwag do danej zmiany powinny one zostać

opublikowane. Autor kodu zostanie powiadomiony o pozytywnym lub nega-
tywnym wyniku przeglądu swojego kodu.

Wracając do patchsetów – warto zauważyć, że jeśli reviewer znalazł błędy

w patchsecie 3, autor kodu powinien je poprawić, tworząc patchset 4. Następ-
nie reviewer przegląda tylko różnice między oboma i sprawdza, czy zlecona
praca została wykonana. To eliminuje wadę pull requestów opisaną wcześniej
w tym artykule.

Rysunek 4. Ekran pozwalający na przeglądanie kodu w Gerrit

Flagowanie zmian

Zatwierdzanie kodu danej zmiany w Gerricie odbywa się za pomocą flag.
Standardowo istnieją dwie flagi – Verified oznaczająca, że kod się kompilu-
je i testy automatyczne dają pozytywny rezultat (oczywiście, może ona być
przyznawana automatycznie przez serwer CI), oraz CodeReview, oznaczająca
wynik przeprowadzonego przeglądu kodu. Typy flag można dowolnie defi-
niować (np. można dodać nową flagę LeaderVerified, która będzie oznaczać,
że oprócz weryfikacji CR przez dowolnego programistę, kod zatwierdził także
team leader). Każda nowa zmiana ma początkowo wartości wszystkich flag
równe 0. Jeśli zmiana przejdzie pozytywnie przegląd kodu – otrzymuje flagę
CodeReview równą +1 lub +2 (w zależności od przyjętej polityki – np. w pro-
jektach open source stosuje się czasem zasadę, że 3 flagi o wartości +1 dla
CodeReview oznaczają, że kod jest sprawdzony, i ustawia się wtedy flagę CR na
+2). Jeśli nie – zmiana otrzymuje flagę CR o wartości -1 lub -2. Jeżeli serwer CI
nie wykryje błędów w kodzie – przyznaje flagę Verified +1 albo -1 w przeciw-
nym razie. Podobnie z pozostałymi flagami.

Zmiana może być dołączona do kodu produkcyjnego wyłącznie, gdy

wszystkie flagi dla zmiany mają przyznane najwyższe możliwe wartości.

JAK ZACZĄĆ?

Aplikację Gerrit w postaci pliku WAR można pobrać ze strony projektu. Do jej
działania konieczne jest stworzenie bazy danych, w której przechowywane
będą informacje o zmianach i wykonanych przeglądach kodu (w chwili pisa-
nia artykułu wspierane są bazy danych H2, MySQL oraz PostgreSQL). Ponadto
wymagana jest Java w wersji 1.7 oraz Git.

Gdy powyższe warunki są spełnione, wystarczy wykonać komendę:

java -jar gerrit.war init -d /path/to/your/gerrit_application_directory

aby zainstalować Gerrita w wybranym katalogu. W trakcie instalacji będzie
trzeba odpowiedzieć na zadawane przez aplikację pytania, m.in. o użytkowni-
ka i hasło do bazy danych, konfigurację serwera SMTP, porty, na których apli-
kacja ma nasłuchiwać połączeń itp. Po instalacji należy uruchomić aplikację,
wykonując komendę:

/path/to/your/gerrit_application_directory/bin/gerrit.sh start

Link do szczegółowych instrukcji instalacji znajduje się na końcu artykułu.

Gerrit powinien być uruchomiony na maszynie dostępnej dla wszystkich

developerów z zespołu, aby możliwe było przesyłanie do niej zmian.

Po zalogowaniu do aplikacji, z poziomu interfejsu użytkownika należy

stworzyć nowy projekt (zakładka Projects / Create new project). Repozytorium
Git dla projektu zostanie stworzone automatycznie.

Git review

Ciekawym narzędziem upraszczającym korzystanie z Gerrita na maszynach de-
veloperów jest git review. Wzbogaca ono Git o nową komendę

review, która

skraca komendy konieczne do wpisywania przy pracy z Gerritem (jak napisano
wcześniej – do komunikacji z aplikacją w zupełności wystarcza Git, jest to jednak
czasem mało wygodne). Do jej działania wymagany jest Python oraz pip do zarzą-
dzania zależnościami. Aby zainstalować git review, wystarczy wykonać komendę:

pip install git-review

Następnie w głównym katalogu projektu należy stworzyć plik .gitreview, który
przekaże narzędziu informacje o instancji Gerrita, z którego ma korzystać, np.:

[gerrit]

host=gerrit.projekt.pl

project=projekt

background image

punkt zwrotny w karierze

www.praca.pl

Developer .NET

Programista Java EE/J2EE

Technical Service Engineer

Programista C#

Linux administrator

IT Support Specialist

Programista Python

Inżynier systemó

w Linux

Mobile

Technolog

y De

veloper

Konsultant Lync

Programista

ASP

.NET

Programista

UI Developer

Network Engineer

Test Analyst

Programista C++

Technical Consultant

Specjalista HelpDesk IT

SharePoint Developer

Administrator Baz Danych

Frontend Developer

Tester Aplikacji

Konsultant ds.

wdrożeń ERP

Konsultant ds. wsparcia IT

Architekt Systemó

w

Android De

veloper

Netw

ork Specialist

iOS Developer

Senior SAP Basis

background image

46

/ 6

. 2014 . (25) /

TESTOWANIE I ZARZĄDZANIE JAKOŚCIĄ

Kolejno w tym samym katalogu należy wykonać komendę inicjalizującą

dodatkowe ustawienia (między innymi stworzenie odpowiedniego remote
dla Gerrita oraz pobranie Change-Id hook).

git review --setup

Change-Id hook

Jak opisano wcześniej, do generowania Change-Id używany jest hook dla re-
pozytorium Git, który dodaje identyfikator zmiany przy tworzeniu commita.
Jeśli projekt nie został zainicjalizowany narzędziem git review, należy go po-
brać manualnie, kopiując go z działającej instancji Gerrita komendą wykona-
ną w katalogu głównym projektu:

scp -p -P 29418 gerrit.projekt.pl:hooks/commit-msg .git/hooks/

PRZYKŁADOWY DZIEŃ Z GERRITEM

Żeby lepiej zobrazować, jak wygląda praca z Gerritem, wcielmy się w progra-
mistę, który ma do wykonania dwa zadania, oraz reviewera, który stworzony
kod zatwierdzi.

W naszym systemie do śledzenia zgłoszeń zadania do wykonania posiada-

ją identyfikatory BUG-123 oraz ISS-456. Zajmiemy się najpierw bugiem:

$ git status

On branch master

$ git pull

Already up-to-date.

$ git checkout -b BUG-123

Switched to a new branch 'BUG-123'

Na początku upewniamy się, że mamy najnowsze zmiany w kodzie produk-
cyjnym (miejsce, od którego zaczniemy naszą pracę). Następnie tworzony jest
nowy topic-branch o nazwie zadania, nad którym będziemy pracować.

Wykonujemy nasze modyfikacje w kodzie i commitujemy je, aby naprawić

błąd w aplikacji.

$ git add .

$ git commit -m "[BUG-123] Fixed this nasty error"

[BUG-123 24b61b4] [BUG-123] Fixed this nasty error

3 files changed, 34 insertions(+), 25 deletions(-)

Dlaczego nie podaliśmy Change-Id? Ponieważ powinien on wygenerować się
sam – sprawdźmy:

$ git log --max-count 1

commit 54d942d49a6177925d686dd17481338ac8bdcef6

Author: fracz fracz@iisg.agh.edu.pl

Date: Fri May 9 10:33:31 2014 +0200

[BUG-123] Fixed this nasty error

Change-Id: I06eb7ca361bf51054ce14a48c9f2ba7488f8fac3

Chnage-Id jest na miejscu. Jeśli hook nie byłby zainstalowany – commit nie
zawierałby identyfikatora zmiany (o ile nie wpisalibyśmy go z palca), a Gerrit
odrzuciłby przesyłaną zmianę z błędem Missing Change-Id in commit message
footer
.

Gdybyśmy nie pracowali z Gerritem, można by przesłać kod komendą

git push do topic-brancha na repozytorium odległym. Gerrit jednak tego
zabrania:

$ git push origin BUG-123

Counting objects: 11, done.

[...]

! [remote rejected] BUG-123 -> BUG-123 (prohibited by Gerrit)

Zapewnia on w ten sposób, że każda nowa zmiana zostanie sprawdzona

przez reviewera (oczywiście, zachowanie to można zmodyfikować, odpo-

wiednio dostosowując uprawnienia do wybranych gałęzi kodu). Jak opisano
wcześniej, należy ją przesłać do poczekalni, czyli gałęzi refs/for/master. Ponie-
waż chcemy dodatkowo przekazać informację o tym, z jakiego topic-brancha
dany commit pochodzi – przesyłamy ją do refs/for/master/BUG-123.

Mając powyższą wiedzę, możemy przesłać więc kod do Gerrita, używając

czysto gitowej komendy:

$ git push origin HEAD:refs/for/master/BUG-123

[...]

remote: New Changes:

remote: http://gerrit.projekt.pl:8080/2050

* [new branch] HEAD -> refs/for/master/BUG-123

Komenda jest długa i nieprzyjemna. Z pomocą przychodzi narzędzie git re-
view, które potrafi wykonać to samo zadanie, wpisując po prostu:

$ git review

[...]

remote: New Changes:

remote: http://gerrit.projekt.pl:8080/2050

* [new branch] HEAD -> refs/for/master/BUG-123

Praca nad zadaniem skończona. Z wyniku działania komendy można odczy-
tać, że nowa zmiana otrzymała Change number równy 2050 (ostatnia liczba w
adresie URL, pod którym można zobaczyć zmianę). Należy także zauważyć, że
zmiana została automatycznie przesłana do topic-brancha na Gerricie o nazwie
równej nazwie gałęzi w repozytorium lokalnym. Jeśli to działanie jest niepożą-
dane, należy przesłać kod do Gerrita komendą

git review –t TOPIC.

Zabierzmy się teraz za 2-gie zadanie. Zaczynamy – jak zawsze – od gałęzi master:

$ git checkout master

Switched to branch 'master'

$ git pull

Already up-to-date.

$ git checkout -b ISS-456

Switched to a new branch 'ISS-456'

I po wykonaniu wszystkich koniecznych zmian implementacyjnych wysyłamy
je do Gerrita:

$ git add .

$ git commit -m "[ISS-456] Add this cool feature"

[...]

$ git review

[...]

remote: New Changes:

remote: http://gerrit.projekt.pl:8080/2051

* [new branch] HEAD -> refs/for/master/ISS-456

Zmiana otrzymała kolejny Change number – 2051.

Od strony reviewera

Mając kod do sprawdzenia, zaczynamy od odwiedzenia Gerrita w przeglądar-
ce internetowej i sprawdzenia listy zmian czekających na nasze zatwierdzenie.
Oczywiście – samo sprawdzenie czytelności kodu nie wystarczy – w ramach
przeglądu należy go zazwyczaj pobrać i sprawdzić, czy działa. Do tego celu na
stronie zmiany Gerrit udostępnia gotowe komendy pobierające daną zmianę
za pomocą

git fetch lub git pull. Kopiując je do schowka i wklejając do

konsoli, możemy w prosty sposób pobrać zmieniony kod. Jeśli jednak uży-
wamy narzędzia git review, wystarczy wykonać komendę

git review -d

NUMER_ZMIANY, aby uzyskać wprowadzone zmiany lokalnie:

$ git review -d 2050

Downloading refs/changes/50/2050/1 from gerrit

Switched to branch "review/wojciech_fracz/BUG-123"

W naszym przypadku reviewer pobiera zmianę dotyczącą buga i stwierdza,
że kod nadal nie poprawia nieprawidłowego działania programu, odrzucając
zmianę (ustawiając flagę CodeReview na -1).

background image

47

/ www.programistamag.pl /

GERRIT CODE REVIEW

Następnie pobiera on zmianę dotyczącą implementacji nowej funkcjo-

nalności komendą git review -d 2051. Implementacja nie zawiera błędów i
aplikacja działa prawidłowo. Wobec tego zmiana zostaje zatwierdzona i dołą-
czona do gałęzi master poprzez wykonanie operacji submit w Gerricie.

Wracając do developera

Autor kodu musi poprawić swoją zmianę dotyczącą buga. Do dzieła:

$ git review -d 2050

Downloading refs/changes/50/2050/1 from gerrit

Switched to branch "review/wojciech_fracz/BUG-123"

// poprawienie kodu

$ git add .

$ git commit –amend

[...]

Niedociągnięcia zostały poprawione. Warto zauważyć, że do pobrania kodu z
Gerrita developer używa tej samej komendy co reviewer.

Przed przesłaniem zmian do Gerrita dobrze byłoby upewnić się, czy nasza

zmiana jest aktualna ze zmianami w gałęzi, do której chcemy dołączyć nasz
kod. Nie jest to wymagane, aczkolwiek im częściej upewniamy się, że nasz
kod jest odpowiedni dla aktualnej postaci aplikacji - tym lepiej. Im wcześniej
pojawią się ewentualne konflikty, tym łatwiej będzie je rozwiązać:

$ git checkout master

Switched to branch 'master'

$ git pull

From ssh://gerrit.projekt.pl:29418/projekt

54d942d..f442c41 master -> origin/master

Updating 54d942d..f442c41

Fast-forward

[...]

Są zmiany – musimy więc uwzględnić je we wprowadzonych przez nas w ko-
dzie modyfikacjach. Z pomocą przychodzi tutaj komenda

git rebase.

$ git checkout BUG-123

Switched to branch BUG-123

$ git rebase master

First, rewinding head to replay your work on top of it...

Applying: [BUG-123] Fixed this nasty error

W trakcie

rebase mogą pojawić się konflikty, które trzeba będzie rozwiązać,

aby Gerrit mógł później dołączyć zaakceptowany kod do docelowej gałęzi.

Czy trzeba o tym pamiętać? Na szczęście – nie. Warto wiedzieć, że te ope-

racje się wykonują, ale wszystkie powyższe możemy zastąpić jedną komendą

git review, która oprócz przesyłania kodu do Gerrita także przed przesłaniem
sprawdza, czy jest on aktualny, i wykonuje

rebase, jeśli jest taka potrzeba. Je-

dynym więc, co musi zrobić developer po poprawieniu swojej zmiany, jest ta
sama komenda, która była użyta do przesłania zmiany za pierwszym razem:

$ git review

Reviewer zostaje powiadomiony o nowym patchset do zmiany, którą oceniał.
Tym razem wszystko wydaje się być w porządku i patchset 2 trafia do gałę-
zi master. Zostaje dołączony bez żadnych problemów, ponieważ kod został
wcześniej zaktualizowany (zrebasowany) do najnowszych zmian w systemie.

CZY TO WSZYSTKO?

Gerrit, poza narzuceniem przebiegu pracy wymuszającego przeprowadza-
nie przeglądów kodu, ma też wiele innych funkcjonalności. Jak można było
zobaczyć w przykładowym przypadku użycia, Gerrit kontroluje dostęp do
repozytoriów gitowych, pozwalając lub zabraniając na wykonanie danych ak-
cji. Możliwe jest łatwe ustalenie, do których gałęzi kodu commity mogą być
przesyłane bezpośrednio, w której gałęzi mogą być dodawane tagi i przez
kogo. Jedna instancja Gerrita może kontrolować wiele projektów. Użytkow-
nicy mogą być łączeni w grupy, którym można przypisać różne uprawnienia
do różnych repozytoriów. Gerrit może być więc używany jako system kontroli
uprawnień do repozytorium, nawet jeśli nie przeprowadza się w danym ze-
spole przeglądów kodu.

Aplikację można łatwo integrować z istniejącymi systemami śledzenia za-

dań takich jak JIRA czy Bugzilla. Jej zachowanie można dostosować do swoich
potrzeb przy użyciu pluginów.

Co ważne – narzędzie jest aktywnie rozwijane, a jego społeczność jest co-

raz większa.

Podsumowanie

Gerrit może być używany zarówno w rozproszonych zespołach, jak i tych prze-
bywających w jednym pomieszczeniu. Zaproponowany przez niego przebieg
pracy w naturalny sposób wprowadza wymierne efekty przeglądów kodu w
życie. Żaden fragment kodu nie zostanie dodany do systemu bez jego pozy-
tywnej weryfikacji, co znacząco wpłynie na jego jakość. Warto zainteresować
się tym narzędziem, gdyż podnosi ono komfort pracy, stopień zadowolenia
pracowników i niezawodność wytwarzanego oprogramowania.

W sieci

P

https://code.google.com/p/gerrit/

– strona główna aplikacji Gerrit

P

https://gerrit-documentation.storage.googleapis.com/Documentation/2.8/install.html

– instrukcja instalacji Gerrita

P

https://gerrit-review.googlesource.com/#/admin/projects/?filter=plugins%252F

– lista dostępnych, oficjalnych pluginów do Gerrita

P

http://www.mediawiki.org/wiki/Gerrit/git-review

– instrukcja instalacji narzędzia git review

P

http://nvie.com/posts/a-successful-git-branching-model/

– opis klasycznego git workflow

P

https://help.github.com/articles/interactive-rebase

– opis komendy git rebase - -interactive

Wojciech Frącz

fracz@iisg.agh.edu.pl

Stażysta w Katedrze Informatyki Akademii Górniczno-Hutnicznej w Krakowie. Czynnie udzie-
la się w projektach realizowanych na uczelni, programując w językach Java, PHP i Javascript.
W ramach pracy dyplomowej opracował aplikację umożliwiającą wykonywanie code review
na urządzeniach mobilnych.

background image

48

/ 6

. 2014 . (25) /

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

Dawid Borycki

WPROWADZENIE

Platforma .NET Micro Framework, w skrócie NMF, jest zestawem bibliotek i na-
rzędzi do tworzenia oprogramowania wbudowanego w urządzenie (firmwa-
re) z wykorzystaniem języka C# oraz metod i obiektów analogicznych do zna-
nych z pełnej wersji platformy .NET. NMF została zoptymalizowana pod kątem
jej wykorzystania na małych urządzeniach elektronicznych, które posiadają
ograniczone zasoby sprzętowe, takie jak pamięć czy częstotliwość taktowania
procesora oraz stosunkowo małe rozmiary.

W pierwszej części artykułu (Programista 4/2014) omówiliśmy różne aspek-

ty programowania urządzeń wbudowanych z wykorzystaniem biblioteki NMF,
a mianowicie tworzenie graficznego interfejsu użytkownika, obsługi przycisków,
panelu dotykowego, a także tworzenie serwisów sieciowych. Wszystkie omó-
wione aspekty były prezentowane z wykorzystaniem emulatora urządzenia
wbudowanego, dostarczanego z tą biblioteką. W tym artykule przygotujemy
firmware dla rzeczywistego urządzenia elektronicznego. Podobnie jak poprzed-
nio będziemy wykorzystywali środowisko programistyczne Visual Studio 2012.

Na rynku dostępnych jest kilka urządzeń wspierających platformę

.NET Micro Framework. Są nimi produkty firm Secret Labs Netduino, GHI
Electronics, Mountaineer Boards czy STMicroelectronics (STM). W tym
artykule zdecydowaliśmy się wykorzystać płytkę STM32F4 Discovery
(

http://www.st.com/web/catalog/tools/FM116/SC959/SS1532/PF252419

). Ten

zestaw uruchomieniowy (Rysunek 1) jest łatwo dostępny oraz stosunkowo
niedrogi. Oficjalna cena na stronach producenta wynosi niecałe piętnaście
dolarów. W popularnych polskich sklepach internetowych dla elektroników
STM32F4 Discovery kosztuje w granicach dziewięćdziesięciu złotych. Dodatko-
wo, urządzenie to nie wymaga programatora, a jedynie dwa kable USB: typu A
do mini-B oraz A do micro-B. Pierwszy z nich to typowy kabel łączący przenośny
dysk HDD 2.5 cala z komputerem i jest wykorzystywany do programowania mi-
krokontrolera oraz dostarcza zasilanie. Drugi to typowy kabel wykorzystywany
do połączenia telefonu z komputerem i w tym artykule posłuży do wgrania pro-
gramu rozruchowego NMF oraz sterującego mikrokontrolerem.

Sercem zestawu STM32F4 Discovery jest mikrokontroler STM32F407VGT6,

goszczący mikroprocesor ARM Cortex-4. Ten ostatni charakteryzuje się sto-
sunkowo niskim zużyciem energii oraz dużą wydajnością. Mikrokontroler
STM32F407VGT6 jest dodatkowo wyposażony w 1MB pamięci nieulotnej
(FLASH) oraz 192KB pamięci typu RAM.

Producent zestawu uruchomieniowego STM32F4 Discovery oddaje do

dyspozycji użytkownika cztery diody LED, znajdujące się pomiędzy przyciska-
mi User (niebieski) oraz Reset (czarny). Diody te oznaczone symbolami LD3-
-LD6 świecą w kolorach pomarańczowym (LD3), zielonym (LD4), czerwonym
(LD5) oraz niebieskim (LD6).

Płytka STM32F4 Discovery posiada również dwa układy mikromechanicz-

ne (MEMS). Pierwszy to cyfrowy akcelerometr, a drugi to cyfrowy mikrofon do-
okólny. W skład zestawu wchodzi dodatkowo przetwornik analogowo-cyfro-
wy audio ze zintegrowanym sterownikiem głośnika. Dźwiękowe urządzenie
wyjścia, takie jak głośnik lub słuchawki można podłączyć za pomocą złącza
mini jack, znajdującego się pod przyciskiem User.

Po podłączeniu płytki do źródła zasilania za pomocą kabla USB typu A

do mini-B zostanie uruchomiony domyślny program sterujący mikrokontro-
lerem. Steruje on diodami LD3-LD6, a także reaguje na sygnały generowane
przez akcelerometr. Po wciśnięciu przycisku User zmianom położenia i przy-
spieszenia płytki w przestrzeni odpowiada zmiana stanu odpowiednich diod.

W kolejnych podrozdziałach przedstawimy serię przykładów wykorzy-

stania platformy .NET Micro Framework do sterowania podzespołami płytki
STM32F4 Discovery.

Rysunek 1. Zestaw uruchomieniowy STM32F4 Discovery. Diody LED oddane

do dyspozycji użytkownika znajdują się pomiędzy niebieskim (User) a czarnym

przyciskiem (Reset). Wersja płytki jest nadrukowana w jej prawym górnym rogu.

Ilustracja przedstawia płytkę w wersji A (symbol MB997A)

Biblioteka .NET Micro Framework.

Programowanie firmware dla urzą-

dzenia STM32F4 Discovery

Platforma .NET Framework jest szeroko wykorzystywana do programowania apli-
kacji desktopowych, internetowych oraz mobilnych. Ponadto najmniejsza wersja tej
biblioteki, czyli .NET Micro Framework (NMF), umożliwia programowanie systemów
i urządzeń wbudowanych, czyli obszaru zarezerwowanego do niedawna wyłącznie dla
natywnych technologii programistycznych. W drugiej części mini-serii artykułów doty-
czących platformy NMF zaprezentujemy jej przykładowe wykorzystanie na rzeczywi-
stym urządzeniu elektronicznym – zestawie uruchomieniowym STM32F4 Discovery.

background image

49

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

INSTALACJA PROGRAMU

ROZRUCHOWEGO

Przed przystąpieniem do programowania firmware należy wgrać do mikro-
kontrolera płytki STM32F4 program rozruchowy ze środowiskiem uruchomie-
niowym Tiny CLR, będącym najmniejszą wersją platformy uruchomieniowej
CLR (ang. Common Language Runtime).

W ogólności bibliotekę .NET Micro Framework można skompilować dla

konkretnego mikrokontrolera za pomocą narzędzi .NET Micro Framework
Porting Kit. Do tego celu konieczne jest wcześniejsze przygotowanie sterow-
ników warstw sprzętowych Hardware Abstraction Layer (HAL) oraz Platform
Abstraction Layer
(PAL). Warstwy te są zbiorem funkcji C++ (sterowników),
wywoływanych przez CLR, które zależą od charakterystyki sprzętowej danego
mikrokontrolera.

Samodzielne wykorzystanie narzędzi Porting Kit wymaga również użycia

dodatkowych kompilatorów (np. Keil). Z tego powodu w tym artykule pomija-
my to zagadnienie i wykorzystamy jedynie pliki binarne przygotowane przez
firmę Oberon Microsystems dla NMF w wersji 4.2.

Instalację programu rozruchowego dla zestawu uruchomieniowego

STM32F4 Discovery zrealizujemy za pomocą aplikacji STM32 ST-LINK Utility
(

http://www.st.com/web/en/catalog/tools/PF258168#

). Umożliwia ona pro-

gramowanie mikrokontrolerów rodziny STM32.

Poszczególne etapy instalacji programu rozruchowego są następujące:

» Pobieramy, a następnie instalujemy aplikację STM32 ST-LINK Utility.

» Podczas procesu instalacji aplikacji STM32 ST-LINK Utility zostaną zainsta-

lowane sterowniki USB dla płytki STM32 F4 Discovery.

» Podłączamy płytkę STM32F4 Discovery z komputerem za pomocą kabla

USB mini-B i uruchamiamy aplikację STM32 ST-LINK Utility.

» Z menu Target wybieramy opcję Connect. Spowoduje to nawiązanie ko-

munikacji z mikrokontrolerem i odczytanie jego pamięci. Zawartość pa-
mięci zostanie wyświetlona w zakładce Device Memory.

» Deinstalujemy domyślny firmware oraz czyścimy zawartość pamięci

FLASH. W tym celu:

» W menu Target klikamy opcję Erase Chip.

» Następnie, z tego samego menu wybieramy opcję Erase Sectors…

» W oknie Flash Memory Mapping (Rysunek 2) klikamy przycisk z ety-
kietą Select all, a następnie przycisk Apply.

» W aplikacji STM32 ST-LINK Utility pozostawiamy aktywne połącze-
nie z mikrokontrolerem.

Rysunek 2. Widok aplikacji STM32 ST-LINK Utility

» Pobieramy archiwum stm32f4discovery.zip spod adresu

http://www.codeplex.

com/Download?ProjectName=netmf4stm32&DownloadId=471396

.

» Rozpakowujemy pobrane archiwum. W efekcie uzyskamy trzy pliki: Tiny-

Booter.hex, ER_CONFIG.hex oraz ER_FLASH.hex.

» W menu Target aplikacji STM32 ST-LINK Utility klikamy opcję Program &

Verify i wskazujemy plik TinyBooter.hex (Rysunek 3).

Rysunek 3. Instalacja programu rozruchowego

» W oknie Download [Tinybooter.hex] (Rysunek 4) klikamy przycisk z etykietą Start.

Rysunek 4. Instalacja programu rozruchowego

Po pomyślnym zainstalowaniu programu rozruchowego podłączamy płytkę
z komputerem za pomocą drugiego kabla USB typu micro-B. Od tej pory ka-
bel USB typu mini-B służy wyłącznie do zasilania zestawu STM32F4 Discove-
ry, a w systemie pojawi się dodatkowe urządzenie pn. STM32.Net Test. Jest
ono widoczne w menedżerze urządzeń w węźle Inne urządzenia (Rysunek
5). Urządzenie to do poprawnej pracy wymaga sterowników USB. Archiwum
ze sterownikami należy pobrać spod adresu:

http://www.codeplex.com/

Download?ProjectName=netmf4stm32&DownloadId=471395

.

Rysunek 5. Menedżer urządzeń Windows z zaznaczonym urządzeniem STM32.Net Test

Pobrane archiwum zawiera trzy pliki: STM32F4_WinUSB.inf, WdfCoInstal-
ler01
009.dll oraz winusbcoinstaller2.dll. Do instalacji urządzenia STM32.Net
Test w sposób jawny wykorzystamy wyłącznie pierwszy z wymienionych pli-
ków. Poszczególne etapy procesu instalacji sterowników są następujące:

» Przechodzimy do menedżera urządzeń, gdzie klikamy prawym przyci-

skiem myszy urządzenie STM32.Net Test.

» Z menu wybieramy opcję Aktualizuj oprogramowanie sterownika…

» W kreatorze aktualizacji sterowników STM32.Net Test klikamy opcję Prze-

glądaj mój komputer w poszukiwaniu oprogramowania.

» W kolejnym kroku klikamy łącze Pozwól mi wybrać z listy sterowników na

moim komputerze. Spowoduje to wyświetlenie listy urządzeń, gdzie klika-
my przycisk z etykietą Dalej.

background image

50

/ 6

. 2014 . (25) /

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

» W oknie opisanym jako wybierz sterownik, który chcesz zainstalować

dla tego sprzętu klikamy przycisk z etykietą z Dysku i wskazujemy plik
STM32F4_WinUSB.inf.

» Klikamy przycisk z etykietą Dalej i potwierdzamy ostrzeżenie o braku pod-

pisu cyfrowego.

» Zamykamy kreator instalacji sterowników i restartujemy płytkę (za pomo-

cą czarnego przycisku Reset płytki).

W przypadku systemów Windows 8 oraz Windows 8.1 instalacja sterowników
niepodpisanych cyfrowo jest domyślnie zablokowana. W takiej sytuacji przed
instalowaniem sterowników urządzenia STM32.Net Test należy odblokować
tę funkcję. Opis tej procedury znajduje się w ramce.

Instalacja sterowników niepodpisanych cyfrowo

w systemach Windows 8/8.1:

P W pasku bocznym (klawisz Windows + C) klikamy Ustawienia, a na-

stępnie Zmień ustawienia komputera.

P Z listy dostępnych ustawień wybieramy grupę Aktualizacje i odzyski-

wanie, a następnie Odzyskiwanie.

P Po prawej stronie okna wyświetli się lista dostępnych opcji, w której

klikamy przycisk z etykietą Uruchom teraz w sekcji Uruchamianie za-
awansowane
. Spowoduje to ponowne uruchomienie systemu.

P System wyświetli listę opcji, z której wybieramy pozycję Rozwiąż pro-

blemy, a następnie Opcje zaawansowane.

P W opcjach zaawansowanych wybieramy element Ustawienia urucha-

miania, po czym klikamy przycisk z etykietą Uruchom ponownie. Spo-
woduje to zrestartowanie systemu.

P Po ponownym uruchomieniu komputera wyświetlona zostanie lista

Ustawienia uruchamiania, w której wybieramy punkt 7 (Wyłącz wymu-
szanie podpisów sterowników
).

W ramach podsumowania tego podrozdziału skonfigurujemy firmware oraz
zaktualizujemy pamięć FLASH mikrokontrolera, aby możliwe było jego pro-
gramowanie z wykorzystaniem platformy .NET Micro Framework. W tym
celu uruchamiamy aplikację .NET Micro Framework Deploy Tool (MFDe-
ploy), wchodzącą w skład platformy NMF (instalowaliśmy ją w poprzednim
artykule). Następnie z listy Device wybieramy pozycję USB, a z listy urządzeń
STM32F4 Test_a7e70ea2. Potwierdzamy poprawność instalacji programu roz-
ruchowego i w tym celu klikamy przycisk z etykietą Ping. W odpowiedzi po-
winniśmy uzyskać łańcuch TinyBooter (Rysunek 6).

Ostatni etap instalacji platformy .NET Micro Framework na płytce STM32F4

Discovery polega na wgraniu plików ER_CONFIG.hex oraz ER_FLASH.hex.
W tym celu w aplikacji MFDeploy klikamy przycisk Browse… i wskazujemy oba
te pliki (Rysunek 7), po czym klikamy przycisk z etykietą Deploy.

Rysunek 6. Widok aplikacji MFDeploy z zaznaczonym urządzeniem STM32F4 Test

Rysunek 7. Wgrywanie platformy .NET Micro Framework do pamięci mikrokontrolera

PROGRAMOWANIE STANU DIOD LED

Po pomyślnej instalacji programu rozruchowego i środowiska CLR na płytce
możemy przystąpić do programowania firmware mikrokontrolera. W tym celu:

» Za pomocą Visual Studio 2012 tworzymy nowy projekt MFDiscovery we-

dług szablonu Console Application (Rysunek 8).

Rysunek 8. Kreator projektu aplikacji .NET Micro Framework w Visual Studio 2012

» W menu Project klikamy opcję MFDiscovery Properties…, a następnie:

» W zakładce Application z listy rozwijanej Target framework wybiera-
my opcję .NET Micro Framework 4.2.

» Przechodzimy na zakładkę .NET Micro Framework i w sekcji Deploy-
ment
z listy rozwijanej Transport wybieramy opcję USB, a na liście urzą-
dzeń wskazujemy pozycję STM32F4 Test_a7e70ea2 (Rysunek 9).

Rysunek 9. Wskazanie docelowego urządzenia dla implementowanej aplikacji

background image

51

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

» Uzupełniamy projekt aplikacji MFDiscovery o referencję do biblioteki Mi-

crosoft.SPOT.Hardware.dll.

» Dodajemy do projektu plik DiscoveryCpuPins.cs i wstawiamy w nim pole-

cenia z Listingu 1.

Listing 1. Zawartość pliku DiscoveryCpuPins.cs

using

System;

using

Microsoft.SPOT;

using

Microsoft.SPOT.Hardware;

namespace

MFDiscovery

{

public

static

class

DiscoveryCpuPins

{

public

static

Cpu

.

Pin

LD3 = (

Cpu

.

Pin

)61;

public

static

Cpu

.

Pin

LD4 = (

Cpu

.

Pin

)60;

public

static

Cpu

.

Pin

LD5 = (

Cpu

.

Pin

)62;

public

static

Cpu

.

Pin

LD6 = (

Cpu

.

Pin

)63;

public

static

Cpu

.

Pin

UserButton = (

Cpu

.

Pin

)0;

public

static

Cpu

.

Pin

Accelerometer = (

Cpu

.

Pin

)67;

}

}

» Projekt MFDiscovery uzupełniamy o plik Enums.cs, a następnie uzupełnia-

my go treścią przedstawioną na Listingu 2.

Listing 2. Definicja typu wyliczeniowego

UserLed

namespace

MFDiscovery

{

public

enum

UserLed

{

LD3,

LD4,

LD5,

LD6

}

}

» Dodajemy do projektu MFIntroduction kolejny plik Leds.cs i definiujemy w

nim klasę

Leds według wzoru z Listingu 3.

Listing 3. Definicja klasy

Leds

using

System;

using

Microsoft.SPOT;

using

Microsoft.SPOT.Hardware;

namespace

MFDiscovery

{

public

sealed

class

Leds

{

private

static

Leds

_instance =

null

;

private

static

object

_lockObject =

new

object

();

private

OutputPort

_ld3Port =

new

OutputPort

(

DiscoveryCpuPins

.LD3,

false

);

private

OutputPort

_ld4Port =

new

OutputPort

(

DiscoveryCpuPins

.LD4,

false

);

private

OutputPort

_ld5Port =

new

OutputPort

(

DiscoveryCpuPins

.LD5,

false

);

private

OutputPort

_ld6Port =

new

OutputPort

(

DiscoveryCpuPins

.LD6,

false

);

private

OutputPort

[] _ledPorts;

private

Leds()

{

// Uporządkowanie portów odpowiada ich lokalizacji na płytce

_ledPorts =

new

OutputPort

[] { _ld4Port,

_ld3Port, _ld5Port, _ld6Port };

}

public

static

Leds

Instance

{

get

{

if

(_instance ==

null

)

{

lock

(_lockObject)

{

if

(_instance ==

null

)

{

_instance =

new

Leds

();

}

}

}

return

_instance;

}

}

private

OutputPort

GetLedPort(

UserLed

userLed)

{

OutputPort

userLedPort =

null

;

switch

(userLed)

{

case

UserLed

.LD3:

userLedPort = _ld3Port;

break

;

case

UserLed

.LD4:

userLedPort = _ld4Port;

break

;

case

UserLed

.LD5:

userLedPort = _ld5Port;

break

;

case

UserLed

.LD6:

userLedPort = _ld6Port;

break

;

}

return

userLedPort;

}

private

void

ChangeSingleLedStatus(

UserLed

userLed,

bool

status)

{

OutputPort

ledPort = GetLedPort(userLed);

if

(ledPort !=

null

)

{

ledPort.Write(status);

}

}

public

void

TurnOn(

UserLed

userLed)

{

ChangeSingleLedStatus(userLed,

true

);

}

public

void

TurnOff(

UserLed

userLed)

{

ChangeSingleLedStatus(userLed,

false

);

}

public

void

Toggle(

UserLed

userLed)

{

OutputPort

ledPort = GetLedPort(userLed);

if

(ledPort !=

null

)

{

bool

isOn = ledPort.Read();

ledPort.Write(!isOn);

}

}

private

void

ChangeStatusOfAllLeds(

bool

status)

{

foreach

(

OutputPort

port

in

_ledPorts)

{

port.Write(status);

}

}

public

void

TurnOffAll()

{

ChangeStatusOfAllLeds(

false

);

}

public

void

TurnOnAll()

{

ChangeStatusOfAllLeds(

true

);

}

public

void

ToggleAll()

{

foreach

(

OutputPort

ledPort

in

_ledPorts)

{

bool

isOn = ledPort.Read();

ledPort.Write(!isOn);

}

}

}

}

background image

52

/ 6

. 2014 . (25) /

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

» Projekt aplikacji MFIntroduction uzupełniamy o plik Discovery.cs, w którym

umieszczamy definicję klasy

Discovery (Listing 4).

Listing 4. Zawartość pliku Discovery.cs

using

System;

using

Microsoft.SPOT;

using

Microsoft.SPOT.Hardware;

namespace

MFDiscovery

{

public

sealed

class

Discovery

{

private

static

Discovery

_instance =

null

;

private

static

object

_lockObject =

new

object

();

private

Leds

_leds =

null

;

private

Discovery()

{

_leds =

Leds

.Instance;

}

public

static

Discovery

Instance

{

get

{

if

(_instance ==

null

)

{

lock

(_lockObject)

{

if

(_instance ==

null

)

{

_instance =

new

Discovery

();

}

}

}

return

_instance;

}

}

public

Leds

Leds

{

get

{

return

_leds; }

}

}

}

» Przechodzimy do edycji pliku Program.cs i modyfikujemy jego zawartość

według wzoru z Listingu 5.

Listing 5. Zawartość pliku Program.cs

using

System;

using

Microsoft.SPOT;

using

System.Threading;

namespace

MFDiscovery

{

public

class

Program

{

private

static

Discovery

_discovery =

Discovery

.Instance;

public

static

void

Main()

{

const

int

msSleepTime = 500;

while

(

true

)

{

_discovery.Leds.Toggle(

UserLed

.LD3);

Thread

.Sleep(msSleepTime);

}

}

}

}

Po skompilowaniu i uruchomieniu projektu MFDiscovery (opcja Debug/Start
debugging
) odpowiednie pliki binarne aplikacji oraz jej zależności zostaną au-
tomatycznie wgrane do nieulotnej pamięci mikrokontrolera, po czym nastąpi
jego zrestartowanie. Od tej pory mikrokontroler będzie realizował nieskoń-
czoną pętlę (Listing 5), w ramach której będzie on naprzemiennie włączał i
wyłączał diodę LD3 o kolorze pomarańczowym.

Kod źródłowy aplikacji nietrudno zmodyfikować, aby mikrokontroler modyfi-

kował stan wszystkich diod jednocześnie. W tym celu wywołanie metody

Leds.

Toggle (Listing 5) wystarczy zastąpić wywołaniem metody Leds.ToggleAll.

Sterowanie diodami z poziomu mikrokontrolera zrealizowaliśmy w oparciu

o klasę

OutputPort, która służy do kontroli stanu portu wyjścia ogólnego przezna-

czenia (GPIO, od ang. General Purpose Input/Output). Konstruktor tej klasy przyjmuje
dwa argumenty. Pierwszy to identyfikator portu mikrokontrolera. Natomiast drugi
pozwala zdefiniować początkowy binarny stan portu (aktywny/nieaktywny).

W klasie

Leds

(Listing 3) zdefiniowaliśmy cztery zmienne typu

OutputPort.

Odpowiadają one poszczególnym portom mikrokontrolera, do których
podłączone są diody. Identyfikatory tych portów, które zebraliśmy w klasie
DiscoveryCpuPins, odczytaliśmy z dokumentacji wykorzystywanego zesta-
wu uruchomieniowego. Mianowicie, z dokumentu UM1472: Discovery kit for
STM32F407/417 lines
(

http://www.st.com/st-web-ui/static/active/en/resource/

technical/document/user_manual/DM00039084.pdf

).

Aktualny stan wybranego portu GPIO można odczytać za pomocą metody

Read

klasy

OutputPort. Z kolei nową wartość ustawia się z użyciem metody Write.

Definicja klasy

Leds

zawiera jeszcze dodatkowe metody, które wykorzy-

stamy w kolejnych przykładach.

Klasy

Leds

oraz

Discovery

zostały zaimplementowane zgodnie ze wzorcem

projektowym o nazwie singleton. Wynika to z faktu, że klasy te stanowią abstrak-
cyjną reprezentację elementów elektronicznych, których liczba jest ściśle określona.
Mianowicie firmware jest uruchomiony na jednej płytce wyposażonej w skończoną
liczbę diod. Nie możemy programowo utworzyć nowych portów GPIO, ani płytki.

W przypadku klas

Leds

oraz

Discovery

statyczna metoda

Instance

jest

bezpieczna w sensie wielowątkowości. Ponadto, instancjonuje ona obiekty
dopiero w momencie uzyskiwania do nich dostępu.

Ze względu na użyte słowo kluczowe

sealed

w deklaracjach klas

Leds

i

Discovery

nie mogą one być klasami bazowymi. Jest to konieczne w celu

zablokowania możliwości definiowania publicznych konstruktorów w ewen-
tualnych klasach pochodnych.

OBSŁUGA PRZERWAŃ

W tej części artykułu uzupełnimy projekt aplikacji MFDiscovery o procedury
umożliwiające obsługę przerwań generowanych na skutek wciśnięcia niebie-
skiego przycisku User. Zadaniem metody obsługującej to zdarzenie będzie
cykliczne przełączenie trybu pracy mikrokontrolera. W ramach tych trybów
zaimplementujemy zmianę stanu diody LD3 oraz wszystkich diod jednocze-
śnie, a także cykliczną i losową zmianę stanu diod. Implementacja tego zada-
nia składa się z następujących etapów:

» W pliku Enums.cs zdefiniujmy typ wyliczeniowy (Listing 6).

Listing 6. Definicja typu wyliczeniowego

WorkingMode

public

enum

WorkingMode

{

ToggleLed = 0,

ToggleAllLeds,

LedSweeping,

ToggleLedRandomly,

None

}

» W klasie Discovery wstawiamy polecenia wyróżnione na Listingu 7.

Listing 7. Tworzenie portu przerwaniowego związanego z przyci-
skiem User.

public

sealed

class

Discovery

{

private

static

Discovery

_instance =

null

;

private

static

object

_lockObject =

new

object

();

private

Leds

_leds =

null

;

private

InterruptPort

_userButton =

null

;

private

Discovery()

{

_leds =

Leds

.Instance;

_userButton =

new

InterruptPort

(

DiscoveryCpuPins

.UserButton,

true

,

Port

.

ResistorMode

.PullDown,

Port

.

InterruptMode

.InterruptEdgeHigh);

}

background image

53

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

public

static

Discovery

Instance

{

get

{

if

(_instance ==

null

)

{

lock

(_lockObject)

{

if

(_instance ==

null

)

{

_instance =

new

Discovery

();

}

}

}

return

_instance;

}

}

public

Leds

Leds

{

get

{

return

_leds; }

}

public

InterruptPort

UserButton

{

get

{

return

_userButton; }

}

}

» Definicję klasy Leds

uzupełnijmy o dwie metody z Listingu 8.

Listing 8. Cykliczne i losowe przełączanie stanu diod

public

void

Sweep()

{

const

int

msSleepTime = 100;

foreach

(

OutputPort

port

in

_ledPorts)

{

bool

isOn = port.Read();

port.Write(!isOn);

System.Threading.

Thread

.Sleep(msSleepTime);

}

}

public

void

ToggleRandomly()

{

Random

r =

new

Random

();

UserLed

ledToToggle = (

UserLed

)r.Next((

int

)

UserLed

.LD6 + 1);

Toggle(ledToToggle);

}

» Zawartość pliku Program.cs zmodyfikujmy według wzoru z Listingu 9.

Listing 9. Cykliczna zmiana stanu pracy mikrokontrolera w osob-
nym wątku

public

class

Program

{

private

static

Discovery

_discovery =

Discovery

.Instance;

private

static

WorkingMode

_currentWorkingMode

=

WorkingMode

.None;

private

static

ManualResetEvent

_taskCompletedEvent

=

new

ManualResetEvent

(

false

);

private

static

bool

_workingModeChanging =

false

;

public

static

void

Main()

{

const

int

msSleepTime = 500;

while

(

true

)

{

_discovery.Leds.Toggle(

UserLed

.LD3);

Thread

.Sleep(msSleepTime);

}

_discovery.UserButton.OnInterrupt += UserButton_OnInterrupt;

Thread

workingModeThread =

new

Thread

(WorkingModeThreadFunction);

workingModeThread.Start();

}

private

static

void

UserButton_OnInterrupt(

uint

data1,

uint

data2,

DateTime

time)

{

ChangeWorkingMode();

}

private

static

void

ChangeWorkingMode()

{

_workingModeChanging =

true

;

_taskCompletedEvent.WaitOne();

_discovery.Leds.TurnOffAll();

if

(++_currentWorkingMode >

WorkingMode

.None)

_currentWorkingMode =

WorkingMode

.ToggleLed;

_workingModeChanging =

false

;

}

private

static

void

WorkingModeThreadFunction()

{

const

int

msDefaultSleepTime = 500;

const

int

msSmallerSleepTime = 100;

while

(

true

)

{

if

(!_workingModeChanging)

{

_taskCompletedEvent.Reset();

switch

(_currentWorkingMode)

{

case

WorkingMode

.ToggleLed:

_discovery.Leds.Toggle(

UserLed

.LD3);

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.ToggleAllLeds:

_discovery.Leds.ToggleAll();

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.LedSweeping:

_discovery.Leds.Sweep();

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.ToggleLedRandomly:

_discovery.Leds.ToggleRandomly();

Thread

.Sleep(msSmallerSleepTime);

break

;

case

WorkingMode

.None:

default

:

Thread

.Sleep(msDefaultSleepTime);

break

;

}

_taskCompletedEvent.Set();

}

}

}

}

W powyższym przykładzie wszystkie dostępne tryby pracy mikrokontrolera są
określone odpowiednimi wartościami typu wyliczeniowego

WorkingMode.

Po skompilowaniu i uruchomieniu aplikacji MFDiscovery nastąpi wgranie

nowej wersji firmware do mikrokontrolera. Tym razem po jego zrestartowaniu
wszystkie diody będą wyłączone, ponieważ aktualnym trybem pracy jest tryb
WorkingMode.None (pole _currentWorkingMode). Po wciśnięciu przycisku
User nastąpi inkrementacja wartości zapisanej w polu

_currentWorking-

Mode, co powoduje zmianę funkcji realizowanej przez mikrokontroler. Po ko-
lei będą to tryby: przełączanie stanu diody LD3, przełączanie stanu wszystkich
diod jednocześnie, cykliczna zmiana stanu diod oraz losowa zmiana ich stanu.

Przerwania generowane po wciśnięciu przycisku User obsłużyliśmy za

pomocą klasy

InterruptPort. Konstruktor tej klasy przyjmuje cztery argu-

menty. Pierwszym z nich jest znany już identyfikator portu. Drugi argument
glitchFilter pozwala ustawić filtr zakłóceń dla wybranego portu. Jeśli filtr
ten zostanie wyłączony, zakłócenia występujące na danym porcie mogą być
zinterpretowane jako faktyczne przerwania. W przypadku przycisku skutkowa-
łoby to wielokrotnym zgłaszaniem przerwania, reprezentującego jego wciśnię-
cie i co za tym idzie wielokrotnym wywołaniu metody

ChangeWorkingMode.

Spowodowałoby to oczywiście błędne przełączanie pomiędzy trybami pracy
mikrokontrolera. Trzeci argument konstruktora klasy

InterruptPort

o nazwie

resistor

służy do konfiguracji trybu rezystora typu pull-up dla danego portu.

Ostatnim argumentem konstruktora jest parametr

interrupt

umożliwiający

zdefiniowanie stanu zbocza lub poziomu sygnału generującego przerwanie.

Po skonfigurowaniu portu przerwaniowego dla przycisku User uzyskujemy

możliwość jego programowej obsługi. W tym celu wystarczy skojarzyć wybraną
metodę ze zdarzeniem

InterruptPort.OnInterrupt. W powyższym przy-

kładzie w ramach metody zdarzeniowej wywołujemy procedurę

ChangeWork-

ingMode, zmieniającą tryb pracy mikrokontrolera.

background image

54

/ 6

. 2014 . (25) /

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

W ramach podsumowania tego podrozdziału zwracamy uwagę, że zaimple-

mentowany tu firmware działa wielowątkowo. Procedury odpowiedzialne za
poszczególne tryby pracy wywoływane są z funkcji wątku roboczego. Natomiast
obsługa zdarzenia kliknięcia przycisku User znajduje się w wątku głównym.

AKCELEROMETR

Płytka STM32F4 Discovery jest wyposażona w mikromechaniczny akcele-
rometr cyfrowy. W  zależności od jej wersji jest to układ o identyfikatorze
LIS302DL w przypadku wersji A i B lub LIS3DSH w przypadku wersji C. Symbol
wersji zestawu uruchomieniowego jest nadrukowany na powierzchni płytki w
postaci ciągu MB997w, gdzie ostatnia litera w symbolizuje wersję i może mieć
jedną z wartości: A, B lub C (zob. Rysunek 1).

W tym artykule korzystamy z wersji C płytki STM i co za tym idzie akcelero-

metru LIS3DSH. Jednakże procedury jego programowania są analogiczne jak
w przypadku LIS302DL.

Przed uzupełnieniem projektu MFDiscovery o konkretne metody pobiera-

jące wartości przyspieszenia z akcelerometru omówimy zasadę jego działa-
nia, tryby pracy oraz mechanizm jego konfiguracji.

ZASADA DZIAŁANIA

Akcelerometr do pomiaru przyspieszenia wykorzystuje podstawowe zasady
mechaniki. Mianowicie, w najprostszym ujęciu akcelerometrem może być ele-
ment o znanej masie zawieszony na sprężynie przymocowanej do obudowy
układu. W fizyce ruch elementu zawieszonego na sprężynie dla dostatecznie
małej wartości tłumienia opisywany jest równaniem oscylatora harmoniczne-
go, które pozwala wyznaczyć przyspieszenie na podstawie zmiany położenia
elementu o znanej masie (dokładniej masy bezwładnej). Zmiana tego położe-
nia odpowiada wydłużeniu lub skróceniu sprężyny.

Dla ustalonej pozycji akcelerometru oraz danych parametrów sprężyny

i tłumienia element zawieszony na sprężynie pozostaje w spoczynku, gdyż
siła wywierana przez sprężynę na ten element jest równa co do wartości wy-
wieranej na niego sile grawitacji. Innymi słowy można powiedzieć, że w tym
przypadku akcelerometr dokonuje pomiaru przyspieszenia ziemskiego. W
związku z tym akcelerometry mierzą przyspieszenie w jednostkach tego przy-
spieszenia, tzn. g = 9,81 m/s

2

.

Zmiana położenia akcelerometru na skutek zewnętrznych sił powoduje

zaburzenie stanu równowagi i w efekcie bezwładne przesunięcie elementu
zawieszonego na sprężynie. Zakres tego przesunięcia jest wprost proporcjo-
nalny do zmiany położenia akcelerometru.

Nowoczesne akcelerometry, które są powszechnie spotykane w wielu

urządzeniach elektronicznych, takich jak telefony komórkowe, tablety, apa-
raty ze stabilizacją obrazu, układy sterowania poduszkami powietrznymi,
charakteryzują się niewielkimi rozmiarami. Z tego powodu wytwarza się je
w krzemie technologią mikromechaniczną. Jednakże, w dalszym ciągu pod-
stawą ich konstrukcji jest element o znanej masie zawieszony na elemencie
sprężystym. Omówienie technologii mikromechanicznej wykraczałoby poza
ramy tego artykułu.

Tryby pracy, komunikacja i konfiguracja
akcelerometru

Układ LIS3DSH jest akcelerometrem małej mocy, który umożliwia mierzenie
przyspieszenia w trzech osiach w jednym z zakresów: ±2g, ±4g, ±6g, ±8g,
±16g z częstotliwością pomiaru z przedziału od 3.125 Hz do 1.6 kHz.

Akcelerometr LIS3DH pracuje w dwóch trybach: wyłączenia (power-down)

oraz normalnym. Bezpośrednio po podłączeniu płytki do zasilania akcelero-
metr przez około 10ms wykonuje rozruch, w trakcie którego pobiera z we-
wnętrznej pamięci nieulotnej parametry niezbędne do jego pracy. Po zakoń-
czeniu rozruchu przechodzi do trybu wyłączenia w celu oszczędzania energii.
Z tego powodu w celu wykonania pomiaru przyspieszenia konieczne jest
przełączenie akcelerometru w stan normalny.

Realizuje się to za pomocą aktualizacji wartości w odpowiednich rejestrach

akcelerometru. Do tego celu konieczne jest jednak wcześniejsze nawiązanie
z nim komunikacji, która jest możliwa również w trybie wyłączenia. Układ
LIS3DH udostępnia dwa rodzaje interfejsów komunikacyjnych. Są nimi: sze-
regowy interfejs urządzeń peryferyjnych (ang. Serial Peripheral Interface /SPI/)
oraz magistrala I2C (ang. Inter-Integrated Circuit).

Mikrokontoler płytki STM32F4 Discovery do kontroli akcelerometru wy-

korzystuje interfejs SPI, który przewiduje, że urządzenia komunikują się za
pomocą trzech linii: danych wejściowych oraz wyjściowych (dla danego urzą-
dzenia peryferyjnego) oraz zegara taktującego.

W bibliotece .NET Micro Framework obsługa interfejsu SPI została zaim-

plementowana w klasie SPI z przestrzeni nazw, a przykład jej wykorzystania
do komunikacji z akcelerometrem przedstawimy w ramach implementacji
klasy. W tym celu uzupełnijmy projekt aplikacji MFDiscovery o dodatkowy plik
Accelerometer.cs i umieśćmy w nim polecenia z Listingu 10.

Listing 10. Implementacja komunikacji z akcelerometrem za pomo-
cą interfejsu SPI

using

System;

using

Microsoft.SPOT;

using

Microsoft.SPOT.Hardware;

namespace

MFDiscovery

{

public

sealed

class

Accelerometer

{

private

static

Accelerometer

_instance =

null

;

private

static

object

_lockObject =

new

object

();

private

SPI

_accelerometerPort;

private

Accelerometer()

{

SPI

.

Configuration

SpiConfiguration =

new

SPI

.

Configuration

(

DiscoveryCpuPins

.Accelerometer,

false

, 0, 0,

true

,

true

, 100,

SPI

.

SPI_module

.SPI1);

_accelerometerPort =

new

SPI

(SpiConfiguration);

}

public

static

Accelerometer

Instance

{

get

{

if

(_instance ==

null

)

{

lock

(_lockObject)

{

if

(_instance ==

null

)

{

_instance =

new

Accelerometer

();

}

}

}

return

_instance;

}

}

private

void

WriteToRegister(

byte

address,

byte

value)

{

byte

[] buffer = { address, value };

_accelerometerPort.Write(buffer);

}

private

byte

ReadFromRegister(

byte

address)

{

const

byte

readWriteCmd = 0x80;

byte

[] buffer = { (

byte

)(address | readWriteCmd) };

byte

[] value = { 0, 0 };

_accelerometerPort.WriteRead(buffer, value);

return

value[1];

}

}

}

Kilka aspektów wykorzystanych w poleceniach z Listingu 10 wymaga dodat-
kowego komentarza. Przede wszystkim konfigurację magistrali SPI zrealizo-
waliśmy za pomocą klasy z przestrzeni nazw. Jej konstruktor przyjmuje osiem
argumentów. Pozwalają one zdefiniować następujące parametry:

background image

55

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

» Port GPIO mikrokontrolera, do którego podłączony jest akcelerometr

ChipSelect_Port.

» Stan portu podczas komunikacji z mikrokontrolerem

ChipSelect_ActivePort.

» Czas (w mikrosekundach) konfiguracji wybranego portu GPIO mikrokon-

trolera –

ChipSelect_SetupTime.

» Czas martwy (w mikrosekundach) wybranego portu GPIO mikrokontrole-

ra –

ChipSelect_HoldTime. Parametr ten określa czas, przez który port

pozostanie w stanie aktywnym po zakończeniu operacji odczytu/zapisu z/
do urządzenia peryferyjnego SPI.

» Rodzaj stanu bezczynności – Clock_IdleState. W przypadku, gdy war-

tość tego parametru to

true, zegar próbkujący będzie w stanie aktywnym

(wysokim) podczas braku aktywności (komunikacji) ze strony urządzenia
peryferyjnego. W przeciwnym wypadku zegar będzie w stanie niskim (low).

» Rodzaj zbocza zegara próbkującego – Clock_Edge. Parametr ten pozwa-

la określić rodzaj zbocza sygnału, na którym będą próbkowane dane prze-
syłanych komunikatów. Wartość

true

oznacza, że dane są próbkowane

na zboczu rosnącym, a

false, że na zboczu opadającym.

» Częstotliwość zegara taktującego (w kilohercach) – Clock_RateKHz.

» Magistrala SPI wykorzystywana do komunikacji – SPI_mod.

Kolejne istotne elementy klasy

Accelerometer

to metody

WriteToReg-

ister

oraz

ReadFromRegister. Służą one do zapisywania i odczytywania

danych ze wskazanych rejestrów. Do tego celu wykorzystują metody

Write

i

WriteRead. Pierwsza umożliwia wysłanie tablicy danych do wybranego

urządzenia peryferyjnego za pomocą danej magistrali SPI. Natomiast druga
wysyła zapytanie i zwraca otrzymaną odpowiedź.

W przypadku komunikacji z akcelerometrem wysyłamy do niego tablice dwu-

bajtową, w której bardziej znaczący bajt zawiera adres rejestru, a mniej znaczący
wartość, która ma być w nim zapisana. Natomiast w przypadku odczytu wartości
ze wskazanego rejestru jego adres jest łączony ze stałą 0x80 za pomocą operatora
bitowej alternatywy. Umożliwia to poinformowanie akcelerometru, że dana ko-
menda jest żądaniem odczytu wartości spod wskazanego adresu.

Jak już wspomnieliśmy, w celu przełączenia akcelerometru w normalny tryb

pracy konieczne jest zapisanie odpowiedniej wartości w danym rejestrze. Zgodnie
z dokumentacją akcelerometru (

http://www.st.com/web/en/resource/technical/

document/datasheet/DM00040962.pdf

) jest to rejestr o nazwie

CTRL_REG4, a jego

adres to 0x20.

Ten rejestr kontrolny przechowuje jeden bajt, w którym cztery najbardziej zna-

czące bity (bity b7 – b4) służą do ustalenia częstotliwości pomiarów przyspieszenia
(ang. Output Data Rate /ODR/). Kolejny bit (b3), BDU (od ang. Block Data Update),
pozwala zdefiniować tryb aktualizacji wartości w rejestrach wyjściowych, przecho-
wujących wyniki pomiarów dla poszczególnych osi. Wartość 1 bitu b3 oznacza
ciągłą aktualizację rejestrów, a 0 blokującą aktualizację rejestrów wyjściowych, w
którym odczyt jest blokowany do momentu aktualizacji obu rejestrów związanych
z daną osią. Ta druga ma duże znaczenie w przypadku wykonywania częstych
odczytów rejestrów wyjściowych. Wynika to z faktu, że w akcelerometrze LIS3DH
wyniki pomiarów przyspieszeń reprezentowane są w postaci liczb dwubajtowych.
Blokująca aktualizacja rejestrów wyjściowych zapewnia, że poprawnie odczytywa-
ne są oba bajty. Innymi słowy – oba z nich dotyczą tego samego pomiaru.

Trzy najmniej znaczące bity (b2-b0) bajtu zapisanego w rejestrze kontrolnym

o numerze 4 (

CTRL_REG4) służą do wskazania osi, wzdłuż których będą reali-

zowane pomiary przyspieszenia. Bit b2 odpowiada osi Z, bit b1 osi Y, a b0 osi X.

Uzupełnimy teraz kod źródłowy aplikacji MFDiscovery o procedury umoż-

liwiające skonfigurowanie akcelerometru. W tym celu:

» Dodajemy do projektu nowy plik AccelerometerRegisterAddresses.cs i uzu-

pełniamy go według wzoru z Listingu 11.

Listing 11. Adresy rejestrów akcelerometru wykorzystywane w artykule

using

System;

using

Microsoft.SPOT;

namespace

MFDiscovery

{

public

static

class

AccelerometerRegisterAddresses

{

public

static

byte

WhoAmI = 0x0F;

public

static

byte

Control4 = 0x20;

public

static

byte

Control5 = 0x24;

public

static

byte

LsbX = 0x28;

public

static

byte

MsbX = 0x29;

public

static

byte

LsbY = 0x2A;

public

static

byte

MsbY = 0x2B;

public

static

byte

LsbZ = 0x2C;

public

static

byte

MsbZ = 0x2D;

}

}

» W pliku Enums.cs wstawmy polecenia z Listingu 12.

Listing 12. Definicja typów wyliczeniowych określających częstotli-
wość pomiarów przyspieszenia (

OutputDataRate) oraz osi akcelero-

metru (

Axis)

public

enum

OutputDataRate

:

byte

{

PowerOff = 0,

Freq_3_125_Hz,

Freq_6_25_Hz,

Freq_12_5_Hz,

Freq_25_Hz,

Freq_50_Hz,

Freq_100_Hz,

Freq_400_Hz,

Freq_800_Hz,

Freq_1600_Hz

}

public

enum

Axis

:

byte

{

X = 0,

Y,

Z

}

» Klasę uzupełnimy o metody z Listingu 13.

Listing 13. Konfiguracja częstości pomiaru przyspieszenia wzdłuż
wybranych osi. Dodatkowa metoda

BoolToByte służy do konwersji

wartości logicznej typu

bool na wartość typu byte

public

void

ConfigureOutputData(

OutputDataRate

outputDataRate,

bool

blockDataUpdate,

bool

xAxisEnabled,

bool

yAxisEnabled,

bool

zAxisEnabled)

{

byte

configurationByte = (

byte

)((

byte

)outputDataRate << 4);

configurationByte = (

byte

)(configurationByte

| BoolToByte(blockDataUpdate) << 3

| BoolToByte(zAxisEnabled) << 2

| BoolToByte(yAxisEnabled) << 1

| BoolToByte(xAxisEnabled));

WriteToRegister(

AccelerometerRegisterAddresses

.Control4,

configurationByte);

}

private

byte

BoolToByte(

bool

value)

{

return

(

byte

)(value ==

true

? 1 : 0);

}

W zupełnie analogiczny sposób konfiguruje się zakres pracy akcelerometru.
Na potrzeby implementacji tej funkcjonalności uzupełnijmy najpierw plik
Enums.cs o definicję typu wyliczeniowego

Scale

(Listing 14), a następnie w

klasie

Accelerometer

zdefiniujmy metodę

ConfigureScale

(Listing 15).

Listing 14. Definicja typu wyliczeniowego

Scale, reprezentującego

zakresy pomiaru przyspieszenia

public

enum

Scale

:

byte

{

g2 = 0,

g4,

g6,

g8,

g16

}

background image

56

/ 6

. 2014 . (25) /

PROGRAMOWANIE SYSTEMÓW OSADZONYCH

Listing 15. Konfiguracja zakresu pomiaru przyspieszenia

public

void

ConfigureScale(

Scale

scale)

{

byte

newValue = (

byte

)((

byte

)scale << 3);

WriteToRegister(

AccelerometerRegisterAddresses

.Control5, newValue);

}

W celu konfiguracji zakresu pomiaru przyspieszenia wykorzystaliśmy rejestr
kontrolny o numerze 5 (

CTRL_REG5). Bity b5-b3 bajtu przechowywanego w

tym rejestrze reprezentują wybrany zakres pomiaru przyspieszenia, określo-
ny wartościami typu wyliczeniowego

Scale. Mianowicie, wartość Scale.g2

odpowiada zakresowi ±2g, wartość

Scale.g4 zakresowi ±4g itd.

W ramach podsumowania tego podrozdziału zaimplementujemy jeszcze

metodę ustawiającą domyślne parametry pracy akcelerometru (częstotliwość
odczytu 100 kHz, wyłączony tryb BDU, odczyt ze wszystkich osi oraz zakres
±2g), a także procedurę weryfikującą poprawność wartości odczytanej z reje-
stru akcelerometru. Obie z nich przedstawia Listing 16.

W dalszej części artykułu odczytanie wartości zapisanej w rejestrze

WHO_AM_I pozwoli nam sprawdzić, czy komunikacja z akcelerometrem zo-
stała nawiązana prawidłowo. Jeśli wartość odczytana z tego rejestru będzie
różna od 0x3F, to będzie to oznaczało brak komunikacji z akcelerometrem.

Listing 16. Domyślna konfiguracja akcelerometru oraz odczyt war-
tości z rejestru WHO_AM_I

public

void

DefaultConfiguration()

{

ConfigureOutputData(

OutputDataRate

.Freq_100_Hz,

false

,

true

,

true

,

true

);

ConfigureScale(

Scale

.g2);

}

public

bool

WhoAmI()

{

bool

isCommandCorrect =

false

;

const

byte

expectedAnswer = 0x3F;

byte

answer = ReadFromRegister(

AccelerometerRegisterAddresses

.WhoAmI);

if

(answer == expectedAnswer)

{

isCommandCorrect =

true

;

}

return

isCommandCorrect;

}

Detekcja i sygnalizowanie położenia

W tym podrozdziale na podstawie wartości przyspieszenia wzdłuż osi X oraz
Y odczytanych z akcelerometru zasygnalizujemy zwrot płytki za pomocą diod
LD3-LD6. Jednak zanim przejdziemy do implementacji tego zadania, omówi-
my sposób reprezentacji wartości przyspieszenia w rejestrach akcelerometru.

Akcelerometr LIS3DSH zwraca wyniki pomiarów przyspieszenia wzdłuż

osi poszczególnych osi w jednostkach mg w postaci dwubajtowych liczb
całkowitych. W konsekwencji wartości pomiaru mogą przyjmować wartości
z przedziału od -32767 do 32768. Jednakże dokładność pomiaru (rozdziel-
czość) oraz konkretne wartości przyspieszenia zależeć będą od ustalonego
wcześniej zakresu pomiaru w jednostkach g. W naszym przykładzie domyśl-
na konfiguracja akcelerometru przewiduje pomiary w zakresie ±2g. W takim
przypadku wartości -2g odpowiadać będzie liczba -32767, a +2g liczba 32768.
Wynika stąd, że zwiększenie zakresu pomiaru do ±4g spowoduje dwukrotne
pogorszenie jego rozdzielczości, ponieważ rozdzielczość jest tu nierozerwal-
nie związana z zakresem pomiaru.

W tym konkretnym przykładzie zakładamy, że zmiana mierzonego przy-

spieszenia będzie związana ze stosunkowo wolną zmianą położenia płytki
STM32F4 Discovery w przestrzeni i w związku z tym zakres mierzonych przy-
spieszeń nie przekroczy wartości z przedziału ±1g, co w przybliżeniu odpo-
wiadać będzie liczbom całkowitym z zakresu od -16384 do 16384. Piszemy „w

przybliżeniu”, ponieważ odczyty przyspieszenia obarczone są błędem pomia-
ru oraz mogą zależeć od szerokości geograficznej, która wpływa na wartości
przyspieszenia ziemskiego g.

Akcelerometr przechowuje wyniki pomiarów wzdłuż jego poszczegól-

nych osi w rejestrach o następujących adresach:

» Oś X: LSB – 0x28, MSB – 0x29.

» Oś Y: LSB – 0x2A, MSB – 0x2B.

» Oś Z: LSB – 0x2C, MSB – 0x2D.

W powyższej liście adresów skrót LSB oznacza bajt najmniej znaczący (od ang.
Least Significat Byte). Natomiast MSB reprezentuje bajt najbardziej znaczący
(MSB, od ang. Most Significant Byte).

W związku z tym po odczytaniu wartości typu byte zapisanych w poszcze-

gólnych rejestrach dla danej osi należy je połączyć w jedną wartość typu
short (Int16).

Oto poszczególne etapy implementacji odczytu oraz interpretacji warto-

ści przyspieszenia w projekcie aplikacji (firmware) MFDiscovery:

» W klasie Discovery

zdefiniujmy metody z Listingu 17.

Listing 17. Odczyt zmierzonych wartości przyspieszenia w wybra-
nym kierunku

public

short

GetAcceleration(

Axis

axis)

{

byte

lsbAddress = 0,

msbAdddress = 0;

switch

(axis)

{

case

Axis

.X:

lsbAddress =

AccelerometerRegisterAddresses

.LsbX;

msbAdddress =

AccelerometerRegisterAddresses

.MsbX;

break

;

case

Axis

.Y:

lsbAddress =

AccelerometerRegisterAddresses

.LsbY;

msbAdddress =

AccelerometerRegisterAddresses

.MsbY;

break

;

case

Axis

.Z:

lsbAddress =

AccelerometerRegisterAddresses

.LsbZ;

msbAdddress =

AccelerometerRegisterAddresses

.MsbZ;

break

;

}

byte

lsb = ReadFromRegister(lsbAddress);

byte

msb = ReadFromRegister(msbAdddress);

return

BytesToShort(lsb, msb);

}

private

short

BytesToShort(

byte

lsb,

byte

msb)

{

return

(

short

)(lsb + (msb << 8));

}

» Przejdźmy do edycji pliku Discovery.cs.

» Klasę uzupełnijmy o pole

private

Accelerometer

_accelerometer =

null

;

» Konstruktor klasy Discovery

zmodyfikujmy według wzoru z Listingu 18.

Listing 18. Konstruktor klasy Discovery

private

Discovery()

{

_leds =

Leds

.Instance;

_userButton =

new

InterruptPort

(

DiscoveryCpuPins

.UserButton,

true

,

Port

.

ResistorMode

.PullDown,

Port

.

InterruptMode

.InterruptEdgeHigh);

_accelerometer =

Accelerometer

.Instance;

_accelerometer.DefaultConfiguration();

}

» Definicję klasy Discovery

uzupełnijmy o polecenia przedstawione na

Listingu 19.

background image

57

/ www.programistamag.pl /

PRZETWARZANIE GEOMETRII PRZY POMOCY TRANSFORM FEEDBACK OPENGL 4.3

Listing 19. Zwrot płytki w przestrzeni jest sygnalizowany za pomo-
cą diod LD3-LD6

public

Accelerometer

Accelerometer

{

get

{

return

_accelerometer; }

}

public

void

DetectAndSignalPosition()

{

if

(_accelerometer.WhoAmI())

{

const

short

threshold = 500;

short

accelerationX = _accelerometer.GetAcceleration(

Axis

.X);

short

accelerationY = _accelerometer.GetAcceleration(

Axis

.Y);

Leds.TurnOffAll();

if

(accelerationX < -threshold)

{

Leds.TurnOn(

UserLed

.LD4);

}

else

if

(accelerationX > threshold)

{

Leds.TurnOn(

UserLed

.LD5);

}

if

(accelerationY < -threshold)

{

Leds.TurnOn(

UserLed

.LD6);

}

else

if

(accelerationY > threshold)

{

Leds.TurnOn(

UserLed

.LD3);

}

}

else

{

Leds.TurnOnAll();

}

}

» W pliku Enums.cs uzupełnijmy typ wyliczeniowy WorkingMode

o wartość

wyróżnioną na Listingu 20.

Listing 20. Definicja dodatkowego trybu pracy mikrokontrolera

public

enum

WorkingMode

{

ToggleLed = 0,

ToggleAllLeds,

LedSweeping,

ToggleLedRandomly,

DetectPosition

,

None

}

» W pliku Program.cs zmodyfikujmy definicję metody WorkingModeTh-

readFunction

według wzoru z Listingu 21.

Listing 21. Detekcja i sygnalizacja zwrotu płytki

private

static

void

WorkingModeThreadFunction()

{

const

int

msDefaultSleepTime = 500;

const

int

msSmallerSleepTime = 100;

while

(

true

)

{

if

(!_workingModeChanging)

{

_taskCompletedEvent.Reset();

switch

(_currentWorkingMode)

{

case

WorkingMode

.ToggleLed:

_discovery.Leds.Toggle(

UserLed

.LD3);

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.ToggleAllLeds:

_discovery.Leds.ToggleAll();

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.LedSweeping:

_discovery.Leds.Sweep();

Thread

.Sleep(msDefaultSleepTime);

break

;

case

WorkingMode

.ToggleLedRandomly:

_discovery.Leds.ToggleRandomly();

Thread

.Sleep(msSmallerSleepTime);

break

;

case

WorkingMode

.DetectPosition:

_discovery.DetectAndSignalPosition();

Thread

.Sleep(msSmallerSleepTime);

break

;

case

WorkingMode

.None:

default

:

Thread

.Sleep(msDefaultSleepTime);

break

;

}

_taskCompletedEvent.Set();

}

}

}

Po skompilowaniu i uruchomieniu aplikacji na płytce ewaluacyjnej STM32F4
Discovery możemy przystąpić do sprawdzenia poprawności detekcji zwrotu
płytki w przestrzeni. W tym celu wystarczy pięciokrotnie wcisnąć przycisk
User, aby aktywować tryb pracy mikrokontrolera związany z wartością

De-

tectPosition typu wyliczeniowego WorkingMode. Zmianie położenia
płytki będzie wówczas odpowiadała zmiana stanu odpowiednich diod.

Ze względu na to, że odczyt przyspieszenia jest obarczony błędem pomiaru,

stan diod ulega zmianie w przypadku, gdy wartość odczytanego przyspieszenia
jest większa od arbitralnie ustalonego progu. W powyższym przykładzie (Listing
19) próg został ustalony na poziomie 500, co w przybliżeniu odpowiada ok. 3%
wartości 1g. Czytelnik może dopasować tę wartość według własnych preferencji.

PODSUMOWANIE

W niniejszym artykule przedstawiliśmy przykładowy projekt aplikacji dla ze-
stawu uruchomieniowego STM32F4 Discovery. W ramach implementacji tego
projektu omówiliśmy stosunkowo proste zagadnienia dotyczące zmiany sta-
nu diod LED oraz bardziej zaawansowane kwestie związane z obsługą komu-
nikacji z urządzeniami peryferyjnymi bazującej na interfejsie SPI. Tę ostatnią
wykorzystaliśmy do odczytu wartości przyspieszenia mierzonego przez akce-
lerometr LIS3DSH.

Wszystkie przykłady zostały zaimplementowane w ramach jednej aplika-

cji, a jej użytkownik może przełączać tryby pracy za pomocą przycisku User
płytki STM32F4 Discovery.

Artykuł powstał przy współpracy z Krzysztofem Dalasińskim

Dawid Borycki

Doktor fizyki. Pracuje w Instytucie Fizyki UMK w Toruniu (obecnie na stażu w University of
California, Davis). Zajmuje się projektowaniem oraz implementacją algorytmów cyfrowej
analizy obrazów i sterowania prototypowymi urządzeniami do obrazowania biomedycznego.
Współautor wielu książek o programowaniu i międzynarodowych zgłoszeń patentowych.

background image

58

/ 6

. 2014 . (25) /

Rafał Kocisz

RECENZJA

T

en nieco szalony, jednakże dający
moc satysfakcji tryb pracy wymu-
sza na nas trochę inne spojrzenie

na narzędzia, z których korzystamy. Weź-
my chociażby tak fundamentalną rzecz,
jak repozytorium kodu źródłowego. Konia
z rzędem temu, kto konfiguruje sobie Git czy
SVN na własnym serwerze. Alternatywą są
takie usługi jak Github czy BitBucket, dzięki
którym konfiguracja i obsługa repozytoriów
staje się dziecinną fraszką. Dziś trudno wy-
obrazić sobie pracę bez tych oraz szeregu
innych serwisów, które niemalże z dnia na
dzień stały się podstawowymi narzędziami
większości z nas…

W ramach niniejszego artykułu chciałbym

przedstawić nowoczesny, polski serwis, któ-
ry ostatnio przykuł moją uwagę: MegiTeam.
Nie ma chyba programisty, który nie natknął
się na problem hostingu serwera. Pomyśl tyl-
ko, jak piękny byłby świat, gdyby środowisko
serwerowe dało się skonfigurować w ciągu
kilkunastu sekund, tak jak tworzy się nowe
repozytorium na GitHubie… Z MegiTeam to
marzenie staje się rzeczywistością!

MegiTeam to usługa hostingu nowocze-

snych technologii w chmurze oferująca goto-
we środowiska programistyczne, dzięki któ-
rym możliwe jest szybkie wdrażanie serwisów
webowych i uruchomienie aplikacji jednym
kliknięciem. Serwis dedykowany jest przede
wszystkim dla twórców oprogramowania,
startupów, agencji interaktywnych oraz przed-
siębiorstw oferujących usługi e-commerce.

MegiTeam – hosting od

programistów dla programistów

Zastanawiałem się ostatnio, jakie wartości (w sensie zawodowym) są najcenniejsze
dla nowoczesnego programisty. Odpowiedzi, które w pierwszej kolejności przy-
szły mi do głowy, to: zwinność, elastyczność, dostępność i czas. Każdy niemalże
programista, którego znam, cierpi na notoryczny brak czasu. A rzeczywistość tech-
nologiczna, która nas otacza, wymusza życie (i pracę) w ciągłym pośpiechu. Nowe
projekty, nowe technologie, nowa wiedza, nowi ludzie – i tak w kółko: oto mantra
programisty Anno Domini 2014…

Rysunek 1. Konfiguracja aplikacji Django

Rysunek 2. Konfiguracja serwerów dodatkowych (Redis, Memcached, Celery)

W zakresie technologii, w ramach Megi-

Team do naszej dyspozycji są:

» Django,

» Ruby on Rails,

» Node.js,

» PHP.

Od strony bazodanowej serwis ofe-

ruje wiodące rozwiązania NoSQL: Redis
i MongoDB.

Technologie to tylko jedna strona me-

dalu. MegiTeam daje dużo więcej w ramach
swoich usług. To, co najbardziej rzuca się

background image

59

/ www.programistamag.pl /

MEGITEAM – HOSTING OD PROGRAMISTÓW DLA PROGRAMISTÓW

w oczy, to bardzo wygodny i intuicyjny panel
administracyjny, który na początek szybko
oraz pewnie przeprowadza użytkownika
przez proces konfiguracji, a później oferuje
szeroki wachlarz opcji, dzięki którym zarzą-
dzanie hostowanym środowiskiem staje się
łatwe i przyjemne.

Duże brawa dla zespołu MegiTeam nale-

żą się za prostotę obsługi; specjaliści UX wy-
konali w tym przypadku kawał dobrej robo-
ty. Aby nie być gołosłownym, na Rysunkach
1 i 2 pokazane są przykładowe zrzuty ekranu
z panelu.

Panel MegiTeam to autorskie rozwiązanie,

dzięki któremu serwis jest w stanie oferować
zarządzane VPS-y (ang. Virtual Private Server),
bez konieczności samodzielnej administracji
i posiadania specjalistycznej wiedzy w tym
zakresie. Operacje takie jak dodanie serwisu
WWW, konta pocztowego czy (S)FTP można
wykonać z poziomu panelu administracyjne-
go. Jeżeli klient potrzebuje niestandardowej
konfiguracji, twórcy serwisu oferują pomoc
ze strony swoich administratorów.

MegiTeam stawia na automatyzację, po-

dobnie jak inni twórcy rozwiązań typu PaaS
(ang. Platform as a Service), pozostawiając
przy tym dostęp do shell'a, co daje mnóstwo
swobody w instalowaniu dodatkowego opro-
gramowania i gwarantuje dostęp do logów
(lepsza diagnostyka problemów). Warto w
tym miejscu podkreślić, że w ramach stan-
dardowej usługi hostingowej oferowanej
przez MegiTeam dostajemy to, za co w innych
PaaS'ach trzeba dodatkowo płacić: Postgres,
Memcached, oraz bazy NoSQL. Co więcej, w
ramach jednego konta hostingowego można
zakładać wiele wirtualnych serwisów, któ-
re znajdują się w obrębie tej samej instancji
chmury, przy czym opłata za nie pozostaje
stała (jest niezależna od ich liczby).

Autorzy serwisu nie zapomnieli również

o drobnych, aczkolwiek wysoce użytecznych
usprawnieniach. Dobrym przykładem tego
rodzaju udogodnienia jest możliwość reje-
stracji za pośrednictwem istniejącego konta
w takich serwisach jak Github, Twitter czy
Facebook (patrz: Rysunek 3). W przypadku

Rysunek 3. Logowanie do panelu administracyjnego

skorzystania z tej opcji szczegółowe dane
użytkownika trzeba wypełnić dopiero wtedy,
gdy potrzebne jest wygenerowanie faktury.

Wielką siłą serwisu MegiTeam oraz warto-

ścią dodaną, która w dużej mierze odróżnia
tę usługę od oferty konkurencji, jest zwin-
ność i elastyczność. Świetnym zobrazowa-
niem tej właściwości jest scenariusz obsługi
skoków obciążenia poprzez dynamiczne do-
kładanie zasobów (gdy okażą się niezbędne)
oraz ich usuwanie (by nie przepłacać, gdy
przestają być potrzebne). Przykład: określo-
na marka pojawia się w popularnym progra-
mie telewizyjnym, prasie, tudzież reklamuje
się poprzez znanego blogera. W efekcie tych
działań na jej stronie powstaje wzmożony
ruch. Abonamentowy hosting współdzielo-
ny najprawdopodobniej nie udźwignie ta-
kiego obciążenia i goście odwiedzający stro-
nę zobaczą błąd 503. Skalowalne serwery
w chmurze, które oferuje MegiTeam, pozwa-
lają zwiększyć zasoby do 64 GB RAM i 16 CPU
w ciągu kilku minut. Co istotne, klient zapłaci
tylko za czas, kiedy zwiększone zasoby były
mu potrzebne. W przypadku krótkich kam-
panii marketingowych wymagających stro-
ny internetowej MegiTeam oferuje opcję ho-
stingu na kilka dni czy nawet godzin. Koszt
takiej usługi to kilka groszy za godzinę. Po
zakończonej kampanii serwis lub konto
można samodzielnie usunąć.

W MegiTeam każdy klient posiada wła-

sny serwer z gwarantowanym procesorem

i pamięcią oraz własne serwery baz danych.
Serwis daje gwarancję, że obciążenie gene-
rowane przez innych klientów nie spowal-
nia działania jego stron. Takie rozwiązanie
to również większe bezpieczeństwo. Nawet
w najbardziej podstawowej opcji możliwe
jest podpisanie umowy powierzenia prze-
twarzania danych osobowych.

MegiTeam jest również bardzo elastycz-

ny w zakresie płatności, które w tym przy-
padku opierają się na doładowaniu konta.
Nie ma stałych abonamentów, co oznacza
brak długoterminowych umów i zobowią-
zań. Jeśli ktoś tęskni za abonamentem z po-
wodu regularnych przypomnień o płatności,
może włączyć automatyczne doładowania
(mailowe przypomnienia z załączoną fakturą
pro-forma przypominające, że już czas doła-
dować konto).

Na koniec trzeba też pochwalić serwis za

profesjonalny i przyjazny support. Doświad-
czeni admini szybko i konkretnie odpowiadają
na każde zgłoszenie dotyczące hostingu, po-
magają w optymalizacji i naprawianiu błędów.

Cóż więcej mogę napisać… Hasło mar-

ketingowe MegiTeam: „od zera do Django
w 30 sekund”, które nie jest bynajmniej
czczą przechwałką, mówi samo za siebie. Dla
mnie od dziś MegiTeam jest domyślną opcją
w zakresie hostingu VPS'ów w chmurze! Ser-
decznie polecam każdemu wypróbowanie
tego serwisu. Znajdziecie go pod adresem

http://www.megiteam.pl/

.

background image

60

/ 6

. 2014 . (25) /

WYWIAD

H

ello World Open to konkurs programistyczny, podczas którego celem
uczestników była implementacja sztucznej inteligencji sterującej wir-
tualną wyścigówką, która porusza się po torach wyścigowych o róż-

nym poziomie trudności, przy zmieniających się warunkach pogodowych.
Do realizacji tego celu organizator udostępnił uczestnikom repozytoria Git ze
zintegrowanymi systemami ciągłej integracji, kilka serwerów testowych oraz
specyfikację protokołu, przy pomocy którego z serwerem gry powinien się
komunikować robot. Drużyny mogły wybierać z całej gamy języków progra-
mowania, takich jak C, C++, Java, C#, Python czy nawet JavaScript.

W finale, który odbył się 10 czerwca w Helsinkach, rywalizowało 8 drużyn

– po jednej z Polski, Słowacji i Finlandii, dwie z Brazylii i trzy z Rosji. Pośród
uczestników znaleźli się pracownicy Google (Tomasz Żurkowski z polskiego
teamu „Need For C” i Luca Mattos Möller z brazylijskiej „Itarama”) oraz Facebo-
oka (Michal Burger ze słowackiej drużyny). Organizatorzy zwrócili szczególną
uwagę na to, aby poziom trudności zwiększał się z każdą rundą, dzięki czemu
we wstępnych etapach zawodów wysokie szanse na dobry wynik mają nawet
początkujący programiści.

Programista może wpływać na dwa czynniki. AI może chcieć zwiększyć

prędkość pojazdu, dodając gazu, lub zmienić pas, jeśli przed nim jest inny zawod-
nik. Dzięki temu podstawy sterowania są bardzo proste, ale jeśli chcesz wygrać –
to bardzo skomplikowane. Wszystkie samochody mają identyczne „silniki” i opo-
ny. Wynik wyścigu zależy od tego, jak szybko uczestnicy są w stanie pokonywać
zakręty – na ile są pewni, że mogą pokonać dany zakręt bez ryzyka wypadnięcia
z toru. Jeśli wyścigówka będzie jechała zbyt szybko, wypadnie poza tor i drużyna
otrzyma 2 sekundy kary, a to bardzo pogarsza sytuację
. – mówi Ville Valtonen,
główny organizator zawodów.

Warty zauważenia jest również sam pomysł, od którego wszystko się za-

częło. Konkurs został zaprojektowany tak, aby rywalizacja mogła być obser-
wowana również przez osoby kompletnie nie związane z programowaniem
czy branżą IT. Analogicznie zresztą jak w przypadku innych sportów – nie trze-
ba być piłkarzem, aby rozumieć ideę rozgrywek piłkarskich i móc kibicować
swoim faworytom.

Kilka minut przed rozpoczęciem finałów i miażdżącym zwycięstwem pol-

skiej drużyny, pytaliśmy ich o nastroje przed ostateczną rozgrywką.

Magazyn Programista: Jesteśmy na Hello World Open 2014, w klubie Cable
Factory w Helsinkach. Jak wam się podoba atmosfera na finałach?

Piotr Żurkowski: Jest super. Naprawdę, przygotowanie niesamowite… i sam
finał też.

MP: Lekki stres przed finałem jest?

Piotr Żurkowski: Jest, ogromny. I nie wierzę, że po nas tego nie widać, jesteśmy
bardzo zestresowani.

Wojciech Jaśkowski: My generalnie od 2 tygodni ledwo śpimy, maksymalnie
po 5-6 godzin. Każdego dnia mówimy sobie, że tym razem położymy się wcze-
śniej. Ja dzisiaj zasnąłem o 3:00, podobnie Piotrek i Tomek.

MP: Nazywacie się „Need For C”. Rozumiem, że to jakieś personalne
zamiłowania?

Piotr Żurkowski: Myśleliśmy o jakiejś fajnej nazwie, oczywiście gra z dzieciń-
stwa – Need For Speed, no i jesteśmy takimi trochę „freakami”, lubimy C++.

MP: Na co dzień pracujecie nad projektami w C, C++?

Tomasz Żurkowski: To zależy, ja na przykład pracuję w Google, przez cały czas
nad projektami w C++. Czas odpowiedzi się liczy – program musi działać szyb-
ko. Tak naprawdę wszyscy znamy C++, lepiej lub gorzej, dla mnie jest to na
przykład mój pierwszy język.

Wojciech Jaśkowski: Piotr mówi za siebie, że wszyscy lubimy C++. Ja akurat go
nie cierpię, ale oni podjęli taką decyzję – na początku tylko oni mieli kodować,
a ja miałem doradzać, ale potem mnie też jakoś mocniej zaangażowało...

MP: To dość nietypowa konfiguracja drużyny: dwóch braci i ich wykładowca?

Wojciech Jaśkowski: W zeszłym roku mieliśmy zajęcia z Piotrem. Generalnie
wcześniej znałem się z Tomkiem, jak on zaczynał studiować, to ja kończyłem
– znaliśmy się głównie poprzez konkursy programistyczne. Ja organizowałem
jeszcze różne „treningi programistyczne”, to taki „łańcuszek znajomości”.

Piotr Żurkowski: Wydaje mi się, że na naszej uczelni (Politechnika Poznańska)
jest po prostu kilka osób dobrych w algorytmach i wszyscy siebie bardzo do-
brze znają. Tak jakoś wyszło.

MP: Skąd pomysł, żeby brać udział w Hello World Open?

Piotr Żurkowski: Kolega pracujący w Helsinkach wysłał mi link, że są takie
zawody i fajnie byłoby wziąć udział. No a jak brać udział – to z bratem. Nie
wiedzieliśmy, że potraktujemy to aż tak bardzo na serio. Na początku miała

Polacy górą! Relacja z finałów

Hello World Open 2014

Kolejny sukces Polaków na międzynarodowych zawodach! Polski team „Need For C”
podczas finałów w Helsinkach w zeszłym miesiącu pokonał ponad 2,5 tysiąca drużyn
z całego świata, tym samym zgarniając tytuł zwycięzcy i ponad 20 tysięcy złotych.

Zwycięzcy Hello World Open 2014. Od lewej: Piotr Żurkowski, Tomasz

Żurkowski, Wojciech Jaśkowski. /fot. Tuomas Sauliala/Karttahuone Oy

background image

61

/ www.programistamag.pl /

POLACY GÓRĄ! RELACJA Z FINAŁÓW HELLO WORLD OPEN 2014

to być po prostu zabawa – a nuż uda się zrobić coś ciekawego. Drużyny były
trzyosobowe, to czemu mieć dwie osoby, skoro można dobrać kogoś jeszcze...

Wojciech Jaśkowski: Piotrek mnie strasznie męczył, bo nie chciałem w tym
brać udziału – po prostu nie miałem na to czasu...

Piotr Żurkowski: Ja chciałem ściągnąć Wojtka – on będzie tylko doradzał i
omawiał taktyki. No i faktycznie – w kwalifikacjach bardzo dużo rozmawiali-
śmy. Robiliśmy trochę researchu ze wszystkich stron, a potem Wojtek się bar-
dzo mocno wkręcił i dotarliśmy aż do finału.

MP: Spędziliście masę, naprawdę masę godzin nad przygotowaniami. To była
dość długa przeprawa do finału. Z osiągnięcia czego jesteście najbardziej dumni?

Tomasz Żurkowski: Dużo zabawy, na pewno dużo zabawy.

Piotr Żurkowski: Rywalizacja z innymi ludźmi, każdy coś napisał i patrzy się,
co oni wymyślili.

Tomasz Żurkowski: To coś innego niż normalna praca, to nie jest pisanie kodu,
który musisz napisać. To bardzo fajne, że można wrócić do młodszych lat,
gdzie było tak dużo kreatywności, myślenia. Mimo wszystko później w róż-
nych przypadkach kreatywność spada. Naprawdę bardzo fajnie bawiło się w
gadanie, wymyślanie różnych rzeczy, pisanie, sprawdzanie, testowanie… Cu-
downe, po prostu cudowne.

MP: W jaki sposób wasze rozwiązanie pokonało prawie 2,5 tysiąca drużyn
z całego świata? Co trzeba zrobić, żeby pokonać tyle różnych ekip?

Wojciech Jaśkowski: Poświęcić więcej czasu i więcej chęci.

Piotr Żurkowski: Naszym dużym atutem był fakt, że mamy duży background
w algorytmice i jesteśmy dość biegli w takich rzeczach. Optymalizacje kodu,
różne tego typu rzeczy nie są nam straszne. Wojtek w ogóle specjalizuje się w
sztucznej inteligencji. Tak jak Tomek mówił – dużo czasu, dużo zacięcia.

MP: Czyli największą trudnością według was były problemy matematyczne?

Tomasz Żurkowski: To zależy, ten konkurs ma bardzo dużo takich „gałęzi”: po
pierwsze trzeba umieć jeździć dobrze, po drugie trzeba dobrze sobie radzić z
innymi samochodami i tak dalej… Każde podejście do konkretnej rzeczy jest
zupełnie inne, ale jednocześnie wszystkie są ze sobą tak powiązane, że bardzo
ciężko to dobrze rozdzielić, ciężko powiedzieć, co było najtrudniejsze.

Wojciech Jaśkowski: Tu nie było dużo matematyki. Ja bym tego nie nazwał
matematyką – to było mnożenie i dzielenie… takie podwórko.

Tomasz Żurkowski: Trochę algorytmów było, trochę fizyki, trochę mate-
matyki. Zobaczyliśmy jakieś pochodne i próbowaliśmy coś oszacować,
aproksymować…

Wojciech Jaśkowski: Ja myślę, że ważny był podział całego kodu na moduły,
tego się nie da ugryźć jednym modułem. Tutaj koledzy ładnie podzielili to na
moduły, które były do pewnego stopnia niezależne. Dzięki temu można było ła-
two podzielić pracę pomiędzy osoby, które wzajemnie niczego sobie nie psuły.

Tomasz Żurkowski: Ten sam początek, sama baza była dobrze napisana i po-
tem można było na nim opakowywać.

Wojciech Jaśkowski: Też było ważne to, że jakieś tam doświadczenie mamy z
różnego rodzaju programowania i wiemy, jak ważne są testy. Dlatego dużo
mamy testów jednostkowych w kodzie, ale również dużo takich testów re-

gresyjnych, mamy swój symulator, który puszczamy np. 10k razy i patrzymy,
czy nie ma żadnego błędu, żadnej błędnej rzeczy w kodzie, po każdej zmianie,
czy coś się nie pogorszyło. Wtedy w miarę bezpieczne jest zmienianie.

MP: Czyli żeby pokonać 2,5 tysiąca drużyn kod w tym konkursie musiał trzy-
mać wysokie standardy?

Piotr Żurkowski: Ilość tego kodu troszeczkę wymusza fakt, żeby był dobrze
napisany, czytelnie i żeby potem rozwinięcie tego, co się robiło miesiąc temu,
nie rozwaliło nagle czegoś innego.

Wojciech Jaśkowski: Mieliśmy dużo refaktoryzacji, one były konieczne z inny-
mi przeróbkami. Pracowaliśmy nad tym od miesiąca, jeżeli pracujesz nad pro-
jektem tyle czasu, to nie możesz sobie pozwolić na to, żeby dzisiaj coś napisać,
a jutro wyrzucić Nie może być błędów. Jak konkurencja ma jeden błąd – to
wygrywamy. To jest też gra błędów w tej chwili.

Tomasz Żurkowski: Kto ma jakie błędy i które się pojawią, dokładnie.

Wojciech Jaśkowski: My na przykład jesteśmy świadomi jednego błędu, który
mamy, od wczoraj jesteśmy świadomi, wiemy, jak go naprawić, ale nie podję-
liśmy się tego – baliśmy się, że naprawa pociągnie za sobą zbyt wiele nega-
tywnych konsekwencji i dorzucimy nowe błędy. Ten, który mamy, objawia się
bardzo rzadko, raz na 10 wyścigów w bardzo specyficznych sytuacjach. Mamy
nadzieję, że się nie wydarzy.

MP: Czyli zgodnie z ideą programowania, przy fixowaniu bugów, doszliście
do problemów, które mało przeszkadzają?

Wojciech Jaśkowski: Ważyliśmy ryzyka. Czy to ryzyko, że mamy błąd i wiemy
o tym, czy to ryzyko, że chcielibyśmy coś tam przebudować, wprowadzimy
nowe rzeczy i już nie będziemy tacy pewni.

Tomasz Żurkowski: Tym bardziej że tu akurat są dwa przypadki – jeżdżenie sa-
memu można było bardzo łatwo przetestować. Później była iteracja z innymi
osobami – trenowaliśmy już z innymi finalistami i wiedzieliśmy, że nasz kod
dobrze działa, nie zastanawialiśmy, czy jest sens cokolwiek zmieniać. Ciężko
było przewidzieć, jak zmiana zareaguje na innych graczy.

Wojciech Jaśkowski: To jest ciekawy aspekt tego konkursu, real time – my mu-
simy de facto odpowiedzieć w czasie 4 milisekund. Musimy zawrzeć w tym
całą logikę sztucznej inteligencji. W celu optymalizacji robimy na przykład
branch and bound

1

. Mnóstwo innego kodu nie można kwalifikować do jed-

nego algorytmu, ale problemy wydajnościowe również były bardzo ważne.

MP: Robiliście zwody na treningach, żeby inni czuli się silniejsi?

Piotr Żurkowski: Mamy nadzieję, że inni tego nie robili. Myślę, że wszystkie
teamy chciały dobrze przetestować to, co mają. Nieprzetestowanie czegoś
w poprzednich dniach może spowodować faktyczne problemy w finale. W
związku z tym zakładamy, że wszyscy pokazali maksimum swoich możliwo-
ści… i czujemy się bardzo silni.

Kod zwycięskiej drużyny wraz z opisem technicznym jest dostępny na GitHubie

2

.

Z organizatorami konkursu i uczestnikami rozmawiał Michał Leszczyński.

1

http://en.wikipedia.org/wiki/Branch_and_bound

2

https://github.com/kareth/helloworldopen2014

background image

62

/ 6

. 2014 . (25) /

PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Dawid Morawiec

WSTĘP

Pierwsze systemy mainframe wymagały ogromnych pomieszczeń, które po brze-
gi wypełnione były urządzeniami I/O. Typowa instalacja zajmowała powierzch-
nię od 200 do 1000 m

2

. Systemy IBM 705 z 1954 r. oraz ich następna generacja

IBM 1401 z 1959 r. były zoptymalizowane pod kątem konkretnych zastosowań,
takich jak inżynieria czy złożone obliczenia laboratoryjne, brakowało im jednak
elastyczności i uniwersalności. Punktem zwrotnym okazał się być wprowadzony
przez IBM w 1964 r. System/360 (Rysunek 1). Numer w nazwie systemu nie jest by-
najmniej przypadkowy, liczba 360 miała oddawać uniwersalność systemu, 360˚
- krąg możliwych zastosowań. S/360 był pierwszym komputerem wykorzystują-
cym możliwość mikroprogramowania, wcześniej lista rozkazów była wbudowa-
na w fizyczną architekturę komputera, tzn. każdej instrukcji odpowiadał osobny
układ elektroniczny odpowiedzialny za jej wykonanie.

W roku 1970 IBM wprowadził na rynek rodzinę maszyn System 370 (Ry-

sunek 2), która potrafiła wykorzystać więcej niż jeden procesor oraz pamięć
dzieloną. Wszystkie elementy w obwodach były wytwarzane na jednym pla-
strze krzemu, System 370 był również pierwszym komputerem implementu-
jącym mechanizm pamięci wirtualnej.

Rysunek 1. Komputer S/360 Model 40

Lata 80-te przyniosły kolejne zmiany na rynku w postaci zwiększenia liczby
instrukcji procesora, rozszerzono adresowanie pamięci oraz dodano wsparcie
dla wielokrotnej przestrzeni adresowej.

Rysunek 2. Komputer S/370 Model 165

W latach 90-tych istotnym zagadnieniem dla IBM, jak i całego sektora informa-
tycznego, było wprowadzenie logicznej partycji (LPAR) – technologii wirtualiza-
cji, która umożliwiła podział zasobów jednej fizycznej maszyny na kilka nieza-
leżnych jednostek. Wielu ekspertów twierdziło, że serwery te nie przetrwają do
końca pierwszej połowy lat 90-tych. Przewidywano, że gwałtowny rozwój kom-
puterów PC oraz niewielkich serwerów doprowadzi do ich zaniku. Zupełnie
odwrotną postawę prezentował IBM, przedstawiając System/390 (Rysunek 3).

Rysunek 3. Komputer S/390

W kolejnym okresie same mainframe, jak i urządzenia I/O stawały się coraz
mniejsze i tańsze, podczas gdy ich funkcjonalność oraz moc obliczeniowa
rosły. Wszystkie systemy „Big Iron” firmy IBM na poziomie aplikacyjnym, od
architektury S/360, są kompatybilne wstecz, co oznacza, że oprogramowanie
napisane dla modeli wcześniejszych może być uruchamiane na aktualnie eks-
ploatowanych serwerach.

Obecnie produkowanym przez IBM systemem jest System z (Rysunek 4),

występujący w dwóch wersjach: Business oraz Enterprise.

Rysunek 4. System z Enterprise

IBM Mainframe

W tym roku serwery mainframe obchodzą 50 rocznicę istnienia. Jest to dobry powód,
aby przybliżyć je czytelnikom Programisty. Oczywiście w tak krótkim opracowaniu nie da
się zawrzeć wszystkich informacji, a te przedstawione niżej stanowią jedynie wstęp do
całej technologii oraz szeregu rozwiązań składających się na tę technologię. Jak zdefi-
niować mainframe? Nie da się zrobić tego jednoznacznie, ponieważ mainframe to zbiór
cech biznesowych i funkcjonalności, a nie hardware i software, z których się składa.

background image

63

/ www.programistamag.pl /

IBM MAINFRAME

ARCHITEKTURA I PROCESOR

Po tym krótkim wstępie zajrzyjmy do wnętrza maszyny. S/360 bazuje na ar-
chitekturze von Neumanna, procesory centralne są oparte na architekturze
CISC, a ich wydajność mierzona jest w MIPS (ang. Million Instructions Per Se-
cond
). Seria System z składa się z kilku różnych procesorów, które wspólnie
określane są jako PU. Każdy z PU charakteryzuje się wielozadaniowością,
może także być zaprogramowany do pełnienia odpowiedniej funkcji:

» CP (Central Processor) – najważniejszy procesor, wykorzystywany przez

system oraz aplikacje użytkownika (procesor ogólnego przeznaczenia);

» SAP (System Assistance Processor) – każdy mainframe ma przynajmniej

jeden procesor tego typu, ponieważ zapewnia on wewnętrzne wsparcie
dla obsługi operacji I/O;

» IFL (Integrated Facility for Linux) – typ zaprojektowany specjalnie do ob-

sługi systemu z/Linux.

» zAAP (System z Application Assist Processor) - dodatkowy procesor zwięk-

szający wydajność aplikacji napisanych w Javie;

» zIIP (System z Integrated Information Processor) – zapewnia wsparcie przy

przetwarzaniu zasobów bazodanowych (bezpośredni dostęp do bazy DB2);

» ICF (Integrated Coupling Facility) – obsługuje klaster maszyn działających

w technologii Parallel Sysplex;

» Spare – procesor zapasowy, jeśli w system wykryje nieprawidłowość w

którymś PU, to może być on zastąpiony przez Spare. W większości przy-
padków odbywa się to bez konieczności przerywania działania systemu.

Rysunek 5. Hardware

PAMIĘĆ

Wraz z rozwojem architektury następował wzrost wartości adresowania pa-
mięci od 24 do 64 bitów (Tabela 1). Wprowadzono pamięć wirtualną oraz stro-
nicowanie, jednak usprawnieniem, które przyniosło najwięcej korzyści, była
implementacja pamięci cache.

Serwer

Adresowanie

S/360

24-bitowe adresowanie (wykorzysta-
nie 16MB pamięci)

S/370

24-bitowe adresowanie (wykorzysta-
nie 16MB pamięci)

S/390

31-bitowe adresowanie (wykorzysta-
nie 2GB pamięci)

System z

64-bitowe adresowanie (wykorzysta-
nie 16EB pamięci)

Tabela 1. Adresowanie pamięci

DYSKI

Do przechowywania danych, programów oraz samego systemu operacyj-
nego w systemie z/OS wykorzystywane są wolumeny DASD – Direct Access
Storage Device (urządzenia pamięciowe o dostępie bezpośrednim). W odróż-
nieniu od systemów UNIX-owych, struktura danych w systemie z/OS nie jest
hierarchiczna - dane przechowywane są sekwencyjnie.

WIRTUALIZACJA

Już w roku 1964 rozpoczęto prace nad systemem umożliwiającym wirtualiza-
cję całej maszyny w celu lepszego wykorzystania zasobów. Ewoluowała ona
od programowej do sprzętowej. Obecnie stosowanym rozwiązaniem przez
IBM jest wirtualizacja w postaci partycjonowania logicznego LPAR, która
umożliwia uzyskanie do 60 odizolowanych systemów.

TYPOWE ZADANIA NA MAINFRAME

Do najczęściej spotykanych zadań, jakimi obciążony jest mainframe, należą:
przetwarzanie transakcyjne (on-line) oraz przetwarzanie wsadowe (ang. batch).

Przetwarzanie wsadowe to zadania wykonywane bez interakcji użytkow-

nika. Operator uruchamia program, następnie oczekuje na wyniki. Charakte-
rystycznym dla przetwarzania wsadowego jest:

» duża ilość danych wejściowych oraz wyjściowych;

» setki/tysiące zadań uruchamianych w określonej kolejności lub jednocześnie;

» mała ilość użytkowników nadzorujących.

Przetwarzanie transakcyjne odbywa się w sposób interaktywny z użytkowni-
kiem. Charakteryzuje się:

» małą ilością danych wejściowych i wyjściowych;

» natychmiastowym czasem odpowiedzi;

» dużą liczbą użytkowników wykonujących wiele transakcji w tym samym czasie.

Dobrym przykładem zastosowania obu przetwarzań są banki: klienci, wypła-
cając pieniądze z bankomatu, wykonują przetwarzanie transakcyjne, pracow-
nicy banku natomiast przetwarzanie wsadowe, kiedy przeliczają salda kont.

ROLE NA SERWERZE

Serwer mainframe jest wykorzystywany przez wielu użytkowników w tym sa-
mym czasie, dodatkowo może być uruchomiona na nim duża liczba aplikacji,
dlatego też konieczne stało się podzielenie ról oraz zadań. Najczęściej spoty-
kanym jest podział na:

» Administratorów systemu;

» Administratorów aplikacji;

» Programistów;

» Operatorów .

SYSTEM Z/OS

Obecnie najpopularniejszym systemem operacyjnym na mainframe jest
z/OS. Jak każdy OS zarządza on platformą oraz tworzy środowisko dla pro-
gramów i zadań. Do ciekawych należy sposób komunikacji użytkownika z
systemem, najpowszechniej wykorzystywany jest interfejs TSO/E (ang. Time
Sharing Option/Extensions
), ISPF (ang. Interactive System Productivity Facility)
oraz powłoka UNIX. Aby zalogować się do TSO, wymagana jest konsola 3270
lub jej emulator TN3270 uruchomiony na komputerze PC (Rysunek 6).

Rysunek 6. Okno logowania do TSO

background image

64

/ 6

. 2014 . (25) /

PROGRAMOWANIE ROZWIĄZAŃ SERWEROWYCH

Jako że TSO działa w trybie tekstowym (Rysunek 7), to w przypadku większości
użytkowników, po zalogowaniu od razu uruchamiany jest ISPF (Rysunek 8).

Rysunek 7. TSO tryb natywny

Rysunek 8. ISPF Okno główne

Na Rysunku 7 widać wydane przez użytkownika polecenie

ALLOC alokujące

zbiór o nazwie

USERID.TEST.CNTL. READY jest odpowiednikiem znaku za-

chęty w systemach UNIX - system czeka na wydanie polecenia. Na Rysunku
8 widać główne okno ISPF, jest to bardziej przyjazna użytkownikowi forma
komunikacji z systemem (użytkownik przemieszcza się pomiędzy opcjami,
wpisując odpowiedni numer w polu

Option ===>). Dodatkowo standar-

dowa konsola 3270 była wyposażana w klawisze funkcyjne od PF1 do PF24
- przycisk PF3 oznacza wyjście z zapisem, a PF1 uruchamia podręczną pomoc.
z/OS posiada warstwę UNIX-ową, do której logujemy się z poziomu TSO lub
poprzez telnet, np. używając programu PuTTY. Ten sposób przedstawia Rysu-
nek 9 i zapewne będzie wydawał się znajomy większości czytelnikom.

Rysunek 9. Połączenie via telnet

JCL I SDSF

JCL (ang. Job Control Language) jest językiem, który powinna znać każda osoba
związana z mainframe, służącym do wykonywania różnych działań systemie,
np. który program chcemy uruchomić, oraz niezbędne zasoby do jego wykona-

nia. W JCL możemy wykonywać polecenia TSO, ISPF oraz bardzo wiele czynno-
ści związanych z systemem oraz potrzebami użytkownika. Przykład zadania JCL
pokazany na Rysunku 10 to sortowanie danych podanych w zdaniu SORTIN.

Rysunek 10. JCL

Po uruchomieniu konkretne zadanie (ang. submmit ), jego przebieg oraz efekt
działania można śledzić w SDSF (ang. System Display and Search Facility).

Rysunek 11. SDSF

Przykład tego, jak nasze zadanie sortujące wygląda w SDSF, prezentują Ry-
sunki 12 i 13.

Rysunek 12. Zadanie MYJOB w SDSF

Rysunek 13. Efekt wykonania sortowania

Na koniec chciałbym nadmienić, że na temat serwerów mainframe, ich funk-
cjonowania i rozwoju, można by napisać dużo dużo więcej, dlatego też bar-
dziej dociekliwych odsyłam do darmowych publikacji znajdujących się na
stronie:

http://www.redbooks.ibm.com/

Dziękuję firmie IBM za umożliwienie wykorzystania ilustracji.

Dawid Morawiec

morawiec.d@gmail.com

Absolwent Wydziału Fizyki i Informatyki Stosowanej Uniwersytetu Łódzkiego. Od dwóch lat pra-
cuje z serwerami mainframe. Na co dzień programuje w języku REXX oraz w szczególnie lubianej
przez siebie Javie. Ponadto interesuje się robotyką i technologiami internetowymi. W wolnym
czasie poświęca się pasji słuchania muzyki oraz czytania książek.

background image
background image

66

/ 6

. 2014 . (25) /

LABORATORIUM BOTTEGA

Paweł Badeński

Z

darza mi się nawet spotykać z sytuacjami, kiedy sfrustrowane zespoły
rezygnują ze spotkań w ogóle. Uważam, że takie podejście to często
„wylewanie dziecka z kąpielą”. Komunikacja oraz feedback są niezbęd-

nymi fundamentami Agile. Popularne metodyki takie jak Scrum wymagają od
członków zespołu prowadzenia określonego typu spotkań (planowanie sprintu,
retrospektywa). Niestety z mojego doświadczenia wynika, że niewiele mówi się
na temat organizowania oraz prowadzenia spotkań. Natomiast wiedza na ten
temat jest jedną z fundamentalnych metaumiejętności efektywnych zespołów.

Dlatego w artykule zaproponuję skuteczne techniki prowadzenia spo-

tkań, które pomagają oszczędzić czas, a jednocześnie wykorzystać wiedzę
wszystkich członków zespołu.

PODSTAWOWE PROTOKOŁY

W artykule przedstawię kilka z podstawowych protokołów (ang. The Core
Protocols
) – strategii efektywnych zespołów opracowanych przez Jima oraz
Michele McCarthy. Autorzy protokołów dostrzegli, że skuteczne zespoły
współdzielą zachowania, które pozwalają im zaoszczędzić czas oraz lepiej się
komunikować. Pełna lista podstawowych protokołów zawiera 12 wzorców – w
artykule przedstawię skrótowo wybrane z nich (Decider, Resolution, Check in
oraz Pass). Pokażę, w jaki sposób mogą być wykorzystane do rozwiązania typo-
wych problemów spotkań. Jeśli zdecydujesz się z nich skorzystać, pełny opis
znajdziesz online pod adresem

http://liveingreatness.com/core-protocols

.

Ponadto warto, abyś zapoznał się z listą Podstawowych Zobowiązań – za-
sad, które stanowią fundament dla wykorzystania protokołów i powinny być
wcześniej zaakceptowane przez zespół.

TYPOWE PROBLEMY Z PROPOZYCJĄ

ROZWIĄZAŃ

Pracując z wieloma zespołami, zauważyłem, że nudne, męczące oraz wydłuża-
jące się spotkania łączą wspólne cechy. Poniżej opiszę cztery, które uważam za
najważniejsze: brak przygotowania, zbaczanie z tematu, rozwlekanie spotka-
nia oraz „burze emocjonalne”. Wraz z problemami zaproponuję techniki, które
pomogą zaadresować te typowe problemy.

Problem: Brak przygotowania

Według mnie dobre spotkanie jest jak dobra historyjka użytkownika i przede
wszystkim ma kryteria akceptacyjne. Niejednokrotnie uczestniczyłem w spo-
tkaniach, gdzie organizator proponował słuszny pomysł, np. ograniczenie

długu technicznego. Niestety brak przygotowania powodował, że spotkanie
kończyło się porażką. Brak przygotowania przybiera różną postać, ale zawsze
negatywnie wpływa na wynik spotkania, przykładowo:

» brak ram spotkania (np. agendy) spowoduje, że spotkanie przerodzi się w

dyskusję filozoficzną, bądź będzie okazją dla członków zespołu do wyraża-
nia opinii niezwiązanych z tematem,

» brak określonej listy uczestników może sprawić, że 80% osób w pomiesz-

czeniu będzie tam niepotrzebnych.

» brak oczekiwanych wyników da iluzję owocnego spotkania, po którym nie

zostaną podjęte żadne praktyczne kroki.

Rozwiązanie: Framework 7P

Bardzo prostą i efektywną techniką, która pozwala na skuteczne przygo-

towanie spotkania, jest framework 7P, opracowany przez Jamesa Macanufo.
Jego nazwa odnosi się do siedmiu artefaktów, które składają się na dobrze
przygotowane spotkanie: cel (ang. Purpose), produkt spotkania (ang. Product),
ludzie (ang. People), proces (ang. process), pułapki (ang. pitfall), przygotowanie
(ang. prep), zagadnienia praktyczne (ang. practical concerns). Poniżej opiszę
pokrótce rolę każdego z elementów:

» cel – jest odpowiedzią na pytanie „Dlaczego?”, tłumaczy istotę spotkania

oraz czemu jest ważne,

» produkt spotkania – identyfikuje listę rzeczy, które oczekujemy, że po-

wstaną podczas spotkania: listy, diagramy, szkice, plany, zadania do zre-
alizowania itp.

» ludzie – lista ludzi, którzy powinni uczestniczyć w spotkaniu. Może rów-

nież określać, kto nie musi lub nie powinien w nim uczestniczyć (np. nowo
rekrutowany pracownik nie będzie uczestniczył w dyskusji na temat jego
przyjęcia)

» proces – określa agendę spotkania oraz jego formę, np. brainstorming,

dyskusja, praca w parach

» pułapki – lista potencjalnych problemów wraz z proponowanymi rozwią-

zaniami, często opiera się o doświadczenia z poprzednich spotkań (np.
uczestnicy spotkania będą przychodzić spóźnieni)

» przygotowanie – lista rzeczy, które można przygotować wcześniej, aby

spotkanie było bardziej efektywne

» zagadnienia praktyczne – konkretne informacje na temat tego, gdzie

spotkanie się odbywa, kiedy, jak tam dotrzeć, oraz co jest wymagane od
uczestników

W tabeli poniżej możesz znaleźć jego zastosowanie na przykładzie, pokazując
jak mogłoby wyglądać przygotowanie retrospektywy.

Brakujący element Agile

Część 5: Spotkania

W tym artykule przedstawię techniki prowadzenia efektywnych i skutecznych spo-
tkań. Jako programista zdaję sobie sprawę, że spotkania są, mówiąc kolokwialnie,
jednym z najbardziej znienawidzonych aspektów naszej pracy. Z mojego doświadcze-
nia wynika, że główną przyczyną jest ich chaotyczna struktura spowodowana brakiem
przygotowania. Każdy dobrze zorganizowany projekt, iterację czy release produktu
poprzedzamy planowaniem oraz zarządzamy ich przebiegiem. W przypadku spotkań
dominuje tzw. podejście „na żywioł”. Z tego powodu tracimy masę czasu, chodząc na
bezcelowe spotkania oraz prowadząc dyskusje o nieistotnych kwestiach.

background image

67

/ www.programistamag.pl /

BRAKUJĄCY ELEMENT AGILE. CZĘŚĆ 5: SPOTKANIA

Cel

Zidentyfikowanie sukcesów oraz problemów
w zakończonej iteracji, w celu wykorzystania tej
wiedzy w kolejnej iteracji

Artefakty

Lista zadań do zrealizowania w kolejnej iteracji
wraz z osobami za nie odpowiedzialnymi

Ludzie

Wszyscy członkowie zespołu, kierownik projek-
tu, product owner (opcjonalnie)

Proces

1. Odczytanie „Pierwszej dyrektywy” Norma Kerth’a
2. Brainstorming sukcesów oraz problemów
w kategoriach „Mad, Sad, Glad” (z użyciem
żółtych karteczek, każdy osobno) – 5 minut.
3. Grupowanie tematów.
4. Wybranie najważniejszych tematów przez
głosowanie.
5. Dyskusja oraz zidentyfikowanie zadań do
zrealizowania – 10 minut na każdy temat.

Pułapki

Brak kierownika projektu – Andrzej upewni się,
że kierownik projektu będzie na spotkaniu

Przygotowanie

Marek zarezerwuje pokój, rzutnik oraz
przyniesie flamastry i żółte karteczki

Zagadnienia praktyczne

Wtorek, 14 maja 14:00 – 16:00, pokój „Marcelina”

Tabela 1. Framework 7P

Warto pamiętać, że framework 7P jest wyłącznie narzędziem i to od Ciebie
zależy, jak je wykorzystasz. Z moich obserwacji wynika, że:

» optymalnie należy stosować framework 7P jako koło ratunkowe w sytu-

acjach, kiedy spotkania zespołu są nieefektywne

» najlepiej, kiedy planowaniem zajmie się jedna osoba, konsultując się z in-

nymi w razie potrzeby

» warto ograniczyć czas planowania (np. poprzez metodę timeboxingu),

aby zapobiec sytuacji, kiedy problem długich spotkań zostanie zastąpiony
przez problem długiego planowania

Problem: Zbaczanie z tematu

Wielokrotnie spotykam się z sytuacjami, że dyskusja przechodzi na tematy
niez wiązane z tematem spotkania. Czasem dzieje się tak, ponieważ cel spo-
tkania nie jest precyzyjnie określony. Zakładając jednak, że spotkanie zostało
dobrze przygotowane, a wciąż pojawia się problem częstych dygresji, warto
skorzystać z pomocy strażnika spotkania (ang. facilitator).

Rozwiązanie: Strażnik spotkania

Uczestnicy spotkania pochłonięci tematem oraz pod wpływem emocji zdarzają
się zapominać o regułach spotkania. Zewnętrzny obserwator, jakim jest strażnik
spotkania, pilnuje, aby wcześniej ustalone reguły były przestrzegane. Jeżeli ze-
spół korzysta z frameworka 7P, strażnik spotkania zapewnia, aby spotkanie nie
wykroczyło poza ramy tematyczne oraz czasowe określone w przygotowanym
planie. Strażnik spotkania jest osobą odpowiedzialną za jego sukces.

Strażnik spotkania dba również o to, aby każdy z uczestników został wy-

słuchany i wypowiedział swoją opinię. Pomocne jest, jeśli zna typy osobowo-
ści uczestników spotkania, np. kto jest gadułą, a kto rzadko zabiera głos na
spotkaniach (o wybranych typach osobowości przeczytasz więcej w dalszej
części artykułu). Może wtedy dopasować sposób facylitacji do specyfiki grupy.

Należy pamiętać, że strażnik spotkania nie może być jednocześnie jego

uczestnikiem. Nawet jeżeli jest członkiem zespołu, powinien na czas spotkania
zachować neutralność. W firmach, w których pracowałem, częstą praktyką było
„wypożyczanie” ludzi z innych zespołów. Ma to dwie zalety. Po pierwsze, osobie
z zewnątrz łatwo jest zachować neutralność. Po drugie, zapewniamy w ten spo-
sób efektywny transfer wiedzy oraz umiejętności pomiędzy różnymi zespołami.

Rozwiązanie: Parking

Zdarza się, że na spotkaniach poruszane są kwestie istotne dla grupy, ale

nie związane z aktualnie omawianym tematem. Członkowie zespołu są nie-
chętni przerwać dyskusję, ponieważ boją się, że temat już więcej nie powróci.
W tym celu warto zaimplementować w zespole koncepcję parkingu – miejsca,

gdzie umieszczane są ważne tematy do późniejszego omówienia. Zazwyczaj
jest to fragment tablicy, bądź ściany, a tematy przywieszane są z użyciem żół-
tych karteczek. Wraz z tematem dobrze jest również dopisać osobę odpowie-
dzialną za temat, w innym przypadku tematy „odstawione” na parking mogą
nie zostać nigdy więcej poruszone (a parking stanie się „cmentarzyskiem”).

Problem: Rozwlekanie spotkania

Klasycznym problemem spotkań jest jego przeciąganie. W wielu sytuacjach
wcześniej przedstawione przeze mnie techniki wystarczą, aby zapobiec
rozwlekaniu spotkania. Zdarza się, że zespołowi brakuje samodyscypliny,
ponieważ jest już przyzwyczajony do długich i bezcelowych spotkań. W ta-
kiej sytuacji ograniczenia czasowe pozwalają wywrzeć przydatną presję na
uczestników spotkania.

Rozwiązanie: Ograniczenia czasowe (ang. timeboxing)

Ograniczenia czasowe polegają na wyraźnym określeniu, ile czasu zespół

poświęci na poszczególne punkty agendy. Po osiągnięciu limitu czasowego
uczestnicy przechodzą do kolejnego punktu agendy – niezależnie od rezultatu.

Spotkałem się z sytuacjami, kiedy po osiągnięciu limitu czasowego zespół

wspólnie decyduje, czy ograniczenie przedłużyć. Z mojego doświadczenia
wynika, że tego typu praktyka powoduje, iż uczestnicy przestają poważnie
traktować ograniczenia czasowe. Dlatego preferuję wariację, w której uczest-
nicy stosują kolejkę FIFO – niedokończony temat umieszczany jest na końcu
kolejki. Dzięki temu realizujemy plan spotkania, a po zakończeniu znamy sku-
mulowany czas niedokończonych tematów. Tego rodzaju feedback pomaga
w przyszłości lepiej planować czas spotkania oraz priotytety tematów.

Jedną z wariacji ograniczeń czasowych jest stosowanie ich do uczenia

zespołu optymalnego wykorzystania czasu spotkania. Po zaplanowaniu spo-
tkania należy spróbować zmniejszyć eksperymentalnie czas spotkania do 1/4
oryginalnych założeń. Część zaoszczędzonego czasu można wykorzystać na
krótką retrospektywę, by podzielić się obserwacjami na temat tego, czego się
nauczyliśmy. Takie doświadczenie może dostarczyć zespołowi źródła ciekawych
wniosków, zwłaszcza że długości spotkań są z reguły dobierane arbitralnie. Dla
przykładu, w jednym z zespołów, gdzie pracowałem, standup zajmował w pew-
nym okresie trwania projektu 45 minut. W niedługim czasie skróciliśmy go do
15 minut, nie odczuwając jakichkolwiek efektów negatywnych.

Oprócz ograniczeń czasowych można również stosować przydatną pro-

cedurę nazywaną po angielsku „timecheck” (pol. kontrola czasu). Wybrana
osoba powinna w regularnych odstępach przypominać o ilości czasu, który
pozostał do zakończenia punktu agendy (bądź całego spotkania). Dzięki temu
pozostali uczestnicy posiadają odpowiednik paska postępu dla spotkania
oraz mogą lepiej zarządzać przebiegiem spotkania.

Ograniczenia czasowe mogą być stosunkowo radykalną techniką dla wielu

zespołów. Warto spojrzeć na nie jako na ćwiczenie, uczące efektywnego wy-
korzystania czasu. Podobnie jak w przypadku ćwiczeń fizycznych brak ogra-
niczeń czasowych powoduje rozleniwienie się zespołu i spadek efektywności.

Rozwiązanie: Protokoły Decider oraz Resolution

W zespołach zwinnych podjęcie decyzji jest często skomplikowane z

uwagi na demokratyczny charakter. Wielokrotnie spotkałem się z sytuacja-
mi impasu, gdzie zespół traci cenne godziny, próbując osiągnąć consensus.
Najlepszym rozwiązanie problemu jest wprowadzenie metody, która pozwala
szybko ocenić stanowiska uczestników.

Tu z pomocą przychodzi protokół Decider. Jedna z osób powinna za-

proponować rozwiązanie sytuacji impasu. Pozostali podejmują decyzję, czy
zgadzają się z propozycją, odpowiadając „Tak”, „Nie”, „Nie mam zdania” lub
„Zdecydowanie nie”. Decyzja zostaje podjęta większością głosów, chyba że co
najmniej jeden z uczestników głosował jako „Zdecydowanie nie”, które ozna-
cza „Nie” całego zespołu.

Jeśli osoba proponująca rozwiązanie jest niezadowolona z wyniku głoso-

wania (ponieważ propozycja została odrzucona), może zastosować protokół
Resolution. W ramach niego zadaje pytanie każdemu z głosujących na „Nie”,

background image

68

/ 6

. 2014 . (25) /

LABORATORIUM BOTTEGA

co mogłoby zmienić ich decyzję. Jeśli jest możliwe zmienić propozycję, tak
aby uzyskać głosy większości, propozycję uznaje się za przyjętą. W przeciw-
nym przypadku należy zakończyć dyskusję i przejść do omawiania kolejnego
tematu. Osoby niezadowolone z jej wyniku mogą próbować „lobbować” swo-
je pomysły już po zakończonym spotkaniu.

Problem: Burze emocjonalne

W wielu zespołach emocje to temat rzadko poruszany w kontekście pracy. W
międzyczasie w kwestii emocji mamy do czynienia z dwoma problematycz-
nymi sytuacjami. Po pierwsze, jeśli jeden z uczestników jest pod wpływem
silnych emocji, np. zdenerwowanie szefa może skutecznie zaprzepaścić całe
spotkanie. Po drugie, uczestnicy będący pod wpływem negatywnych emocji
mają ograniczone umiejętności logicznego myślenia (o czym pisałem w po-
przednich częściach serii).

Rozwiązanie: Protokoły Check in oraz Pass

Protokół Check in pozwala zrozumieć konteks emocjonalny każdego z

uczestników spotkania. Wymaga on, żeby każdy z uczestników przed rozpo-
częciem spotkania zadeklarował swoje emocje poprzez wybranie co najmniej
spośród listy czterech: zły, smutny, zadowolony, obawiający się (ang. mad, sad,
glad, afraid). Opcjonalnie może podać krótki kontekst do podanej przez siebie
emocji. Dzięki temu wszyscy uczestnicy spotkania dostają czytelny kontekst
emocji innych na początku spotkania, i mogą skuteczniej reagować na komu-
nikaty innych.

Wspomnę również, że protokół Check in można także stosować w sytu-

acji, kiedy „jesteś myślami gdzie indziej”, np. zostałeś oderwany od pisania
skomplikowanego algorytmu. Możesz wtedy powiedzieć, że obawiasz się, po-
nieważ Twoje rozkojarzenie może nie pozwolić Ci na efektywne uczestnictwo
w spotkaniu.

W przypadku jeśli w ogóle nie możesz, bądź nie chcesz, uczestniczyć w

spotkaniu, powinieneś zastosować protokół Pass. Pozwala on całkowicie zre-
zygnować z uczestnictwa w spotkaniu. Osobiście uważam, że obecność takie-
go protokołu może pomóc wielu zespołom, gdzie istnieje niepisana reguła
obowiązku uczestnictwa w każdym spotkaniu. Oczywiście, należy uważać,
żeby tego protokołu nie nadużywać.

METODY NIEKONWENCJONALNE

Oprócz opisanych wyżej metod, zdarza mi się stosować również techniki, któ-
re mogą wydawać się niestandardowe w kontekście pracy. Dwie najważniej-
sze to medytacja oraz proste ćwiczenia fizyczne.

Medytacja pozwala na uspokojenie emocji oraz uporządkowanie myśli

przed rozpoczęciem spotkania. Programista oderwany od klawiatury wciąż
myśli bardzo logicznie i dopóki nie „przełączy kontekstu” ma obniżoną inteli-
gencję społeczną. Medytacja może być stosowana nawet przez kompletnego
laika oraz jest metodą zweryfikowaną naukowo, co przemawia na jej korzyść.
Warto wiedzieć, że jest fundamentem programu rozwoju liderów Search Insi-
de Yourself w firmie Google od 2008 roku.

Proste ćwiczenia fizyczne takie jak wspólne ziewanie oraz przeciąganie się

pozwalają na dotlenienie mózgu i mogą się okazać zbawienne dla powodze-
nia poobiednich spotkań. W przypadku spotkań odbywających się pod ko-
niec dnia można rozważyć przeprowadzenie go w całości na stojąco.

WPŁYW OSOBOWOŚCI

Organizacja efektywnego spotkania wymaga wzięcia pod uwagę osobowości
jego uczestników. Należy przykładowo pamiętać, że niektóre osoby wyrażają
swoje opinie od samego początku spotkania, podczas gdy inni preferują do-
brze zrozumieć jego kontekst, aby wypowiedzieć swoje zdanie. Dobrze jest,
jeśli strażnik spotkania zna preferencje uczestników, bądź potrafi szybko je
wychwycić – w ten sposób może dopasować spotkanie do potrzeb grupy. Po-
niżej przedstawię 3 przykładowe (celowo przejaskrawione) typy osobowości,
zwracając uwagę na to, że każda z nich ma zarówno słabe, jak i mocne strony.

Gaduła

Gaduła to uczestnik, który już od samego początku wyraża swoją opinię. Ma
zdanie na każdy temat poruszany na spotkaniu. Oczywistym problemem w
tym przypadku jest łatwość, z jaką potrafi zdominować spotkanie, i często
zanudzić innych uczestników, którzy nie mogą dojść do głosu. Jego mocną
stroną jest „rozkręcanie” spotkania oraz utrzymywanie wysokiej energii – jest
w stanie ciągle dostarczać nowych informacji oraz przemyśleń.

Mistrz dygresji

Mistrz dygresji ciągle rozpoczyna nowe wątki podczas dyskusji, przez co po-
trafi łatwo zdestabilizować spotkanie. Z drugiej strony jest nieoceniony pod-
czas sesji brainstormingu. Ma również duży talent do generowania nowych
pomysłów oraz pomaga zespołowi spojrzeć na omawiane tematy z różnych
perspektyw.

Cichy obserwator

Cichy obserwator nigdy nie wyraża swojej opinii w pierwszej połowie spotka-
nia, a czasem nie odzywa się w ogóle. Przedstawiciel tego typu osobowości
nie zakłóca spotkania, jak w przypadkach opisanych wyżej, więc łatwo o nim
zapomnieć. Należy jednak pamiętać, że osoba, która nie mówi, często uważ-
nie słucha. Cichy obserwator zdarza się być nieprzeciętnym słuchaczem, a za-
pytany o zdanie może dostarczyć uczestnikom wnikliwych obserwacji.

PODSUMOWANIE

W tym artykule opisałem typowe problemy nieefektywnych spotkań oraz
zaproponowałem techniki, które pomagają je rozwiązać. Chcę zauważyć,
że przedstawione metody są w istocie proste i możliwe do wprowadzenia
od ręki. Warto zwrócić uwagę, że większość z prezentowanych przeze mnie
rozwiązań ma również dodatkowe zalety. Framework 7P pozwala na lepsze
poznanie procesu, zwłaszcza w kontekście spotkań cyklicznych, które są jego
częścią. Wykorzystanie Podstawowych Protokołów da szanse na zwiększenie
skuteczności komunikacji w zespole. Obecność strażników spotkań z innych
zespołów spowoduje wymianę wiedzy oraz umiejętności w ramach firmy.

Paweł Badeński

pawel.badenski@gmail.com

Do niedawna konsultant w firmie ThoughtWorks, gdzie pracował jako programista, trener
oraz coach. Obecnie trener i konsultant w firmie Bottega IT Solutions. Bloguje pod adresem

http://the-missing-link-of-agile.com

. Pasjonat improwizacji teatralnej, psychologii stosowa-

nej i neurobiologii oraz ich zastosowania w kontekście tworzenia oprogramowania.

background image
background image

70

/ 6

. 2014 . (25) /

STREFA CTF

Krzysztof "vnd" Katowicz-Kowalewski

CTF

CONFidence DS CTF 2014 (Offline)

https://ctf.dragonsector.pl/
http://files.dragonsector.pl/2014/confidence/main/

Liczba uczestników
(z niezerową liczbą punktów)

11

System punktacji zadań

Od 50 (proste) do 200 (średnio-trudne) punktów.

Liczba zadań

17

Podium

1. liub (1078 pkt.)
2. dcua (1078 pkt.)
3. 4c...80fd Sector (500 pkt.)

O CTFIE

W tym wydaniu magazynu „Programista” opiszemy organizowane przez nas
zawody, które odbyły się podczas konferencji CONFidence pod koniec maja
tego roku. Skąd ten wybór? W numerze tym chcielibyśmy przedstawić Wam
nie tylko sposób rozwiązania wybranego zadania, ale również z naszej per-
spektywy to, co działo się za kulisami, czyli jak doszło do stworzenia opisywa-
nego zadania oraz jakie po drodze napotkaliśmy trudności i wyzwania.

Zdobyć flagę…

CONFidence DS CTF 2014 – web200

Średnio co około dwa tygodnie gdzieś na świecie odbywają się komputerowe Cap-
ture The Flag – zawody, podczas których kilku-, kilkunastoosobowe drużyny stara-
ją się rozwiązać jak najwięcej technicznych zadań z różnych dziedzin informatyki:
kryptografii, steganografii, programowania, informatyki śledczej, bezpieczeństwa
aplikacji internetowych itd. W serii „Zdobyć flagę…“ co miesiąc publikujemy wybra-
ne zadanie pochodzące z jednego z minionych CTFów wraz z jego rozwiązaniem.

WEB200

W części zadań z kategorii web - szczególnie wysoko punktowanych - moż-
na zauważyć pewien schemat: uczestnik powinien obejść zabezpieczenia
strony internetowej, ściągnąć plik z programem wykonywanym przez CGI,
dowiedzieć się, co on robi, a następnie znaleźć błąd i go wykorzystać. W kon-
sekwencji dostajemy więc pewnego rodzaju hybrydę kategorii web, reverse-
-engineering
i pwn. Organizując zawody CTF podczas konferencji CONFidence,

background image

71

/ www.programistamag.pl /

ZDOBYĆ FLAGĘ… CONFIDENCE DS CTF 2014 – WEB200

chcieliśmy nieco zerwać z tym schematem i stworzyć zadanie, które odno-
siłoby się jedynie do jednej kategorii – w tym wypadku do bezpieczeństwa
aplikacji webowych. Aby nie stworzyć zadania zbyt łatwego, postanowiliśmy
podzielić je na dwie części, tak aby każda część wymagała od gracza nieco
innego podejścia.

Jeśli chcecie spróbować własnych sił i rozwiązać zadanie bez wcześniej-

szego przeczytania solucji, możecie pobrać dysk maszyny wirtualnej, na której
oryginalnie było hostowane zadanie:

http://files.dragonsector.pl/2014/confidence/main/web200.qcow

.

Po uruchomieniu systemu powinien on pobrać adres IP z serwera DHCP, jeśli
jednak będziecie zmuszeni ręcznie zmodyfikować ustawienia sieciowe, może-
cie zalogować się do systemu przy pomocy loginu i hasła „root”. Oryginalnie
gracze nie mieli jednak możliwości zalogowania się do systemu i przedstawio-
ne tutaj dane powinny być używane jedynie w celu rozwiązywania proble-
mów z konfiguracją maszyny wirtualnej.

CZĘŚĆ I – UZYSKANIE DOSTĘPU DO

SERWERA

Link umieszczony w treści zadania (

http://letterpress.local

) kierował nas na

stronę wirtualnej, nowo powstałej gazety o nazwie LetterPress. Z informacji,
które udało nam się tam wyczytać, wynikało, że redakcja wykupiła hosting na
bardzo bezpiecznym serwerze, a sama strona powstała niedawno i cały czas
trwają pracę nad jej wyglądem i treścią. Z rzeczy, które rzucały się w oczy, moż-
na wymienić niedziałający panel logowania i kilka zakładek, m.in. z newsami,
notką o gazecie i informacją o rekrutacji. Ze wszystkich tych rzeczy największą
uwagę powinna zwracać na siebie właśnie wzmianka o rekrutacji, gdzie byli-
śmy w stanie przesłać swój adres e-mail i zdjęcie (wedle którego mielibyśmy
zostać wybrani do rozmowy kwalifikacyjnej). Po przesłaniu zdjęcia i podaniu
e-maila otrzymywaliśmy link podobny do następującego:

http://letterpress.local/uploads.

php?path=jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/your@address.net

Jak łatwo można było się domyślić, zdjęcie, które przesyłaliśmy, znajdowało
się gdzieś na serwerze - jeśli bylibyśmy w stanie umieścić w nim kod PHP i
przekonać serwer Apache, aby go wykonał, zyskalibyśmy dostęp do wyko-
nywania dowolnych poleceń. Jedyny problem w tym momencie stanowiło
„domyślenie się”, w jakim katalogu znajdują się pliki użytkowników. Większość
osób nie miało jednak z tym problemu i sprawdzało plik robots.txt:

User-agent: *

Disallow: /client-data/

Wiedząc o ukrytym folderze „client-data”, nietrudno było wpaść na pomysł, że
do wgranego przed chwilą pliku możemy odwołać się bezpośrednio:

http://letterpress.local/client-data/

jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/your@address.net

O ile skrypt uploads.php prezentował wgrane przez nas pliki jako plik obrazów
i dostarczał je w dokładnie takiej samej postaci, w jakiej zostały one wgrane,
tak sam serwer Apache po przejściu na bezpośrednią lokalizację pliku spraw-
dzał jego rozszerzenie i w momencie, kiedy było ono związane z dynamiczną
akcją, uruchamiał odpowiedni interpreter. W ten sposób, wgrywając plik JPEG
z komentarzem EXIF:

<?php eval($_GET[0]); ?>, byliśmy w stanie uru-

chomić po stronie serwera dowolne polecenie.

CZĘŚĆ II – REKONESANS SYSTEMU

I ESKALACJA UPRAWNIEŃ

Jak pewnie zauważyliście, pierwsza część zadania nie powinna stanowić więk-
szego problemu dla osób znających podstawowe podatności stron interne-
towych. Prawdziwym wyzwaniem w tym zadaniu było odczytanie flagi, która
znajdowała się na innym virtual hoście i pod kontrolą innego użytkownika niż
ten, do którego mieliśmy dostęp. Zaraz po tym, jak uzyskaliśmy możliwość
wykonywania dowolnych poleceń, mogliśmy się zorientować, że jedyną rze-
czą dzielącą nas od flagi jest hasz SHA-256, który był sprawdzany po stronie
serwera w panelu logowania. Widać to bardzo dobrze na listingu poniżej:

<?php

function

auth

(

$login

,

$password

)

{

if

(!

is_string

(

$login

)

||

preg_match

(

"

/

^[

a

-

zA

-

Z0

-

9

]+$

/

"

,

$login

)

!==

1

)

return

false

;

if

(!

is_string

(

$password

)

||

preg_match

(

"

/

^[

a

-

zA

-

Z0

-

9

]+$

/

"

,

$password

)

!==

1

)

return

false

;

return

(

$login

===

"owner"

)

&&

(

hash

(

"sha256"

,

$password

)

===

"72041376c9cf38150e0031e73fa2ec46e92729a172628b4efae9866c82221487"

)

;

}

if

(

isset

(

$_POST

[

"login"

],

$_POST

[

"password"

])

&&

auth

(

$_

POST

[

"login"

],

$_POST

[

"password"

]))

{

echo

file_get_contents

(

"/var/www/your-secure-hosting.local/flag.txt"

)

;

}

?>

Jednak łatwo można było się domyślić, że nie na tym polegała trudność tego za-
dania. Łamanie skrótów haseł nie jest nigdy niczym przyjemnym na zawodach
tego typu, głównie dlatego, że mało istotne staje się doświadczenie i wiedza gra-
cza, a dużą większą rolę odgrywa moc obliczeniowa jego sprzętu. Aby więc dać
wszystkim graczom równe szanse, postanowiliśmy ustawić hasło administratora

reklama

background image

72

/ 6

. 2014 . (25) /

STREFA CTF

na losowy 256-znakowy ciąg znaków – oczywiście, jeśli komuś udałoby się zna-
leźć kolizję i pasujące hasło, to bez wątpienia gracz ten zasługiwałby na nagrodę
w postaci 200 punktów (choć oczywiście znalezienie takiego hasła było równie
prawdodopodobne co trafienie szóstki w Lotto, i to 10 razy pod rząd).

Skrypt PHP, który sprawdzał hasło, posiadał odpowiednie uprawnienia, po-

nieważ był uruchamiany przez proces Apache i dzięki mechanizmowi suPHP
użytkownikiem, na prawach którego wykonywał się interpreter, był „hosting”.
Mając jednak dostęp do konta „letterpress”, nie bylibyśmy w stanie uruchomić
ani suPHP, ani suEXEC. Celowo zakomentowane linie w plikach konfiguracyj-
nych Apache oraz pozostawione wykonywalne pliki o nazwie „php-fcgi-star-
ter” miały naprowadzić graczy na to, że to właśnie suPHP/suEXEC jest słabym
punktem całego systemu. Gdybyśmy byli w stanie uruchomić dowolny plik
użytkownika „hosting”, jako skrypt, PHP moglibyśmy przeczytać flagę - flag.txt
jest w końcu zwykłym plikiem tekstowym. Można się tego domyślić zarówno
po rozszerzeniu, jak i po tym, że do jego wyświetlania używana jest funkcja
file_get_contents. Jeśli więc podamy ten plik do wykonania interpretero-
wi PHP, to otrzymamy jego dokładną zawartość.

EKSPLOITACJA SUPHP/SUEXEC

Podstawowym problemem jest więc uruchomienie suEXEC lub suPHP z po-
ziomu użytkownika „letterpress”. W tym opisie skupimy się głównie na rozwią-
zaniu z wykorzystaniem suEXEC + SSI – tworząc wzorcowe rozwiązanie zada-
nia, spodziewaliśmy się, że to ono będzie najprostsze w implementacji. Jeśli
spojrzymy na źródła suEXEC, możemy się przekonać, że wywołanie skryptu
nie jest bezpośrednio możliwe. Jedynie skonfigurowany przy instalacji użyt-
kownik ma możliwość wydawania poleceń programowi /usr/lib/apache2/
suexec i domyślnie jest nim „www-data”. Pozostając przy pomyśle wykorzy-
stania suEXEC w celu przeczytania flagi, możemy zadać sobie pytanie – w jaki
sposób możemy wykonać komendę z poziomu Apache? Odpowiedź jest roz-
wiązaniem całej zagadki, a z logów, które przeglądaliśmy, to właśnie ten ele-
ment przysporzył graczom najwięcej problemów. Kluczem jest wykorzysta-
nie wewnętrznych mechanizmów serwera Apache. Gdy zbadamy dokładniej
konfigurację virtual hosta, do którego mamy dostęp, zobaczymy, że parametr

AllowOverride“ nie jest w ogóle zdefiniowany – oznacza to, że posiada on

domyślną wartość „

All“. Parametr ten jest ustawiany w plikach konfiguracyj-

nych głównie po to, aby niemożliwe było nadpisanie konfiguracji serwera.
Wykorzystując zaistniałą sytuację, możemy stworzyć własny plik .htaccess
i wykorzystać skrypty SSI, które są wykonywane na prawach użytkownika
„www-data”. W przeciwieństwie do suEXEC, które wykonuje wszystkie moż-
liwe dynamiczne operacje z poziomu zdefiniowanego konta systemowego,
moduł suPHP – wykorzystywany przez Apache – odnosi się jedynie do skryp-
tów PHP. Plik .htaccess włączający obsługę SSI może wyglądać następująco:

AddType text/html .shtml

AddOutputFilter INCLUDES .shtml

Options All

Równolegle z dodawaniem pliku .htaccess możemy przygotować w stworzo-
nym katalogu skrypt sample.sh, który będzie instruował suEXEC, w jaki sposób
ma uruchomić plik flag.txt:

#!/bin/sh

export REDIRECT_STATUS="200"

export SCRIPT_FILENAME="/var/www/your-secure-hosting.local/flag.txt"

cd "/var/www/your-secure-hosting.local/"

exec /usr/lib/apache2/suexec-pristine 1001 1001 php-fcgi-starter

Ostatnim elementem po dodaniu .htaccess jest uruchomienie naszego sam-
ple.sh
z uprawnieniami użytkownika „www-data”. Możemy to zrobić przez
wspieraną przez SSI komendę

exec:

<!--#exec cmd="/var/www/letterpress.local/htdocs/client-data/

jrepzzo4d033jdxhi1w9bzmfkzr3ahjk/sample.sh" -->

Po otwarciu tak spreparowanego pliku .shtml poznajemy flagę.

ZA KULISAMI

Tworząc zadanie tego typu, mieliśmy na celu zapewnić graczom możliwie reali-
styczne warunki, w których mógłby zaistnieć błąd. Nie było to jednak tak banal-
ne jak mogło się to na początku wydawać, gdyż poza stworzeniem środowiska
odwzorowującego rzeczywisty system musieliśmy zapewnić sobie minimalną
kontrolę nad działaniem systemu i treścią plików hostowanych przez HTTP.
Pierwszy problem, jaki pojawił się podczas testów, to użytkownik, do którego
powinny należeć pliki. Ponieważ użytkownik, na prawach którego wykonywa-
ny był skrypt, musiał być identyczny z właścicielem pliku, to musieliśmy w jakiś
sposób zabezpieczyć się przed próbą modyfikowania strony przez złośliwych
graczy. Na szczęście ten problem udało się rozwiązać w prosty sposób poprzez
dodanie flagi +i (immutable) do każdego pliku i folderu, które powinny pozostać
niezmienione na czas trwania tego zadania. Pozostał jednak problem folderu
„client-data", który nie mógł posiadać flagi immutable ze względu na to, że za-
pisywane do niego były pliki użytkowników. Problem nie był krytyczny, mógł
on sprzyjać jedynie „podejrzeniu” rozwiązanego zadania przez inną osobę.
Mogliśmy poradzić sobie z tym na wiele różnych sposób – myśleliśmy m. in.
o podmianie polecenia chmod – co mogło być bez problemu ominięte przez
wywołanie odpowiedniej funkcji w PHP lub bezpośrednie wywołanie binarne-
go kodu. W grę wchodziło również wykorzystanie patcha na syscall chmod lub
użycie MAC (Mandatory Access Control, np. SELinux) i stworzenie polityki bez-
pieczeństwa, która uniemożliwiałaby poznanie folderów innych użytkowników.
Żaden z tych sposobów nie wydał się nam wystarczająco lekki, aby mógł zostać
zastosowany bez straty w postaci realności zadania, nie chcieliśmy też „celować
do wróbla z armaty”. Dlatego też postanowiliśmy postawić na dosyć nietypowe
zagranie, pozostawić ten problem niezabezpieczony i sprawdzić, czy komukol-
wiek uda się wpaść na pomysł dodania praw +r do folderu user-data, tak aby
był w stanie dostać się do plików innych graczy. Jednak ani w czasie aktywnego
monitorowania zadania, ani po analizie logów nie udało nam się znaleźć żad-
nych dowodów na to, że ktokolwiek próbował dostawać się do plików współ-
graczy. Mają u nas duży plus za uczciwość! :)

PODSUMOWANIE

Ostatecznie warto wspomnieć, że zadanie warte było 200 punktów i rozwiązała
je jedna osoba, gratulujemy! Tym opisem chcielibyśmy również przypomnieć, że
nie zawsze do eskalacji uprawnień na serwerze dochodzi na skutek niskopozio-
mowego błędu jak przepełnienie bufora lub w wyniku sławnych ostatnio sytuacji
wyścigu – czasami, tak jak w przypadku omówionego w tym artykule błędu, prze-
łamanie zabezpieczeń danego środowiska jest możliwe na skutek błędów konfi-
guracyjnych i niewystarczającej wiedzy administratora. Dlatego też niezmiernie
istotne jest, aby w momencie wdrażania konkretnych rozwiązań do środowisk
produkcyjnych być świadomym, jak tak naprawdę one działają i czy na pewno
w odpowiedni sposób określiliśmy, jak mają się one zachowywać.

Rozwiązania zadania web200 zostały nadesłane przez Dragon Sector

– jedną z Polskich drużyn CTFowych.

http://dragonsector.pl/

background image
background image

74

/ 6

. 2014 . (25) /

KLUB LIDERA IT

Mariusz Sieraczkiewicz

TAJEMNICA MISTRZÓW

REFAKTORYZACJI

Zapraszam do zapoznania się z dziesiątą i zarazem ostatnią częścią mojego
artykułu, w całości poświęconego zagadnieniu refaktoryzacji. Będę kontynu-
ował, rozpoczęty w poprzednim numerze, temat tajemnicy mistrzów refak-
toryzacji oraz przedstawię ostateczne wskazówki, które pomogą Ci komplek-
sowo zrozumieć jej istotę i niezbędność we współczesnym programowaniu.

Kierunek wprowadzania interfejsów

Często spotykałem się z sytuacją, kiedy w systemie powstawał interfejs (np.
SecurityManager) oraz jedna implementacja (np. SecurityManager-
Impl). Później nie powstawały nowe implementacje. Nie jest to zbyt dobra
strategia (o ile używana biblioteka lub szkielet aplikacyjny nie wymaga takiej
konstrukcji). Powoduje ona nadmierne mnożenie bytów, a w konsekwencji
zaciemnia strukturę projektu.

Interfejsy warto wyodrębniać dopiero wtedy, kiedy rzeczywiście występuje
więcej niż jedna implementacja.

Inny przykład

Poniżej zamieszczam inną implementację interfejsu

PageIterator, opartą o

inny silnik słownika. Proponuję Ci czytelniku napisanie własnej implementacji
dla wprawy. Najważniejsze, aby realizowała założony interfejs w analogiczny
sposób, jak ma to miejsce w klasie

DictPageIterator.

Listing 1. Implementacja interfejsu PageIterator

<java>

package

pl.bnsit.webdictionary;

import

java.io.BufferedReader;

import

java.io.IOException;

import

java.io.InputStreamReader;

import

java.net.MalformedURLException;

import

java.net.URL;

import

java.util.ArrayList;

import

java.util.Iterator;

import

java.util.List;

import

java.util.regex.Matcher;

import

java.util.regex.Pattern;

public class

OnetPageIterator

implements

PageIterator {

private

BufferedReader bufferedReader =

null;

private

Iterator <String> wordIterator =

null;

public

OnetPageIterator(String wordToFind) {

List <String> words = prepareWordsList(wordToFind);

wordIterator = words.iterator ();

}

Jak całkowicie odmienić sposób pro-

gramowania, używając refaktoryzacji

(część 10)

Większość programistów wie, co to refaktoryzacja, zna zalety wynikające z jej sto-
sowania, zna również konsekwencje zaniedbywania refaktoryzacji. Jednocześnie
wielu programistów uważa, że refaktoryzacja to bardzo kosztowny proces, wyma-
ga wysiłku i brak na nią czasu w szybko zmieniających się warunkach biznesowych.
Zapraszam do kolejnej części artykułu poswięconego zagadnieniu refaktoryzacji.

@Override

public boolean

hasNext () {

return

wordIterator.hasNext ();

}

@Override

public

String next () {

return

wordIterator.next ();

}

private

List <String> prepareWordsList(String wordToFind) {

List <String> result =

new

ArrayList <String>();

String urlString =

"http://portalwiedzy.onet.pl/tlumacz.html?qs="

+ wordToFind +

"&tr=ang - auto &x =0& y=0"

;

try

{

bufferedReader =

new

BufferedReader (

new

InputStreamReader (

new

URL(urlString). openStream ()));

result = extractWords ();

}

catch(

MalformedURLException e) {

throw new

WebDictionaryException (e);

}

catch(

IOException e) {

throw new

WebDictionaryException (e);

}

finally

{

dispose ();

}

return

result;

}

private boolean

hasNextLine(String line) {

return(

line !=

null)

;

}

private

List <String> extractWords () {

List <String> result =

new

ArrayList <String>();

try

{

String line = bufferedReader.readLine ();

Pattern pattern = Pattern

.compile (

".*?<div class = a2b style =\"padding: "

+

"0px 0 1px 0px\">\\s?(<a href=\".*?\">)?"

+

"(.*?)(</a>)?&nbsp;.*?<BR>(.*?)</div>.*?"

);

while(

hasNextLine(line)) {

Matcher matcher = pattern.matcher(line);

while(

matcher.find ()) {

String englishWord

=

new

String(matcher.group(2).getBytes (),

"ISO -8859 -2 "

);

String polishHTMLFragment

=

new

String(matcher.group(4).getBytes (),

"ISO -8859 -2 "

);

List <String> words

= extractTranslation (

englishWord, polishHTMLFragment +

"<BR>"

);

result.addAll(words);

}

line = bufferedReader.readLine ();

}

}

catch(

IOException e) {

throw new

WebDictionaryException (e);

}

background image

75

/ www.programistamag.pl /

JAK CAŁKOWICIE ODMIENIĆ SPOSÓB PROGRAMOWANIA, UŻYWAJĄC REFAKTORYZACJI (CZĘŚĆ 10)

return

result;

}

private

List <String> extractTranslation(String englishWord,

String polishHTMLFragment) {

List <String> result =

new

ArrayList <String>();

attern pattern = Pattern

.compile (

"(<B>\\d+</B>\\s)?(.*?)<BR>"

);

Matcher matcher = pattern.matcher(polishHTMLFragment);

while(

matcher.find ()) {

String polishWord = matcher.group (2);

result.add(polishWord);

result.add(englishWord);

}

return

result;

}

private void

dispose () {

try

{

if(

bufferedReader !=

null)

{

bufferedReader.close ();

}

}

catch(

IOException ex) {

hrow new

WebDictionaryException (ex);

}

}

}

</java>

Strategia skutecznych programistów: Usuwaj
powtórzenia

Powtórzenia w kodzie to źródło wszelkiego zła! Jak drobne nie byłoby to po-
wtórzenie, jest duże prawdopodobieństwo, że prędzej czy później ujawnią się
efekty uboczne. Statystyki oparte na moich własnych obserwacjach prowa-
dzą do wniosku – w około 70% przypadków zastosowanie antywzorca Kopiuj-
-Wklej
powoduje powstanie trudnych do wykrycia błędów. Dlatego:

Kiedy widzisz powtórzenie, zastanów się, czy warto zrefaktoryzować kod.

Podobnie jest w klasach

DictPageIterator oraz OnetPageIterator –

metoda

dispose, next, hasNext, hasNextLine, konstruktor, pola klasy są

identyczne lub niemal identyczne i prawdopodobnie w kolejnych implemen-
tacjach również takie będą.

Refaktoryzacja: Wydzielenie klasy
abstrakcyjnej

Można pokusić się o refaktoryzację Wydzielenia klasy abstrakcyjnej – stworzyć
klasę, która będzie zawierać wspólne definicje. Znowu chciałbym zwrócić
uwagę na fakt, iż stworzenie klasy abstrakcyjnej nastąpiło, gdyż pojawiła się
taka potrzeba w trakcie rozwoju aplikacji. Często spotykam się z przypadkami,
kiedy takie klasy tworzone są na „w razie czego”, ,,być może w przyszłości się
przyda'' – niepotrzebnie mnożąc byty. Przykładowy kod znajduje się poniżej.

Listing 2. Przykład wydzielania klasy abstrakcyjnej

<java>

package

pl.bnsit.webdictionary;

import

java.io.BufferedReader;

import

java.io.IOException;

import

java.util.Iterator;

import

java.util.List;

abstract public class

AbstractPageIterator

implements

PageIterator

{

protected

BufferedReader bufferedReader =

null;

protected

Iterator <String> wordIterator =

null;

public

AbstractPageIterator () {

super

();

}

protected void

init (String wordToFind) {

List <String> words = prepareWordsList (wordToFind);

wordIterator = words.iterator ();

}

abstract protected

List <String> prepareWordsList (String

wordToFind);

@Override

public boolean

hasNext () {

return

wordIterator.hasNext ();

}

@Override

public

String next () {

return

wordIterator.next ();

}

protected boolean

hasNextLine (String line) {

return

(line !=

null)

;

}

protected void

dispose () {

try

{

if

(bufferedReader !=

null)

{

bufferedReader.close ();

}

}

catch

(IOException ex) {

throw new

WebDictionaryException (ex);

}

}

}

</java>

Żeby jednak nie produkować nadmiernej ilości kodu źródłowego, nie będę
zamieszczał w artykule kodu źródłowego klas dziedziczących z

Abstract-

PageIterator. Możesz to Czytelniku potraktować jako ćwiczenie.

Najważniejsze odkrycie!

Chciałbym zwrócić twoją uwagę na konstrukcję klasy

AbstractPage-

Iterator. Wydzielona metoda init realizuje pewien algorytm, którego
jednym z kroków jest metoda

prepareWordsList, ta zaś konkretyzuje się

w klasach odziedziczonych z

AbstractPageIterator. Jest to nic inne-

go jak realizacja wzorca

Metody szablonu. Tak! A przecież nic takiego nie

planowaliśmy.

Konsekwentne stosowanie podstawowych technik refaktoryzacji oraz od-
powiedniego rozdzielania odpowiedzialności prowadzi do wzorców pro-
jektowych!

To odkrycie było jednym z najważniejszym przesunięć paradygmatu

1

w moim

życiu programisty. Dotarło do mnie, że duża część wzorców projektowych
objawia się światu, jeśli stosuje się proste zasady refaktoryzacji i wydzielania
odpowiedzialności. Wszystko nagle stało się proste, a zasady programowania
obiektowego nabrały nowego sensu.

Z tą myślą chciałbym Cię czytelniku pozostawić w tym miejscu. Refakto-

ryzacja, wzorce projektowe, testowanie to wspaniałe tematy, które można
eksplorować całe życie. Przez 10 części tego artykułu przebyliśmy drogę po-
przez krainę refaktoryzacji. Jeśli chcesz eksplorować temat głębiej, zachęcam
do dalszej lektury książek źródłowych, uczestnictwa w szkoleniach czy trenin-
gach związanych z tym tematem… i używania w praktyce.

Jeśli raz „zarazisz“ się chorobą zwaną refaktoryzacją, nie ma już później

odwrotu. Gwarantuję, że jest to jedna z najwspanialszych zmian, jaką można
wprowadzić do sposobu programowania.

Mistrzostwo… zobacz, co się zmieniło

Refaktoryzacja to jedna z technik mistrzów – najlepszych programistów. Im
częściej dokonujesz refaktoryzacji, tym staje się to łatwiejsze, tak iż w locie roz-
poznajesz sytuacje, w których możesz stworzyć zrefaktorowane rozwiązanie.

1

Stephen Covey, 7 nawyków skutecznego działania

background image

76

/ 6

. 2014 . (25) /

KLUB LIDERA IT

Żeby odnaleźć różnicę, porównaj implementację końcową z tego artykułu

z początkową, umieszczoną w części pierwszej. W zasadzie obie wersje robią
to samo, w prawie taki sam sposób. Jednak „prawie“ czyni wielką różnicę.

Ten artykuł przedstawił najbardziej użyteczne techniki refaktoryzacji. Owe

20%, które najczęściej się przydaje. Jeśli chcesz dowiedzieć się więcej – na
końcu znajdziesz kilka wskazówek, gdzie szukać dalej.

PRAGMATYZM PRZEDE WSZYSTKIM

W poprzednim rozdziale wspomniałem, nie przez przypadek, że stosowanie
refaktoryzacji jest bardzo zaraźliwe, ale też niesie niebezpieczeństwo nega-
tywnych skutków jej nieodpowiedniego stosowania.

Szczególnie na początku fascynacja refaktoryzacją jest bardzo niebez-

pieczna, choć niezwykle przyjemna. Najchętniej refaktoryzację robiłoby się
na każdym kroku, dążąc do tego, aby program był idealny. Jednak nie o to
chodzi.

Tworzenie oprogramowania polega na tworzeniu najprostszego możliwe-
go kodu, które realizuje założone wymagania.

Przedstawione w tym artykule przykłady miały na celu pokazać sposób
myślenia towarzyszący procesowi refaktoryzacji. Najważniejszy jest kon-
tekst, w jakim powstaje dane rozwiązanie, i to on jest bazą do tego, aby
podjąć decyzję, czy należy zastosować daną refaktoryzację czy nie.

Dlaczego refaktoryzacja nie jest dobra na
wszystko

Tworzenie oprogramowania jest częścią biznesu, który z kolei służy przede
wszystkim wytwarzaniu wartości w sposób efektywny. Dlatego chociażby z
tego punktu widzenia refaktoryzacji należy używać z rozwagą. Jeśli będziemy
stosować ją nieodpowiednio, w pewnym momencie staniemy się całkowicie
nieefektywni. Jednak jedno nie ulega wątpliwości – refaktoryzować trzeba.

Dziesięć przykazań dotyczących
refaktoryzacji

Oto dziesięć przykazań dotyczących refaktoryzacji

2

:

1. Jeśli kod już istnieje, refaktoryzuj, gdy dany fragment przynajmniej dwa, a

najlepiej trzy razy sprawił Ci kłopot, bo był źle napisany.

2. Jeśli kod już istnieje, refaktoryzuj, gdy dany fragment często ulega

zmianom.

3. Jeśli kod piszesz po raz pierwszy, staraj się na bieżąco eliminować zapachy

kodu.

4. Naucz się refaktoryzować w locie – gdy nabierzesz wprawy, wiele refakto-

ryzacji odbędzie się w głowie.

2

brak wiarygodnych danych odnośnie źródła pochodzenia ;-)

5. Refaktoryzuj ewolucyjnie, a nie rewolucyjnie – stosuj metodę Małych kro-

ków, wprowadzaj jak najmniejsze zmiany.

6. Refaktoryzuj regularnie – tylko wtedy efekty prawa wzrostu entropii nie

zdominują Cię.

7. Refaktoryzuj wtedy, gdy masz napisane testy lub jeśli refaktoryzacja jest

automatycznie kontrolowana przez środowisko programistyczne.

8. Przede wszystkim stosuj najprostsze refaktoryzacje: wydzielanie odpowie-

dzialności (wydzielanie klasy, metody lub pola), zmiana nazwy, nazywanie
warunków i dekompozycja algorytmu na składowe.

9. Używaj refaktoryzacji z rozwagą – jeśli widząc kod pierwszy raz na oczy,

chcesz go natychmiast refaktoryzować, bez względu na to, czy będziesz
się nim zajmował czy nie, to oznacza, że jesteś w poważnych kłopotach.

10. Ciesz się tym, co robisz – dzięki refaktoryzacji programowanie jest jeszcze

przyjemniejsze.

I CO DALEJ… – INNE ŹRÓDŁA

Refaktoryzacja to rozległy temat, któremu można poświęcić niemal całe życie.
Poniżej znajduje się kilka wskazówek, gdzie można dalej szukać informacji do-
tyczących tej techniki.

Szkolenia

Moim zdaniem najlepiej napisana książka, artykuł czy materiał video nigdy
nie da tego, co bezpośredni kontakt z doświadczonym trenerem, który prze-
prowadzi przez niuanse technik refaktoryzacji i innych technik obiektowości.
A co najważniejsze, trener odpowie na twoje pytania. Jeśli szukasz sposobu na
to, aby jak najszybciej i jak najlepiej się nauczyć opisywanych technik, zapra-
szamy na szkolenia. Więcej na

http://www.bnsit.pl

.

Trening indywidualny

Jest formą zdobywania umiejętności, która umożliwia pełne dostosowanie spo-
sobu nabywania kompetencji. Trening to indywidualne spotkania online, dzięki
którym razem z trenerem poznasz dokładnie to, czego potrzebujesz, lub bę-
dziesz potrzebował w realizowanych projektach. Więcej na

http://www.bnsit.pl.

Źródła

Zdecydowanie dwa najlepsze, bezdyskusyjne źródła zaawansowanej wiedzy:

P Refaktoryzacja. Ulepszanie struktury istniejącego kodu, Martin Fowler

i inni, WNT 2006

P Refaktoryzacja do wzorców projektowych, Joshua Kerievsky, Helion 2005

W sieci

Oczywiście Google twoim zbawieniem. Hasło: refactoring, refaktoryzacja,
refaktoring. Ponadto:

P

http://www.refactoring.com

– blog utrzymywany przez Martina Fowlera

P

http://mbartyzel.blogspot.com

– blog Michała Bartyzela

P

http://msieraczkiewicz.blogspot.com

– mój własny blog

P

http://www.jaceklaskowski.pl

– blog Jacka Laskowskiego

Mariusz Sieraczkiewicz

m.sieraczkiewicz@bnsit.pl

Od ponad ośmiu lat profesjonalnie zajmuje się tworzeniem oprogramowania.
Zdobyte w tym czasie doświadczenie przenosi na pole zawodowe w BNS IT, gdzie
jako trener i konsultant współpracuje z jednymi z najlepszych polskich zespołów
programistycznych. Jego obszary specjalizacji to: zwinne procesy, czysty kod,
architektura, efektywne praktyki inżynierii oprogramowania.

background image
background image

78

/ 6

. 2014 . (25) /

KLUB DOBREJ KSIĄŻKI

Rafał Kocisz

A

gile to grupa metodyk wy-
twarzania oprogramowania,
która cieszy się coraz to więk-

szą popularnością. Agile po polsku
znaczy: zwinny, sprawny, zręczny, co
znajduje odbicie w założeniach, na
których opierają się wspomniane wy-
żej metodologie.

Wydaje się, że zwinne metodyki

wytwarzania oprogramowania sztur-
mem zdobyły rynki zachodnie (nie
pamiętam już, kiedy ostatnio miałem
do czynienia z zagranicznym klien-
tem, który chciałby prowadzić projekt

kaskadowo). W tej sytuacji na usta ciśnie się pytanie: jak wygląda kwestia ich po-
pularności na rynku polskim? Bez przeprowadzenia stosownych badań trudno
to dokładnie stwierdzić. Patrząc jednak na liczbę polskojęzycznych książek i pu-
blikacji nawiązujących do tej tematyki (a także opierając się na moich doświad-
czeniach zawodowych związanych ze współpracą z polskimi klientami), mogę
pokusić się o postawienie tezy, że zainteresowanie metodologiami typu Scrum
czy Kanban nieustannie rośnie również i na naszym, rodzimym podwórku. Bardzo
cieszy mnie fakt, że oprócz tłumaczeń zachodnich książek traktujących o tej te-
matyce pojawiają się również solidne pozycje polskich autorów, przedstawiające
Agile z bliższej nam perspektywy.

Jedną z takich pozycji, którą chciałbym przedstawić dziś w ramach Klu-

bu Dobrej Książki, jest tekst autorstwa Marka Krzemińskiego, zatytułowany
Agile. Szybciej, łatwiej, dokładniej. Lektura ta powstała z myślą o osobach pra-
cujących w branży wytwarzania oprogramowania, które rozpoczynają swoją
przygodę z praktykami programowania zwinnego i w związku z tym planują
wypróbować je w praktyce.

Książka autorstwa pana Marka nie jest szczególnie obszerna, co pozwala

przyswoić ją sobie w stosunkowo krótkim czasie. Tekst napisany jest bardzo
przyjaznym, lekkim i prostym w odbiorze językiem: książkę czyta się łatwo i
przyjemnie. Styl autora przypomina mi nieco teksty Wujka Boba (czyli Roberta
C. Martina, autora takich tytułów jak Mistrz Czystego Kodu czy Czysty Kod oraz
wielu innych książek, a jednocześnie guru w zakresie Zwinności). Co do samej
treści, książka podzielona jest na wstęp, cztery rozdziały właściwe, bibliogra-
fię, załączniki oraz skorowidz. Rzućmy okiem na zawartość rozdziałów.

Rozdział pierwszy (Po co to wszystko? U mnie przecież działa!) to krótka, a

przy tym lekka i zabawna część wprowadzająca, która świetnie spełnia swoją
rolę: w bardzo inteligentny sposób zachęca czytelnika do zagłębienia się w
kolejne rozdziały. Jak to robi? Przeczytaj i przekonaj się sam!

Rozdział drugi (To świetna zabawa!) pokazuje, że spotkanie z Agile nie

musi być wcale nudne i poważne, a wręcz przeciwnie: im więcej jest w tym
zabawy, tym lepiej.

Rozdział trzeci (Pierwszy dzień w szkole). Po dwóch luźnych i krótkich roz-

działach wprowadzających czas na bardziej konkretną treść. W tym rozdziale
czytelnik znajdzie odpowiedzi na takie pytania jak:

» czym jest metodyka zwinnego wytwarzania oprogramowania?

» jakie metodyki zwinnego wytwarzania oprogramowania mamy do

dyspozycji?

» kiedy powinno się wybrać daną metodykę?

» jakie pojawiają się problemy podczas wdrażania zadanej metodyki?

» jakie niesie ona korzyści.

Autor omawia wymienione powyżej kwestie w kontekście trzech najbardziej
popularnych metodyk Agile, jakimi są: Programowanie ekstremalne, Scrum
oraz Kanban.

Rozdział czwarty (Jedziemy z materiałem! - praktykujemy) to przegląd kon-

kretnych elementów ww. metodyk. Rozdział ten jest podzielny na trzy części:
agile dla pierwszaków, starszaków oraz dla „najstarszaków". Czytelnik zapozna
się tutaj z takimi elementami zwinnych metodologii jak:

» codzienne spotkania (ang. daily stand-up),

» historyjki użytkownika,

» tablica zadań,

» praca w cyklach iteracyjnych i planowanie iteracji,

» demonstracja,

» retrospektywy,

» programowanie w parach,

» definicja ukończenia zadań (ang. definition of done),

» lista historyjki użytkownika (ang. product backlog),

» planning poker,

» wykres wypalania iteracji (ang. burn down chart),

» prędkość zespołu (ang. team velocity),

» ciągła integracja,

» programowanie sterowane testami,

» samo-organizujący się zespół.

To, co bardzo pozytywnie uderzyło mnie w omawianej książce, to język: zwię-
zły, jasny i pełen humoru, oraz styl: luźny i bezpośredni. Trzeba jednak w tym
miejscu zdecydowanie podkreślić, iż pomimo prostoty języka na każdym eta-
pie lektury czuje się, że autor jest doświadczonym specjalistą w tematyce, którą
omawia. Inna sprawa to drobne, ale bardzo miłe polskie akcenty w tekście. W
trakcie lektury zagranicznych książek traktujących o tematyce zawodowej nie-
raz zdarzało mi się obserwować nawiązania do kina bądź literatury, zazwyczaj
anglojęzycznej. Zawsze w takich sytuacjach było mi trochę smutno, bo przecież
w polskiej kulturze nie brakuje dzieł literackich czy obrazów, do których warto
nawiązywać. Tym większą radość sprawiły mi cytaty z takich klasyków polskiego
kina jak „Rejs” czy „Wielki Szu” wplecione zgrabnie w treść książki.

Podsumowując: Agile. Szybciej, łatwiej, dokładniej to rewelacyjna pod ką-

tem merytorycznym treść połączona z lekkim, barwnym językiem. Jeśli szu-
kasz solidnego i przystępnego wprowadzenia w tematykę zwinności, to bez
wahania sięgnij po książki autorstwa Marka Krzemińskiego. Nie pożałujesz.

Agile. Szybciej, łatwiej, dokładniej

Agile. Szybciej, łatwiej, dokładniej

Autor: Marek Krzemiński

Stron: 248

Wydawnictwo: Helion

Data wydania: 2014/04/23

background image
background image

Wyszukiwarka

Podobne podstrony:
Programista 03 2014 iNTERnet
Ekonomia ćwiczenia program PS1 2014 2015 (1)
Młoda Polska WYKŁAD (04 06 2014)
9 06 2014 Lechhowski
10 06 2014 Komunikacja
10 4 06 2014 liryki lozańskie
logistyk 06 2014 praktyczny arkusz
arkusz 2 opm chemia z tutorem 12 06 2014 klasy przedmaturalne
grupa 8 11 06 2014
Pytania26 06 2014
Wykład 06 2014
Wyróżnione, SWPS, Psychologia, psychologia (1 Rok), Materiały SESJA 06.2014
Pomoc społeczna, służby społeczne, praca socjalna program prezentacji 2014 15
Prawo wykład 11 06 2014
Charaktery 06 2014
e 06 2014 01 X k
interna 2014, interna ENDOKRYNOLOGIA - czerwiec 2014
D2014000089701 Prawo Geodezyjno Kartograficzne z dn 05 06 2014
4 06 2014 Linert

więcej podobnych podstron