Programista 03 2014 iNTERnet

background image

3

/

2014

(

22

)

www

programistamag

pl

Cena 22.90 zł (w tym VAT 8%)

Index: 285358

MUSTACHE - CZYLI SZABLONY W JAVASCRIPT • ORM W PHP • ZASADY SOLID • BRAKUJĄCY ELEMENT W AGILE

Poznaj tajemnice
IronPython

Prawdziwe CUDA
z liczbą PI

Mój debugger dla
Windows

Przedstawimay przykła-
dy integracji platformy
.NET z językiem Python

Przybliżamy wartość
liczyby PI za pomocą
algorytmy Monte Carlo

Breakpointy, operacje
na pamięci wirtualnej
i kontekście procesora

Akka

wydajny szkielet dla aplikacji

wielowątkowych

background image
background image
background image

4

/ 3

. 2014 . (22) /

REDAKCJA/EDYTORIAL

Słoneczne dni zbliżają się wielkimi krokami, nic więc nie stoi na

przeszkodzie, aby znaleźć wygodną pozycję w hamaku (lub chociaż w
firmowej kuchni) i przystąpić do lektury najnowszego wydania magazy-
nu. Programista prezentuje bardzo przekrojową wiedzę w postaci arty-
kułów, ubarwioną cyklami takimi jak „Zdobyć flagę” czy „Klub Lidera IT”,
których jak zwykle można się spodziewać również w tym wydaniu.

Programistów używających Javy powinien zainteresować temat

okładkowy; Akka, czyli framework napisany w Scali służący do zwięk-
szania skalowalności aplikacji. Artykuł porusza temat implementowa-
nia obliczeń w modelu opartym o aktorów.

Wcześniejsze stwierdzenie o przekrojowości udowadnia propor-

cjonalna ilość stron, na których znajduje się dla odmiany kod w C#. Do
developerów wykorzystujących wysokopoziomowy język Microsoftu
skierowane są artykuły: „Wprowadzenie do Microsoft Roslyn CTP”
oraz „Wykorzystanie zasad SOLID podczas wytwarzania oprogramo-
wania w paradygmacie obiektowym”.

Warto też zwrócić uwagę na artykuł Dawida Boryckiego, który w

tym wydaniu „wprowadza Python'a w świat .NET”. Ponadto, osoby

znające C++ mogą robić „CUDA z liczbą Pi” tuż po przeczytaniu propo-
zycji Marka Sawerwaina.

Mateusz „j00ru” Jurczyk w drugiej części cyklu pt. „Jak napisać

własny debugger dla Windows” przedstawia bardziej zaawansowane
aspekty budowy debuggera, skupiając się na takich zagadnieniach jak
operowanie na pamięci wirtualnej, obsługa punktów wstrzymania, od-
czytywanie i zapisywanie kontekstu procesora.

Czym jest technologia 5G i jakie będą związane z nią korzyści? Na

te (między innymi) pytania odpowie Bartosz Ciepluch – dyrektor Eu-

ropejskiego Centrum Inżynierii i Oprogramowania NSN we Wrocławiu

Zapraszamy do lektury!

Z wyrazami szacunku, 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:

ArtDruk –

www.artdruk.com

ul. Napoleona 4

05-230 – Kobyłka

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

5

/ www.programistamag.pl /

SPIS TREŚCI

BIBLIOTEKI I NARZĘDZIA

IronPython, czyli integracja platformy .NET z językiem Python........................................................

Dawid Borycki

6

Wprowadzenie do Microsoft Roslyn CTP..............................................................................................

Aleksander Kania

14

Wstęp do WPF – część 3: Stylowania kontrolek ciąg dalszy...............................................................

Wojciech Sura

20

PROGRAMOWANIE APLIKACJI WEBOWYCH

Mustache – czyli szablony w JavaScript.................................................................................................

Piotr Tołłoczko

24

PROGRAMOWANIE SYSTEMOWE

Jak napisać własny debugger w systemie Windows – część 2..........................................................

Mateusz “j

00ru” Jurczyk

28

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Akka – wydajny szkielet dla aplikacji wielowątkowych.......................................................................

Tomasz Nurkiewicz

36

CUDA z liczbą Pi.........................................................................................................................................

Marek Sawerwain

44

INŻYNIERIA OPROGRAMOWANIA

Wykorzystanie zasad SOLID podczas wytwarzania oprogramowania w paradygmacie obiektowym....

Wojciech Czabański

50

PROGRAMOWANIE BAZ DANYCH

ORM w PHP z wykorzystaniem wzorca Active Record........................................................................

Jędrzej Czarnecki

54

ANKIETA

Ankieta magazynu Programista: „Proces wytwarzania oprogramowania w Twojej firmie”........

60

LABORATORIUM BOTTEGA

Brakujący element Agile. Część 2: Wprowadzanie feedbacku w życie...............................................

Paweł Badeński

62

WYWIAD

5G made in Wrocław. Rozmowa z Bartoszem Ciepluchem,
Dyrektorem Europejskiego Centrum Inżynierii i Oprogramowania NSN we Wrocławiu..............


66

STREFA CTF

Zdobyć flagę… RuCTF Quals 2014 – Nyan-task...................................................................................

Gynvael Coldwind

68

KLUB LIDERA IT

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

Mariusz Sieraczkiewicz

72

KLUB DOBREJ KSIĄŻKI

Scrum. Praktyczny przewodnik po najpopularniejszej metodyce Agile...........................................

Rafał Kocisz

76

background image

6

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Dawid Borycki

WPROWADZENIE

Python jest dynamicznym, wieloplatformowym i darmowym językiem pro-
gramowania, który w pierwszej wersji pojawił się w 1990 roku. Jego cechą cha-
rakterystyczną jest przejrzystość oraz duża czytelność składni, co skraca czas
potrzebny na analizę kodu źródłowego podczas jego rozwoju i utrzymania.

Biblioteka standardowa języka Python umożliwia szybkie tworzenie wy-

dajnych aplikacji sieciowych, bazodanowych, wielowątkowych oraz gier (2D
i 3D), a z pomocą dodatkowych bibliotek, jak na przykład GTK+ czy PyQt,
możliwe jest tworzenie wieloplatformowych aplikacji desktopowych. Ta ce-
cha jest szczególnie przydatna, jeśli dana aplikacja ma działać poprawnie nie
tylko na systemach operacyjnych Windows, ale również na Mac OS i innych
platformach.

Język Python znajduje również swoje zastosowanie w badaniach nauko-

wych i obliczeniach numerycznych, gdyż oferowane przez niego wyrażenia
wspierają proceduralny model programowania.

Dynamiczność języka Python sprawia, że deklaracja zmiennych nie wy-

maga użycia typu, a definicje obiektów mogą ulegać modyfikacjom podczas
interpretacji kodu.

Możliwości oferowane przez Python można rozszerzać za pomocą modu-

łów, implementowanych z wykorzystaniem języków programowania C, C++,
Java (Jython) oraz języków platformy .NET, z których najbardziej popularnymi
są C# i Visual Basic.

Integracja platformy .NET z Pythonem jest możliwa dzięki IronPythonowi,

który jest implementacją Python for .NET wykonaną w całości w języku C#
przez Microsoft. IronPython jest zestawem narzędzi umożliwiających kompi-
lację kodu języka Python do kodu pośredniego IL (od ang. Intermediate Lan-
guage
), który jest następnie kompilowany w trybie JIT (ang. just-in-time) przez
środowisko uruchomieniowe (maszynę wirtualną) .NET oznaczaną skrótem
CLR (od ang. Common Language Runtime). Kompilowanie w trybie JIT oznacza
kompilację kodu pośredniego do języka maszynowego w trakcie uruchamia-
nia (działania) aplikacji.

Ponieważ język Python jest językiem dynamicznym, to kod pośredni po-

wstający za jego pomocą jest uruchamiany pod kontrolą dynamicznej wersji
środowiska CLR o nazwie Dynamic Language Runtime (DLR). Ta ostatnia stano-
wi zestaw dodatkowych usług dla CLR umożliwiających wykorzystanie języ-
ków dynamicznych do tworzenia aplikacji dla platformy .NET. DLR obsługuje
nie tylko Python, ale również inne języki dynamiczne, takie jak: Lisp, Smalltalk,
JavaScript, PHP, Ruby, ColdFusion, Lua, Cobra czy Groovy.

Dzięki powyższemu IronPython działa również w drugą stroną, co ozna-

cza, że umożliwia wykorzystanie języka Python do tworzenia aplikacji bazują-
cych na platformie .NET. W związku z tym IronPython stanowi pomost łączący
dobrodziejstwa języka Python i biblioteki .NET.

Warto wspomnieć, że alternatywnym narzędziem do IronPythona jest

CPython. Ten artykuł poświęcę jednak tematyce integracji języka Python z

platformą .NET za pomocą narzędzia IronPython. Opis rozpocznę od skonfi-
gurowania Visual Studio 2013 do pracy z IronPythonem, aby w kolejnym kro-
ku przedstawić mechanizmy umożliwiające interpretację kodu utworzonego
z wykorzystaniem języka Python w aplikacjach .NET. Następnie pokażę, w jaki
sposób wykorzystywać klasy i metody dostarczane przez platformę .NET w
skryptach Pythona. W ramach podsumowania zaimplementuję analizę typu
pliku graficznego za pomocą biblioteki standardowej Pythona.

INSTALACJA NARZĘDZI PYTHON

TOOLS W VISUAL STUDIO 2013

W celu utworzenia projektu wykorzystującego IronPythona w Visual Stu-
dio 2013 (VS 2013) należy pobrać odpowiedni pakiet instalacyjny z witryny

http://ironpython.codeplex.com/releases/view/90087

.

Środowisko VS 2013 można uzupełnić o zestaw dodatkowych narzę-

dzi wspierających programowanie z użyciem języka Python, który nosi na-
zwę Python Tools for Visual Studio (PTVS) i można go zainstalować na kilka
sposobów. Pierwszy z nich polega na wykorzystaniu hiperłącza Get Python
Tools for Visual Studio
, które jest dostępne w grupie Other languages/Python
(zob. Rysunek 1) w kreatorze nowego projektu VS 2013. Po ustaleniu nazwy
i lokalizacji projektu wystarczy kliknąć przycisk z etykietą OK, co spowoduje
utworzenie pustego projektu i wyświetlenie strony internetowej z przyci-
skiem umożliwiającym pobranie narzędzi Python Tools 2.0 dla Visual Studio
(Rysunek  2). Alternatywnie, narzędzia te można pobrać samodzielnie z wi-
tryny

https://pytools.codeplex.com/releases/view/103102

. Narzędzia PTVS

można również zainstalować za pomocą konsoli menadżera pakietów NuGet
w VS2013 (menu Tools/Library Package Manager/Package Manager Console),
wydając polecenie

Install-Package IronPython.

Rysunek 1. Kreator New Project w Visual Studio 2013 z zaznaczonym

elementem Get Python Tools for Visual Studio

IronPython, czyli integracja

platformy .NET z językiem Python

Interdyscyplinarne projekty informatyczne, realizowane przez wiele zespołów, wy-
magają integracji kodu źródłowego powstającego z użyciem różnych narzędzi oraz
języków programowania. Osobiście spotkałem się już kilkukrotnie z koniecznością
integracji oprogramowania tworzonego w oparciu o język Python z bibliotekami
.NET i vice versa. Przydatnym narzędziem okazał się być wówczas IronPython, któ-
ry umożliwia dwukierunkową integrację Pythona z platformą .NET. W tym artukule
omówię podstawowe właściwości tego narzędzia.

background image

7

/ www.programistamag.pl /

IRONPYTHON, CZYLI INTEGRACJA PLATFORMY .NET Z JĘZYKIEM PYTHON

Rysunek 2. Przycisk z hiperłączem umożliwiającym pobranie

Po pobraniu i zainstalowaniu Python Tools 2.0, Visual Studio będzie zawiera-
ło dodatkowe szablony projektów, umożliwiające między innymi tworzenie
aplikacji Windows Forms oraz Silverlight z wykorzystaniem języka Python (Ry-
sunek 3). Jednakże, w przypadku projektów Windows Forms narzędzia Python
Tools 2.0
nie umożliwiają wizualnego projektowania interfejsu użytkownika.

Rysunek 3. Lista szablonów projektów dostarczanych wraz z Python Tools 2.0

for Visual Studio 2013

WITAJ, PYTHONIE! W ŚWIECIE .NET

Przejdę teraz do utworzenia aplikacji Windows Forms, wykorzystującej język
C#, bibliotekę .NET 4.5.1 oraz skrypty języka Python. Realizacja tego zadania
polega na wykonaniu poniższych czynności:

1. W Visual Studio utwórzmy nowy projekt aplikacji o nazwie PythonHello-

World według szablonu Windows Forms Application.

2. Projekt aplikacji uzupełnijmy o referencję (opcja Project/Add reference...)

do bibliotek IronPython.dll oraz Microsoft.Scripting.dll, znajdujących się w
folderze, w którym zainstalowano IronPythona. Domyślnie, dla wersji 2.7,
jest to folder Program Files (x86)\IronPython 2.7.

3. W nagłówku pliku Form1.cs umieśćmy polecenia importujące przestrzenie

nazw

IronPython.Hosting oraz Microsoft.Scripting.Hosting:

using

IronPython.Hosting;

using

Microsoft.Scripting.Hosting;

4. W klasie

Form1 zadeklarujmy dwa prywatne pola:

private

ScriptEngine

_scriptEngine =

Python

.CreateEngine();

private

ScriptScope

_scriptScope;

5. Na formularzu aplikacji umieśćmy jeden przycisk o nazwie

buttonHel-

loWorld i etykiecie Hello, world!

6. Utwórzmy domyślną metodę zdarzeniową przycisku i zdefiniujmy ją we-

dług wzoru z Listingu 1.

7. Konstruktor klasy

Form1 uzupełnijmy o polecenie wyróżnione na Listingu 2.

Listing 1. Uruchomienie skyptu Pythona w aplikacji Windows Forms

private

void

buttonHelloWorld_Click(

object

sender,

EventArgs

e)

{

const

string

helloWorldScript =

@"def HelloWorld(): return 'Hello, world!'"

;

engine.Execute(helloWorldScript, _scriptScope);

dynamic

scriptFunction = _scriptScope.GetVariable(

"HelloWorld"

);

MessageBox

.Show(scriptFunction().ToString());

}

Listing 2. Konstruktor klasy Form1

public

Form1()

{

InitializeComponent();

_scriptScope = _scriptEngine.CreateScope();

}

W ramach przykładu z Listingu 1 utworzyłem prosty skrypt Pythona, złożony
z jednej procedury o nazwie

HelloWorld. Jej zadaniem jest zwrócenie lite-

rału o treści Hello, world!, który jest następnie prezentowany w ramach okna
modalnego.

Skrypt Pythona, zapisany w stałej

helloWorldScript, został urucho-

miony (lub bardziej formalnie zinterpretowany) z poziomu aplikacji .NET za
pomocą metody

Execute klasy ScriptEngine. Ta ostatnia implementuje

język Python dla platformy DLR i jest zasadniczym elementem IronPythona,
umożliwiającym wykorzystanie kodu języka Python w aplikacjach opierają-
cych swoje działanie na bibliotece .NET.

W celu odczytania wartości zwracanych przez daną procedurę Pythona

należy uzyskać dostęp do kontekstu (zakresu) danego skryptu, który imple-
mentuje klasa

ScriptScope. Udostępnia ona między innymi metodę Get-

Variable, umożliwiającą uzyskanie dostępu do metody lub zmiennej w
kontekście skryptu na podstawie ich nazwy.

Zgodnie z tym, co było wcześniej powiedziane, język Python jest dyna-

miczny i z tego powodu nieznane są a priori typy wartości, zwracanych przez
skrypty. W związku z tym w Listingu 1 wykorzystałem słowo kluczowe

dy-

namic, wprowadzone w czwartej wersji języka C#. Umożliwia ono wyłączenie
sprawdzania typów zmiennych na etapie kompilacji. Dzięki temu zabiegowi
typy zmiennych są ustalane dopiero w trakcie uruchamiania aplikacji, jak to
ma standardowo miejsce w przypadku języków dynamicznych.

TYPY ZŁOŻONE, OBSŁUGA

WYJĄTKÓW I WYKORZYSTANIE

OBIEKTÓW PLATFORMY .NET

W poprzednim rozdziale pokazałem, w jaki sposób można uruchomić skrypt
Pythona oraz uzyskać dostęp do zwracanych przez niego wartości. IronPy-
thon umożliwia również przekazywanie typów złożonych do funkcji skryp-
tów. Odpowiednie instancje klas można następnie wykorzystywać w ramach
funkcji skryptów zupełnie tak samo jak w przypadku innych języków platfor-
my .NET. Kolejny przykład będzie ilustrował te możliwości, a jego implemen-
tacja składa się z następujących kroków:
1. Projekt aplikacji PythonHelloWorld uzupełnijmy o plik Osoba.cs, a następ-

nie umieśćmy w nim polecenia z Listingu 3.

2. Na formularz aplikacji PythonHelloWorld umieśćmy kontrolkę typu

List-

Box o nazwie listBoxWyniki oraz kolejny przycisk z etykietą Zmiana
danych
.

3. Utwórzmy domyślną metodę zdarzeniową przycisku i zdefiniujmy ją

zgodnie z Listingiem 4.

background image

8

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Listing 3. Definicja klasy Osoba

using

System;

namespace

PythonHelloWorld

{

public

class

Osoba

{

private

byte

_wiek;

public

string

Imie {

get

;

set

; }

public

string

Nazwisko {

get

;

set

; }

public

byte

Wiek

{

get

{

return

_wiek; }

set

{

const

byte

wiekMaksymalny = 100;

if

(

value

<= wiekMaksymalny)

{

_wiek =

value

;

}

else

{

throw

new

ArgumentException

();

}

}

}

public

Osoba(

string

imie,

string

nazwisko,

byte

wiek)

{

this

.Imie = imie;

this

.Nazwisko = nazwisko;

this

.Wiek = wiek;

}

public

override

string

ToString()

{

string

osoba = Imie.ToString() +

" "

+ Nazwisko.ToString() +

" ("

+ Wiek +

")"

;

return

osoba;

}

}

}

Listing 4. Modyfikacja właściwości instancji klasy Osoba za pomocą
skryptu języka Python

private

void

buttonZmianaDanych_Click(

object

sender,

EventArgs

e)

{

Osoba

osoba =

new

Osoba

(

"Dawid"

,

"Borycki"

, 31);

listBoxWyniki.Items.Clear();

listBoxWyniki.Items.Add(

"Dane przed zmianą: "

+ osoba);

const

string

zmienDaneScript =

@"def ZmienDane(osoba):

osoba.Imie = 'Zuzanna'

osoba.Nazwisko = 'Borycka'

osoba.Wiek = 1

return osoba"

;

_scriptEngine.Execute(zmienDaneScript, _scriptScope);

dynamic

scriptFunction = _scriptScope.GetVariable(

"ZmienDane"

);

listBoxWyniki.Items.Add(

"Dane po zmianie: "

+ scriptFunction(osoba));

}

Przykładowy wynik działania metody zdarzeniowej z Listingu 4 przedstawi-
łem na Rysunku 4.

Rysunek 4. Aktualizacja wartości zapisanych w instancji klasy Osoba

W tym miejscu warto zwrócić uwagę na dodatkowy aspekt przykładu z

Listingów 3 i 4. Chodzi mianowicie o obsługę wyjątków, które mogą być zgła-
szane podczas próby przypisania do pola

Wiek klasy Osoba wartości więk-

szych od 100.

W powyższym przykładzie mamy dwie możliwości obsługi wyjątków

zgłaszanych przez skrypty Pythona. Pierwszy polega na otoczeniu blo-
kiem instrukcji

try, catch ostatnich trzech poleceń w definicji metody

buttonZmianaDanych_Click, które są odpowiedzialne za interakcje z Py-
thonem (Listing 5). Natomiast drugi sposób polega na bezpośredniej obsłudze
wyjątków wewnątrz funkcji skryptu (Listing 6). Reasumując, IronPython umoż-
liwia obsługę zarówno wyjątków platformy .NET, jak i wyjątków języka Python.

Listing 5. Obsługa wyjątków zgłaszanych podczas uruchamiania
skryptu

private

void

buttonZmianaDanych_Click(

object

sender,

EventArgs

e)

{

Osoba

osoba =

new

Osoba

(

"Dawid"

,

"Borycki"

, 31);

listBoxWyniki.Items.Clear();

listBoxWyniki.Items.Add(

"Przed zmianą: "

+ osoba);

const

string

zmienDaneScript =

@"def ZmienDane(osoba):

osoba.Imie = 'Zuzanna'

osoba.Nazwisko = 'Borycka'

osoba.Wiek = 101

return osoba"

;

try

{

_scriptEngine.Execute(zmienDaneScript, _scriptScope);

dynamic

scriptFunction = _scriptScope.GetVariable(

"ZmienDane"

);

listBoxWyniki.Items.Add(

"Po zmianie: "

+ scriptFunction(osoba));

}

catch

(

Exception

ex)

{

MessageBox

.Show(ex.Message);

}

}

Listing 6. Obsługa wyjątków platformy .NET wewnątrz skryptu
Pythona

private

void

buttonZmianaDanych_Click(

object

sender,

EventArgs

e)

{

Osoba

osoba =

new

Osoba

(

"Dawid"

,

"Borycki"

, 31);

listBoxWyniki.Items.Clear();

listBoxWyniki.Items.Add(

"Przed zmianą: "

+ osoba);

const

string

zmienDaneScript =

@"def ZmienDane(osoba):

import clr

import System

clr.AddReference('System.Windows.Forms')

from System.Windows.Forms import MessageBox

try:

import System

osoba.Imie = 'Zuzanna'

osoba.Nazwisko = 'Borycka'

osoba.Wiek = 101

except System.ArgumentException as e:

MessageBox.Show(e.Message, 'Błąd')

return osoba"

;

_scriptEngine.Execute(zmienDaneScript, _scriptScope);

dynamic

scriptFunction = _scriptScope.GetVariable(

"ZmienDane"

);

listBoxWyniki.Items.Add(

"Po zmianie: "

+ scriptFunction(osoba));

}

Definicja metody zdarzeniowej z Listingu 6 dodatkowo przedstawia wykorzy-
stanie obiektów i procedur zaimplementowanych w bibliotekach platformy
.NET (ang. .NET assembly). W celu wywołania wybranej funkcji należy naj-
pierw, za pomocą metody

clr.AddReference, załadować odpowiednią bi-

bliotekę, a następnie należy zaimportować wybrane klasy z wykorzystaniem
pary poleceń

from, import.

background image

9

/ www.programistamag.pl /

IRONPYTHON, CZYLI INTEGRACJA PLATFORMY .NET Z JĘZYKIEM PYTHON

W powyższym przykładzie ograniczyłem się do wywołania statycznej me-

tody

Show klasy MessageBox. Jednakże, w analogiczny sposób można wyko-

rzystać pozostałe klasy dostępne w bibliotekach platformy .NET oraz własne
biblioteki zarządzane.

KOMPILACJA SKRYPTU

IronPython udostępnia przydatną klasę

ScriptSource, która umożliwia

między innymi kompilację skryptu w celu przyspieszenia jego działania.
Skompilowany skrypt może być następnie uruchamiany wielokrotnie z wyko-
rzystaniem różnych kontekstów.

Uzupełnię teraz projekt aplikacji PythonHelloWorld o procedury ilustrują-

ce przykładowe użycie klasy

ScriptSource. W tym celu:

1. Umieśćmy na formularzu aplikacji PythonHelloWorld dodatkowy przycisk

z etykietą Kompilacja skryptu.

2. Utwórzmy domyślną metodę zdarzeniową przycisku i zdefiniujemy ją

zgodnie z Listingiem 7, który dodatkowo zawiera definicję pomocniczej
metody

WyswietlWynikOrazCzasWykonania.

Listing 7. Obsługa wyjątków platformy .NET wewnątrz skryptu Pythona

private

void

WyswietlWynikOrazCzasWykonania(

int

n,

ref

System.Diagnostics.

Stopwatch

stopWatch)

{

dynamic

sumFunc = _scriptScope.GetVariable(

"Suma"

);

dynamic

sum = sumFunc(n);

stopWatch.Stop();

listBoxWyniki.Items.Add(

"Wynik sumowania: "

+ sum.ToString()

+

", czas wykonania [ms]: "

+ stopWatch.ElapsedMilliseconds);

}

private

void

buttonKompilacja_Click(

object

sender,

EventArgs

e)

{

const

string

simpleScript =

@"def Suma(n):

i = 1

suma = 0

while i <= n:

suma += i

i += 1

return suma"

;

const

int

n = 1000000;

System.Diagnostics.

Stopwatch

stopWatch =

new

System.Diagnostics.

Stopwatch

();

// Uruchomienie skryptu bez kompilacji

stopWatch.Start();

_scriptEngine.Execute(simpleScript, _scriptScope);

WyswietlWynikOrazCzasWykonania(n,

ref

stopWatch);

// Kompilacja i uruchomienie skryptu

ScriptSource

scriptSource = _scriptEngine.

CreateScriptSourceFromString(simpleScript);

scriptSource.Compile();

stopWatch.Start();

scriptSource.Execute(_scriptScope);

WyswietlWynikOrazCzasWykonania(n,

ref

stopWatch);

}

Zadaniem skryptu, użytego w metodzie z Listingu 7, jest zsumowanie n ko-
lejnych liczb całkowitych. Natomiast przykładowe wyniki generowane przez
funkcję

buttonKompilacja_Click przedstawiłem na Rysunku 5. Nietrud-

no zauważyć, że zgodnie z przewidywaniami czas wykonania skompilowane-
go skryptu jest znacznie krótszy niż jego nieskompilowanej wersji.

Rysunek 5. Porównanie czasu wykonywania skompilowanych

i nieskompilowanych skryptów

BIBLIOTEKA STANDARDOWA

PYTHONA

Język Python posiada rozbudowaną bibliotekę standardową (STD) oraz sze-
reg innych przydatnych bibliotek dystrybuowanych w postaci tak zwanych
modułów. W tym rozdziale, na przykładzie modułu

imghdr wchodzącego

w skład STD Pythona, pokażę, w jaki sposób uzyskać dostęp do wybranego
modułu za pomocą IronPythona. Odpowiednie procedury zaimplementuję w
ramach aplikacji PythonHelloWorld. Oto one:
1. Przejdźmy do edycji pliku Form1.cs i umieśćmy w nim metody

Przygo-

tujSkrypt oraz PobierzTypObrazu, których definicje przedstawiłem
na Listingach 8 i 9.

Listing 8. Przygotowanie funkcji skrypt, wykorzystującego moduł
imghdr z biblioteki standardowej Pythona

private

void

PrzygotujSkrypt()

{

// Domyślna ścieżka do biblioteki standardowej dla IronPython 2.7

string

stdPath =

@"c:\Program Files (x86)\IronPython 2.7\Lib"

;

// Konfiguracja ścieżki poszukiwań

var

paths = _scriptEngine.GetSearchPaths();

paths.Add(stdPath);

_scriptEngine.SetSearchPaths(paths);

const

string

imageHeaderScript =

@"def ImageHeader(path):

import imghdr

return imghdr.what(path)"

;

// Kompilacja skryptu

_scriptSourceImgHdr = _scriptEngine.

CreateScriptSourceFromString(imageHeaderScript);

_scriptSourceImgHdr.Compile();

}

Listing 9. Metoda rozpoznająca format obrazu

private

string

PobierzTypObrazu(

string

filePath)

{

string

format =

"Nieznany"

;

try

{

_scriptSourceImgHdr.Execute(_scriptScope);

dynamic

imgHeader = _scriptScope.GetVariable(

"ImageHeader"

);

format = imgHeader(filePath).ToString();

}

catch

(

Exception

)

{

}

return

format;

}

2. Konstruktor klasy Form1 uzupełnijmy o wywołanie metody

Przygotuj-

Skrypt (Listing 10).

Listing 10. Konstruktor klasy Form1

public

Form1()

{

InitializeComponent();

_scriptScope = _scriptEngine.CreateScope();

PrzygotujSkrypt();

}

3. Na formularzu aplikacji PythonHelloWorld umieśćmy kolejny przycisk o

nazwie

buttonAnalizujFormat i etykiecie Analizuj format.

4. Utwórzmy domyślną metodę zdarzeniową do wstawionego przycisku

według wzoru z Listingu 11.

background image

10

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Listing 11. Analiza typu wybranego obrazu

private

void

buttonAnalizujFormat_Click(

object

sender,

EventArgs

e)

{

OpenFileDialog

openFileDialog =

new

OpenFileDialog

();

if

(openFileDialog.ShowDialog() == System.Windows.Forms.

DialogResult

.OK)

{

string

filePath = openFileDialog.FileName;

listBoxWyniki.Items.Add(

"Plik: "

+ System.IO.

Path

.

GetFileName(filePath)

+

" Typ: "

+ PobierzTypObrazu(openFileDialog.FileName));

}

}

Zasada działania powyższego przykładu polega na odczytaniu typu obrazu
na podstawie jego zawartości. Do tego celu wykorzystałem moduł

imghdr

z biblioteki standardowej Pythona. Moduł ten udostępnia statyczną funkcję
what, która analizuje nagłówek wskazanego pliku z obrazem i na tej podsta-
wie zwraca informacje o jego formacie. Wynik zwracany przez funkcję

what

ma postać jednego z następujących łańcuchów znakowych:

rgb, gif, pbm,

pgm, ppm, tiff, rast, xbm, jpeg, bmp lub png. Przykładowe wyniki genero-
wane przez metody z Listingów 8-11 przedstawiłem na Rysunku 6.

Rysunek 6. Przykład działania aplikacji PythonHelloWorld w zakresie analizy

Kilka aspektów powyższego rozwiązania wymaga dodatkowego komenta-
rza. Przede wszystkim w metodzie z Listingu 8 użyłem stałej znakowej w celu
wskazania ścieżki do STD Pythona. Jednakże, w ogólnym przypadku ścieżkę
do tej biblioteki określa się za pomocą zmiennej środowiskowej

IRONPY-

THONPATH. Dzięki temu uzyskuje się przenaszalność aplikacji.

W celu wykorzystania zmiennej środowiskowej do przechowania ścieżki

do biblioteki standardowej Pythona należy postąpić następująco:
1. Uruchommy narzędzie właściwości systemu Windows (Rysunek 7). W tym

celu kliknijmy prawym przyciskiem ikonę Komputer i z menu konteksto-
wego wybierzmy opcję Właściwości lub w Panelu sterowania kliknijmy
hiperłącze System.

Rysunek 7. Właściwości systemu Windows

2. Kliknijmy odnośnik Zaawansowane ustawienia systemu, znajdujący się po

lewej stronie ekranu z Rysunku 7.

3. W kolejnym oknie (Rysunek 8) kliknijmy przycisk z etykietą zmienne

środowiskowe.

Rysunek 8. Zaawansowane ustawienia systemu Windows

4. W kreatorze Zmienne środowiskowe (Rysunek 9) w grupie zmienne syste-

mowe kliknijmy przycisk z etykietą Nowa...

Rysunek 9. Lista zmiennych środowiskowych systemu Windows

5. W oknie Nowa zmienna systemowa w polu nazwa zmiennej wpiszmy

IRONPYTHONPATH, a w polu wartość zmiennej ścieżkę do biblioteki stan-
dardowej Pythona (Rysunek 10). Kliknijmy przycisk z etykietą OK, a na-
stępnie zamknijmy pozostałe okna.

Rysunek 10. Tworzenie i konfiguracja nowej zmiennej systemowej

6. Wylogujmy się z systemu, a następnie zalogujmy się ponownie w celu ak-

tualizacji informacji o zmiennych systemowych.

7. Zmodyfikujmy definicję metody

PrzygotujSkrypt (Listing 8) według

wzoru z Listingu 12.

background image

11

/ www.programistamag.pl /

IRONPYTHON, CZYLI INTEGRACJA PLATFORMY .NET Z JĘZYKIEM PYTHON

Listing 12. Przykł. wykorzystania zmiennej środowiskowej IRONPYTHON-
PATH do zlokalizowania ścieżki do biblioteki standardowej Pythona

private

void

PrzygotujSkrypt()

{

// Domyślna ścieżka do biblioteki standardowej dla IronPython 2.7

//string stdPath = @"c:\Program Files (x86)\IronPython 2.7\Lib";

const

string

envPath =

"IRONPYTHONPATH"

;

string

stdPath =

Environment

.GetEnvironmentVariable(envPath);

// Konfiguracja ścieżki poszukiwań

var

paths = _scriptEngine.GetSearchPaths();

paths.Add(stdPath);

_scriptEngine.SetSearchPaths(paths);

const

string

imageHeaderScript =

@"def ImageHeader(path):

import imghdr

return imghdr.what(path)"

;

// Kompilacja skryptu

_scriptSourceImgHdr = _scriptEngine.

CreateScriptSourceFromString(imageHeaderScript);

_scriptSourceImgHdr.Compile();

}

Dzięki powyższej zmianie dostęp do biblioteki standardowej Pythona będzie
realizowany za pomocą zmiennej środowiskowej. W związku z tym do popraw-
nego odnalezienia STD Pythona na innych komputerach wymagane będzie
jedynie poprawne zdefiniowanie zmiennej środowiskowej

IRONPYTHONPATH.

Warto zwrócić uwagę również na fakt, że alternatywnie bibliotekę stan-

dardową Pythona można zaimportować bezpośrednio w funkcji skryptu.
Odpowiednie zmiany wymagane w definicji metody

PrzygotujSkrypt wy-

różniłem na Listingu 13.

PODSUMOWANIE

W ramach niniejszego artykułu omówiłem podstawowe elementy IronPytho-
na. Pokazałem, w jaki sposób uruchamiać i kompilować skrypty Pythona w
ramach aplikacji Windows Forms. Dodatkowo przedstawiłem mechanizmy
umożliwiające wykorzystanie bibliotek platformy .NET w skryptach Pythona.

Listing 13. Przykład wykorzystania zmiennej środowiskowej IRON-
PYTHONPATH w funkcji skryptu języka Python

private

void

PrzygotujSkrypt()

{

// Domyślna ścieżka do biblioteki standardowej dla IronPython 2.7

//string stdPath = @"c:\Program Files (x86)\IronPython 2.7\Lib";

//const string envPath = "IRONPYTHONPATH";

//string stdPath = Environment.GetEnvironmentVariable(envPath);

// Konfiguracja ścieżki poszukiwań

//var paths = _scriptEngine.GetSearchPaths();

//paths.Add(stdPath);

//_scriptEngine.SetSearchPaths(paths);

const

string

imageHeaderScript =

@"def ImageHeader(path):

import sys

import System

envPath = 'IRONPYTHONPATH'

sys.path.append(System.Environment.GetEnvironmentVariable(envPath))

import imghdr

return imghdr.what(path)"

;

// Kompilacja skryptu

_scriptSourceImgHdr = _scriptEngine.

CreateScriptSourceFromString(imageHeaderScript);

_scriptSourceImgHdr.Compile();

}

W ramach podsumowania zaprezentowałem przykładowe wykorzystanie
modułów z biblioteki standardowej Pythona.

Na zakończenie warto wspomnieć o niektórych ograniczeniach IronPy-

thona wynikających ze specyfiki języka Python. Jednym z nich jest szczególny
sposób uzyskiwania dostępu do parametrów przekazywanych przez referen-
cję (z użyciem słów kluczowych

ref i out), co wynika z faktu, że w języku

Python argumenty przekazywane są przez wartość. W takiej sytuacji zaktu-
alizowana wartość parametru przekazanego przez referencję jest zwracana
razem z wynikiem danej metody. Innym ograniczeniem IronPythona jest brak
natywnej obsługi metod rozszerzających (ang. extension methods).

Jednakże pomimo kilku swoich ograniczeń IronPython jest narzędziem

godnym uwagi podczas integracji języka Python z biblioteką .NET.

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.

reklama

background image

Seattle

Frankfurt

Warszawa

Praga

Wiedeń

Sztokholm

San José

Miami

Los Angeles

Dallas

Atlanta

Newark

Londyn

Paryż

Waszyngton

Chicago

Toronto

Amsterdam

Nowoczesne centra danych 1&1 należą do najbezpieczniejszych i najbardziej wydajnych w Europie.
Redundantna sieć szkieletowa o przepustowości ponad 300 Gbit/s zapewnia maksymalną dostępność usług.

WYDAJNIEJSZY

DZIĘKI SIECI CDN

BEZPIECZNIEJSZY

DZIĘKI SITELOCK

PEWNIEJSZY

DZIĘKI

GEOREDUNDANCJI

* Wszystkie pakiety hostingowe: gwarancja zwrotu pieniędzy w ciągu 30 dni od zamówienia. Umowa zawierana na rok. Podana miesięczna cena promocyjna obowiązuje przez cały czas trwania umowy.

Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin promocji na www.1and1.pl. Ceny nie zawierają VAT (23%).

DOMENY

|

E-MAIL

|

STRONY WWW

|

HOSTING

|

SERWERY

MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW

NOWY HO STING

Nasza nowa sieć CDN (Content Delivery
Network) o zwiększonej wydajności
zapewnia nieprzerwane działanie
serwisów internetowych.

NOWOŚĆ!

Teraz także na urządzeniach

mobilnych. Co więcej, od tej chwili
system 23 rozproszonych na świecie
punktów PoP i sieci szkieletowych
dokonuje cachingu nie tylko treści
statycznych, ale również dynamicznych.
Tym samym radykalnie przyspiesza
obsługę żądań.

1&1 SiteLock aktywnie chroni Twoją
stronę przed złośliwym oprogramo-
waniem, nieupoważnionym
dostępem oraz innymi zagrożeniami
ze strony hakerów.

W PAKIECIE:

codzienny skan pod

kątem malware oraz gruntowny skan
witryny co 30 dni - w ten sposób
ochronisz siebie oraz odwiedzających
przed wirusami
i trojanami.

Nasza georedundanta infrastruktura
gwarantuje najwyższe bezpieczeń-
stwo i niezawodność. Przechowu-
jemy Twoje pliki równocześnie
w dwóch niezależnych centrach danych
w Europie. W przypadku awarii
jednego z nich system automatycznie
przełącza się na drugie, a Twoje dane
pozostają dostępne online. Maksy-
malna dostępność jest dodatkowo
wspomagana przez codzienny backup
całej infrastruktury.

SITELOCK.COM

Sprawdzono

5-MAR-2014

NIE WYKRYTO MALWARE

Site

Lock

MAPPL1403C1P_420x297+5_KB_46L.indd 1

26.02.14 17:04

background image

Seattle

Frankfurt

Warszawa

Praga

Wiedeń

Sztokholm

San José

Miami

Los Angeles

Dallas

Atlanta

Newark

Londyn

Paryż

Waszyngton

Chicago

Toronto

Amsterdam

Nowoczesne centra danych 1&1 należą do najbezpieczniejszych i najbardziej wydajnych w Europie.
Redundantna sieć szkieletowa o przepustowości ponad 300 Gbit/s zapewnia maksymalną dostępność usług.

WYDAJNIEJSZY

DZIĘKI SIECI CDN

BEZPIECZNIEJSZY

DZIĘKI SITELOCK

PEWNIEJSZY

DZIĘKI

GEOREDUNDANCJI

* Wszystkie pakiety hostingowe: gwarancja zwrotu pieniędzy w ciągu 30 dni od zamówienia. Umowa zawierana na rok. Podana miesięczna cena promocyjna obowiązuje przez cały czas trwania umowy.

Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin promocji na www.1and1.pl. Ceny nie zawierają VAT (23%).

DOMENY

|

E-MAIL

|

STRONY WWW

|

HOSTING

|

SERWERY

MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW

NOWY HO STING

Nasza nowa sieć CDN (Content Delivery
Network) o zwiększonej wydajności
zapewnia nieprzerwane działanie
serwisów internetowych.

NOWOŚĆ!

Teraz także na urządzeniach

mobilnych. Co więcej, od tej chwili
system 23 rozproszonych na świecie
punktów PoP i sieci szkieletowych
dokonuje cachingu nie tylko treści
statycznych, ale również dynamicznych.
Tym samym radykalnie przyspiesza
obsługę żądań.

1&1 SiteLock aktywnie chroni Twoją
stronę przed złośliwym oprogramo-
waniem, nieupoważnionym
dostępem oraz innymi zagrożeniami
ze strony hakerów.

W PAKIECIE:

codzienny skan pod

kątem malware oraz gruntowny skan
witryny co 30 dni - w ten sposób
ochronisz siebie oraz odwiedzających
przed wirusami
i trojanami.

Nasza georedundanta infrastruktura
gwarantuje najwyższe bezpieczeń-
stwo i niezawodność. Przechowu-
jemy Twoje pliki równocześnie
w dwóch niezależnych centrach danych
w Europie. W przypadku awarii
jednego z nich system automatycznie
przełącza się na drugie, a Twoje dane
pozostają dostępne online. Maksy-
malna dostępność jest dodatkowo
wspomagana przez codzienny backup
całej infrastruktury.

SITELOCK.COM

Sprawdzono

5-MAR-2014

NIE WYKRYTO MALWARE

Site

Lock

MAPPL1403C1P_420x297+5_KB_46L.indd 1

26.02.14 17:04

* Wszystkie pakiety hostingowe: gwarancja zwrotu pieniędzy w ciągu 30 dni od zamówienia. Umowa zawierana na rok. Podana miesięczna cena promocyjna obowiązuje przez cały czas trwania umowy.

Niniejszy materiał promocyjny nie stanowi oferty w rozumieniu kodeksu cywilnego. Ogólne warunki handlowe i regulamin promocji na www.1and1.pl. Ceny nie zawierają VAT (23%).

1and1.pl

DOMENY

|

E-MAIL

|

STRONY WWW

|

HOSTING

|

SERWERY

MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW

NOWY HO STING

MIESIĄC

30 DNI

NA PRÓBĘ

TELEFON

PORADA

SPECJALISTY

PEWNOŚĆ

DZIĘKI GEO-

REDUNDANCJI

22 116 27 77

Maksymalna dostępność dzięki georedundancji

Ponad 300 Gbit/s przepustowości

Gwarantowane nawet 2 GB RAM

NOWOŚĆ!

1&1 CDN powered by CloudFlare

®

NOWOŚĆ!

Skan bezpieczeństwa 1&1 SiteLock

w pakiecie

NOWOCZESNA TECHNOLOGIA

1&1 SEO Ekspert

1&1 SiteAnalytics

Google Sitemaps

SKUTECZNY MARKETING

NetObjects Fusion

®

2013

1&1 Kreator Stron Mobilnych

NOWOŚĆ!

PHP 5.5, Perl, Python, Ruby

POTĘŻNE NARZĘDZIA

Ponad 140 popularnych aplikacji (Drupal™,

WordPress , Joomla!™, TYPO3, Magento

®

...)

Wsparcie eksperta od aplikacji

CENTRUM APLIKACJI

Domena .pl gratis

Nielimitowana powierzchnia, transfer,

konta e-mail i bazy danych MySQL

System Linux lub Windows

WSZYSTKO W KOMPLECIE

MOCNE PAKIETY

DLA ZAWODOWCÓW

OD

4,

90

zł/mies.*

MAPPL1403C1P_420x297+5_KB_46L.indd 2

26.02.14 17:04

background image

14

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Aleksander Kania

PROJEKT ROSLYN CTP

Na dzień dzisiejszy narzędzia służące do tworzenia kodu (IDE) są już na tyle
rozbudowane, że programista nie musi się trudzić w wielu aspektach. Przy-
kładowo, ostatnio Microsoft wprowadził do Visual Studio możliwość wyszuki-
wania i automatycznego wstawiania kodu prosto z StackOverflow. Jak by nie
patrzeć, jest to naprawdę wielkie uproszczenie.

Jest jednak coś, co do dzisiaj dla wielu programistów pozostaje tajemnicą

– mowa tutaj o kompilatorach. Można je nazwać pewnego rodzaju czarnymi
skrzynkami, ponieważ tak naprawdę przeciętny programista nie zdaje sobie
sprawy, co się dzieje tam w środku po zażądaniu kompilacji. Można więc spo-
kojnie założyć, że programista .NET, tworzący kod C#/VB dla aplikacji GUI, nie
musi przejmować się tym, co robi kompilator. Może się jednak zdarzyć, że bę-
dzie musiał stworzyć narzędzie do badania kodu i wtedy właśnie pomocne
może okazać się narzędzie Roslyn, dzięki któremu programista może wyko-
rzystać te same struktury danych i algorytmy, z których korzysta kompilator
w celu przetworzenia naszego kodu na kod pośredni (CIL). Roslyn zapewnia
również, że informacje te będą dokładne i kompletne.

W niniejszym artykule przedstawione zostaną podstawy tworzenia narzę-

dzi przy pomocy Roslyn. Kolejno omówione będą:

» Proste operacje na drzewie składniowym

» Wykorzystanie zapytań LINQ

» Klasa służąca do analizowania drzewa składniowego

Na samym końcu zbudujemy aplikację wykorzystującą poznane wcześniej
techniki.

DRZEWO SKŁADNIOWE

(SYNTAX TREE)

Omówienie narzędzia Roslyn należy rozpocząć od Syntax API, które umożli-
wia analizę składniową, którą kompilatory używają do zrozumienia języków
C# oraz Visual Basic. Drzewa te są tworzone przez dokładnie ten sam parser,
postaci syntaktycznej badanego kodu, który jest używany przez kompilator
podczas budowania projektu przez programistę. Takie drzewo daje nam peł-
ne odzwierciedlenie postaci syntaktycznej języka, zawiera dyrektywy, słowa
kluczowe, operatory, a nawet spacje. Drzewa tego nie można zmieniać – raz
stworzone nie może zostać już zmodyfikowane

Oto cztery główne klasy składające się na strukturę drzewa składniowego:

» SyntaxTree – reprezentuje całe drzewo składniowe.

» SyntaxNode – reprezentuje wszystkie konstrukcje językowe, takie jak np.

wyrażenia, dyrektywy czy deklaracje.

» SyntaxTrivia – odpowiada w drzewie za reprezentowanie spacji, ko-

mentarzy oraz dyrektyw preprocesora.

» SyntaxToken – jak łatwo się domyślić, klasa ta reprezentuje identyfikato-

ry, operatory oraz słowa kluczowe.

UZYSKANIE INFORMACJI

O WĘZŁACH DRZEWA

Przed przystąpieniem do pracy konieczne jest zainstalowanie Roslyn w projek-
cie. W tym celu w konsoli instalującej pakiety (NuGet) należy wydać polecenie:

Install-Package Roslyn

Po chwili Roslyn powinien zostać zainstalowany w naszym projekcie. Musimy
zaimportować wymagane przestrzenie nazw. Są to kolejno:

using Roslyn.Compilers

using Roslyn.Compilers.CSharp

using Roslyn.Services

W przypadku, kiedy w naszym projekcie używamy języka Visual Basic,

Ros-

lyn.Compilers.CSharp należy zamienić na Roslyn.Compilers.Visu-
alBasic. Musimy jednak pamiętać, że możemy dołączyć tylko jedną z tych
dwóch opcji, w przeciwnym wypadku Roslyn odmówi nam współpracy. Infor-
macje o błędzie, jaki zobaczymy, to:

Error 3 'CompilationUnitSyntax' is an ambiguous reference between

Roslyn.Compilers.CSharp.CompilationUnitSyntax' and 'Roslyn.

Compilers.VisualBasic.CompilationUnitSyntax'

Dla języka Visual Basic importowanie przestrzeni nazw powinno wyglądać tak:

Imports Roslyn.Compilers

Imports Roslyn.Compilers.VisualBasic

Imports Roslyn.Services

Po upewnieniu się, że Roslyn został poprawnie zainstalowany i dołączyliśmy
wszystkie wymagane przestrzenie nazw, pora zacząć pisać nasz długo oczeki-
wany kod. Pierwszy program będzie miał za zadanie wypisać w oknie konsoli
cały swój kod oraz nazwy poszczególnych jego części, np. nazwę przestrzeni
nazw, nazwę klasy, nazwę metody (w tym przypadku będzie to metoda

Main).

Pierwszym krokiem jest utworzenie instancji abstrakcyjnej klasy

Syntax-

Tree oraz skorzystanie ze statycznej metody ParseText, która jako swój ar-
gument przyjmuje kod, który chcemy poddać analizie. Niech będzie to kod
programu, który standardowo generuje się podczas tworzenia projektu apli-
kacji konsolowej. Przykład ten ilustruje Listing 1.

Listing 1. Kod metody

Main umieszczony w zmiennej typu SyntaxTree

SyntaxTree

myTree =

SyntaxTree

.ParseText(

@"

using System;

using System.Collections.Generic;

using System.Linq;

using System.Text;

using System.Threading.Tasks;

Wprowadzenie do

Microsoft Roslyn CTP

Nierzadko zdarza się, że musimy znaleźć w swoim kodzie jakieś zależności, np. sporzą-
dzić listę klas, które napisaliśmy, aby później móc przekazać je innemu programiście.
Takie operacje wymagają od nas skorzystania z analizatora składniowego. Celem
tego artykułu jest przybliżenie developerom projektu Roslyn, opracowanego przez
Microsoft, dzięki któremu możemy tworzyć przydatne narzędzia do analizowania
kodu źródłowego pod kątem występowania przeróżnych wyrażeń czy też zależności.

background image

15

/ www.programistamag.pl /

WPROWADZENIE DO MICROSOFT ROSLYN CTP

namespace Roslyn_Introduction

{

class Program

{

static void Main(string[] args)

{

}

}

}"

);

Dzięki znakowi „@” przed właściwym kodem, możliwe jest stworzenie tzw. do-
słownego literału znakowego.

W tym przykładzie wykorzystano metodę

ParseText, jednakże nierzad-

ko kod, który będzie analizowany, ma o wiele więcej linii niż ten przedstawio-
ny na Listingu 1. W takim przypadku warto skorzystać ze statycznej metody
ParseFile, która jako swój argument przyjmuje ścieżkę do pliku .cs lub .vb,
który ma zostać poddany analizie. Ze względu na małą ilość kodu w tym przy-
kładzie wykorzystano statyczną metodę

ParseText.

Kolejnym krokiem jest uzyskanie węzła głównego drzewa składni. W tym

celu należy zadeklarować zmienną typu

CompilationUnitSyntax, jed-

nakże twórcy Roslyn zalecają skorzystanie ze zmiennej, której to kompilator
wnioskuje, jakiego będzie typu, czyli

var. Poniżej znajdują się wspomniane

sposoby uzyskania drzewa składniowego:

CompilationUnitSyntax

root = myTree.GetRoot();

lub

var

root = myTree.GetRoot();

Węzeł znajduje się teraz w zmiennej

root. Jeśli wypiszemy ją na ekran przy

pomocy

Console.WriteLine(), otrzymamy kompletny kod naszego pro-

gramu, który umieściliśmy w drzewie (pominięte zostaną jedynie dołączone
przestrzenie nazw). Teraz zabezpieczamy naszą aplikację przed zamknięciem
się po kompilacji np. umieszczając na końcu metody

Main następującą linię:

Console

.ReadLine();

i ustawiamy na niej breakpoint, ponieważ za chwilę wykorzystamy debbuger
w celu poznania typów.

ClassDeclarationSyntax

Służy do oznaczenia klasy

NamespaceDeclarationSyntax Służy do oznaczenia przestrzeni nazw
CompilationUnitSyntax

Służy do oznaczenia jednostki kompilacji

Tabela 1. Przykładowe typy wykorzystywane do analizy drzewa

Zanim jednak skompilujemy nasz program, powinniśmy przypisać do ja-
kiejś zmiennej pierwszy element naszego drzewa. Elementami drzewa są po
prostu elementy składni, np. w tym przypadku pierwszym elementem jest
przestrzeń nazw oznaczona w drzewie jako

NamespaceDeclaration, tutaj

o nazwie

Roslyn_Introduction. Zrobić to można w następujący sposób:

var

firstMember =

(

NamespaceDeclarationSyntax

)root.Members[0];

Ta linia kodu nie jest jakoś szczególnie wymagana, jednak w późniejszym
czasie dzięki takiej sztuczce kod wygląda czytelniej, zwłaszcza jeśli chodzi o
analizowanie drzewa w oknie debuggera.

Należy upewnić się, że breakpoint ustawiony jest na odpowiedniej linii

metody

Main, a następnie skompilować projekt. Przejdźmy teraz do okna de-

buggera i wyszukajmy zmienną

firstmember. Rysunek 1 przedstawia okno

debuggera dla naszej aplikacji.

Rysunek 1. Okno debuggera Visual Studio

Powyższy rysunek sugeruje, że pierwszym elementem drzewa jest

Name-

spaceDeclarationSyntax – jest to oznaczenie deklaracji przestrzeni nazw
analizowanego kodu. Roslyn sugeruje nam, że teraz powinniśmy się właśnie
do tego odnieść, aby przygotować takie drzewo, jakie generuje Roslyn.

var

helloWorldDeclaration =

(

NamespaceDeclarationSyntax

)firstMember;

Kolejnym krokiem jest wypisanie na ekran już samego identyfikatora prze-
strzeni nazw.

Console

.WriteLine(

"Namespace name: {0}"

, myNamespaceDeclaration.

Name);

Roslyn udostępnia nam wiele właściwości i metod, którymi możemy ope-
rować na zmiennych naszego drzewa. Przykładowo metoda

GetText()

dostępna dla naszej zmiennej

firstmember da nam taki sam efekt (wypisze

wszystko, co znajduje się w naszej przestrzeni nazw), jakbyśmy wypisali na
ekran cały węzeł, o czym wspomniałem wcześniej.

Skompilujmy kod ponownie. Tym razem w oknie debuggera przeana-

lizujmy zmienną

helloWorldDeclaration, a dokładnie jej właściwość o

nazwie

Members. Warto zauważyć, że jest to jeden element, a jego nazwa to

ClassDeclarationSyntax, co sugeruje, że kolejna nasza zmienna powin-
na zawierać w sobie klasę oraz jej nazwę.

var

classDeclaration = (

ClassDeclarationSyntax

)

namespaceDeclaration.Members[0];

Analogicznie postępujemy i dla tej zmiennej, aż w końcu dojdziemy do końca
drzewa, czyli metody

Main, która w tym przypadku nic w sobie nie zawiera.

Metoda

Main przyjmuje jednak jakiś parametr i jest tutaj jawnie poda-

na jego nazwa, czyli args. Podczas analizowania metody w oknie debuggera
również jest to uwzględnione we właściwości o nazwie

ParameterList. Jeśli

chcemy wypisać na ekranie konsoli nazwę naszego parametru czy też typ, lub
oba naraz, to musimy się jakoś do tego odnieść. Można to zrobić w taki sposób:

var

parameterInMainMethod = (

ParameterSyntax

)

methodDeclaration.ParameterList.Parameters[0];

Wykorzystujemy tutaj dwie właściwości, z czego drugą z nich traktuje-
my jako kolekcję, dlatego też odnosimy się do jej zerowego (pierwsze-
go) elementu. Kolekcja, do której należy właściwość

Parameters, to

SeparatedSyntaxList<TNode>.

Metoda

Main posiada również kod i także możemy zobaczyć to w oknie

debuggera, a konkretnie we właściwości

Body. Widzimy tam SyntaxToken

oznaczający zamknięcie i otwarcie nawiasu klamrowego.

Ostatecznie nasz kod powinien wyglądać tak jak na Listingu 2.

Listing 2. Kompletny kod zapisujący do zmiennych kolejne
elementy drzewa

var

root = myTree.GetRoot();

var

firstMember = (

NamespaceDeclarationSyntax

) root.Members[0];

var

helloWorldDeclaration =

(

NamespaceDeclarationSyntax

)firstMember;

var

classDeclaration = (

ClassDeclarationSyntax

)

namespaceDeclaration.Members[0];

var

methodDeclaration = (

MethodDeclarationSyntax

)

classDeclaration.Members[0];

var

parameterInMainMethod = (

ParameterSyntax

)

methodDeclaration.ParameterList.Parameters[0];

Console

.WriteLine(helloWorldDeclaration.Name);

Console

.WriteLine(classDeclaration.Identifier);

Console

.WriteLine(methodDeclaration.Identifier);

Console

.WriteLine(parameterInMainMethod);

background image

16

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Po skompilowaniu tego na ekranie konsoli powinniśmy zobaczyć naszą

metodę, standardową klasę oraz wypisane nazwy: przestrzeni nazw, samej
klasy, metody oraz nazwę parametru, który przyjmuje args.

LINQ – SZYBSZE POZYSKIWANIE

INFORMACJI Z KODU

Pozyskiwanie informacji z kodu z wykorzystaniem debuggera, co zostało
przedstawione wcześniej, nie jest najlepszym sposobem podczas tworzenia
większych aplikacji. O wiele wygodniej i ładniej można zrobić to, wykorzystu-
jąc zapytania LINQ, dzięki nim w łatwy sposób możemy sobie stworzyć osob-
ne kolekcje na np. nazwy klasy lub metod, które wykorzystujemy w kodzie,
aby w późniejszym czasie mieć do nich szybszy dostęp.

Pierwsze, co należy zrobić, aby wykorzystać kolejny przykład, to dodać do

drzewa kilka przykładowych deklaracji klas, np.:

class Foo {}

class Foo2 {}

class Roslyn{}

class Roslyn_Intro{}

Teraz celem zapytania LINQ będzie odczytanie wymienionych klas z drzewa i
wypisanie ich nazw na ekran konsoli. Stwórzmy sobie zmienną kolekcji, która
będzie przechowywała te nasze klasy, a później przy pomocy zapytania LINQ
umieśćmy je w niej.

var

className =

from

name

in

root.DescendantNodes().

OfType<

ClassDeclarationSyntax

>()

where

name.Identifier.ValueText !=

"Program"

select

name.

Identifier.ValueText;

Zapytanie „szuka” w drzewie potomków, które są typu

ClassDeclara-

tionSyntax, i ich identyfikator różni się od ”Program”, bo jest to klasa
główna, która nas nie interesuje, a następnie zwraca nazwę klasy.

Poniższy fragment kodu prezentuje przykładowy sposób, w jaki możemy

przedstawić na ekranie konsoli listę dodanych wcześniej klas:

foreach

(

var

c

in

className)

Console

.WriteLine(

"Class name: {0}"

, c);

Powyższy kod powinien dać następujący rezultat:

Class name: Foo

Class name: Foo2

Class name: Roslyn

Class name: Roslyn_Intro

Zapytanie LINQ, które przedstawione zostało wyżej, jest jednym z prost-
szych, jednak można też nieco skomplikować i zacząć „wymagać” od Roslyn
szukania interesujących niuansów, np. wszystkich prywatnych pól w klasie.
Przykładowo:

var

privateProperties =

from

property

in

root.DescendantNodes().

OfType<

PropertyDeclarationSyntax

>()

where

property.Modifiers.

ToString().Contains(

"private"

)

select

property.Identifier;

Dzięki takiemu zapytaniu na ekranie konsoli pojawi się lista identyfikatorów
(nazw) wszystkich prywatnych właściwości klasy.

To już mogłoby być w zasadzie wszystko, co powinniśmy wiedzieć o anali-

zowaniu składni przy pomocy Roslyn, jednak aby nasza wiedza była napraw-
dę kompletna i żebyśmy mogli tworzyć poważniejsze narzędzia do analizo-
wania naszych projektów, należy jeszcze wspomnieć o klasie

SyntaxWalker,

która jest dostępna w Roslyn, i służy stricte do (jak sugeruje nawet jej nazwa)
„spacerowania” po drzewie, czyli, jak można się łatwo domyśleć, do jego ana-
lizowania pod kątem występowania (jak się zaraz przekonamy) wielu innych
elementów w naszym kodzie.

WPROWADZENIE DO KLASY

SYNTAXWALKER

Klasa

SyntaxWalker umożliwia bardzo wygodne tworzenie typów, któ-

re potem można wykorzystać do wyciągania kodu potrzebnych informacji.
Przykładowo mamy w programie klasę

„Osoba”, która ma właściwości: imię,

nazwisko, adres zamieszkania oraz imię zwierzaka domowego. Naszym za-
daniem jest sporządzić raport, jakie właściwości wykorzystujemy podczas
korzystania z naszej klasy, aby później w czasie optymalizacji naszego kodu
pozbyć się tych właściwości, których nigdzie nie wykorzystujemy. Możemy
wtedy zbudować klasę i przeciążyć jedną z metod SyntaxWalker’a, aby spo-
rządzić listę wszystkich właściwości z klasy „

Osoba”.

To tylko jedno z wielu zastosowań klasy

SyntaxWalker. W celu uzyskania

efektu, który opisano powyżej, można zastosować przeciążoną metodę

Vis-

itPropertyDeclaration i, tak jak zostało wspomniane, jest to jedna z wielu,
ponieważ w deklaracji SyntaxWalker’a możemy znaleźć ich całe mnóstwo, nawet
takie, które pomogą nam w odszukaniu w kodzie wystąpień wyrażeń lambda.

Wybrane metody klasy

SyntaxWaler zostały przedstawione w Tabeli 2.

VisitConstructorDeclaration

Wyszukuje w klasie wszystkie
wystąpienia konstruktora

VisitMetodDeclaration

Wyszukuje w klasie wszystkie
wystąpienia metod

VisitPropertyDeclaration

Wyszukuje w klasie wszystkie
wystąpienia właściwości

VisitUsingDirective

Pozwala nam wyszukać wszystkie
używane w kodzie przestrzenie
nazw zdefiniowane przy pomocy
dyrektywy using

VisitVariableDeclaration

Odnajduje zmienne występujące w
naszej klasie

VisitParenthesizedLambdaEx-

pression

Odnajduje wyrażenia lambda, które
posiadają parametr

Tabela 2. Wybrane metody klasy SyntaxWalker oraz ich przeznaczenie

Metody zebrane w tej tabeli to naturalnie tylko część możliwości oferowa-
nych przez Roslyn. Resztę możemy zobaczyć, podglądając deklarację klasy
SyntaxVisitor, po której dziedziczy SyntaxWalker. Warto tutaj wspo-
mnieć, że w sieci nie znajdziemy kompletnej dokumentacji Roslyn, tak więc
podgląd metod w Visual Studio jest tutaj jedynym rozsądnym wyjściem.

My napiszemy teraz kod, który wyszuka w naszym programie wszystkie wy-

rażenia lamda, które posiadają przynajmniej jeden parametr, oraz określi język,
w jakim zostały zaimplementowane, a także jak nazywa się element składni,
dzięki któremu zostały uzyskane. W tym celu wykorzystamy klasę

SyntaxWalk-

er oraz przeciążoną metodę VisitParenthesizedLambdaExpression.

W celu skorzystania z metod udostępnianych przez

SyntaxVisitor

musimy stworzyć klasę dziedziczącą po

SyntaxWalker, a następnie przecią-

żyć metodę

VisitParentheizedLamdaExpression.

Kod naszej klasy przedstawia Listing 3.

Listing 3. Przykładowa klasa implementująca przeciążoną wersję
metody ParenthesizedLamdaExpression

class

CustomRefactor

:

SyntaxWalker

{

public

readonly

List

<

ParenthesizedLambdaExpressionSyntax

> lambdas =

new

List

<

ParenthesizedLambdaExpressionSyntax

>();

public

override

void

VisitParenthesizedLambdaExpression

(

ParenthesizedLambdaExpressionSyntax

node)

{

lambdas.Add(node);

base

.VisitParenthesizedLambdaExpression(node);

}

}

background image

17

/ www.programistamag.pl /

WPROWADZENIE DO MICROSOFT ROSLYN CTP

Jak widać – na samym początku stworzono listę, która jako swój parametr
generyczny przyjmuje interesujący nas typ. Następnie przeciążamy metodę
i każde wystąpienie interesującego nas wyrażenia lambda zapisujemy do
wcześniej utworzonej listy. Przeciążona metoda działa rekurencyjnie.

W tym momencie warto sobie zadań pytanie: w jaki sposób tę klasę wy-

korzystać? Albo jeszcze lepiej: skąd wziąć typ parametru, którego wymaga
przeciążona metoda?

Odpowiedź jest prosta – Roslyn samo za nas znajdzie odpowiednią i wy-

korzysta ją w sposób, w jaki ją zaimplementowaliśmy (przeciążając ją), co w
tym przypadku oznacza zapisanie wszystkich występujących w kodzie wyra-
żeń lambda (za parametrem) do generycznej listy. Wykorzystamy w tym celu
metodę

Visit, której w tym przypadku przeciążać nie trzeba.

Interesujące nas drzewo możemy teraz pozyskać w taki sposób:

SyntaxTree

myTree =

SyntaxTree

.ParseText(

@" List<int> numbers = new List<int>();

numbers.Add(1);

numbers.Add(2);

numbers.Add(4);

numbers.Add(3);

numbers.Add(7);

numbers.Add(8);

var evenNumbers = numbers.FindAll((int i) => i % 2 ==

0).ToList();"

);

var

root = myTree.GetRoot();

Tym razem w naszym drzewie tworzymy prostą listę, która przechowuje kilka
liczb typu całkowitego. Przy pomocy delegata

Predicate<T> oraz wyraże-

nia lambda filtrujemy z listy liczby parzyste i umieszczamy je w nowej liście.
Kolejno, korzystając z pętli

foreach, lub jakiejkolwiek innej, możemy wypi-

sać wynik na ekran.

Teraz przyszła pora na wykorzystanie naszej klasy służącej do „spacerowa-

nia” po drzewie.

Jej przykładowe użycie przedstawia poniższy fragment kodu:

CustomRefactor

customRefactor =

new

CustomRefactor

();

customRefactor.Visit(root);

foreach

(

var

lambda

in

customRefactor.lamdas)

Console

.WriteLine(lambda.GetText());

Console

.WriteLine(

"Language: {0}"

, customRefactor.lamdas.

First().Language);

Console

.WriteLine(

"Kind: {0}"

, customRefactor.lambdas.First().

Kind);

Jak zostało wspomniane wcześniej, należy wykorzystać metodę

Visit, która

automatycznie wybierze potrzebną metodę i zwróci oczekiwany wynik. Na
ekranie konsoli pojawi się nasze wyrażenie lambda w pełnej postaci.

(int i) => i % 2 == 0

Language: C#

Kind: ParenthesizedLambdaExpression

Dodatkowo wyświetlony zostaje język, w jakim wyrażenie zostało napisane,
oraz węzeł, który za takie wyrażenie odpowiada.

APLIKACJA WYKORZYSTUJĄCA

ROSLYN

Aby podsumować wszystko, czego się dowiedzieliśmy, napiszemy teraz pro-
stą aplikację Windows Forms wykorzystującą Roslyn i poznane wcześniej w
artykule metody uzyskiwania informacji o kodzie źródłowym.

Zacznijmy od utworzenia projektu i zainstalowania pakietu Roslyn, co

było opisane wcześniej.

Stwórzmy teraz aplikację Windows Forms, o formularzu przedstawionym

na Rysunku 2.

Aplikacja przedstawiona na tym rysunku będzie miała za zadanie wczytać

plik z kodem C# lub VB, przeanalizować go, a następnie wedle życzenia użyt-
kownika wypisać któryś z elementów programu.

Rysunek 2. Okienko aplikacji wykorzystującej Roslyn

Najpierw implementujemy metodę do wczytywania pliku, następnie spraw-
dzamy, czy jest to plik .cs lub .vb, a następnie tworzymy drzewo i przystępu-
jemy do analizy. Nasz prosty analizator będzie umożliwiał wyszukanie w pliku
klas, zdarzeń, właściwości, pól oraz metod. Warto zwrócić tutaj uwagę, że do
wczytania kodu użyliśmy tym razem metody

ParseFile, o której wspomnia-

no wcześniej. Dodatkowo aplikacja posiada możliwość analizy kodu napisa-
nego w Visual Basic, co jest widoczne podczas sprawdzania rozszerzenia pliku.

Kod implementujący zdarzenia odpowiednich kontrolek:

Listing 4. Kod obsługi zdarzeń głównego okna aplikacji

using

Roslyn.Compilers;

using

Roslyn.Compilers.CSharp;

using

Roslyn.Services;

namespace

RoslynForms

{

public

partial

class

RoslynApp

:

Form

{

private

string

_filePath;

private

SyntaxTree

_tree;

private

CompilationUnitSyntax

_root;

private

Analyser

_syntaxAnalyser;

public

RoslynApp()

{

InitializeComponent();

}

private

void

openFileButton_Click(

object

sender,

EventArgs

e)

{

openClassFile.ShowDialog();

fileName.Text = System.IO.

Path

.GetFileName(openClassFile.

FileName);

this

._filePath = openClassFile.FileName;

//ścieżka do

otwartego pliku

if

(fileName.Text.Contains(

".cs"

) || fileName.Text.

Contains(

".vb"

))

{

this

._tree =

SyntaxTree

.ParseFile(

this

._filePath);

_root = _tree.GetRoot();

analyseButton.Enabled =

true

;

}

else

{

MessageBox

.Show(

"To nie jest plik .cs lub .vb"

);

}

}

private

void

analyseButton_Click(

object

sender,

EventArgs

e)

{

_syntaxAnalyser =

new

Analyser

();

_syntaxAnalyser.Visit(_root);

MessageBox

.Show(

"Przeanalizowano."

);

}

private

void

showButton_Click(

object

sender,

EventArgs

e)

{

resultBox.Clear();

if

(radioClass.Checked)

foreach

(

var

f

in

_syntaxAnalyser.fileClass)

resultBox.Text += f + System.

Environment

.NewLine;

//sprawdzanie, czy zaznaczony jest inny radiobutton. Kod dostepny

jest na stronie magazynu

else

if

(radioProperties.Checked)

foreach

(

var

prop

in

_syntaxAnalyser.properties)

resultBox.Text += prop + System.

Environment

.NewLine;

}

}

}

background image

18

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Kolejnym krokiem jest zaimplementownie klasy, która będzie analizować

wybrany plik z kodem C# lub VB. W tym celu najpierw tworzymy kolekcje,
które będą przechowywać interesujące nas elementy składni. Kolejno w kon-
struktorze inicjujemy je, a potem przeciążamy odpowiednie metody klasy
SyntaxWalker, dostosowując je do potrzeb naszej aplikacji.

Kompletną klasę

Analyser przedstawia Listing 5.

Implemetnację klasy zaczniemy od stworzenia sześciu kolekcji generycz-

nych (list), które będą przechowywać poszczególne fragmenty analizowane-
go kodu. Kolejno w konstruktorze następuje utworzenie instancji kolekcji.

Następnie wystarczy już przeciążyć wybrane metody odziedziczone po

klasie

SyntaxWalker i przy ich pomocy dodać elementy kodu do odpowied-

nich, stworzonych wcześniej kolekcji.

Listing 5. Klasa Analyser

using

Roslyn.Compilers;

using

Roslyn.Compilers.CSharp;

using

Roslyn.Services;

namespace

RoslynForms

{

class

Analyser

:

SyntaxWalker

{

public

List

<

string

> fileClass {

get

;

set

; }

public

List

<

string

> methods {

get

;

set

; }

public

List

<

string

> properties {

get

;

set

; }

public

List

<

string

> privateFileds {

get

;

set

; }

public

List

<

string

> publicFields {

get

;

set

; }

public

List

<

string

> events {

get

;

set

; }

public

Analyser()

{

fileClass =

new

List

<

string

>();

methods =

new

List

<

string

>();

properties =

new

List

<

string

>();

privateFileds =

new

List

<

string

>();

publicFields =

new

List

<

string

>();

events =

new

List

<

string

>();

}

public

override

void

VisitClassDeclaration(

ClassDeclarationSyntax

node)

{

fileClass.Add(node.Identifier.ToString());

base

.VisitClassDeclaration(node);

}

public

override

void

VisitMethodDeclaration(

MethodDeclarationSyntax

node)

{

methods.Add(node.Identifier.ToString());

base

.VisitMethodDeclaration(node);

}

public

override

void

VisitPropertyDeclaration(

PropertyDeclarationSyntax

node)

{

properties.Add(node.Identifier.ToString());

base

.VisitPropertyDeclaration(node);

}

public

override

void

VisitFieldDeclaration(

FieldDeclarationSyntax

node)

{

if

(node.Modifiers.ToString().Contains(

"private"

))

privateFileds.Add(node.ToString());

else

if

(node.Modifiers.ToString().Contains(

"public"

))

publicFields.Add(node.ToString());

base

.VisitFieldDeclaration(node);

}

public

override

void

VisitEventDeclaration(

EventDeclarationSyntax

node)

{

events.Add(node.Identifier.ToString());

base

.VisitEventDeclaration(node);

}

}

}

Rysunek 3 przedstawia przykładowe wyniki wygenerowane przez aplika-

cję do analizy pliku z kodem VB.

Rysunek 3. Działanie aplikacji do analizy pliku

PODSUMOWANIE

Artykuł rozpoczęliśmy od omówienia manualnej wędrówki po drzewie skła-
dniowym, następnie pokazane zostało, w jaki sposób zbudowane jest drzewo
z poziomu debuggera wbudowanego w Visual Studio, dzięki czemu łatwiej
nam było pozyskiwać kolejne jego elementy i operować na nich. Dowiedzie-
liśmy się również, że możemy pisać zapytania LINQ, aby szybciej operować na
drzewie. Ostatecznie zapoznaliśmy się z klasą

SyntaxWalker, która została

stworzona w celu ułatwienia nam operacji na drzewie w taki sposób, aby-
śmy nie musieli robić tego manualnie, a jedynie przy pomocy odpowiednio
przeładowanych metod, i stworzyliśmy kompletną aplikację wykorzystującą
poznane metody.

Wszystko, co zostało tutaj opisane, to zaledwie ułamek możliwości, które

daje nam Roslyn. Trudno powiedzieć, czy projekt ten będzie rozwijany dalej,
ale biorąc pod uwagę, że wydanie ostatniej wersji nastąpiło w czerwcu 2012
r., jest to wątpliwe. Jakiś czas temu pojawiły się co prawda zapowiedzi, że
narzędzie to ma być wbudowane w Visual Studio, ale czy jest to pewne? Czas
pokaże – na dzień dzisiejszy nie ma na ten temat żadnych informacji.

ŹRÓDŁA I DODATKOWE

INFORMACJE:

W artykule wykorzystano opis analizy syntaktycznej przy pomocy Roslyn
dostępny na stronie:

http://msdn.microsoft.com/en-us/vstudio/roslyn.aspx

.

Powyższy odnośnik zawiera również dodatkowe informacje o możliwościach
opisywanego narzędzia, takie jak analiza semantyczna kodu i tworzenie
skryptów oraz stanowi jedyną pewnego rodzaju dokumentację. Na dzień dzi-
siejszy Roslyn współpracuje poprawnie z Visual Studio 2010, 2012 oraz 2013.

Aleksander Kania

kania.aleksander@gmail.com

Autor jest młodym, ciągle doskonalącym swoje umiejętności programistą, w wolnym czasie
lubi napisać ciekawy program lub artykuł. Odbył 3-miesięczny staż jako programista w ra-
mach nauki w technikum. Uczestnik licznych konferencji o tematyce IT oraz jeden z członków
koła naukowego Politechniki Śląskiej (SKN IPIJ).

background image

Samsung_Knox_TAB_210x297Programista.indd 1

3/24/14 3:00 PM

background image

20

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Wojciech Sura

STYLOWANIE ELEMENTÓW LIST

Myślę, że kontrolki prezentujące dane można podzielić na trzy główne kategorie.
Do pierwszej z nich zaliczymy te, które wyświetlają jednolite dane – na przykład
przycisk, pole wyboru albo pole tekstowe. Druga kategoria obejmuje kontrolki,
które prezentują użytkownikowi dane w postaci listy lub tablicy (na przykład
pole listy), w trzeciej z kolei znajdą się te, które wyświetlają dane hierarchiczne
(na przykład drzewo). Wszystkie trzy kategorie podlegają temu samemu me-
chanizmowi pozwalającemu przeprojektować je przy pomocy szablonu (

Con-

trolTemplate), ale w przypadku dwóch ostatnich często nie ma konieczności
ingerowania w wygląd kontrolki aż w takim stopniu: mamy bowiem możliwość
przygotowania odpowiednich szablonów dla samych elementów.

Najwygodniej będzie pracować na jakichś konkretnych danych. Przypuść-

my na przykład, że chcemy wyświetlić informacje o okolicznych atrakcjach tury-
stycznych. Model (w kontekście MVVM) może wówczas wyglądać następująco.

Listing 1. Model

public

class

TouristAttraction

{

public

override

string

ToString()

{

return

Name;

}

public

string

Name {

get

;

set

; }

public

string

Address {

get

;

set

; }

public

BitmapImage

Photo {

get

;

set

; }

}

public

class

TouristAttractions

:

List

<

TouristAttraction

>

{

public

TouristAttractions()

:

base

()

{

}

}

Zgodnie z ideą MVVM, musimy przygotować również viewmodel, przy po-
mocy którego będziemy udostępniać model widokowi. Nie będzie to nic
skomplikowanego:

Listing 2. Viewmodel

public

class

MainWindowViewModel

{

private

TouristAttractions

model;

public

MainWindowViewModel(

TouristAttractions

model)

{

if

(model ==

null

)

throw

new

ArgumentNullException

(

"model"

);

this

.model = model;

}

public

IEnumerable

<

TouristAttraction

> Attractions

{

get

{

return

model;

}

}

}

Konieczny będzie również kod, który przygotuje okno do pracy:

Listing 3. Widok

private

MainWindowViewModel

model;

public

MainWindow()

{

InitializeComponent();

TouristAttractions

attractions =

new

TouristAttractions

()

{

// Tu inicjujemy przykładowe dane

};

model =

new

MainWindowViewModel

(attractions);

DataContext = model;

}

Tyle kodu w C# wystarczy, byśmy mogli zacząć pracować nad wyświetlaniem
danych. Wstawmy teraz nasze dane do kontrolki

ListBox i zobaczmy, jak

będą wyglądały. Co tu dużo mówić, nic specjalnego.

Listing 4. Wyświetlenie danych w polu listy

<

ListBox

ItemsSource

="{

Binding

Attractions

}"

Margin

="4" />

Rysunek 1. Dane wyświetlone wewnątrz listy

Wstęp do WPF – część 3:

Stylowania kontrolek ciąg dalszy

Po przeczytaniu poprzednich części artykułu możemy mieć już pewne wyobrażenie
na temat tego, jak bardzo elastycznie można modelować interface użytkownika w
WPFie. Wiemy też, że przy pomocy szablonów (ControlTemplate) mamy możliwość
przeprojektowania wyglądu kontrolki praktycznie od zera. Idźmy dalej! Ciekawe,
jak daleko leżą granice możliwości tego frameworka...

background image

21

/ www.programistamag.pl /

WSTĘP DO WPF – CZĘŚĆ 3: STYLOWANIA KONTROLEK CIĄG DALSZY

ITEMTEMPLATE

Najprostszym sposobem przygotowania stylu dla elementów listy bę-
dzie skorzystanie z jej własności

ItemTemplate: własność ta pozwala

bowiem ustalić szablon, który zostanie później zastosowany dla każdego
elementu. W odróżnieniu od szablonów

ControlTemplate, którymi mo-

żemy opisywać kontrolki, do opisania elementów listy wykorzystamy klasę
DataTemplate.

Pierwszym krokiem w procesie przygotowywania szablonu będzie po-

informowanie WPFa, jakiego typu dane są wyświetlane. Możemy to zrobić,
wypełniając własność

DataType klasy DataTemplate. Musimy jednak

wcześniej nauczyć się, w jaki sposób można wprowadzić w XAMLu zaimple-
mentowany przez nas typ.

Po pierwsze, definiujemy osobną przestrzeń nazw XML, której przypo-

rządkujemy namespace, zawierający poszukiwany przez nas typ. Wygląda to
następująco:

Listing 5. Definiowanie skrótu do przestrzeni nazw

<

Window

x

:

Class

="StylingLists.MainWindow"

xmlns

="http://schemas.microsoft.com/winfx/2006/xaml/

presentation"

xmlns

:

x

="http://schemas.microsoft.com/winfx/2006/xaml"

xmlns

:

local

="clr-namespace:StylingLists"

Title

="MainWindow"

Height

="250"

Width

="320">

Nazwa po prefiksie

xmlns: jest dowolna; może to być pojedyncza litera

lub słowo. Zauważyłem, że bardzo często zdarza się, że „lokalny” namespa-
ce – czyli ten, w którym znajduje się klasa reprezentująca okno, nazywa się
właśnie prefiksem

local:. Od momentu, w którym dodamy powyższą de-

klarację przestrzeni nazw XML, wszystko, co poprzedzimy prefiksem

local:,

będzie odnosiło się do typów zdefiniowanych w namespace

StylingLists.

Po zdefiniowaniu prefiksu skorzystamy teraz z rozszerzonej składni XAML,

aby wskazać właściwy typ:

Listing 6. Określenie typu szablonu

<

ListBox.ItemTemplate

>

<

DataTemplate

DataType

="{

x

:

Type

local

:

TouristAttraction

}

">

(...)

Warto na marginesie wspomnieć, że w projektowaniu GUI bardzo aktywnie
wspiera nas IDE – choćby w ten sposób, że podpowiada dostępne klasy, które
znajdują się we wskazanym namespace.

Rysunek 2. CodeInsight dla XAML

Po określeniu typu elementu, który będzie wyświetlany przy pomocy szablo-
nu, możemy już zaprojektować jego wygląd. W trakcie tego procesu będzie-
my musieli oczywiście wskazać WPFowi, w które miejsca ma wstawić dane z
elementu, ale tu z pomocą przychodzi nam mechanizm bindingów (poznali-
śmy go w pierwszej części artykułu). Okazuje się bowiem, że w trakcie genero-
wania zawartości listy, każdej kopii szablonu WPF przekazuje reprezentowane
przez nią dane poprzez jej

DataContext. Innymi słowy: żeby dostać się do

dowolnej własności stylowanych danych, wystarczy po prostu napisać krótki
binding. W praktyce może to wyglądać tak (pogrubiona czcionka wskazuje na
miejsca dostępu do danych):

Listing 7. Szablon dla elementu

<

ListBox

ItemsSource

="{

Binding

Attractions

}"

Margin

="4">

<

ListBox.ItemTemplate

>

<

DataTemplate

DataType

="{

x

:

Type

local

:

TouristAttraction

}">

<

DockPanel

>

<

Image

DockPanel.Dock

="Left"

Height

="64"

Margin

="3"

Source

="{

Binding

Photo

}

" />

<

Label

DockPanel.Dock

="Bottom"

Content

="{

Binding

Address

}

"

Foreground

="Gray"/>

<

Label

Content

="{

Binding

Name

}

"

FontSize

="16" />

</

DockPanel

>

</

DataTemplate

>

</

ListBox.ItemTemplate

>

</

ListBox

>

Jeśli poinformujemy wcześniej przy pomocy własności

DataType, jaki typ

opisujemy, środowisko i tym razem pomoże nam wybrać właściwe własności.

Rysunek 3. CodeInsight dla XAML

Po uruchomieniu programu nasza lista będzie wyglądała zupełnie inaczej.

Rysunek 4. Lista z szablonem elementów

NIEJEDNOLITE DANE

A co jeśli na liście znajdują się elementy różnych typów? Czy i wówczas istnie-
je możliwość przygotowania dla nich szablonów?

Przypuśćmy na przykład, że chcemy rozszerzyć klasę prezentującą atrak-

cje turystyczne:

background image

22

/ 3

. 2014 . (22) /

BIBLIOTEKI I NARZĘDZIA

Listing 8. Rozszerzenie klasy TouristAttraction

public

class

DetailedTouristAttraction

:

TouristAttraction

{

public

double

Rating {

get

;

set

; }

}

Umieśćmy teraz instancję takiego typu na naszej liście.

ListBox prawidłowo

wyświetli ją pośród innych, ale zabraknie dodatkowej informacji, którą chcie-
libyśmy pokazać użytkownikowi.

Rysunek 5. Dodatkowy element

W tym przypadku nie mamy jak skorzystać z

ItemTemplate, ponieważ wła-

sność ta pozwala na zdefiniowanie szablonu dla elementów jednego konkret-
nego typu. Istnieje jednak sposób na to, by przygotować takich szablonów
więcej: w tym celu wystarczy umieścić je w zasobach

ListBoxa. Podczas

wyświetlania elementów listy WPF odnajdzie je i wykorzysta do wizualizacji
danych. Nasz szablon będzie teraz wyglądał tak (pogrubiona czcionka wska-
zuje zmiany pomiędzy oboma

DataTemplate'ami):

Listing 9. Szablony dla różnych typów

<

ListBox

ItemsSource

="{

Binding

Attractions

}"

Margin

="4">

<

ListBox.Resources

>

<

DataTemplate

DataType

="{

x

:

Type

local

:

TouristAttraction

}">

<

DockPanel

>

<

Image

DockPanel.Dock

="Left"

Height

="64"

Margin

="3"

Source

="{

Binding

Photo

}" />

<

Label

DockPanel.Dock

="Bottom"

Content

="{

Binding

Address

}"

Foreground

="Gray"/>

<

Label

Content

="{

Binding

Name

}"

FontSize

="16" />

</

DockPanel

>

</

DataTemplate

>

<

DataTemplate

DataType

="{

x

:

Type

local

:

DetailedTouristAttraction

}">

<

DockPanel

>

<

Image

DockPanel.Dock

="Left"

Height

="64"

Margin

="3"

Source

="{

Binding

Photo

}" />

<

Label

DockPanel.Dock

="Bottom"

Content

="{

Binding

Address

}"

Foreground

="Gray"/>

<

ProgressBar

Minimum

="0"

Maximum

="100"

Value

="{

Binding

Rating

}"

Width

="200"

Height

="12"

HorizontalAlignment

="Left"

Margin

="4"

DockPanel.

Dock

="Bottom" />

<

Label

Content

="{

Binding

Name

}"

FontSize

="16" />

</

DockPanel

>

</

DataTemplate

>

</

ListBox.Resources

>

</

ListBox

>

Teraz po uruchomieniu aplikacji elementom zostaną nadane szablony zależ-
nie od ich typów.

Rysunek 6. Osobne szablony dla różnych typów elementów

PODSUMOWANIE

Co tu dużo mówić, w kwestii modelowania wyglądu aplikacji WPF Micro-
soft odwalił kawał dobrej roboty. Opisany przeze mnie mechanizm ma kilka
wielkich zalet: daje nam praktycznie całkowitą dowolność w projektowaniu
elementów listy, oszczędzając jednocześnie ogromnej ilości pracy: łatwo
bowiem wyobrazić sobie, ile trzeba byłoby się narobić, aby podobny efekt
uzyskać w większości popularnych frameworków graficznych. A trzeba mieć
cały czas świadomość, że do tej pory omówiłem tylko niewielką część me-
chanizmów, z których może skorzystać programista. Mam nadzieję, że mój
artykuł zainspiruje do zabawy w projektowanie interface'u użytkownika przy
pomocy frameworka Windows Presentation Foundation.

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
background image

24

/ 3

. 2014 . (22) /

PROGRAMOWANIE APLIKACJI WEBOWYCH

Piotr Tołłoczko

WSTĘP

W praktyce każdy początkujący programista, który pisze dynamiczny kod dla
stron internetowych, gdzie wymagane jest dynamiczne wyświetlanie kodu
HTML po stronie klienta, łączy kod JavaScript z html'em. W efekcie kod robi się
coraz bardziej rozbudowany, nieczytelny, a w konsekwencji trudny do póź-
niejszej edycji. Poniżej można zobaczyć przykład takiego kodu.

Listing 1. Przykładowy kod JavaScript + HTML

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script

>

$

(

function

() {

var uzytkownicy

= [{

imie

:

'Adam'

,

nazwisko

:

'Kozłowski'

},

{

imie

:

'Jan'

,

nazwisko

:

'Kowalski'

}];

$

.

each

(

uzytkownicy

.

reverse

(),

function

(

index

,

uzytkownik

) {

$

(

'#lista_uzytkownikow'

).

append

(

'<h1>'

+

uzytkownik

.

imie

+

'</h1>

\n

\

<br><h2>'

+

uzytkownik

.

nazwisko

+

'</h2><br><br>'

);

});

});

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_uzytkownikow"

></

div

>

</

body

>

</

html

>

W takim przypadku przychodzi nam z pomocą system szablonów, za po-
mocą których jesteśmy w stanie oddzielić logikę od widoku aplikacji. W
codziennej pracy przy większych projektach używanie systemu szablonów
jest wręcz niezbędne w przypadku dynamicznie zmieniającego się kodu. W
artykule chciałbym opisać jeden z takich systemów o nazwie „mustache”,
który działa po stronie klienta, a co za tym idzie cały proces renderowania
kodu przejmuje przeglądarka klienta, odciążając nasz serwer. System, który
będę opisywać, z powodzeniem jest używany przez takie firmy jak Apple
czy WorkInField. W artykule nie będę zajmował się przygotowaniem środo-
wiska pracy, wypiszę tylko podstawowe elementy, jakie będą potrzebne do
uruchomienia poniższych kodów. Wychodzę z założenia, że użytkownik na
tym etapie posiada już podstawową wiedzę na temat środowiska apache i
jego konfiguracji, ewentualnie używa gotowych systemów jak np. xampp,
gdzie całe środowisko jest już skonfigurowane i przygotowane do pracy po
instalacji oprogramowania.

CZYM JEST MUSTACHE

Mustache to tzw. „logic-less template system”, czyli system, który nie posiada
żadnych instrukcji typu (for,else,if,...). System zbudowany jest w całości z tzw.

„tagów”. Mustache można obecnie zastosować w różnych językach progra-
mowania np. JavaScript, Python, PHP, Objective-C, Andorid, Lua i wielu in-
nych. Dostępne kompilacje można znaleźć na stronie projektu, która znajduje
się pod adresem

http://mustache.github.io/

.

CO BĘDZIE NAM POTRZEBNE

» dowolny edytor, np. NetBeans

» biblioteka mustache.js –

https://github.com/janl/mustache.js/

» server www, np. xampp

OPIS PODSTAWOWYCH TAGÓW:

» {{name}} – wyświetla wartość z pola „name”, jeżeli nie będzie takiego

pola, nic nie zostanie wyświetlone ( wszystkie tagi html'owe są escape'o-
wane, czyli np. znak „<” zostanie zamieniony na „&lt;”)

» {{{name}}},{{*name}} – to samo co wyżej, tylko że tekst HTML jest

typu „unescape”

» {{#cars}}some text{{/cars}} – tzw. sekcja rozpoczyna się „#”, a

kończy „/”, sekcja to blok tekstu, który może się wykonać 0,1 lub więcej
razy, zależy to od wartości pola „cars”. Jeżeli wartością pola „cars” będzie
null, undefined , false, 0, NaN lub pole nie będzie zdefiniowane, to blok
„some text” nie wykona się ani razu.

» {{^cars}}come text{{/cars}} – odwrotność powyższej funkcji, czyli

blok jest wyświetlany tylko w przypadku, kiedy „cars” jest typu

null, un-

defined, false lub posiada pustą listę „[]”

» {{! coments}} – komentarz, który zostaje zignorowany przy tworzeniu

zawartości

» {{> partial}} – częściowy wydzielony kod HTML

ROZPOCZYNAMY

Na początek stworzymy przykładowy kod pokazujący użycie funkcji

to_html.

Uruchamiamy nasz ulubiony edytor, np. NetBeans, i wklejamy kod z Listingu
2. Wszystkie projekty, jakie przedstawię w artykule, potrzebują dodatkowo
pluginu jQuery pobieranego bezpośrednio ze strony

code.jquery.com. W

pierwszym kroku wczytujemy pliki jQuery i mustache.js(1), następnie możemy
rozpocząć projektowanie naszej aplikacji.W kolejnym kroku tworzymy obiekt
typu „json” o nazwie „gra” reprezentujący pojedynczą grę typu Battlefield 4
(2). Obiekt ten składa się z dwóch zmiennych:

nazwa i opis. Wszystkie przy-

kłady zawierają z góry zdefiniowane obiekty, ale kluczem w zaawansowanych
projektach jest pobranie danych z serwera przy użyciu funkcji np. z biblioteki
jQuery $.ajax lub $.getJSON. Następnie będzie utworzony nasz szablon (3), w
miejsca, gdzie mają trafić wartości z obiektu „gra”, wstawiamy odpowiednio
tagi

{{nazwa}} i {{opis}}. Kolejny krok to użycie funkcji to_html, która

przyjmuje dwa parametry. W pierwszym parametrze wstawiamy przygo-
towany szablon, w drugim należy podać obiekt, który będzie przetwarzany
przez ten szablon (4). W wyniku wykonania funkcji elementem zwrotnym jest

Mustache

– czyli szablony w JavaScript

Mustache to prosty i wydajny system szablonów, który z powodzeniem można
zastosować w kodzie JavaScript w celu oddzielenia logiki od widoku aplikacji.
Obecnie każda nowa strona WWW nie może obejść się bez stosowania JavaScript'u
w celu dynamicznej prezentacji kodu HTML. System z powodzeniem można używać
w urządzeniach mobilnych.

background image
background image

26

/ 3

. 2014 . (22) /

PROGRAMOWANIE APLIKACJI WEBOWYCH

kod HTML, który w kolejnej linii przekazujemy do funkcji

jQuery $.html()

w celu wyświetlenia na stronie WWW(5).

Listing 2. Przykładowy kod użycia funkcji Mustache.to_html()

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>//{

1

}

<

script

>

$

(

function

() {

var gra

= {

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta odsłona

serii strzelanek pierwszoosobowych'

};//{

2

}

var template

=

"

<

h1

>{{

nazwa

}}</

h1

><

br

><

h2

>{{

opis

}}</

h2

><

br

><

br

>

"

;//{

3

}

var html

=

Mustache

.

to_html

(

template

,

gra

);//{

4

}

$

(

'#lista_gier'

).

html

(

html

);//{

5

}

});

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

Jest wiele możliwości deklarowania szablonów przy użyciu mustache, me-
tody te podobne są do deklarowania plików css czy javascript. W kolejnym
naszym przykładzie przerobimy kod z Listingu 2, przenosząc nasz kod HTML
pomiędzy tagi

<script> i umieszczając gdzieś w dokumencie HTML. W

celu zabezpieczenia przeglądarki przed niepożądanym wykonaniem skryp-
tu musimy zmienić typ MIME np. na

"text/template" (3). W dokumen-

cie możemy stworzyć dowolną ilość szablonów, lecz należy pamiętać, aby
każdy szablon miał unikalne „id”. Jak wcześniej, rozpoczynamy od zadekla-
rowania obiektu gra (1). Następnie wczytujemy za pomocą funkcji

jQuery

$.html() (2) zawartość kodu znajdującą się pomiędzy elementami <script
id="gameTemplate" type="text/template"></script> (3), a resztę
kodu pozostawiamy bez zmian. Zastosowanie takiego rozwiązania daje nam
możliwość oddzielenia szablonu od logiki skryptu, oraz pracy bezpośrednio
na czystym kodzie HTML z elementami „tagów”.

Listing 3. Przykład wydzielenia szablonu

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>

<

script

>

$

(

function

() {

var gra

= {

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta odsłona

serii strzelanek pierwszoosobowych'

};//{

1

}

var template

=

$

(

'#gameTemplate'

).

html

();//{

2

}

var html

=

Mustache

.

to_html

(

template

,

gra

);

$

(

'#lista_gier'

).

html

(

html

);

});

</

script

>

<

script id

=

"gameTemplate"

type

=

"text

/

template"

>//{

3

}

<

h1

>{{

nazwa

}}</

h1

><

br

><

h2

>{{

opis

}}</

h2

><

br

><

br

>

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

PĘTLE I FUNKCJE

Wszystko, co do tej pory poznaliśmy, nie pozwala nam na wprowadzenie np.
prostej listy elementów. W tym celu powinniśmy zastosować specjalne tagi

umownie nazwane sekcjami. W kolejnych przykładach chciałbym zaprezen-
tować możliwość wygenerowania przykładowej listy gier na podstawie do-
starczonego obiektu. Obiekt ten może być również pobierany za pomocą
funkcji ajax np.

$.getJSON(). Rozpoczynamy od rozbudowy naszego obiek-

tu o nazwie „gra” o kolejne elementy (1). Proszę zauważyć, że tablica obiektów
z grami została przypisana do elementu „gry”. Następnie w szablonie zastoso-
wałem wspomnianą wcześniej sekcję. Kod pomiędzy tagami

{{#gry}}{{/

gry}} zostaje wyświetlony tyle razy, ile elementów znajduje się w obiekcie
gra pod kluczem „gry”.

Listing 4. Przykład zastosowania sekcji w szablonie

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>

<

script

>

$

(

function

() {

var gra

= {

gry

:[{

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta

odsłona serii strzelanek pierwszoosobowych'

},

{

nazwa

:

'GTA5'

,

opis

:

'kolejna odsłona kultowej serii

gangsterskich gier'

},

{

nazwa

:

'Test Drive Unlimited'

,

opis

:

'wyścigi

samochodowe'

}]};//{

1

}

var template

=

$

(

'#gameTemplate'

).

html

();//{

2

}

var html

=

Mustache

.

to_html

(

template

,

gra

);

$

(

'#lista_gier'

).

html

(

html

);

});

</

script

>

<

script id

=

"gameTemplate"

type

=

"text

/

template"

>//{

3

}

{{

#gry

}}

<

ul

>

<

li

><

h1

>{{

nazwa

}}</

h1

><

h2

>{{

opis

}}</

h2

></

li

>

</

ul

>

{{/

gry

}}

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

Jeżeli w naszym obiekcie będzie znajdować się tablica elementów, możemy w
bloku sekcji użyć

{{.}} w celu wypisania kolejnych elementów. Na Listingu 5

dodamy rodzaj platformy, na jaką dana gra została wyprodukowana, używa-
jąc sekcji z kropką pokażemy dla każdego tytułu te nazwy.

Listing 5. Przykład użycia kropki w sekcji

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>

<

script

>

$

(

function

() {

var gra

= {

gry

:[{

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta

odsłona serii strzelanek pierwszoosobowych'

,

system

:[

'ps3'

,

'xbox'

,

'steam'

]},

{

nazwa

:

'GTA5'

,

opis

:

'kolejna odsłona kultowej serii

gangsterskich gier'

,

system

:[

'ps3'

,

'steam'

]},

{

nazwa

:

'Test Drive Unlimited'

,

opis

:

'wyścigi

samochodowe'

,

system

:[

'ps3'

]}]};//{

1

}

var template

=

$

(

'#gameTemplate'

).

html

();//{

2

}

var html

=

Mustache

.

to_html

(

template

,

gra

);

$

(

'#lista_gier'

).

html

(

html

);

});

</

script

>

<

script id

=

"gameTemplate"

type

=

"text

/

template"

>//{

3

}

{{

#gry

}}

background image

27

/ www.programistamag.pl /

MUSTACHE – CZYLI SZABLONY W JAVASCRIPT

<

ul

>

<

li

><

h1

>{{

nazwa

}}</

h1

><

h2

>{{

opis

}}</

h2

>

{{

#system

}}{{.}},{{/

system

}}

</

li

>

</

ul

>

{{/

gry

}}

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

Następnym elementem, o jaki rozbudujemy nasz kod, będzie użycie prostej
funkcji, która do ceny netto doda podatek VAT i wyświetli wartość brutto pro-
duktu. Jak widać, możemy w szablonie robić nawet bardziej skomplikowane
obliczenia, nie wkładając logiki w nasz szablon aplikacji.

Listing 6. Przykład użycia funkcji w zwracanym obiekcie

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>

<

script

>

$

(

function

() {

var gra

= {

gry

:[{

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta

odsłona serii strzelanek pierwszoosobowych'

,

system

:[

'ps3'

,

'xbox'

,

'steam'

],

cena

:

100

,

vat

:

0.22

},

{

nazwa

:

'GTA5'

,

opis

:

'kolejna odsłona kultowej serii

gangsterskich gier'

,

system

:[

'ps3'

,

'steam'

],

cena

:

150

,

vat

:

0.22

},

{

nazwa

:

'Test Drive Unlimited'

,

opis

:

'wyścigi samochodowe'

,

system

:[

'ps3'

],

cena

:

40

,

vat

:

0.22

}],

cena_z_vat

:

function

(){

return this

.

cena

+

this

.

cena

*this.

vat;}};//{1}

var template

=

$

(

'#gameTemplate'

).

html

();//{

2

}

var html

=

Mustache

.

to_html

(

template

,

gra

);

$

(

'#lista_gier'

).

html

(

html

);

});

</

script

>

<

script id

=

"gameTemplate"

type

=

"text

/

template"

>//{

3

}

{{

#gry

}}

<

ul

>

<

li

><

h1

>{{

nazwa

}}</

h1

><

h2

>{{

opis

}}:

cena

:

{{

cena_z_vat

}}</

h2

>

{{

#system

}}{{.}},{{/

system

}}

</

li

>

</

ul

>

{{/

gry

}}

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

Funkcja

to_html, którą używamy w naszych skryptach, może przyjmo-

wać opcjonalny trzeci parametr, w którym możemy podać część wydzielo-
nego kodu HTML, czyli kolejny szablon, który jest wstawiany w dane miejsce
szablonu głównego. Jak widać na przykładzie kodu z Listingu 7 w szablonie
możemy zagnieżdżać kolejny szablon.

Listing 7. Przykład zagnieżdżonego szablonu html

<!

DOCTYPE html

>

<

html

>

<

head

>

<

meta charset

=

"UTF-8"

>

<

script src

=

"http

://

code

.

jquery

.

com

/

jquery-1

.

10

.1

.

min

.

js"

></

script

>

<

script src

=

"mustache

.

js"

></

script

>

<

script

>

$

(

function

() {

var gra

= {

gry

:[{

nazwa

:

'Battlefield 4'

,

opis

:

'Czwarta

odsłona serii strzelanek pierwszoosobowych'

,

system

:[

'ps3'

,

'xbox'

,

'steam'

],

cena

:

100

,

vat

:

0.22

},

{

nazwa

:

'GTA5'

,

opis

:

'kolejna odsłona kultowej serii

gangsterskich gier'

,

system

:[

'ps3'

,

'steam'

],

cena

:

150

,

vat

:

0.22

},

{

nazwa

:

'Test Drive Unlimited'

,

opis

:

'wyścigi samochodowe'

,

system

:[

'ps3'

],

cena

:

40

,

vat

:

0.22

}],

cena_z_vat

:

function

(){

return this

.

cena

+

this

.

cena

*this.

vat;}};//{1}

var template

=

$

(

'#gameTemplate'

).

html

();//{

2

}

var czesc_html

= {

lista

:

"

<

h1

>{{

nazwa

}}</

h1

><

h2

>{{

opis

}}:

cena

: {{

cena_z_vat

}}</

h2

>

"

};

var html

=

Mustache

.

to_html

(

template

,

gra

,

czesc_html

);

$

(

'#lista_gier'

).

html

(

html

);

});

</

script

>

<

script id

=

"gameTemplate"

type

=

"text

/

template"

>//{

3

}

{{

#gry

}}

<

ul

>

<

li

>

{{>

lista

}}

{{

#system

}}{{.}},{{/

system

}}

</

li

>

</

ul

>

{{/

gry

}}

</

script

>

</

head

>

<

body

>

<

div

id

=

"lista_gier"

></

div

>

</

body

>

</

html

>

PODSUMOWANIE

Podsumowując, oddzielony kod logiki od widoku aplikacji to kod łatwiejszy w
późniejszej edycji, rozbudowie i użyciu w kilku miejscach projektu.

Używanie systemu mustache.js zapewnia większą przejrzystość w kodzie,

co ma ogromny wpływ na efektywność zmian i jakość projektu. Zachęcam
wszystkich do eksperymentowania i poznawania tego prostego systemu.

Piotr Tołłoczko

Programista z ponad dziesięcioletnim stażem, związany z programowaniem w PHP,
C, Android i Java. W VIWA Entertainment powierzono mu stworzenie systemu
tataka.com. Swoją karierę zawodową zaczynał od administrowania i instalacji sieci
(m.in. we wszystkich oddziałach PGNiG w województwie Zachodniopomorskim).

background image

28

/ 3

. 2014 . (22) /

PROGRAMOWANIE SYSTEMOWE

Mateusz “j00ru” Jurczyk

W

pierwszym artykule serii „Jak napisać własny debugger w syste-
mie Windows" opublikowanym w numerze 02/2014 magazynu
Programista omówiliśmy podstawowy schemat działania debug-

gera oraz przedstawiliśmy prosty szablon debuggera napisany w języku C++.
W niniejszym artykule przedstawiamy bardziej zaawansowane aspekty budo-
wy debuggera, skupiając się na operowaniu na pamięci wirtualnej, obsłudze
punktów wstrzymania, odczytywaniu i zapisywaniu kontekstu procesora, ob-
słudze wyjątków oraz praktycznym zastosowaniu wszystkich wymienionych
wcześniej operacji. Zapraszamy do lektury.

AKTYWNE INTERAKCJE

Z PROCESAMI

Jak wspomnieliśmy w poprzednim artykule, debuggery w systemie Windows
działają na zasadzie obsługi zdarzeń – kiedy w kontekście monitorowanego
procesu wydarzy się coś, co zasługuje na uwagę debuggera, system wysyła
do niego odpowiednią notyfikację, wstrzymując w tym samym czasie dzia-
łanie obserwowanego procesu, dając w ten sposób debuggerowi czas na
odpowiednią reakcję. Jeśli ów debugger wyłącznie odbiera informacje o zda-
rzeniach, nie ingerując w żaden sposób w tok działania aplikacji, działa on
w sposób pasywny. Aby funkcjonalny debugger mógł osiągnąć jakikolwiek
znaczący cel, musi on conajmniej odczytywać stan programu wykraczający
poza domyślne informacje przekazane przez system w strukturach opisują-
cych poszczególne zdarzenia, a najczęściej zachowywać się wręcz w sposób
inwazyjny – modyfikować zawartość pamięci, rejestrów, flag oraz innych ele-
mentów stanowiących ogół stanu procesu (np. otwarte zasoby). Debug API
oddaje programistom do dyspozycji wiele funkcji umożliwiających operowa-
nie na poszczególnych częściach stanu aplikacji – najważniejsze z nich zosta-
ną omówione w kolejnych sekcjach.

Jako że system Windows aż do roku 2012 wspierał wyłącznie procesory o

architekturze IA-32, IA-64 (Itanium) oraz x86-64, artykuł ten został napisany z
myślą właśnie o tych platformach. Wszystkie przytoczone przykłady urucha-
miane były na 32-bitowych procesorach i systemach operacyjnych.

Pamięć i wirtualna przestrzeń adresowa

Pamięć operacyjna – niezbędny element działania dowolnego programu
komputerowego – może być adresowana na wiele sposobów, w zależności

od architektury procesora. Wszystkie procesory z rodziny Intel korzystają z
tzw. architektury von Neumanna, w której dane aplikacji są przechowywane
wspólnie z instrukcjami, a adresowanie obu typów pamięci wykonuje się w
ten sam sposób. Jest to istotne spostrzeżenie, ponieważ oznacza to, że nieza-
leżnie od typu pamięci, na której nasz debugger będzie operował, będziemy
używali tego samego interfejsu i wyłącznie od nas zależało będzie, w jaki spo-
sób będziemy interpretowali przetwarzane dane.

Adresowanie pamięci na 32-bitowych procesorach z rodziny x86 w śro-

dowisku Windows odbywa się przy pomocy pary: 16-bitowego selektora
segmentu oraz 32-bitowego przesunięcia wewnątrz owego segmentu. W
momencie odwołania do pamięci procesor tłumaczy adres wirtualny na
32-bitowy adres liniowy poprzez dodanie 32-bitowego adresu bazowego
segmentu do przesunięcia. Ze względu na fakt, że system Windows używa
tzw. płaskiego modelu pamięci, adres bazowy większości segmentów jest
równy zero. Wyjątek stanowi rejestr o nazwie

fs – adres początkowy segmen-

tu, na który ów rejestr wskazuje, to początek systemowego regionu opisują-
cego aktualny wątek; jedną z przechowywanych tam informacji jest wskaźnik
na funkcję obsługi wyjątków, stąd w kodzie binarnym natywnych aplikacji dla
systemu Windows często spotkać można odwołania do pamięci pod adre-
sami

fs:0 i fs:4. W przypadku pozostałych rejestrów segmentowych seg-

mentacja jest mechanizmem transparentnym, tj. odwołania takie jak

cs:N

czy

ds:N tłumaczone są na adres liniowy o wartości N. W drugiej kolejności

adres liniowy tłumaczony jest na adres fizyczny przy pomocy tzw. tabeli stron
(ang. page table) – systemowej struktury danych zarządzanej przez menadżer
pamięci będący częścią jądra.

Platformy 64-bitowe o architekturze AMD64 prawie całkowicie porzuciły

wsparcie dla mechanizmu segmentacji, a operacje na pamięci odbywają się
bezpośrednio przy użyciu 64-bitowych adresów liniowych. W dalszej części
artykułu używamy terminu „adres" w rozumieniu „adres liniowy", 32-bitowy
dla architektury x86 lub 64-bitowy dla architektury x86-64.

Każdy proces działający w środowisku Windows posiada swoją własną

przestrzeń adresową. Jedną z konsekwencji takiego modelu jest fakt, że po-
prawny wskaźnik w kontekście jednego procesu nie może zostać bezmyślnie
użyty w kontekście innej aplikacji. Aby odczytać lub zmodyfikować pamięć
zewnętrznego procesu, konieczne jest użycie odpowiednich funkcji API, któ-
rych deklaracje przedstawiono na Listingu 1. Funkcje te jako parametry przyj-
mują kolejno: uchwyt na proces, którego dotyczy operacja, adres w kontek-
ście owego procesu, wskaźnik na bufor, do którego mają zostać odczytane lub

Jak napisać własny debugger

w systemie Windows – część 2

Interfejs Debug API dostępny w systemie Windows co najmniej od czasów Win-
dowsa XP posiada ogromny potencjał, który może być wykorzystany do różnora-
kich celów przez dowolnego pasjonata niskopoziomowych aspektów informatyki,
zainteresowanego badaniem i monitorowaniem przebiegu działania aplikacji, two-
rzeniem zabezpieczeń antypirackich czy rozpakowywaniem plików wykonywalnych
zabezpieczonych tzw. protectorami lub packerami plików PE. Wchodzenie w inte-
rakcję z zewnętrznymi procesami poprzez odczytywanie i zapisywanie wartości re-
jestrów, pamięci operacyjnej oraz obsługiwanie pochodzących z nich zdarzeń może
służyć wielu celom, jednak jedno jest pewne - płynna znajomość tej części WinAPI
jest istotnym elementem wiedzy o działaniu systemu Windows, w szczególności
zaś wiedzy o fundamentalnych prawach, jakimi rządzi się wykonywanie programów
na linii system operacyjny – procesor.

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

30

/ 3

. 2014 . (22) /

PROGRAMOWANIE SYSTEMOWE

pobrane do zapisu dane, liczbę bajtów oraz wskaźnik na zmienną, do której
trafia informacja o liczbie bajtów, które udało się przetworzyć. W przypadku
pomyślnego wykonania operacji funkcje zwracają wartość

TRUE; w przeciw-

nym wypadku wartość

FALSE.

Listing 1. Deklaracje funkcji systemowych odpowiedzialnych za
operacje na pamięci zewnętrznych procesów

BOOL

WINAPI

ReadProcessMemory

(

_In_

HANDLE

hProcess

,

_In_

LPCVOID

lpBaseAddress

,

_Out_

LPVOID

lpBuffer

,

_In_ SIZE_T nSize

,

_Out_ SIZE_T

*

lpNumberOfBytesRead

)

;

BOOL

WINAPI

WriteProcessMemory

(

_In_

HANDLE

hProcess

,

_In_

LPVOID

lpBaseAddress

,

_In_

LPCVOID

lpBuffer

,

_In_ SIZE_T nSize

,

_Out_ SIZE_T

*

lpNumberOfBytesWritten

)

;

Podczas modyfikowania pamięci nie należącej do lokalnego procesu należy
zachować szczególną ostrożność – musimy pamiętać, że działanie takie jest
swego rodzaju „operowaniem na żywym organizmie", a zagrożeniem wyni-
kającym z niesynchronizowanego dostępu do pamięci są sytuacje wyścigu,
które mogą prowadzić do nieoczekiwanego zachowania lub niestabilno-
ści aplikacji. W zależności od natury danych, które zapisujemy, prawidłowa
synchronizacja może okazać się nietrywialnym zadaniem; na przykład, jeśli
modyfikujemy dwa lub więcej bajtów w sekcji kodu, nie wystarczy wyłącznie
wstrzymać działania wszystkich wątków w procesie (co dzieje się automa-
tycznie w momencie obsługi zdarzenia debuggera), gdyż wciąż możliwa by-
łaby sytuacja, w której jeden z wątków zostaje zatrzymany wewnątrz modyfi-
kowanego regionu. Wątek taki w momencie wznowienia wylądowałby albo w
środku skopiowanej w to miejsce instrukcji, albo rozpocząłby wykonanie od
instrukcji nieprzystającej do stanu, w którym obecnie znajduje się procesor.
W obu przypadkach niemożliwe byłoby poprawne działanie programu, który
zostałby natychmiastowo zamknięty w trybie awaryjnym.

Kontekst procesora

Kontekst procesora każdego z wątków jest obok pamięci operacyjnej naj-
ważniejszym elementem stanu procesu. Przy jego pomocy debugger może
osiągać rozmaite, ciekawe efekty, jak pokazane zostało w dalszej części arty-
kułu. Obok informacji o kontekście procesora (wartości rejestrów ogólnego
przeznaczenia, segmentowych, FPU i pozostałych, a także wartości flag) w
strukturze

CONTEXT znajduje się pole ContextFlags, którego wartość mówi

o tym, którymi elementami kontekstu zainteresowany jest użytkownik. Pole
to jest maską bitową flag takich jak

CONTEXT_CONTROL, CONTEXT_INTEGER

czy

CONTEXT_SEGMENTS.

Wskaźnik na strukturę

CONTEXT przekazuje się funkcjom GetThreadCon-

text oraz SetThreadContext, których nazwy w zupełności opisują oferowa-
ną przez nie funkcjonalność. Listing 2 przedstawia przykładowy kod modyfiku-
jący wartość rejestru

EAX w wątku identyfikowanym przez uchwyt hThread.

Listing 2. Przykładowy kod zmieniający część kontekstu procesora
wybranego wątku

CONTEXT

ctx

;

memset

(&

ctx

,

0

,

sizeof

(

ctx

))

;

ctx

.

ContextFlags

=

CONTEXT_INTEGER

;

if

(!

GetThreadContext

(

hThread

,

&

ctx

))

{

// Wystapil blad.

}

ctx

.

Eax

=

0x0badbeef

;

if

(!

SetThreadContext

(

hThread

,

&

ctx

))

{

// Wystapil blad.

}

Większość rejestrów przechowuje istotne dane w bezpośredniej formie

– wyjątek stanowią wyłącznie rejestry segmentowe na 32-bitowych proce-
sorach z rodziny x86, których wartości są jedynie selektorami – indeksami
w systemowej strukturze Global Descriptor Table. W większości przypadków
debugger nie jest zainteresowany szczegółami dotyczącymi konfiguracji seg-
mentów, gdyż ich domyślne wartości są dobrze znane (adres bazowy równy

0

i limit równy

0xffffffff), jednak w pewnych przypadkach może potrzebo-

wać on informacji na temat konkretnego segmentu, najczęściej wspomniane-
go wcześniej segmentu

fs. W tej sytuacji z pomocą przychodzi nam funkcja

GetThreadSelectorEntry, która tłumaczy wskazany selektor na strukturę
LDT_ENTRY zawierającą wszystkie informacje opisujące dany segment. Po-
nieważ jest to w zasadzie struktura procesora (dokładny opis znaczenia po-
szczególnych pól można znaleźć np. w tomie „Intel 64 and IA-32 Architectures
Software Developer's Manual, Volume 3A: System Programming Guide, Part
1", w rozdziale 3.4.5 zatytułowanym „Segment Descriptors"), jej format jest
dość skomplikowany, a wiele pól jest rozbitych na mniejsze, kilkubitowe czę-
ści. Przykładowy kod odczytujący i wypisujący adres bazowy segmentu

fs

aktualnego wątku został przedstawiony na Listingu 3.

Listing 3. Przykład wykorzystania funkcji

GetThreadSelectorEntry

do pobrania adresu obszaru Thread Environment Block

CONTEXT

ctx

;

memset

(&

ctx

,

0

,

sizeof

(

ctx

))

;

ctx

.

ContextFlags

=

CONTEXT_SEGMENTS

;

if

(!

GetThreadContext

(

GetCurrentThread

(),

&

ctx

))

{

return

EXIT_FAILURE

;

}

LDT_ENTRY

ldt_entry

;

if

(!

GetThreadSelectorEntry

(

GetCurrentThread

(),

ctx

.

SegFs

,

&

ldt_

entry

))

{

return

EXIT_FAILURE

;

}

DWORD

base

=

(

ldt_entry

.

HighWord

.

Bytes

.

BaseHi

<<

24

)

|

(

ldt_entry

.

HighWord

.

Bytes

.

BaseMid

<<

16

)

|

ldt_entry

.

BaseLow

;

printf

(

"

fs: segment base:

%x\n

"

,

base

)

;

W ramach ciekawostki można wspomnieć tutaj, że o ile dedykowany debug-
ger zaprojektowany do jednego, konkretnego celu może pozwolić sobie na
ignorowanie faktu istnienia mechanizmu segmentacji na 32-bitowych plat-
formach, debuggery ogólnego użytku powinny zapewniać ich pełne wspar-
cie, ponieważ debugowany program może tworzyć własne segmenty o nie-
zerowym adresie bazowym i używać ich do adresowania kodu, danych lub
stosu. Jak się jednak okazuje, nie wszystkie z powszechnie używanych debug-
gerów przestrzegają tej zasady, w związku z czym segmentacja może zostać
użyta jako efektywna metoda utrudniania analizy działania programu. Więcej
informacji na ten temat znaleźć można w poście „Protected Mode Segmenta-
tion as a powerful anti-debugging measure" na blogu autora [1].

MECHANIZMY DEBUGGERA

Dysponując już podstawowymi, niskopoziomowymi narzędziami do analizy i
modyfikacji stanu procesów, możemy na ich podstawie budować bardziej skom-
plikowane mechanizmy, takie jak deasemblacja kodu, monitorowanie przebiegu
wykonywania programu za pomocą tzw. single steppingu czy ustawianie i zarzą-
dzanie punktami wstrzymania. Szczegóły implementacyjne wymienionych funk-
cjonalności zostały opisane w poszczególnych sekcjach poniżej.

Deasemblacja kodu

Jedną z ważniejszych części debuggerów, niezależnie od pełnionej przez nie
funkcji, jest możliwość tłumaczenia kodu maszynowego (binarnej reprezen-
tacji kodu wykonywalnego) na zrozumiałe dla człowieka mnemoniki. System
Windows nie udostępnia interfejsów umożliwiających deasemblację kodu, a
własna implementacja owej funkcjonalności znacznie wykracza poza zakres

background image

31

/ www.programistamag.pl /

JAK NAPISAĆ WŁASNY DEBUGGER W SYSTEMIE WINDOWS – CZĘŚĆ 2

niniejszego artykułu (temat ów zasługuje właściwie na osobne opracowanie),
stąd konieczne jest użycie jednej z wielu dostępnych w Internecie bibliotek o
otwartych źródłach stworzonych właśnie w tym celu. Za przykład takich bi-
bliotek posłużyć mogą projekty takie jak distorm [2], BeaEngine [3], libdisasm
[4] lub udis86 [5]. My skorzystamy z tej ostatniej, choć nic nie stoi na prze-
szkodzie, by we własnym projekcie użyć innej, lepiej spełniającej konkretne
wymagania biblioteki.

Moduł udis86 powstał z myślą o programistach, którzy chcieliby w sposób

szybki i prosty zaimplementować deasemblację we własnym produkcie, stąd
wykorzystanie jej w kodzie C lub C++ jest trywialnie proste: tworzymy struk-
turę typu

ud_t, w pierwszej kolejności wywołujemy na niej funkcję ud_init,

następnie definiujemy podstawowe właśności kodu binarnego i formatu
wyjścia przy użyciu

ud_set_mode, ud_set_syntax oraz jednej z funkcji z

rodziny

ud_set_input_, i jesteśmy gotowi do wykonania deasemblacji za

pomocą

ud_disassemble, a następnie pobrania informacji na temat ostat-

niej instrukcji poprzez procedury takie jak

ud_insn_len, ud_insn_hex,

ud_insn_asm czy ud_insn_mnemonic. Przykładowy kod źródłowy progra-
mu wczytującego dane ze strumienia standardowego wejścia i wypisującego
ich mnemoniczną reprezentację jako 32-bitowy kod notacji Intel został przed-
stawiony na Listingu 4.

Listing 4. Prosty disassembler wykorzystujący podstawowe funk-
cjonalności biblioteki udis86

#include

<

cstdio

>

#include

<

udis86.h

>

int

main

()

{

ud_t ud_obj

;

ud_init

(&

ud_obj

)

;

ud_set_input_file

(&

ud_obj

,

stdin

)

;

ud_set_mode

(&

ud_obj

,

32

)

;

ud_set_syntax

(&

ud_obj

,

UD_SYN_INTEL

)

;

while

(

ud_disassemble

(&

ud_obj

))

{

printf

(

"

%s\n

"

,

ud_insn_asm

(&

ud_obj

))

;

}

return

0

;

}

O ile deasemblacja „statycznych" danych w postaci pliku wykonywalnego
znajdującego się na dysku twardym jest prosta, to proces tłumaczenia na
mnemoniki bajtów odpowiadających np. aktualnej instrukcji zdalnego proce-
su jest już odrobinę trudniejszy. Musimy w tym miejscu odpowiedzieć sobie
na pytanie: ile bajtów może maksymalnie zajmować pojedyncza instrukcja w
architekturach x86 oraz x86-64 (rozkazy na tych platformach kodowane są
przez ciągi binarne o zmiennej długości), a także rozważyć przypadki brzego-
we, w których dane binarne instrukcji znajdują się na krawędzi stron pamięci,
a dalsza strona nie jest podmapowana w procesie itp.

Aby odpowiedzieć na pierwsze pytanie, warto zasięgnąć informacji u

źródła, czyli manuali firmy Intel. W tomie „Intel Architecture Instruction Set
Extensions Programming Reference ”, w sekcji „4.1.10 AVX Instruction Length”
znajdziemy następującą informację:

The maximum length of an Intel 64 and IA-32 instruction remains 15 bytes.

Wiemy już więc, że w celu „objęcia" deasemblacją całości każdej poprawnej
instrukcji wystarczy odczytać spod rejestru EIP lub RIP 15 bajtów. Jeśli owe
piętnaście bajtów znajduje się w obrębie jednej strony pamięci, mamy gwa-
rancję, że zostaną one w całości poprawnie odczytane. Potencjalny problem
pojawia się, gdy niektóre z nich „wystają" poza aktualną stronę, wobec czego
nie możemy zakładać, że kolejna strona również istnieje i jest podmapowana.
Choć sytuacja, w której poprawna instrukcja znajduje się (i jest wykonywana)
na krawędzi dostępnego obszaru pamięci wirtualnej, jest niezwykle rzadka,
to poprawna implementacja debuggera wymaga jej rozpatrzenia. Poprawny
kod deasemblacji aktualnej instrukcji wątku zatrzymanego na skutek zdarze-
nia debuggera przedstawiony został na Listingu 5; w pesymistycznym przy-

padku dwukrotnie (lub więcej razy, jeśli któreś z wywołań nie odczyta wszyst-
kich bajtów naraz) wywołuje on funkcję

ReadProcessMemory.

Listing 5. Kod deasemblujący aktualną instrukcję na bazie bibliote-
ki udis86

// Pobierz kontekst procesora.

CONTEXT

ctx

;

memset

(&

ctx

,

0

,

sizeof

(

ctx

))

;

ctx

.

ContextFlags

=

CONTEXT_CONTROL

;

GetThreadContext

(

threads

[

debug_ev

.

dwThreadId

],

&

ctx

)

;

// Deasembluj instrukcje.

const

ULONG

kMaxInstrLength

=

15

;

const

ULONG

kSmallPageSize

=

(

1

<<

12

)

;

BYTE

opcode

[

kMaxInstrLength

]

;

SIZE_T bytes_read

=

0

;

LPBYTE

pc

=

(

LPBYTE

)

ctx

.

Eip

;

while

(

bytes_read

<

kMaxInstrLength

)

{

SIZE_T remote_bytes_read

;

if

(!

ReadProcessMemory

(

processes

[

debug_ev

.

dwProcessId

],

&

pc

[

bytes_read

],

&

opcode

[

bytes_read

],

std

::

min

(

kMaxInstrLength

bytes_read

,

(((

SIZE_T

)&

pc

[

bytes_

read

]

+

kSmallPageSize

)

&

~(

kSmallPageSize

1

))

(

SIZE_T

)&

pc

[

bytes_read

]),

&

remote_bytes_read

))

{

break

;

}

bytes_read

+=

remote_bytes_read

;

}

ud_t ud_obj

;

ud_init

(&

ud_obj

)

;

ud_set_mode

(&

ud_obj

,

32

)

;

ud_set_syntax

(&

ud_obj

,

UD_SYN_INTEL

)

;

ud_set_input_buffer

(&

ud_obj

,

opcode

,

bytes_read

)

;

if

(

ud_disassemble

(&

ud_obj

)

>

0

)

{

printf

(

"

[

%.8x

]

%s\n

"

,

ctx

.

Eip

,

ud_insn_asm

(&

ud_obj

))

;

}

else

{

printf

(

"

[

%.8x

] invalid

\n

"

,

ctx

.

Eip

)

;

}

W ten oto sposób nasz debugger zyskał możliwość wyświetlania aktualnej
instrukcji w dowolnym momencie działania analizowanej aplikacji.

Single stepping

Wiele współczesnych architektur procesorów udostępnia tzw. flagę pułapki
(ang. Trap Flag), umożliwiającą przerwanie działania programu po wykona-
niu pierwszej instrukcji następującej po instrukcji, która ustawiła tę flagę.
Funkcjonalność ta umożliwia śledzenie wykonywania aplikacji instrukcja po
instrukcji, bez konieczności korzystania z breakpointów lub innych wolniej-
szych i skomplikowanych mechanizmów.

Flaga ta właśnie pod nazwą Trap Flag (w skrócie TF) dostępna jest na pro-

cesorach z rodziny x86 i x86-64, zajmując ósmy (licząc od zera) bit rejestru
flag. Oznacza to, że wykonanie operacji

or na rejestrze EFLAGS lub RFLAGS z

wartością 0x100 poskutkuje wygenerowaniem wyjątku procesora o numerze
1 (Debug Exception, w skrócie #DB), reprezentowanego w systemie Windows
przez kod wyjątku

EXCEPTION_SINGLE_STEP. Poprzez operacje na fladze TF

i odpowiednią obsługę wyjątków, debugger może pomyślnie śledzić tok dzia-
łania programu instrukcja po instrukcji. Przykładowy kod ustawiający flagę TF
dla danego wątku przedstawiony jest na Listingu 6.

Listing 6. Przykład kodu ustawiającego flagę TF dla wstrzymanego
wątku

BOOL

SetThreadTrapFlag

(

HANDLE

hThread

)

{

const

unsigned

int

kX86TrapFlag

=

(

1

<<

8

)

;

CONTEXT

ctx

;

memset

(&

ctx

,

0

,

sizeof

(

ctx

))

;

ctx

.

ContextFlags

=

CONTEXT_CONTROL

;

background image

32

/ 3

. 2014 . (22) /

PROGRAMOWANIE SYSTEMOWE

if

(!

GetThreadContext

(

threads

[

debug_ev

.

dwThreadId

],

&

ctx

))

{

return

FALSE

;

}

ctx

.

EFlags

|=

kX86TrapFlag

;

if

(!

SetThreadContext

(

threads

[

debug_ev

.

dwThreadId

],

&

ctx

))

{

return

FALSE

;

}

return

TRUE

;

}

Breakpointy

Punkty wstrzymania, znane szerzej jako breakpointy, to konkretne miejsca
w kodzie binarnym programu (jednoznacznie identyfikowane przez ad-
res liniowy, pod którym się znajdują), w których jego wykonywanie zostaje
przerwane, a kontrola oddana debuggerowi w celu zbadania i ewentualnej
modyfikacji stanu aplikacji. Są one podstawowym narzędziem analizy dzia-
łania procesu, stosowane prawdopodobnie w każdym publicznie dostępnym
debuggerze – możliwość wstrzymania aplikacji w konkretnym miejscu po-
zwala programiście lub samemu debuggerowi na interakcję z zewnętrznym
procesem dokładnie wtedy, gdy jest to pożądane, przy jednoczesnym braku
obniżania szybkości działania programu (jak ma to miejsce w przypadku sin-
gle steppingu). Zanim jednak zabierzemy się za włączenie tej fundamentalnej
funkcjonalności do naszego debuggera, warto zastanowić się, w jaki sposób
punkty wstrzymania działają właściwie na poziomie procesora.

Krótki wstęp

Na platformach z rodziny x86 oraz x86-64 termin „breakpoint” nie jest cał-

kowicie jednoznaczny, gdyż istnieją aż trzy rodzaje punktów wstrzymania: so-
ftware'owe breakpointy na wykonanie, sprzętowe (hardware'owe) breakpointy
na wykonanie oraz breakpointy na dostęp do pamięci. Pierwsze dwa z nich
umożliwiają zatrzymanie działania aplikacji w momencie próby wykonania in-
strukcji pod konkretnym adresem kolejno poprzez użycie instrukcji „

int3” oraz

wykorzystanie tzw. „rejestrów debuggera”; trzeci typ punktów wstrzymania
umożliwia śledzenie miejsc w programie, w których następuje odczyt lub zapis
do konkretnych komórek pamięci. W niniejszym artykule opiszemy wyłącznie
breakpointy software'owe; szczegółowy opis i implementacja pozostałych ro-
dzajów punktów wstrzymania znajdzie się w kolejnym artykule z niniejszej serii.
W ramach ciekawostki można tutaj nadmienić, że w 2010 roku haker o pseudo-
nimie „Czernobyl” odkrył oraz częściowo udokumentował nieznane wcześniej
sprzętowe wsparcie debugingu w procesorach firmy AMD. Więcej informacji na
ten temat znaleźć można w licznych internetowych źródłach [6][7].

W zbiorze instrukcji procesorów Intela znajdziemy instrukcję o nazwie

int3” reprezentowaną przez pojedynczy bajt o wartości 0xcc – to właśnie

na tej instrukcji opiera się cały mechanizm breakpointów w architekturach
x86 oraz x86-64. Wykonanie owej instrukcji w kontekście dowolnego procesu
w systemie powoduje bezwarunkowe wygenerowanie wyjątku procesora o
numerze 3 – Breakpoint Exception (

#BP) obsługiwanego przez jądro systemu.

Windows – jak w przypadku każdego wyjątku – sprawdza, czy pod dany pro-
ces podpięty jest debugger, i jeśli odpowiedź na to pytanie jest pozytywna,
przekazuje ów wyjątek do obsłużenia. W przeciwnym razie wywoływane są
funkcje obsługi wyjątków w samej zainteresowanej aplikacji. Aby ustawić
breakpoint w monitorowanym procesie, debugger nadpisuje pierwszy bajt
instrukcji znajdującej się pod interesującym go adresem właśnie bajtem

0xcc

(zapisując wcześniej oryginalną wartość), zaś usunięcie punktu wstrzymania
wiąże się z przywróceniem oryginalnego bajtu pod wskazanym adresem.
Podmieniając instrukcję, debugger gwarantuje sobie, że w momencie próby
wykonania instrukcji w tym miejscu wygenerowany zostanie wyjątek, który
może zostać przez niego obsłużony.

W głowie uważnego czytelnika powinno zrodzić się w tym miejscu py-

tanie – skoro ustawianie oraz usuwanie breakpointów jest procesem tak in-
wazyjnym, polegającym wyłącznie na wstawieniu w odpowiednim miejscu
instrukcji generującej wyjątek, to dlaczego zamiast „

int3“ nie użyć dowolnej

innej instrukcji, która również bezwarunkowo wygeneruje wyjątek, na przy-

kład

ud2 (rozkaz powodujący wyjątek #UD)? Powodów jest kilka. Wyjątek #BP

został przeznaczony przez inżynierów firmy Intel właśnie w celu wsparcia
dla obsługi debuggerów – jest to jego jedyne zastosowanie, co przejawia się
również w zachowaniu systemów operacyjnych. System Windows tłumaczy
wyjątek o numerze 3 (i tylko jego) na kod

EXCEPTION_BREAKPOINT, który

jednoznacznie wskazuje na fakt, że wyjątek został wygenerowany w związku
z punktem wstrzymania, a nie np. błędem w aplikacji. Dlaczego nie używa się
więc instrukcji „

int N” ze stałym parametrem o wartości 3 („int 3”)? Jest tak,

ponieważ instrukcja ta zapisywana jest przy pomocy dwóch bajtów:

0xcd

0x03. O ile nadpisywanie instrukcji inną instrukcją o długości jednego bajta
jest stuprocentowo bezpieczne pod kątem spójności wykonywania progra-
mu, nie można tego samego powiedzieć o dłuższych instrukcjach. Wyobraź-
my sobie na przykład następujący scenariusz: chcemy ustawić breakpoint na
początku pewnej funkcji, której prolog reprezentowany jest przez typowe
instrukcje odpowiedzialne za ustawienie ramki stosu:

00000000 55 push ebp

00000001 89E5 mov ebp,esp

00000003 57 push edi

00000004 56 push esi

00000005 53 push ebx

Jeśli w celu ustawienia punktu wstrzymania użyjemy instrukcji „int 3”, kod
funkcji zmieni się w następujący sposób:

00000000 CD03 int 0x3

00000002 E557 in eax,0x57

00000004 56 push esi

00000005 53 push ebx

Zakładając, że jeden z wątków procesu zamierzał właśnie wykonać instrukcję

mov ebp, esp”, oto, w jaki sposób zmieniłaby się wskazywana przez rejestr

Eip instrukcja:

00000001 03E5 add esp,ebp

00000003 57 push edi

00000004 56 push esi

00000005 53 push ebx

Jak widać, semantyka instrukcji uległa znacznej zmianie po tym, jak jej pierw-
szy bajt został nadpisany drugim bajtem instrukcji „

int 3”. Z tego też wzglę-

du zaleca się użycie specjalnej instrukcji „

int3”, która powstała właśnie z

myślą o tym, by umożliwić bezpieczną implementację punktów wstrzymania.

Wznawianie wykonania

Ustawienie breakpointu przy pomocy omówionej w poprzedniej sekcji in-

strukcji oraz obsłużenie wygenerowanego przez nią wyjątku jest proste – żad-
na z tych czynności nie wymaga raczej głębszego zastanowienia. Jeśli mamy
do czynienia z jednorazowym punktem wstrzymania, nasza praca kończy się
w tym miejscu – wstawiamy instrukcję „

int3”, w momencie jej wykonania ob-

sługujemy wyjątek i przywracamy pod odpowiednim adresem bajt oryginal-
nej instrukcji, a następnie wznawiamy wykonanie i na zawsze zapominamy o
owym breakpoincie. Sytuacja jest nieco bardziej skomplikowana w momencie,
gdy chcemy, by punkt wstrzymania znajdował się pod danym adresem na stałe,
tj. by wszystkie, a nie tylko pierwsza próba wykonania instrukcji pod zadanym
adresem spowodowały wygenerowanie wyjątku. Z jednej strony nie chcemy na
zawsze usunąć bajtu

0xcc, z drugiej zaś – musimy to tymczasowo uczynić, by

oryginalna instrukcja znajdująca się w tym miejscu mogła się wykonać.

Aby osiągnąć oba cele, posłużymy się w tym miejscu opisaną wcześniej

funkcjonalnością single steppingu: w momencie wystąpienia wyjątku

EX-

CEPTION_BREAKPOINT przywrócimy w kodzie oryginalny bajt oraz ustawimy
w kontekście danego wątku flagę TF. W konsekwencji poprawnie wykona się
nadpisana wcześniej instrukcja, po której nastąpi wyjątek

EXCEPTION_SIN-

GLE_STEP, kiedy to możemy ponownie ustawić breakpoint na swoje miejsce.
Skoro znamy już teorię stojącą za mechanizmem punktów wstrzymania, czas
przyjrzeć się realnej implementacji w języku C++.

background image

33

/ www.programistamag.pl /

JAK NAPISAĆ WŁASNY DEBUGGER W SYSTEMIE WINDOWS – CZĘŚĆ 2

Implementacja

W celu poprawnej implementacji opisanego wyżej mechanizmu, musimy

na początku zdefiniować dodatkowe struktury danych, potrzebne do prze-
chowywania niezbędnych informacji na temat aktualnego stanu monitoro-
wanego procesu i tego, w jaki sposób zaingerował w niego nasz debugger.
Dwie podstawowe struktury zostały przedstawione na Listingu 7.

Listing 7. Przykładowe struktury danych i definicje typów wymaga-
ne do poprawnej obsługi punktów wstrzymania

typedef

struct

tagBREAKPOINT BREAKPOINT

,

*

PBREAKPOINT

;

typedef

struct

tagBREAKPOINTS BREAKPOINTS

,

*

PBREAKPOINTS

;

typedef

VOID

(*

PBREAKPOINT_HANDLER

)(

PBREAKPOINT breakpoint

,

DEBUG_EVENT

*

debug_ev

,

PDWORD

cont_status

)

;

struct

tagBREAKPOINT

{

LPVOID

address

;

BYTE

byte

;

PBREAKPOINT_HANDLER handler

;

};

struct

tagBREAKPOINTS

{

std

::

map

<

DWORD

,

LPVOID

>

pending_bps

;

std

::

map

<

LPVOID

,

PBREAKPOINT

>

bps

;

};

Struktura

BREAKPOINTS zawiera informacje o wszystkich aktywnych punk-

tach wstrzymania w formie tablicy asocjacyjnej (obiekt

bps) mapującej adres

breakpointa na jego deskryptor oraz podobną tablicę o nazwie

pending_

bps przechowującą dane na temat wątków znajdujących się pomiędzy ob-
służeniem wyjątku

EXCEPTION_BREAKPOINT a przywróceniem breakpointa.

Kod obsługi

EXCEPTION_SINGLE_STEP wykorzysta ową mapę, by wiedzieć,

pod jakim adresem powinno przywrócić się punkt wstrzymania dla danego
wątku. Z kolei struktura

BREAKPOINT zawiera pola takie jak: adres wirtualny

breakpointa, oryginalny bajt instrukcji oraz wskaźnik na funkcję obsługi da-
nego breakpointa.

W dalszej kolejności możemy zdefiniować pomocnicze funkcje służące do

dodawania oraz usuwania punktów wstrzymania – ich definicje znajdziemy
na Listingu 8. Przykładowe implementacje każdej z nich zostały pokazane ko-
lejno na Listingach 9, 10 i 11. Ponieważ przedstawiają one w zasadzie techniki
i mechanizmy opisane wyżej, nie wymagają one dalszego komentarza.

Listing 8. Definicje pomocniczych funkcji instalujących oraz usuwa-
jących punkty wstrzymania

BOOL

AddBreakpoint

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

,

LPVOID

address

,

PBREAKPOINT_HANDLER handler

)

;

BOOL

RemoveBreakpoint

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

,

LPVOID

address

)

;

BOOL

RemoveAllBreakpoints

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

)

;

Listing 9. Przykładowa implementacja funkcji AddBreakpoint

BOOL

AddBreakpoint

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

,

LPVOID

address

,

PBREAKPOINT_HANDLER handler

)

{

// Jesli pod danym adresem istnieje juz breakpoint,

// zakoncz wywolanie sukcesem.

if

(

breakpoints

->

bps

.

find

(

address

)

!=

breakpoints

->

bps

.

end

())

{

return

TRUE

;

}

// Odczytaj oryginalny bajt instrukcji.

SIZE_T bytes_processed

;

BYTE

byte

;

if

(!

ReadProcessMemory

(

hProcess

,

address

,

&

byte

,

sizeof

(

BYTE

),

&

bytes_processed

))

{

return

FALSE

;

}

// Nadpisz pierwszy bajt instrukcji opkodem "int3".

BYTE

int3_byte

=

0xcc

;

if

(!

WriteProcessMemory

(

hProcess

,

address

,

&

int3_byte

,

sizeof

(

BYTE

),

&

bytes_processed

))

{

return

FALSE

;

}

// Wypelnij strukture deskryptora.

PBREAKPOINT new_breakpoint

=

new

BREAKPOINT

;

new_breakpoint

->

address

=

address

;

new_breakpoint

->

byte

=

byte

;

new_breakpoint

->

handler

=

handler

;

breakpoints

->

bps

[

address

]

=

new_breakpoint

;

return

TRUE

;

}

Listing 10. Przykładowa implementacja funkcji RemoveBreakpoint

BOOL

RemoveBreakpoint

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

,

LPVOID

address

)

{

SIZE_T bytes_processed

;

// Zweryfikuj, czy wskazany breakpoint faktycznie istnieje.

if

(

breakpoints

->

bps

.

find

(

address

)

==

breakpoints

->

bps

.

end

())

{

return

FALSE

;

}

// Przywroc oryginalny bajt instrukcji.

PBREAKPOINT breakpoint

=

breakpoints

->

bps

[

address

]

;

if

(!

WriteProcessMemory

(

hProcess

,

address

,

&

breakpoint

->

byte

,

sizeof

(

BYTE

),

&

bytes_processed

))

{

return

FALSE

;

}

// Zwolnij i usun deskryptor punktu wstrzymania.

delete

breakpoints

->

bps

[

address

]

;

breakpoints

->

bps

.

erase

(

address

)

;

return

TRUE

;

}

Listing 11. Przykładowa implementacja funkcji
RemoveAllBreakpoints

BOOL

RemoveAllBreakpoints

(

PBREAKPOINTS breakpoints

,

HANDLE

hProcess

)

{

BOOL

success

=

TRUE

;

while

(!

breakpoints

->

bps

.

empty

())

{

// Jesli usuwanie jednego z breakpointow nie powiedzie sie,

// kontynuujemy usuwanie

w celu pozbycia sie tak wielu punktow

// wstrzymania, jak to tylko mozliwe.

if

(!

RemoveBreakpoint

(

breakpoints

,

hProcess

,

breakpoints

->

bps

.

begin

()->

first

))

{

success

=

FALSE

;

}

}

return

success

;

}

Przy pomocy owych funkcji oraz niewielkiej dozy wykorzystującego ich kodu
obsługującego wyjątki

EXCEPTION_BREAKPOINT oraz EXCEPTION_SIN-

GLE_STEP możemy zaimplementować w pełni funkcjonalny mechanizm bre-
akpointów, na podstawie którego da się budować już debuggery przeznaczo-
ne do konkretnych zadań. Przykład takiego debuggera przedstawiony został
w następnej sekcji.

PRAKTYCZNE ZASTOSOWANIE

Skoro potrafimy już nie tylko odbierać i obsługiwać podstawowe zdarzenia
debuggera, ale także deasemblować kod aplikacji, „przeskakiwać" o jedną in-
strukcję dalej oraz zatrzymywać działanie programu w konkretnym miejscu,
czas wykorzystać te umiejętności do zbudowania debuggera o konkretnym,
przydatnym zastosowaniu. W kolejnej sekcji pokażemy zatem, w jaki sposób
można stworzyć debugger śledzący i wypisujący wszystkie wykonywane in-
strukcje znajdujące się w głównym obrazie pliku wykonywalnego przy pomo-
cy mechanizmu single stepping, otrzymując proste narzędzie profilingowe,
zdolne wskazać, ile instrukcji programu wykonuje się w trakcie jego działania,
które instrukcje lub grupy instrukcji są najczęściej wykonywane oraz dostar-
czyć innych, potencjalnie użytecznych informacji.

background image

34

/ 3

. 2014 . (22) /

PROGRAMOWANIE SYSTEMOWE

Śledzenie działania programu

Schemat działania niniejszego rozwiązania przedstawia się następująco:

» W momencie wystąpienia zdarzenia CREATE_PROCESS_DEBUG_EVENT

ustawiamy punkt wstrzymania na punkt wejścia programu – pierwszą
instrukcję należącą do uruchamianej aplikacji, oraz zachowujemy adres
bazowy pliku wykonywalnego.

» W momencie wystąpienia wyjątku EXCEPTION_BREAKPOINT po raz

pierwszy (breakpoint wygenerowany domyślnie przez system), pobiera-
my informację o rozmiarze głównego obrazu wykonywalnego w pamięci
wirtualnej. W przypadku kolejnych wystąpień (wskazujących na natrafie-
nie na ustawiony przez nasz debugger punkt wstrzymania) obsługujemy
wyjątek w tradycyjny, opisany wcześniej sposób – tymczasowo przywra-
cając oryginalny bajt instrukcji oraz ustawiając Trap Flag w kontekście
procesora.

» W momencie wystąpienia wyjątku EXCEPTION_SINGLE_STEP, jeśli jest

to wyjątek związany z poprzednio obsłużonym breakpointem, przywra-
camy punkt wstrzymania. Ponadto, ponownie ustawiamy flagę TF oraz
deasemblujemy aktualną instrukcję, jeśli jej adres wpada w obliczony
wcześniej zakres pamięci głównego pliku wykonywalnego.

W zasadzie sposoby na wykonanie wszystkich oprócz jednej ze wspomnia-
nych wyżej operacji zostały już wcześniej opisane – nowym elementem jest
tu wyłącznie pobieranie informacji o rozmiarze sekcji odpowiadającej głów-
nemu plikowi wykonywalnego w pamięci.

Podczas zdarzenia

CREATE_PROCESS_DEBUG_EVENT w strukturze CRE-

ATE_PROCESS_DEBUG_EVENT otrzymujemy adres bazowy obrazu w polu
lpBaseOfImage – nie znajdziemy tam jednak rozmiaru owego obrazu. W
tym miejscu możemy posłużyć się jednak interfejsem o nazwie Process Sta-
tus API, oferującym zestaw funkcji i makr ułatwiających interakcję z innymi
procesami na niskim poziomie abstrakcji [8]. Jedną z funkcji dostępnych w
ramach owego interfejsu jest

GetModuleInformation, która na podstawie

uchwytu procesu oraz adresu bazowego wypełnia strukturę

MODULEINFO,

zawierającą m.in. rozmiar obrazu w polu

SizeOfImage. Funkcji tej możemy

użyć jednak dopiero, kiedy nowo tworzony, monitorowany proces w pełni za-
kończy inicjalizację, a więc najlepiej podczas pierwszego wystąpienia wyjątku
EXCEPTION_BREAKPOINT. Warto również pamiętać, że w celu korzystania z
PSAPI należy dołączyć w kodzie źródłowym nagłówek Psapi.h oraz linkować z
odpowiednią biblioteką (psapi.lib lub równoważną).

Przykładowa implementacja debuggera śledzącego działanie programu

została przedstawiona na Listingu 12. Wyszczególnione zostały wyłącznie
istotne fragmenty kodu obsługi zdarzeń

CREATE_PROCESS_DEBUG_EVENT

oraz

EXCEPTION_DEBUG_EVENT, gdyż to właśnie tam znajduje się prawie

cała logika naszej aplikacji.

Listing 12. Przykładowa implementacja debuggera wypisującego
wszystkie wykonane instrukcje należące do głównego pliku wyko-
nywalnego programu

case

CREATE_PROCESS_DEBUG_EVENT

:

{

if

(

debug_ev

.

u

.

CreateProcessInfo

.

hFile

!=

NULL

)

{

CloseHandle

(

debug_ev

.

u

.

CreateProcessInfo

.

hFile

)

;

}

processes

[

debug_ev

.

dwProcessId

]

=

debug_ev

.

u

.

CreateProcessInfo

.

hProcess

;

threads

[

debug_ev

.

dwThreadId

]

=

debug_ev

.

u

.

CreateProcessInfo

.

hThread

;

// Ustaw breakpoint na pierwszej instrukcji programu.

AddBreakpoint

(&

breakpoints

,

processes

[

debug_ev

.

dwProcessId

],

(

LPVOID

)

debug_ev

.

u

.

CreateProcessInfo

.

lpStartAddress

,

NULL

)

;

// Zapisz adres bazowy programu.

image_base

=

debug_ev

.

u

.

CreateProcessInfo

.

lpBaseOfImage

;

break

;

}

case

EXCEPTION_DEBUG_EVENT

:

{

cont_status

=

DBG_EXCEPTION_NOT_HANDLED

;

LPVOID

excp_address

=

debug_ev

.

u

.

Exception

.

ExceptionRecord

.

ExceptionAddress

;

switch

(

debug_ev

.

u

.

Exception

.

ExceptionRecord

.

ExceptionCode

)

{

case

EXCEPTION_BREAKPOINT

:

{

cont_status

=

DBG_CONTINUE

;

// Czy mamy do czynienia ze znanym breakpointem?

if

(

breakpoints

.

bps

.

find

(

excp_address

)

!=

breakpoints

.

bps

.

end

())

{

// Wywolaj zdefiniowana funkcje obslugi punktu wstrzymania.

if

(

breakpoints

.

bps

[

excp_address

]->

handler

!=

NULL

)

{

breakpoints

.

bps

[

excp_address

]->

handler

(

breakpoints

.

bps

[

excp_address

],

&

debug_ev

,

&

cont_status

)

;

}

// Ustaw Trap Flag w EFlags i napraw Eip.

SetThreadTrapFlag

(

threads

[

debug_ev

.

dwThreadId

])

;

// Zapamietaj, ze watek wymaga przywrocenia wyjatku przy

okazji

// nastepnego wyjatku SINGLE_STEP.

breakpoints

.

pending_bps

[

debug_ev

.

dwThreadId

]

=

excp_address

;

// Tymczasowo usun breakpoint.

RemoveBreakpoint

(&

breakpoints

,

processes

[

debug_

ev

.

dwProcessId

],

excp_address

)

;

}

else

/* Domyslny breakpoint generowany przez system

operacyjny */

{

MODULEINFO module_info

;

GetModuleInformation

(

processes

[

debug_ev

.

dwProcessId

],

(

HMODULE

)

image_base

,

&

module_info

,

sizeof

(

module_info

))

;

image_size

=

module_info

.

SizeOfImage

;

}

break

;

}

case

EXCEPTION_SINGLE_STEP

:

{

if

(

breakpoints

.

pending_bps

.

find

(

debug_ev

.

dwThreadId

)

!=

breakpoints

.

pending_bps

.

end

())

{

// Przywroc breakpoint.

AddBreakpoint

(&

breakpoints

,

processes

[

debug_ev

.

dwProcessId

],

breakpoints

.

pending_bps

[

debug_ev

.

dwThreadId

],

NULL

)

;

breakpoints

.

pending_bps

.

erase

(

debug_ev

.

dwThreadId

)

;

}

// Oznacz wyjatek jako poprawnie obsluzony.

cont_status

=

DBG_CONTINUE

;

// Ponownie ustaw Trap Flag.

SetThreadTrapFlag

(

threads

[

debug_ev

.

dwThreadId

])

;

// Deasembluj instrukcje, jesli nalezy do glownego obrazu

// wykonywalnego.

if

((

SIZE_T

)

excp_address

>=

(

SIZE_T

)

image_base

&&

(

SIZE_T

)

excp_address

<

((

SIZE_T

)

image_base

+

image_size

))

{

// Tutaj nastepuje deasemblacja aktualnej instrukcji

// przedstawiona na Listingu 5.

}

}

break

;

}

if

(!

debug_ev

.

u

.

Exception

.

dwFirstChance

)

{

TerminateProcess

(

processes

[

debug_ev

.

dwProcessId

],

0

)

;

}

break

;

}

Słowa wyjaśnienia może wymagać tutaj część obsługi wyjątku breakpointa,
w którym dekrementujemy wartość rejestru Eip tuż obok ustawienia flagi TF.
Po wykonaniu instrukcji „

int3” i przekazaniu kontroli do debuggera, rejestr

Eip jest przesunięty o 1 w przód, wskazując na to, co w tym momencie jest wg
procesora kolejną instrukcją do wykonania. Aby wznowić wykonanie instruk-
cji z przywróconym pierwszym bajtem, musimy „ręcznie” odjąć ów jeden bajt
podczas obsługi

EXCEPTION_BREAKPOINT.

background image

35

/ www.programistamag.pl /

JAK NAPISAĆ WŁASNY DEBUGGER W SYSTEMIE WINDOWS – CZĘŚĆ 2

Po uruchomieniu tak skonstruowanego debuggera na prostej aplikacji

przedstawionej na Listingu 13, naszym oczom powinna ukazać się lista około
450 linii, podobna do tej przedstawionej w skróconej formie poniżej:

[00401283] mov dword [esp], 0x1

[0040128a] call dword [0x4060f4]

[00401290] call 0xfffffd70

[00401000] push ebx

[00401001] sub esp, 0x38

...

[00401b73] mov eax, 0x1

[00401b78] add esp, 0x1c

[00401b7b] ret

[00401448] mov eax, 0x1

[0040144d] add esp, 0x1c

[00401450] ret 0xc

Listing 13. Prosty program testowy, na którym uruchamiany jest
nasz debugger

#include

<

cstdio

>

#include

<

cstdlib

>

#include

<

string

>

using

namespace

std

;

int

main

()

{

for

(

unsigned

int

i

=

0

;

i

<

10

;

i

++)

{

fprintf

(

stderr

,

"

Hello, world!

\n

"

)

;

}

return

0

;

}

Na podstawie tak skonstruowanego pliku możemy wyznaczyć najczęściej
wykonywane w programie regiony poprzez proste przetwarzanie przy użyciu
ciągu komend „

debugger.exe test.exe | sort | uniq – -count |

sort – n – r”:

11 [004013fe] jnz 0xffffffca

11 [004013fc] test al, al

11 [004013f9] setbe al

11 [004013f4] cmp dword [esp+0x1c], 0x9

10 [00401c30] jmp dword [0x406118]

10 [004013f0] inc dword [esp+0x1c]

10 [004013eb] call 0x845

10 [004013e4] mov dword [esp], 0x403064

10 [004013dc] mov dword [esp+0x4], 0x1

10 [004013d4] mov dword [esp+0x8], 0xe

10 [004013d0] mov [esp+0xc], eax

10 [004013cd] add eax, 0x40

10 [004013c8] mov eax, [0x4060fc]

2 [00401c70] jmp dword [0x4060c4]

2 [004019c8] jz 0x8

2 [004019c6] test ecx, ecx

2 [004019c0] mov ecx, [0x40503c]

Nasz debugger w obecnej formie jest dobrym punktem wyjściowym do bar-
dziej zaawansowanych interakcji z zewnętrzną aplikacją: reagowanie wyłącz-
nie na konkretny typ instrukcji, zmiana semantyki lub wręcz całkowita emu-
lacja niektórych instrukcji, monitorowanie częstotliwości i rodzaju dostępu
do pamięci, zrzucanie pamięci w wybranych momentach działania aplikacji
itp. Jak przekonamy się w kolejnych artykułach z serii, opisane w tym nume-
rze mechanizmy znajdują zastosowanie w debuggerach implementujących
właściwie dowolną funkcjonalność, lecz jest to dopiero czubek góry lodowej
– na opisanie wciąż oczekują inne typy punktów wstrzymania, pomocnicza
biblioteka DbgHelp i oferowane przez nią możliwości, a także kolejne przy-
kłady zastosowań dedykowanych debuggerów w codziennej pracy progra-
mistów języków natywnych. To wszystko znajdzie swoje miejsce w tekstach
publikowanych w nadchodzących miesiącach – tymczasem już teraz serdecz-
nie zachęcamy czytelników do samodzielnych eksperymentów z Debug API.
Miłej zabawy!

W sieci

P [1]

http://j00ru.vexillium.org/?p=866

P [2]

https://code.google.com/p/distorm/

P [3]

http://www.beaengine.org/

P [4]

http://bastard.sourceforge.net/libdisasm.html

P [5]

http://udis86.sourceforge.net/

P [6]

http://www.theregister.co.uk/2010/11/15/amd_secret_debugger/

P [7]

http://hardware.slashdot.org/story/10/11/12/047243/hidden-debug-mode-found-in-amd-processors

P [8]

http://msdn.microsoft.com/en-us/library/windows/desktop/ms684884%28v=vs.85%29.aspx

Mateusz “j00ru” Jurczyk

j00ru.vx@gmail.com

Mateusz specjalizuje się w metodach odnajdowania oraz wykorzystywania podatności w po-
pularnych aplikacjach klienckich oraz systemach operacyjnych. Na codzień pracuje w firmie
Google na stanowisku Information Security Engineer, w wolnych chwilach prowadzi bloga
związanego z bezpieczeństwem niskopoziomowym (

http://j00ru.vexillium.org

).

reklama

background image

36

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Tomasz Nurkiewicz

A

kka czerpie wiele inspiracji z Erlanga, próbując przenieść do wirtual-
nej maszyny Javy wydajność, stabilność i skalowalność znaną z języka
Erlang. Programy napisane pod kontrolą Akki nie korzystają jawnie z

wątków i nie współdzielą globalnej pamięci. Zamiast tego otrzymujemy do
dyspozycji aktorów – lekkie obiekty współpracujące ze sobą, jedynie przesy-
łając sobie nawzajem komunikaty (obiekty). System napisany w Akka przypo-
mina raczej graf niezależnych procesów wysyłających sobie wiadomości. Bez
zbędnego zwlekania spójrzmy, jak stworzyć prostego aktora.

TWORZENIE AKTORÓW

Poniższy kod pokazuje, jak utworzyć system (kontener) aktorów, powołać do ży-
cia jednego aktora i wysłać mu komunikat. Ponieważ Akka jest napisana w Scali,
większość przykładów w tym artykule również używa tego języka. Niemniej jed-
nak Akka oferuje natywne API dla Javy, które poznany w stosownej chwili.

Listing 1. Tworzenie jednego aktora

case

class

Welcome(name: String)

class

WelcomeActor

extends

Actor {

override def receive = {

case

Welcome(n) =>

println(s

"Hello, $n"

)

}

}

object Main

extends

App {

val system = ActorSystem(

"Magazyn"

)

val actor: ActorRef =

system.actorOf(Props[WelcomeActor])

actor ! Welcome(

"Tomek"

)

println(

"Sent"

)

Thread.sleep(1000)

system.shutdown()

}

Ten prosty fragment kodu posłuży nam do wprowadzenia większości funda-
mentalnych cech Akki. Klasa

Welcome to komunikat, który wyślemy aktorowi

WelcomeActor. Teoretycznie jako komunikatów możemy używać dowol-
nych struktur danych (np. samego typu

String), jednak przyjęło się korzy-

stać z

case class, domyślnie niezmiennych (ang . immutable) odpowiedni-

ków Java beanów. Ponieważ komunikaty są wysyłane przez jednego aktora,
a odbierane przez innego, bezwzględnie powinny być niezmienne, aby unik-
nąć przypadkowego współdzielenia i modyfikowania tej samej struktury da-
nych. Dalej widzimy definicję aktora

WelcomeActor. Jest to zwykły, niewielki

obiekt dziedziczący po

akka.actor.Actor i implementujący metodę re-

ceive. Za każdym razem, gdy do aktora zostanie wysłany jakikolwiek komu-
nikat, aktor decyduje, czy i jak go obsłużyć. W naszym przypadku odbieramy
wiadomość

Welcome i wypisujemy jej treść na ekran.

Obiekt

Main to kompletny program uruchamiający Akka. Najpierw two-

rzymy

ActorSystem o wskazanej nazwie. Następnie prosimy system o stwo-

rzenie dla nas aktora z implementacją

WelcomeActor. Proszę zwrócić uwagę

na wartość

actor – nie jest ona typu WelcomeActor – mimo że aktora tego

typu stworzyliśmy – ale

akka.actor.ActorRef. Akka bardzo skrzętnie

ukrywa przed nami instancję aktora, opakowując ją w referencję

ActorRef.

Są po temu dwa ważne powody: po pierwsze, nigdy nie powinniśmy mieć
dostępu do stanu oraz metod aktora. Jedyna komunikacja z aktorem ma się
odbywać poprzez wymianę komunikatów. Po drugie,

ActorRef dba o to, aby

do aktora nigdy nie trafił więcej niż jeden komunikat w danej chwili. Innymi
słowy jedną z najsilniejszych gwarancji dostarczanych przez framework jest
jednowątkowe wywoływanie metody

receive każdego aktora. Więcej niż

jeden wątek nigdy nie zostanie dopuszczony do metody

receive.

Enigmatyczne wywołanie

actor ! Welcome("Tomek"), składnią nawiązu-

jące do Erlanga, powoduje asynchroniczne wysłanie komunikatu

Welcome do

aktora

actor. Zatrzymajmy się na chwilę przy tym wyrażeniu. Wysłany komu-

nikat

Welcome zostanie obsłużony asynchronicznie (tj. w innym wątku) poprzez

wywołanie

receive docelowego aktora. Nie mamy zatem żadnej gwarancji,

czy napis "

Sent" pojawi się na ekranie przed czy po "Hello, Tomek". Ponadto

gdybyśmy do tego samego aktora wysłali tysiące wiadomości, nawet z wielu róż-
nych wątków, obsługa kolejnego komunikatu nastąpiłaby dopiero po skończo-
nej obsłudze poprzedniego. Na chwilę obecną możemy sobie wyobrazić aktora
jako kolejkę komunikatów oraz dokładnie jeden dedykowany wątek (per aktor)
pobierający kolejne komunikaty i wołający

receive. Akka nie jest tak zaimple-

mentowana, ale pomoże nam to wyobrazić sobie, co oferuje framework. Skoro
aktor jest w praktyce jednowątkowy, poniższy kod jest całkowicie bezpieczny,
nawet jeśli nasz aktor byłby wołany przez setki innych aktorów czy wątków:

Listing 2. Aktor ze stanem wewnętrznym

class

WelcomeActor

extends

Actor {

private

var seenNames =

new

HashSet[

String

]()

override def receive = {

case

Welcome(n) =>

if

(seenNames contains n) {

println(s

"Ignoring $n"

)

}

else

{

seenNames += n

println(s

"Hello, $n"

)

}

}

}

Gdyby metoda

receive mogła być zawołana przez dowolnie wiele wątków

jednocześnie, musielibyśmy jakoś synchronizować dostęp do kolekcji

seen-

Names. Jednak dzięki Akka, nawet jeśli w jednej chwili setki wątków jedno-
cześnie zasypie naszego aktora komunikatami, będą one obsługiwane jeden
po drugim. Jak nietrudno zauważyć, gdy kolejka (w nomenklaturze Akka:
mailbox) aktora rośnie, czas jego reakcji może nieoczekiwanie rosnąć. Istnieje

Akka – wydajny szkielet dla aplikacji

wielowątkowych

Akka to framework radykalnie zmieniający sposób pisania skalowalnych, wielową-
tkowych aplikacji. Zamiast ręcznego zarządzania wątkami oraz wymiany informacji
poprzez współdzieloną pamięć i synchronizację, Akka proponuje model obliczeń
oparty o aktorów. Każdy aktor jest niezależnym, wyizolowanym obiektem, a komu-
nikacja pomiędzy nimi odbywa się jedynie poprzez asynchroniczną wymianę komu-
nikatów. Takie podejście stwarza zupełnie nowe możliwości, ale również wyzwania.

background image

37

/ www.programistamag.pl /

AKKA – WYDAJNY SZKIELET DLA APLIKACJI WIELOWĄTKOWYCH

wiele technik, by temu zapobiec, ale jedną z najważniejszych jest unikanie
blokowania w metodzie

receive. Spanie (Thread.sleep()), oczekiwanie

na blokadach i semaforach czy wszelkie formy aktywnego oczekiwania (ang.
busy waiting) są niemile widziane. W praktyce blokowanie w

receive stano-

wi antywzorzec i należy go unikać za wszelką cenę. Jest to niestety szczegól-
nie trudne w przypadku wejścia-wyjścia oraz istniejących bibliotek.

Aktor może naturalnie obsłużyć więcej niż jeden rodzaj komunikatów:

Listing 3. Wiele komunikatów obsługiwanych przez aktora

case

class

Welcome(name: String)

case

class

GoodBye(name: String)

class

WelcomeActor

extends

Actor {

override def receive = {

case

Welcome(n) =>

println(s

"Hello, $n"

)

case

GoodBye(n) =>

println(s

"Bye, $n"

)

}

}

//...

actor ! Welcome("Tomek")

actor ! GoodBye("Tomek")

Nie zastanawialiśmy się jednak jeszcze, co się stanie, gdy do aktora wyślemy
nieobsługiwany komunikat? W takiej sytuacji Akka zawoła metodę

Actor.

unhandled(), której domyślna implementacja publikuje specjalny komu-
nikat

UnhandledMessage i w konsekwencji loguje wiadomość na ekranie.

Ponieważ wysyłający komunikat nigdy nie dowie się (np. poprzez wyjątek),
że jego wiadomość dotarła do aktora, ale ten nie umiał jej obsłużyć, przesło-
nięcie metody

unhandled() własną implementacją jest dobrym pomysłem.

KOMUNIKACJA POMIĘDZY AKTORAMI

Dotychczas stworzyliśmy tylko jednego aktora. Powiedzieliśmy też, że w pew-
nym sensie z każdym aktorem związany jest jeden dedykowany wątek do ob-
sługi jego skrzynki odbiorczej (kolejki przychodzących komunikatów). Jest to
wygodna metafora myślowa, całe szczęście Akka nie jest tak zaimplementowa-
na. W praktyce niewielką pulą wątków dzielą się wszyscy aktorzy w systemie – a
ponieważ aktor nie powinien blokować wątku w jakikolwiek sposób, o czym już
mówiliśmy, system pracuje wydajnie nawet na zaledwie kilkunastu wątkach.

Sam aktor jest bardzo lekkim obiektem, twórcy frameworku twierdzą, że

możemy stworzyć do dwóch i pół miliona aktorów na jeden gigabajt pamięci.
Stąd też nie powinniśmy się bać tworzenia i utrzymywania dużej ilości akto-
rów w ramach jednego systemu. Często na potrzeby podziału pracy replikuje-
my jednego aktora, jeszcze częściej tworzymy nowego aktora np. dla każdego
żądania HTTP czy użytkownika. Poniższy przykład pokazuje jednego aktora,
który podczas tworzenia powołuje do życia drugiego (

EmailActor) i wysyła

mu komunikat w odpowiedniej chwili:

Listing 4. Dwóch współpracujących aktorów

case

class

Welcome(name: String)

class

WelcomeActor

extends

Actor {

private

val emailActor =

context.actorOf(Props[EmailActor])

override def receive = {

case

Welcome(n) =>

println(s

"Hello, $n"

)

emailActor ! Email(n +

"@example.com"

)

}

}

case

class

Email(address: String)

class

EmailActor

extends

Actor {

override def receive = {

case

Email(address) =>

println(s

"Sending e-mail to $address"

)

}

}

Warto zwrócić uwagę, jak podczas obsługi wiadomości

Welcome wysyłamy

inną wiadomość do

EmailActor. Ponownie cała komunikacja jest asynchroni-

czna. Alternatywna implementacja mogłaby tworzyć

EmailActor na żądanie:

Listing 5. Tworzenie jednorazowego aktora na żądanie

class

WelcomeActor

extends

Actor {

override def receive = {

case

Welcome(n) =>

println(s

"Hello, $n"

)

val emailActor =

context.actorOf(Props[EmailActor])

emailActor ! Email(n +

"@example.com"

)

}

}

case

class

Email(address: String)

class

EmailActor

extends

Actor {

override def receive = {

case

Email(address) =>

println(s

"Sending e-mail to $address"

)

context.stop(self)

}

}

Wywołanie

context.stop(self) jest bardzo istotne. Wyrażenie self

przypomina

this, ale zamiast zwracać referencję do bieżącego obiektu,

zwraca referencję do

ActorRef, opakowującego naszego aktora. Ponieważ

EmailActor jest tworzony na żądanie i tylko do obsługi tego jednego ko-
munikatu, musimy pamiętać o jego wyłączeniu. W przeciwnym wypadku cią-
gle tworzone nowe instancje

EmailActor powodowałyby wyciek pamięci.

Alternatywnym rozwiązaniem byłoby wysłanie do utworzonego aktora spe-
cjalnego komunikatu:

emailActor ! PoisonPill. PoisonPill jest rozu-

mianym przez każdego aktora komunikatem, powodującym jego wyłączenie.

GWARANCJE DOSTARCZENIA

WIADOMOŚCI

Ponieważ cała komunikacja w Akka jest asynchroniczna, z reguły wysyłający
żądanie nie wie, kiedy i czy w ogóle dotarło ono do adresata. Tak rozluźnione
gwarancje po pierwsze ułatwiają implementację. Przede wszystkim jednak
nie stwarzają iluzji niezawodności, tak trudnej do zapewniania, zwłaszcza w
rozproszonych systemach. O ile w ramach jednej maszyny wirtualnej Akka
zapewnia dostarczenie komunikatu do odbiorcy, o tyle w przypadku zdal-
nych aktorów takiej pewności nie mamy nigdy. Z tego powodu zawsze po-
winniśmy oczekiwać odpowiedzi na każde żądanie, jeśli chcemy mieć pew-
ność dostarczenia. Nie nauczyliśmy się jednak jak dotąd wysyłać i odbierać
odpowiedzi. Wyobraźmy sobie, że poznany wcześniej

EmailActor pragnie

potwierdzić poprawne wysłanie e-maila po otrzymaniu komunikatu

Email:

Listing 6. Wykorzystanie referencji do nadawcy

case

class

Welcome(name: String)

class

WelcomeActor

extends

Actor {

private

val emailActor =

context.actorOf(Props[EmailActor])

override def receive = {

case

Welcome(n) =>

println(s

"Hello, $n"

)

emailActor ! Email(n +

"@example.com"

)

case

EmailSent(addr) =>

println(s

"Welcome sent to $addr"

)

}

}

case

class

Email(address: String)

case

class

EmailSent(address: String)

class

EmailActor

extends

Actor {

override def receive = {

case

Email(address) =>

println(s

"Sending e-mail to $address"

)

sender() ! EmailSent(address)

}

}

background image

38

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Wspomnieliśmy wcześniej, że komunikacja między aktorami jest zawsze

jednokierunkowa. Nie jest to do końca prawdą. Wewnątrz metody

receive

mamy dostęp do specjalnej metody

sender(), która zwraca ActorRef,

wskazujący na aktora-nadawcę. Innymi słowy, obsługując komunikat, zawsze
wiemy, od kogo on pochodzi. Co prawda w naszym wypadku wiemy, że ko-
munikat

Email pochodzi od WelcomeActor, ale czynimy EmailActor bar-

dziej elastycznym, nie wiążąc się na stałe z tym aktorem. Zamiast tego wysyła-
my komunikat z potwierdzeniem

EmailSent do kogokolwiek, kto uprzednio

wysłał

Email. Widać doskonale, jak WelcomeActor deklaruje zainteresowa-

nie komunikatem

EmailSent i go obsługuje wewnątrz receive.

Warto sobie zadać pytanie, co wskazuje metoda

sender(), gdy komunikat

do aktora został wysłany spoza Akki, tj. nie z aktora? Ponieważ nadawca wiado-
mości wtedy nie istnieje,

sender() wskazuje specjalnego aktora systemowego,

zwanego

deadLetters. W praktyce wiadomość taka jest tracona. Co jednak,

jeśli chcemy mimo wszystko zapytać aktora i otrzymać odpowiedź? Na szczęście
Akka w takiej sytuacji może stworzyć niewielkiego, tymczasowego aktora, peł-
niącego rolę jednorazowego nadawcy. Ten tymczasowy aktor zniknie w chwili
otrzymania jednej odpowiedzi. Dla nas mechanizm ten jest niewidoczny:

Listing 7. Tymczasowy aktor i wzorzec ask

implicit val timeout: Timeout = 1.second

import concurrent.ExecutionContext.Implicits.global
val future: Future[Any] = actor ? Identify(())

future.andThen{

case Success(ActorIdentity(_, Some(a))) =>

println(s"Found $a")

case _ =>

println("Not found")

}

Powyższy listing wymaga dogłębnej analizy. Każdy aktor rozumie komuni-
kat

Identify i automatycznie odpowiada komunikatem ActorIdentity.

Dzięki temu mechanizmowi możemy odpytać każdy

ActorRef, czy „pod

spodem” jest żyjący, prawidłowo funkcjonujący aktor. Może się bowiem zda-
rzyć, że referencja, którą dysponujemy, wskazuje nieosiągalnego, nieodpo-
wiadającego bądź zatrzymanego aktora.

Wracając do kodu, pierwsze dwie linie definiują czas oczekiwania na od-

powiedź oraz pulę wątków, w której zostanie obsłużony komunikat powrotny.
Właściwe odpytanie aktora odbywa się przy użyciu operatora

?, który zastępuje

znany nam wykrzyknik. Operator ten nosi nazwę „ask” i tak też można go wywo-
łać:

actor ask Identify(()). O ile zwykłe wysłanie komunikatu jest wyraże-

niem typu

Unit, o tyle pytajnik zwraca Future[Any]. Obiekt typu Future jest

uchwytem do rezultatu, który jeszcze nie nadszedł – wszak wysłaliśmy dopiero
żądanie i dopiero czekamy na odpowiedź. Ostatnie wyrażenie (

andThen) reje-

struje kod, który wykona się, gdy nadejdzie odpowiedź. Jeśli będzie ona w nie-
oczekiwanym formacie lub będziemy na nią czekali zbyt długo, zobaczymy błąd.

Warto jeszcze raz podkreślić, że Akka obsługując komunikat

Identify

lub jakikolwiek inny, jako nadawcę widzi specjalnego tymczasowego aktora.
Gdybyśmy komunikat taki wysłali z innego aktora, używając wykrzyknika ("

!",

zwanego tell), nadawcą byłby po prostu ten aktor.

NADZÓR I HIERARCHIA

Dotychczas stworzyliśmy dwóch aktorów:

WelcomeActor i EmailActor. O

ile ten pierwszy został stworzony „z zewnątrz”, o tyle ten drugi utworzyliśmy
wewnątrz

WelcomeActor. Okazuje się, że ta różnica ma kolosalne znaczenie.

Otóż działający aktorzy tworzą drzewo: jeśli jeden aktor tworzy innego, staje
się on jego rodzicem, a wszyscy wykreowani aktorzy – dziećmi. Każdy aktor
może również dowiedzieć się, kto jest jego rodzicem (nadzorcą – a zatem kto
go stworzył), korzystając z

context.parent, oraz poznać swoje dzieci (con-

text.children). Dzięki temu uzyskujemy możliwość wysłania komunikatu
do naszego rodzica bądź propagacji wiadomości do wszystkich dzieci. Nie to
jest jednak najważniejsze.

Okazuje się, że prawem i obowiązkiem każdego aktora jest nadzorowanie

pracy swoich dzieci. Jeśli dziecko aktora podczas obsługi komunikatu z jakie-
gokolwiek powodu rzuci wyjątkiem, rodzic (nadzorca) zostanie o tym poin-
formowany. Odpowiedzialność nie kończy się jednak na tym. Aktor nadzoru-
jący może zdecydować, czy wadliwie pracujące dziecko może kontynuować,
a może powinno zostać wyłączone i zastąpione nowym? Możemy również
zażądać wymiany wszystkich nadzorowanych aktorów, a nie tylko wadliwego
– czy wręcz eskalować problem wyżej. Oto krótki przykład, pokazujący jak
aktor-rodzic może reagować na błędy swoich dzieci:

Listing 8. Własna implementacja

supervisorStrategy

class

WelcomeActor

extends

Actor {

override def supervisorStrategy = OneForOneStrategy(){

case

_:

NullPointerException

=> Restart

case

f:

FileNotFoundException

if

f.getMessage contains

"server.conf"

=> Escalate

case

_:

ConnectException

=> Resume

}

Przesłaniając metodę

supervisorStrategy, możemy zdecydować, jak za-

reaguje aktor na różne wyjątki, rzucone przez którekolwiek z dzieci. Dostępne
instrukcje to:

» Resume – pozwala kontynuować pracę dziecka, jeśli wyjątek jest

niekrytyczny

» Restart – Akka niszczy aktora i tworzy nową instancję. Oczywiście stan

aktora jest tracony (resetowany). Istniejące referencje na aktora (

Actor-

Ref) nie tracą ważności, ale wysyłają komunikaty do nowej instancji

» Stop – jw., ale aktor nie jest ponownie tworzony

» Escalate – propagacja błędu wyżej w hierarchii

Dodatkowo mamy do wyboru dwie strategie:

OneForOneStrategy i All-

ForOneStrategy. W przypadku tej drugiej akcje Stop i Restart będą do-
tyczyły wszystkich dzieci, a nie tylko tego, w którym wystąpił błąd. Restarto-
wanie aktorów wydaje się dość drastycznym zachowaniem, ale pozwala na
utrzymanie stabilnego stanu aplikacji, pomimo występowania błędów. Utrata
kilku transakcji czy komunikatów w wielu przypadkach jest bezpieczniejsza
od pozostawiania całego systemu w niespójnym stanie. Domyślnie strategia
restartuje każdego aktora, który wyrzuci wyjątek podczas obsługi komunikatu.

Warto zapamiętać, że o błędzie w aktorze zawsze dowiaduje się jego ro-

dzic, a nie aktor, który wysłał problematyczny komunikat. Możliwe jest jednak
bycie informowanym o zatrzymaniu dowolnego aktora w systemie, nie tylko
naszego dziecka. W tym celu musimy posiadać referencję (

ActorRef) aktora,

którego chcemy monitorować:

Listing 9. Monitorowanie życia dowolnego aktora w systemie

context watch someActor

override def receive = {

case

Terminated(actor) =>

println(

"Oops!"

)

//...

}

Jeśli aktor wskazywany przez referencję

someActor zatrzyma się, otrzymamy

komunikat

Terminated. Warto zwrócić uwagę, że nie zostaniemy poinfor-

mowani o restartach monitorowanego aktora – one powinny pozostać dla
nas niewidoczne.

WYSZUKIWANIE AKTORÓW

W dotychczasowych przykładach jeden aktor komunikował się z drugim, któ-
rego sam stworzył. Z reguły jednak zachodzi konieczność bardziej złożonej
komunikacji, nie tylko wzdłuż relacji rodzic-dziecko. Aby wysłać komunikat
do aktora, musimy znać jego referencję (

ActorRef). Istnieje kilka technik

background image

39

/ www.programistamag.pl /

AKKA – WYDAJNY SZKIELET DLA APLIKACJI WIELOWĄTKOWYCH

zdobycia takiej referencji: przekazanie aktorowi przez konstruktor podczas
tworzenia, wysłanie referencji w komunikacie oraz wyszukanie aktora w drze-
wie po nazwie. Zajmijmy się tą ostatnią możliwością. Każdego aktora można
opcjonalnie nazwać, co jest dobrą praktyką. Skoro aktorzy tworzą strukturę
drzewiastą, każdego aktora można odnaleźć, korzystając z hierarchicznych
ścieżek, podobnie jak w systemie plików:

Listing 10. Tworzenie i wyszukiwanie nazwanych aktorów

val actor: ActorRef =

system.actorOf(Props[WelcomeActor], "welcome")

//...

class

WelcomeActor

extends

Actor {

private

val emailActor =

context.actorOf(Props[EmailActor],

"email"

)

//...

}

//...

val w = system.actorSelection("/user/welcome")

val e = system.actorSelection("/user/welcome/email")

w ! Welcome("W")

Tym razem aktorzy otrzymali nazwy

"welcome" i "email". Następnie wi-

dzimy, w jaki sposób można odnaleźć dowolnego aktora w systemie, znając
jedynie jego nazwę. Technicznie rzecz biorąc, wartość zwrócona przez

ac-

torSelection() nie jest typu ActorRef, ale na nasze potrzeby możemy
ją tak traktować. Przykład ilustruje, że bazowym aktorem dla wszystkich
aktorów stworzonych przez nas jest

/user. Ponieważ EmailActor został

utworzony wewnątrz

WelcomeActor, jego nazwa używa nazwy welcome

jako rodzica. O wiele ciekawsze jest wyszukiwanie aktorów wewnątrz kodu
innego aktora. Możliwe wtedy staje się wyszukiwanie przy użyciu ścieżek
względnych. Wyobraźmy sobie, że chcemy wysłać wiadomość do wszystkich
naszych aktorów-dzieci. W tym celu możemy się posłużyć wyrażeniem

con-

text.actorSelection("*") ! msg. Nawiasem mówiąc, prostszym roz-
wiązaniem będzie użycie wbudowanej metody

context.children. Może-

my też pokusić się o poszukanie „rodzeństwa”, czyli innych aktorów mających
tego samego rodzica, co my. W tym celu możemy nawigować do „katalogu”
nadrzędnego i poszukać dzieci prostym wyrażeniem:

context.actorSe-

lection("../*"). Wyobraźmy sobie teraz, że aktor welcome ma dwóch ak-
torów potomnych:

email1 i email2. Wewnątrz aktora email1 można łatwo

uzyskać referencję do aktora

email2, używając wyrażenia "../email2".

Wspomnieliśmy wcześniej, że wynik wyrażenia

actorSelection()

można traktować jak referencję na aktora, ale nie do końca. Po pierwsze
obiekt

ActorSelection (bo taki typ zwraca actorSelection()) może

reprezentować więcej niż jednego aktora, co umożliwia implementację roz-
głaszania komunikatów do wielu aktorów docelowych. Jednak co ważniejsze,
wyszukanie docelowych aktorów odbywa się leniwie, dopiero w chwili wy-
słania komunikatu. Ma to znaczenie głównie w przypadku zdalnych aktorów,
którzy z przyczyn od nas niezależnych mogą pojawiać się i znikać w czasie
życia aplikacji.

STANOWOŚĆ I ZMIANA

ZACHOWANIA

Poznani dotychczas aktorzy posiadali jedną metodę

receive, zajmującą się

obsługą komunikatów. Okazuje się jednak, że zaskakująco często zbiór ob-
sługiwanych komunikatów oraz sposób reagowania na nie zmienia się we-
wnątrz jednego aktora. Wyobraźmy sobie prostego aktora, który otrzymuje
liczby całkowite i początkowo je ignoruje. Jednak gdy otrzyma komunikat
Subscribe, zaczyna przesyłać sumę dotychczas otrzymanych liczb do wy-
branego aktora. Do stanu pierwotnego można wrócić, wysyłając komunikat
Unsubscribe. Dość prymitywna implementacja powyższej logiki wygląda-
łaby następująco:

Listing 11. Naiwna implementacja aktora posiadającego dwa różne
zachowania

case

class

Subscribe(target: ActorRef)

case object Unsubscribe

class

Adder

extends

Actor {

private

var target: Option[ActorRef] = None

private

var sum = 0

private

var ignored = 0

override def receive = {

case

Subscribe(r) =>

if

(target.isEmpty) {

target = Some(r)

println(s

"Ignored: $ignored"

)

ignored = 0

}

case

Unsubscribe =>

target = None

sum = 0

case

x: Int =>

target match {

case

Some(t) =>

sum += x

t ! sum

case

None =>

ignored += 1

}

}

}

Implementacja ta jest raczej prymitywna, ponieważ aktor ma wyraźnie dwa
stany – gdy inny aktor jest ustawiony jako odbiorca (

target) lub nie. Ponadto

zmienna

ignored ma sens tylko przy nieustawionym odbiorcy, a sum – tylko

przy ustawionym. Wreszcie należy zauważyć, że sposób obsługi komunika-
tów różni się, w zależności od tego, w jakim stanie jest obecnie aktor. Zajmij-
my się najpierw tym ostatnim problemem. Okazuje się, że zamiast metody
receive możemy użyć dowolnej innej (o zgodnej sygnaturze), a tej zmiany
można dokonać w dowolnej chwili przy użyciu metody

become:

Listing 12. Stanowy aktor, używający metody

become

class

Adder

extends

Actor {

private

var target: Option[ActorRef] = None

private

var sum = 0

private

var ignored = 0

def receive = receiveWhenUnsubscribed

def receiveWhenUnsubscribed: Receive = {

case

Subscribe(r) =>

target = Some(r)

println(s

"Ignored: $ignored"

)

ignored = 0

context become receiveWhenSubscribed

case

Unsubscribe =>

//ignore

case

x: Int =>

ignored += 1

}

def receiveWhenSubscribed: Receive = {

case

Subscribe(r) =>

//ignore

case

Unsubscribe =>

target = None

sum = 0

context become receiveWhenUnsubscribed

case

x: Int =>

sum += x

target.get ! sum

}

}

Na początku przypisujemy do metody

receive metodę receiveWhenUn-

subscribed, co oznacza, że to właśnie ona będzie przy starcie odpowiedzial-
na za obsługę komunikatów. Warto zwrócić uwagę, że nie ma już warunkowej
logiki zarówno w

receiveWhenUnsubscribed, jak i w receiveWhenSub-

scribed. Wiemy bowiem, w jakim stanie jest aktor w chwili odebrania ko-
munikatu. Część przychodzących wiadomości traci sens, jeśli są odebrane w
niewłaściwym stanie. Gdybyśmy pominęli je zupełnie w różnych implemen-

background image

40

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

tacjach

receive, kod działałby tak samo. Niestety nieobsłużony komunikat

trafia jako zdarzenie na wewnętrzną szynę komunikatów i generuje ostrzeże-
nie w logach. Z tego względu lepiej po prostu ignorować takie komunikaty
już na poziomie samego aktora.

Powiedzieliśmy, że przypisanie

receive = receiveWhenUnsubscribed

deklaruje, która metoda zamiast

receive ma obsługiwać komunikat. Jednak

co ciekawsze, możliwa jest podmiana aktualnej metody obsługującej komu-
nikaty „w locie”. Służy do tego wywołanie

context.become(...). Łatwo w

powyższym kodzie zauważyć miejsca, gdzie przechodzimy ze stanu

Unsub-

scribed w stan Subscribed i z powrotem. Akka potrafi również zapamiętać
wszystkie wywołania

become na specjalnym stosie i wracać do poprzednich

w odwrotnej kolejności (

unbecome). Nasz kod jest co prawda dłuższy, ale

znacznie łatwiejszy do analizy. Niestety nadal zmienne wykorzystywane tylko
w jednym stanie są widoczne globalnie. Ponadto kompilator nie ostrzeże nas
przed niepoprawnym wykorzystaniem opcjonalnej zmiennej

target (bez-

warunkowe wywołanie

Option.get w target.get zawsze powinno bu-

dzić zastrzeżenia). Istnieje jednak sprytna składniowa sztuczka, która domyka
zmienne tak, aby były widoczne tylko w konkretnym stanie – oraz usuwa ko-
nieczność ich czyszczenia:

Listing 13. Domknięcia na zmiennych stanu aktora

class

Adder

extends

Actor {

def receive = receiveWhenUnsubscribed(0)

def receiveWhenUnsubscribed(ignored: Int): Receive = {

case

Subscribe(r) =>

println(s

"Ignored: $ignored"

)

context.become(

receiveWhenSubscribed(0, r))

case

Unsubscribe =>

//ignore

case

x: Int =>

context.become(

receiveWhenUnsubscribed(

ignored + 1))

}

def receiveWhenSubscribed(

sum:

Long

,

target: ActorRef): Receive = {

case

Subscribe(r) =>

//ignore

case

Unsubscribe =>

context.become(

receiveWhenUnsubscribed(0))

case

x: Int =>

context.become(

receiveWhenSubscribed(

sum + x, target))

target ! sum

}

}

Na pozór aktor taki jest zupełnie bezstanowy, bowiem wszystkie zmienne
związane z aktualnym stanem zostały domknięte w metodach

receive*.

Gdy pragniemy zmienić stan bądź zmienne stanu, po prostu wołamy

con-

text.become(...). Kod powyżej jest zdecydowanie najtrudniejszy do
analizy w pierwszej chwili, ale dzięki takiej strukturze kompilator może nam
pomóc w uniknięciu potencjalnych błędów. Gdyby okazało się, że nawet taka
mikro-architektura nam nie wystarcza, Akka dostarcza pełne wsparcie dla
stanowych aktorów (włącznie z definiowaniem grafu stanów i przejść) przy
użyciu cechy

akka.actor.FSM.

HARMONOGRAMOWANIE ZADAŃ

Bardzo często nasz aktor będzie potrzebował koordynować swoją pracę z cza-
sem. Przykładowe przypadki użycia:
1. Aktor po odebraniu komunikatu powinien otrzymać drugi nie później niż

po sekundzie.

2. Po odebraniu komunikatu powinniśmy na niego zareagować dopiero po

sekundzie.

3. Raz na sekundę musimy wysłać komunikat innemu aktorowi, by spraw-

dzić, czy tamten jest ciągle aktywny.

Na pierwszy rzut oka moglibyśmy po prostu uśpić bieżący wątek

(

Thread.sleep()) na określony czas i bez trudu zaimplementować powyż-

sze wymagania. Niestety w Akka wszelka forma spania, blokowania, aktyw-
nego oczekiwania etc. jest surowo wzbroniona, ponieważ może w krótkim
czasie doprowadzić do spowolnienia, destabilizacji czy nawet zakleszczenia
systemu. Jednak ponieważ tego rodzaju scenariusze są niezwykle częste,
Akka ma wbudowane narzędzie do harmonogramowania (ang. scheduler).
Najprostszym, niejawnym wykorzystaniem schedulera jest określenie maksy-
malnego czasu oczekiwania na dowolny przychodzący komunikat:

Listing 14. Informowanie aktora, jeśli zbyt długo czeka na komunikat

case object Beat

class

HeartBeatReceiver

extends

Actor {

override def preStart() = {

context setReceiveTimeout 1.second

}

def receive = {

case

Beat =>

println(

":-)"

)

case

ReceiveTimeout =>

println(

":-("

)

}

}

Aktor

HeartBeatReceiver monitoruje pracę jakiegoś systemu, który po-

winien przysyłać komunikaty kontrolne (

Beat) częściej, niż raz na sekundę.

Metoda

context,setReceiveTimeout() sprawia, że jeśli w ciągu sekun-

dy nie nadejdzie jakikolwiek komunikat, do naszego aktora zostanie wysła-
ny

ReceiveTimeout. Oznacza on, że przez ponad sekundę nie odebraliśmy

żadnego komunikatu. Oczywiście za każdym razem, gdy odbieramy jakikol-
wiek komunikat, licznik czasu jest restartowany.

Poniższy przykład ilustruje implementację aktora, który każdy otrzy-

many komunikat wysyła do innego wskazanego aktora, ale dopiero po
sekundzie:

Listing 15. Metoda

scheduleOnce()

class

DelayedActor

extends

Actor {

private

implicit val ec = context.dispatcher

def receive = {

case

(msg, target: ActorRef) =>

context.system.scheduler.scheduleOnce(

1.second, target, msg

)

}

}

//...

delayed ! ("Foo" – > target)

delayed ! ("Bar" – > target)

Aktor rozumie komunikaty będące parami „dowolny obiekt – > aktor do-
celowy”. Po otrzymaniu takiego komunikatu przesyła dany obiekt do ak-
tora docelowego, ale dopiero po upłynięciu sekundy. Przyzwyczajeni do
programowania opartego o wątki moglibyśmy się zastanawiać, dlaczego
najzwyczajniej w świecie po odebraniu komunikatu nie uśpić wątku na
jedną sekundę i potem wysłać go dalej? Każdy aktor w systemie potra-
fi przetwarzać tysiące komunikatów na sekundę, w tysiącach aktorów
jednocześnie – i to na zaledwie kilku-kilkunastu wątkach. Jest to jednak
możliwe tyko i wyłącznie wtedy, gdy żaden aktor nie blokuje wątków. Z
tego względu powinniśmy bezwzględnie unikać blokowania czy usypia-
nia wewnątrz aktora. Użycia schedulera jest idiomatyczną i bezpieczną
alternatywą. Ostatni przykład użycia schedulera to wysyłanie komunikatu
periodycznie, co określony czas:

background image

41

/ www.programistamag.pl /

AKKA – WYDAJNY SZKIELET DLA APLIKACJI WIELOWĄTKOWYCH

Listing 16. Periodyczne wysyłanie komunikatu do samego siebie

case object Flush

class

Adder

extends

Actor {

private

implicit val ec = context.dispatcher

override def preStart() {

context.system.scheduler.schedule(

1.second, 1.second, self, Flush)

}

override def postRestart(reason:

Throwable

) {}

private

var sum = 0

override def receive = {

case

x: Int =>

sum += x

case

Flush =>

println(sum)

sum = 0

}

}

Nasz aktor sumuje przychodzące liczby w wewnętrznym liczniku (

sum). Jed-

nocześnie co sekundę wysyła samemu sobie komunikat

Flush, który zeruje

ten licznik. Wyrażenie

schedule(1.second, 1.second, self, Flush)

jest uruchamiane przy starcie aktora, powodując wysłanie do siebie same-
go (

self) komunikatu Flush co sekundę (drugi parametr) z początkowym

opóźnieniem również wynoszącym sekundę. Należy pamiętać, że scheduler
wysyła wiadomości do wskazanej referencji na aktora (

ActorRef), zatem

jeśli aktor zostanie zrestartowany, komunikaty nadal będą trafiały do nowej
instancji aktora. Warto o tym pamiętać – gdybyśmy wywołali

schedule()

bezpośrednio w konstruktorze, każdy restart harmonogramowałby po raz ko-
lejny wysłanie

Flush. Ten błąd sprawiałby, że w ciągu sekundy dostawaliby-

śmy aż dwa komunikaty

Flush (a ściślej tyle, ile razy aktor był (re)startowany).

Stąd dobrą praktyką jest używanie metod

preStart() i postRestart(),

odpowiednio reagujących na cykl życia aktora. Pusta implementacja

post-

Restart() jest konieczna, ponieważ implementacja domyślna woła pre-
Start(), czego chcemy uniknąć.

ROUTOWANIE WIADOMOŚCI

Nauczyliśmy się dotychczas, że jeden aktor może przetwarzać co najwyżej
jeden komunikat w danej chwili. Stoi to w pozornej sprzeczności z reklamo-
waną skalowalnością i wydajnością aplikacji napisanych w oparciu o Akka. W
rzeczywistości możemy deklaratywnie skalować nasz system poprzez klono-
wanie i przezroczyste routowanie komunikatów pomiędzy aktorami w puli.
Wyobraźmy sobie, że napisaliśmy aktora potrafiącego wykonywać zapytania
na bazie danych. Niestety sterownik bazy jest blokujący, w związku z czym
nasza naiwna implementacja potrafi wykonywać tylko jedno zapytanie w
jednej chwili:

Listing 17. Aktor wykonujący blokujące zapytanie na bazie danych

class

SqlActor

extends

Actor {

override def receive = {

case

sql:

String

=>

//Długie zapytanie o resultSet...

sender() ! resultSet

}

}

Musimy sobie zdać sprawę z faktu, że dopóki

SqlActor nie skończy prze-

twarzania jednego zapytania

sql (dopóki nie opuści metody receive),

wszystkie inne zapytania, pochodzące od innych aktorów/wątków, będą
czekały cierpliwie w kolejce. Naturalnie baza danych może obsłużyć wię-
cej zapytań niż jedno w danej chwili. Na szczęście aby przetwarzać więcej
zapytań równolegle, wystarczy odpowiednio zmodyfikować sposób, w jaki
tworzymy aktora:

Listing 18. Utworzenie aktora z wbudowanym routingiem

val sqlActor = system.actorOf(Props[SqlActor].

withRouter(

RoundRobinRouter(nrOfInstances = 10)))

Warto zauważyć, że implementacja aktora

SqlActor nie zmieniła się. Nato-

miast zmianie uległ sposób jego tworzenia. W naszym wypadku do aktora
dołączyliśmy

RoundRobinRouter, który w rzeczywistości stworzy 10 instan-

cji

SqlActor i ukryje je pod routerem. Wysyłając komunikat do sqlActor,

tak naprawdę wysyłamy go do routera, który używa algorytmu round robin.
Oznacza to, że router wysyła pierwszy komunikat do pierwszego aktora, drugi
do drugiego itd. Jedenasty komunikat trafi z powrotem do aktora pierwsze-
go. Gwarantuje to jednakowe obłożenie każdego z dziesięciu aktorów. Ostat-
nie zdanie jest dyskusyjne – jeśli z jakiegoś powodu co dziesiąty komunikat
zawiera znacznie wolniejsze zapytanie, jeden aktor będzie co prawda otrzy-
mywał tyle samo wiadomości, ale jego skrzynka odbiorcza będzie znacznie
dłuższa. Aby zapobiec temu negatywnemu zjawisku, wystarczy zamienić
RoundRobinRouter na nieco bardziej złożony SmallestMailboxRouter.
Ta prosta zmiana sprawi, że jeśli którykolwiek aktor będzie się opóźniał z ob-
sługą komunikatów, nowe wiadomości będą go przez pewien czas omijały.

Jak widać użycie routerów jest bardzo nieinwazyjne. Jakby tego było

mało, router można podpiąć pod aktora także z poziomu pliku konfigura-
cyjnego application.conf (plik ten poznamy za chwilę), zupełnie pomijając
zmiany w kodzie. Ponadto router w przezroczysty sposób zmienia nadawcę
komunikatu zanim prześle go do konkretnego aktora. Dzięki temu gdy aktor
docelowy (np.

SqlActor) odsyła odpowiedź do referencji sender(), trafia

ona do oryginalnego nadawcy, a nie pośrednika, jakim jest router.

TYPOWANI AKTORZY

Wszystkie implementacje aktorów, jakie poznaliśmy dotychczas, przyjmowa-
ły i odsyłały komunikaty. Jest to spójna abstrakcja, gdy nasz system korzysta
tylko z Akki. Może się jednak zdarzyć, że fragment aplikacji niezwiązany z ak-
torami (warstwa webowa, odziedziczony kod etc.) chciałby się z nimi komu-
nikować. W tym celu możemy stworzyć tzw. typowanego aktora (ang. typed
actor
), który udostępnia silnie typowany interfejs. Wróćmy do naszego przy-
kładu

SqlActor, ale rozbudowanego o dodatkowe operacje:

Listing 19.

SqlActor

wzbogacony o szereg dodatkowych operacji

case

class

Insert(sql: String)

case

class

Query(sql: String)

case

class

QueryResult(resultSet: Map[String, AnyRef])

case

class

Update(sql: String)

case

class

UpdateResp(success: Boolean)

class

SqlActor

extends

Actor {

override def receive = {

case

Insert(sql) =>

//...

case

Query(sql) =>

//...

sender() ! QueryResult(

/* ... */

)

case

Update(sql) =>

//...

sender() ! UpdateResp(

/* ... */

)

}

}

Aktor ten działa poprawnie, jednak współpraca z nim z zewnątrz jest kłopotli-
wa. Byłoby dużo prościej pracować z silnie typowanym interfejsem

Dao, na któ-

rym możliwe byłoby wołanie metod i oczekiwanie na odpowiedź. Poniżej po-
kazano, jak opakować aktora i udostępnić go jako interfejs (ang. „trait” – cecha):

Listing 20. Implementacja typowanego aktora

trait Dao {

def insert(sql:

String

): Unit

def query(sql:

String

): Map[

String

, AnyRef]

def update(sql:

String

): Future[Boolean]

}

background image

42

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

class

JdbcDao

extends

Dao {

override def update(sql:

String

) = {

//...

Future.successful(

/*...*/

)

}

override def query(sql:

String

) = {

//...

Map[

String

, AnyRef](

/*...*/

)

}

override def insert(sql:

String

) {

//...

}

}

Właściwie mamy do czynienia ze zwykłym interfejsem i implementacją, po-
wyższy kod nie ma nic wspólnego z Akką. Kod kliencki również może korzystać
z interfejsu

Dao, jakby była to zwykła klasa. W rzeczywistości jednak między

interfejsem a implementacją tworzony jest aktor. Parametry wejściowe każdej
metody są opakowywane w komunikat i wysyłane do tego aktora, a rezultat
jest wysyłany z powrotem. Stąd typy wartości zwracanych zasługują na odro-
binę uwagi. Jeśli metoda zwraca

Unit (czyli nie zwraca nic), będzie to równo-

ważne wysłaniu komunikatu do aktora bez oczekiwania na odpowiedź. Meto-
da zwracająca

Future[T] odpowiada odpytaniu aktora przy użyciu wzorca

ask (operator pytajnika –

?) i otrzymaniu w odpowiedzi obiektu Future. Zwra-

canie po prostu obiektu (jak w metodzie

query()) sprawi, że wątek kliencki

zawiesi się w oczekiwaniu na odpowiedź. Z tego względu na metody typowa-
nego aktora niezwracające ani

Unit, ani Future należy szczególnie uważać,

jeśli wołamy je z poziomu innego aktora. Bogatsi w tę wiedzę możemy utwo-
rzyć instancję typowanego aktora i wywołać na niej kilka metod:

Listing 21. Tworzenie i wykorzystanie typowanego aktora

private

val dao: Dao = TypedActor(system).

typedActorOf(TypedProps[JdbcDao]())

dao.insert("Foo")

val result: Map[String, AnyRef] = dao.query("Bar")

val f: Future[Boolean] = dao.update("Buzz")

Jako że każda metoda typowanego aktora jest w rzeczywistości obsługą
komunikatu, typowany aktor nigdy nie ma uruchomionej więcej niż jednej
metody ze swojego interfejsu. Niestety póki co nie istnieje prosty sposób
podpięcia routera do typowanego aktora. Mało tego, ponieważ bardzo łatwo
wprowadzić blokowanie po stronie klienckiej, zaleca się rozważne i oszczęd-
ne korzystanie z typowanych aktorów.

KONTROLOWANIE WĄTKÓW

Aplikacje pracujące pod kontrolą Akki działają najlepiej, gdy kod aktorów jest
nieblokujący (używa tylko procesora, nie śpi, nie blokuje się na semaforach,
nie używa klasycznego I/O) i całkowicie sterowany zdarzeniami. Tak napisa-
nych aktorów możemy tworzyć w ogromnych ilościach i będą oni pracowali
sprawnie na bardzo niewielkiej liczbie wątków. Jest to istotne, ponieważ koszt
utworzenia, utrzymania oraz przełączania wątków jest znaczący. Mało tego,
Akka obsługuje komunikaty w paczkach, domyślnie (parametr threshold) po
pięć. Często jednak nie mamy wyboru i część naszych aktorów musi używać
blokującego kodu – nienapisanego z myślą o aktorach, np. JDBC. Wtedy do-
brym pomysłem jest skonfigurowanie osobnych, dedykowanych puli wątków
dla takich aktorów. Konfigurację taką umieszczamy w pliku application.conf
w katalogu głównym na CLASSPATH:

Listing 22. Deklaracja nowej puli wątków (dispatchera)

jdbc-dispatcher {

type = Dispatcher

executor = "fork-join-executor"

fork-join-executor {

parallelism-min = 100

parallelism-max = 100

}

throughput = 10

}

Pula powyżej będzie się składała ze stu wątków, a każdy aktor obsłuży

maksymalnie dziesięć komunikatów, nim odda wątek innym. Samo zadekla-
rowanie nowej puli jest jednak niewystarczające. Trzeba jeszcze wskazać zbiór
aktorów, którzy będą obsługiwali komunikaty, korzystając z tej puli. Możemy
to zrobić bezpośrednio w kodzie:

Props[JdbcActor].withDispatch-

er("jdbc-dispatcher"). Lepiej jednak decyzję, jak rozdysponujemy na-
szymi wątkami, opóźnić i również zdefiniować ją w pliku konfiguracyjnym:

Listing 23. Przypisanie dedykowanej puli wątków do aktora

akka {

actor {

deployment {

/jdbcActor {

dispatcher = jdbc-dispatcher

}

}

}

}

Gdzie

jdbcActor to nazwa aktora nadana podczas jego tworzenia. Taki wpis

w konfiguracji sprawi, że

JdbcActor będzie dysponował dedykowanymi

wątkami, niezależnymi od reszty systemu. Z jednej strony może to pomóc
w odpowiednim podziale mocy obliczeniowej w naszej aplikacji. Z drugiej –
utrudni zagłodzenie reszty systemu. Trzeba jedynie pamiętać, że dzieci dane-
go aktora nie dziedziczą jego puli wątków (w terminologii Akki: dispatchera),
trzeba go nadać explicite.

Skoro już jesteśmy przy dostosowywaniu wydajności aplikacji – domyśl-

nie skrzynka odbiorcza (kolejka) oczekujących komunikatów każdego aktora
jest nieograniczona. Oznacza to, że gdy jeden aktor staje się wąskim gardłem
i jego kolejka zapełnia się, jego czas odpowiedzi oraz zapotrzebowanie na
pamięć wzrastają. Aby temu zapobiec, dobrą praktyką jest zdefiniowanie
ograniczonej skrzynki odbiorczej:

Listing 24. Dedykowana skrzynka odbiorcza

akka {

actor {

deployment {

/jdbcActor {

mailbox = bounded-mailbox

}

}

}

}
bounded-mailbox {

mailbox-type = "akka.dispatch.BoundedMailbox"

mailbox-capacity = 5

mailbox-push-timeout-time = 2s

}

Taki fragment w pliku application.conf sprawi, że aktor o nazwie

jdbcActor

zostanie utworzony z kolejką o maksymalnym rozmiarze pięciu komunikatów.
Jeśli ktoś będzie próbował wysłać wiadomość do aktora o zapełnionej skrzyn-
ce, operacja wysłania zablokuje się (w wątku wołającego) na co najwyżej dwie
sekundy. Jest to pożądane zachowanie – klient produkujący zbyt wiele ko-
munikatów jest spowalniany, zapobiegając ogólnej destabilizacji systemu.
Spowolnienie to może ulec eskalacji, ponieważ spowolniony producent sam
spowolni przetwarzanie własnych komunikatów. Ponieważ blokowanie wąt-
ku wewnątrz aktora jest zła praktyką w Akka, dobrą alternatywą jest ustawie-
nie parametru

mailbox-push-timeout-time na 0. W takiej sytuacji próba

wysłania komunikatu do zapełnionej skrzynki odbiorczej aktora docelowego
zakończy się natychmiastowym odrzuceniem komunikatu. Co ważne, w obu
przypadkach aktor wysyłający komunikat nie zostanie poinformowany o nie-
udanej próbie umieszczenia wiadomości w kolejce. Akka promuje biznesowe
potwierdzenia, sama nie gwarantując dostarczenia komunikatów do celu.

background image

43

/ www.programistamag.pl /

AKKA – WYDAJNY SZKIELET DLA APLIKACJI WIELOWĄTKOWYCH

NATYWNE API W JAVIE

W przeciwieństwie do wielu bibliotek i frameworków napisanych w Scali,
Akka posiada natywne API przystosowane do pracy w Javie (znacznie udo-
skonalone w Javie 8). Implementacja aktorów nie jest może tak wygodna, ale
jest możliwa i wiele projektów używa Akki, nie będąc napisanymi w Scali. Po-
niższy przykład to dość dokładny klon pierwszego przykładu z tego artykułu:

Listing 25. Prosty aktor napisany w Javie

final

class

Welcome {

public

final

String

name;

public

Welcome(

String

name) {

this

.name = name;

}

}

class

WelcomeActor

extends

UntypedActor {

@Override

public

void

onReceive(

Object

message) {

if

(message

instanceof

Welcome) {

final

Welcome msg = (Welcome) message;

final

String

name = msg.name;

System

.out.println(name);

}

else

{

unhandled(message);

}

}

}

class

Main {

public

static

void

main(

String

[] args) {

ActorSystem system =

ActorSystem.create(

"Magazyn"

);

ActorRef welcome = system.actorOf(

Props.create(WelcomeActor.class));

welcome.tell(

new

Welcome(

"Tomek"

),

ActorRef.noSender());

system.shutdown();

}

}

Rzuca się w oczy nieco bardziej rozwlekły i mniej elegancki kod:
1. Tworząc obiekt komunikatu, musimy zapewnić, że będzie on niezmienny

(ang. immutable). Używam finalnych pól oraz typów, sama klasa też jest final-
na. Na potrzeby tego przykładu uznałem, że getter (

getName()) jest zbędny.

2. Metoda

onReceive() (odpowiednik receive) używa niewygodnego opera-

tora

instanceof zamiast dopasowywania (ang. pattern matching, znanego ze

Scali). Dodatkowo musimy explicite wywołać metodę

unhandled(), aby poin-

formować framework, że nie udało się nam przetworzyć komunikatu.

3. Wysyłając komunikat jednokierunkowy, musimy określić, kto jest jego

nadawcą. Najczęściej będzie to

noSender() albo self().

Poza tymi szczegółami na poziomie składni, aktorzy pisani w Javie nie różnią
się niczym od swoich natywnych odpowiedników. Wewnątrz aktora mamy
dostęp do metody

sender(), podobnie możemy wysyłać komunikat i ocze-

kiwać na odpowiedź (używając idiomu

Patterns.ask(...)). Wreszcie

możliwe jest też tworzenie typowanych aktorów w Javie.

MONITOROWANIE

Nie jest tajemnicą, że monitorowanie aplikacji sterowanych zdarzeniami i
komunikatami jest trudne. Akka niestety nie odbiega od tej reguły. Istnieje
jednak wiele technik, które znacznie ułatwią nam pracę. Po pierwsze należy
odpowiednio skonfigurować logowanie, przede wszystkim włączając szereg
komunikatów diagnostycznych oraz przekierowując standardowe logi do
SLF4J, a potem np. do Logbacka:

Listing 26. Diagnostyczna konfiguracja

akka {

log-config-on-start = on

loggers = ["akka.event.slf4j.Slf4jLogger"]

loglevel = "DEBUG"

actor {

debug {

receive = on

autoreceive = on

lifecycle = on

unhandled = on

}

}

}

Powyższy fragment pliku application.conf sprawi, że cała konfiguracja zosta-
nie wypisana na ekran przy starcie (bywa pomocne przy diagnozowaniu pro-
blemów), logi trafią do SLF4J, w szczególności będą to informacje o odebra-
nych i zignorowanych komunikatach oraz restartach aktorów. Niestety, aby
działało logowanie wszystkich odebranych komunikatów, metoda

receive

każdego aktora musi być otoczona specjalną deklaracją:

def receive =

LoggingReceive {...}. W przypadku dużych systemów będziemy praw-
dopodobnie potrzebowali bardziej przekrojowych narzędzi. Tutaj warto
przyjrzeć się programowi Typesafe Console.

PODSUMOWANIE

Akka wymaga kompletnej zmiany myślenia o projektowaniu systemów. Mu-
simy nauczyć się dzielić pracę pomiędzy aktorów, efektywnie wykorzystywać
ich zalety i znać granice. Z jednej strony otrzymujemy elegancki interfejs
programistyczny, uwalniający nas od problemów związanych z wielowątko-
wością. Z drugiej musimy się pogodzić z rozluźnionymi gwarancjami doty-
czącymi dostarczenia wiadomości oraz nieuniknionymi problemami przy
debugowaniu i diagnozowaniu problemów.

Nie każda aplikacja powinna używać Akki. Ogromna wydajność połączona

z architekturą opartą o wymianę komunikatów czyni ten framework niezwykle
ciekawym dla systemów przetwarzających strumienie danych, reagujących na
zdarzenia czy obsługujących duże ilości równoległych transakcji. Z kolei duże
aplikacje o bardzo złożonych scenariuszach biznesowych bądź takie, które mu-
szą używać blokujących bibliotek, mogą nie skorzystać zbyt wiele z tej techno-
logii. Zwłaszcza jeśli wydajność i przepustowość nie grają kluczowej roli.

Wszystkie przykłady testowane na Akka w wersji 2.3.0. Podziękowania dla

Artura Stanka za techniczną recenzję.

Tomasz Nurkiewicz

nurkiewicz@gmail.com

Od wielu lat programuje zawodowo w Javie. Uwielbia back-end, toleruje JavaScript. Pasjonat
języków około-Javowych. Zakochany w wykresach, analizie danych i raportowaniu. Redaktor
techniczny książek „Learning Highcharts“ oraz „Getting started with IntelliJ IDEA“. Na co
dzień programuje funkcyjnie dla sektora bankowego. Zaangażowany w open source, wyróż-
niony DZone’s Most Valuable Blogger (

http://nurkiewicz.blogspot.com

).

background image

44

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Marek Sawerwain

R

ozpoczynając naukę CUDA, jak zawsze warto zrealizować kilka pro-
stych programów. Jednym z takich zadań jest wyznaczenie przybli-
żenia wartości liczby Pi. Możemy to zrobić na kilka sposobów, jedną

z możliwości jest np. wykorzystanie tzw. metody Monte Carlo. Podstawowa
implementacja dla procesora zajmuje dosłownie jedną małą kartkę z zeszytu
(Listing 1). Jednak jej przerobienie na wersję równoległą o wysokiej wydajno-
ścią wbrew pozorom nie jest takie trywialne. Co oznacza, iż nadaje się w sam
raz, aby zapoznać się z technologią CUDA.

LICZBA PI – WYZNACZANIE

PRZYBLIŻONEJ WARTOŚCI

Istnieje wiele technik wyznaczania przybliżonej wartości liczby Pi, my wybie-
rzemy metodę opartą o technikę Monte Carlo.

Metoda Monte Carlo przybliżająca wartość liczby Pi jest dość łatwa do im-

plementacji. Główne zadanie sprowadza się do losowania liczby z zakresu od
minus jeden do jeden. Wylosowane liczby będą stanowić współrzędne punk-
tu. Należy także określić, ile punktów będzie losowanych, ogólnie założymy, iż
będzie to

N punktów. Dla każdego punktu sprawdzamy, czy mieści wewnątrz

okręgu:

x

2

+ y

2

<= 1. Wszystkie punkty, które znajdują się wewnątrz okrę-

gu, również musimy policzyć i oznaczymy ich liczbę przez

pinc (skrót od an-

gielskiego point in circle). Ponieważ stosunek pola okręgu do pola kwadratu
wynosi

pi / 4, to łatwo podać wzór, który posłuży nam do obliczenia liczby

pi, a będzie to 4 * pinc / N.

Rysunek 1 podsumowuje powyższy akapit. Obrazuje on ideę, z jakiej sko-

rzystamy, pisząc program do wyznaczania przybliżenia wartości liczby Pi. Ry-
sunek 1 przedstawia okrąg o promieniu

r=1. Okrąg został wpisany w kwadrat,

stąd też wiadomo, iż kwadrat oznaczony przez

P

1

ma pole równe (2r)

2

. Stosu-

nek powierzchni pola okręgu i kwadratu daje nam możliwość wyznaczenia
przybliżenia liczby Pi.

P

2

= πr

2

r

P

1

= a

2

, a = 2r

Rysunek 1. Ogólna idea przybliżania wartości liczby Pi za pomocą metody

Monte Carlo

PRZYBLIŻANIE WARTOŚCI LICZBY PI

– WERSJA SZEREGOWA

Przed opracowaniem wersji równoległej, warto przygotować wersję szeregową,
gdzie obliczenia będą wykonywane tylko przez jeden procesor. Należy też wy-
brać, w jaki sposób będziemy generować liczby pseudolosowe (generator liczb
pseudolosowych będziemy oznaczać skrótem PRNG, od ang. pseudorandom
number generator). Wersja szeregowa może zostać opracowana w języku C lub w
C++. Ponieważ nowa wersja standardu C++ przyniosła także nowe API, do gene-
racji liczb pseudolosowych w postaci pliku nagłówkowego o nazwie

random, dla-

tego my wykorzystamy te nowe możliwości i opracujemy wersję dla języka C++.

Wersja szeregowa nie jest skomplikowana, losujemy zgodnie z podanym

wcześniej algorytmem zbiór par punktów, i dla każdego punktu sprawdzamy,
czy przynależy do wnętrza okręgu. Dlatego sam program nie jest zbyt duży,
co znajduje potwierdzenie na Listingu 1.

Naturalnie, przed właściwą pętlą losującą punkty należy utworzyć obiekt

PRNG. W nowym API C++ nie nastręcza to dodatkowych kłopotów. Należy
utworzyć dwa obiekty, jeden reprezentujący generator liczb pseudoloso-
wych, w programie z Listingu 1 jest to obiekt

rng wykorzystujący bardzo po-

pularny generator o nazwie Mersenne Twister MT19937.

Sam generator nie jest w naszym przypadku wystarczający, bowiem do

przybliżania liczby Pi potrzebne są wartości w zakresie od

-1 do 1. Oczekuje-

my także, iż wartości będę równomiernie rozmieszczone, dlatego korzystamy
z obiektu

dist o typie uniform_real_distribution. Podczas tworzenia

obiektu podaliśmy wartości

-1 oraz 1 opisujące, w jakim zakresie mają być

generowane liczby pseudolosowe.

Kolejna czynność związana jest z inicjalizacją PRNG i jest podobna jak przy

stosowaniu starszej funkcji

rand, tj. musimy dokonać inicjalizacji tzw. zarodka

generatora za pomocą metody

seed:

rng.seed((

unsigned

int

)time(

nullptr

));

Wykorzystujemy naturalnie aktualną wartość czasu, za pomocą funkcji

time.

Odczytanie wartości z generatora liczb pseudolosowych sprowadza się do
użycia obiektu

dist jako obiektu funkcyjnego oraz podania w argumencie

obiektu reprezentującego generator:

x = dist(rng);

Jedyna pętla

for (Listingu 1) jest odpowiedzialna za wylosowanie odpowiedniej

liczby punktów (ich ilość została już określona przez zmienną

max_counter). Dla

każdej pary punktów

x, y sprawdzamy, czy przynależą do okręgu jednostkowe-

go. Jeśli tak jest, to zwiększamy wartość licznika

counter. Całkowita ilość wylo-

sowanych punktów oraz ilość punktów przynależących do wnętrza okręgu są po-
trzebne, aby zrealizować nasze zadanie: wyznaczyć przybliżoną wartość liczby Pi.

Same operacje związane z obliczeniem przybliżenia sprowadzają się do

następującej linii kodu:

epi = 4.0 * (

double

)counter / (

double

)max_counter;

CUDA z liczbą Pi

Technologia CUDA oferowana przez firmę NVIDIA stała się bardzo popularna, jeśli
chodzi o technologię wykorzystania kart graficznych w szeroko rozumianych obli-
czeniach. Nie jest to jedyna technologia tego typu, bowiem jest jeszcze standard
OpenCL oraz rozwiązanie Microsoftu o nazwie C++AMP. Jednakże mimo iż CUDA
jest dedykowana tylko dla kart NVIDIA, to wydaje się, iż oferuje największą ela-
styczność w procesie tworzenia oprogramowania dla GPU „zielonych”.

background image
background image

46

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

Pozostaje już tylko wyświetlić wartość zmiennej

epi oraz wykorzystując

zdefiniowaną wartość liczby Pi w postaci definicji

M_PI. Łatwo także określić,

z jaką dokładnością udało się obliczyć przybliżenie wartości Pi. Wystarczy bo-
wiem odjąć dokładną wartość M_PI oraz przybliżoną, my dodatkowo zasto-
sujemy funkcję

abs, tj. wartość bezwzględną, aby otrzymać wartość błędu.

Listing 1. Przybliżanie wartości liczby Pi w C++ – wersja szeregowa

#include

<ctime>

#include

<cmath>

#include

<random>

#include

<iostream>

#ifndef

M_PI

#define

M_PI 3.14159265358979323846

#endif

using

namespace

std;

mt19937

rng;

uniform_real_distribution

<

double

> dist(-1.0, 1.0);

double

x, y, epi;

long

i, counter = 0, max_counter = 1000000;

int

main() {

rng.seed((

unsigned

int

)time(

nullptr

));

for

( i=0 ; i < max_counter ; i++) {

x = dist(rng);

y = dist(rng);

if

( (x*x + y*y) < 1) counter++;

}

epi = 4.0 * (

double

)counter / (

double

)max_counter;

cout <<

"wartość pi = "

<<

M_PI

<< endl;

cout <<

"przybliżona wartość pi = "

<< epi << endl;

cout <<

"błąd = "

<< fabs((

double

)

M_PI

- epi) << endl;

return

0;

}

Instalacja pakietu CUDA Toolkit

Instalacja pakietu CUDA Toolkit nie wymaga żadnych specjalnych zabie-
gów. W przypadku systemu Windows należy ściągnąć archiwum ze strony
NVIDII i dokonać instalacji. Niestety, trzeba posiadać Visual Studio, a do-
kładnie odpowiedni kompilator języka C++. Obecnie dla wersji 5.5 (wersja
6.0 w momencie pracy nad artykułem posiadała status RC, więc została
pominięta) są to wersje Visual C++ 9.0, 10.0 oraz 11.0, czyli odpowiednio
Visual Studio 2008, 2010 oraz 2012. Dla wersji 2012 można wykorzystać
też odmianę Express.

W przypadku, gdy korzystamy z systemu Linux, niestety mamy jesz-

cze większe zamieszanie z kompilatorami i dodatkowo z biblioteką GLIBC.
Dystrybucje oferują różne odmiany GCC oraz biblioteki GLIBC. Warto za-
tem sprawdzić, czy używana dystrybucja posiada przygotowane pakiety
dla CUDA Toolkit, jak np. Ubuntu oraz ArchLinux. W tym przypadku insta-
lacja pakietu CUDA Toolkit nie przysparza żadnych kłopotów. Dlatego naj-
wygodniej zainstalować Toolkit za pomocą systemu pakietów w ramach
używanej przez nas dystrybucji. W przypadku systemu Linux lepiej używać
dystrybucji 64-bitowej, bowiem możemy napotkać dodatkowe kłopoty z
obsługą pamięci operacyjnej.

CUDA C/C++ – PRZYKŁAD NA

ROZGRZEWKĘ

Zamiana wersji szeregowej podanej w poprzednim punkcie na wersję równo-
ległą to naturalnie dobry sposób, aby zapoznać się z pakietem CUDA C/C++.
Nim jednak przystąpimy do głównego zadania, podamy dwa proste przykła-
dy w celu zapoznania się z technologią CUDA.

Listing 2 przedstawia kod źródłowy przykładu z cyklu „Witaj Świecie!!!”, ale

sam komunikat jest wyświetlany przez jądro obliczeniowe. Tutaj trzeba do-
powiedzieć, że GPU, które oferują tzw. poziom obliczeniowy 2.0 lub wyższy.
(ang. compute capability), pozwalają na stosowanie

printf bezpośrednio.

Dla starszych kart należy używać

cuPrintf, które w pewnym sensie tylko

„udaje” działanie standardowej funkcji

printf.

W obydwu przypadkach efekt jest jednak podobny, zobaczymy komuni-

kat, wygenerowany przez GPU. Jednak w programie, jeśli chcemy skorzystać z
cuPrintf, należy wcześniej wywołać funkcję cudaPrintfInit. Następnie
po zakończeniu działania jądra obliczeniowego wywołanie funkcji o nazwie
cudaPrintfDisplay spowoduje wyświetlenie komunikatów, czyli dopiero
po wykonaniu jądra obliczeniowego. Należy także zakończyć obsługę

cu-

Printf wywołaniem cudaPrintfEnd.

Wywołanie jądra obliczeniowego też przedstawia się nieco inaczej niż dla

typowej funkcji C/C++:

cuda_kernel<<<1,1>>>();

Wyrażenie <<<1,1>>> oznacza, iż będzie wykorzystany jeden blok oraz jeden
wątek. Inny przykład <<<2,4>>> oznacza, iż uruchomione zostaną dwa bloki, a
w każdym bloku cztery wątki. W każdym wątku wykonuje się ten sam kod, do-
datkowo

cuPrintf zawsze wyświetla numer bloku oraz wątku. Choć cuPrintf

to dość proste rozwiązanie, to może pomóc podczas sprawdzania poprawności
jądra obliczeniowego, szczególnie gdy dopiero zapoznajemy się z CUDA.

Sposób kompilacji programu w CUDA C/C++ zależy naturalnie od tego,

czy stosujemy jakieś środowisko GUI. Najprościej jednak skompilować pro-
gram z poziomu konsoli, np.:

nvcc example1.cu -I.

Przy czym pliki cuPrintf.cu oraz cuPrintf.cuh muszą się znaleźć w tym samym
katalogu, co plik źródłowy example1.cu.

Listing 2. Przykład na początek w CUDA C/C++

#include

<iostream>

#include

"cuPrintf.cu"

using

namespace

std;

__global__

void

cuda_kernel() {

cuPrintf(

"Witaj Świecie!!!\n"

);

}

int

main(

int

argc,

char

*argv[]) {

cudaPrintfInit();

cuda_kernel<<<1,1>>>();

cudaPrintfDisplay(

stdout

,

true

);

cudaPrintfEnd();

return

0;

}

CUDA C/C++, DRUGI PRZYKŁAD NA

ROZGRZEWKĘ

Listing 3 przedstawia drugi przykład, gdzie tym razem jądro obliczeniowe jest
bardziej skomplikowane. Nasze zadanie będzie polegać bowiem na tym, aby w
tablicy znaków umieścić napis „

Cześć!!!!” (choć pozbawiony polskich zna-

ków). Każda litera napisu zostanie umieszczona niezależnie od pozostałych pod
wskazanym miejscem. Oznacza to, iż możemy zrealizować to zadanie w pełni
równolegle. Poszczególne wątki będą się zajmować tylko jedną literą.

Aby tak się stało, w naszej procedurze obliczeniowej należy odczytać

identyfikator, który określi numer wątku, na jakim działamy:

int

idx = blockIdx.x;

Za pomocą otrzymanej liczby będziemy wskazywać miejsce zapisu po-

szczególnych liter, wykorzystując instrukcję

switch. Wymaga to naturalnie

odpowiedniego sposobu wywołania naszej funkcji. Stosując

blockIdx.x,

odnosimy się tylko do numeru bloku, dlatego wywołanie jest następujące:

cuda_kernel<<<32, 1>>>( dev_msg );

background image

47

/ www.programistamag.pl /

CUDA Z LICZBĄ PI

Oznacza to, iż zastosujemy trzydzieści dwa bloki obliczeniowe, a w każ-

dym bloku aktywny będzie jeden wątek, co oznacza, iż łącznie liczba aktyw-
nych wątków jest równa trzydzieści dwa. Nasz napis jest krótszy, więc nie
wszystkie bloki zostaną wykorzystane. Ograniczenie się tylko do jednego
wątku na blok również nie jest optymalne, choć w naszym edukacyjnym przy-
kładzie nie jest to takie ważne.

Listing 3. Bardziej zaawansowany przykład na początek w CUDA C/C++

#include

<iostream>

using

namespace

std;

char

*dev_msg;

char

*host_msg;

__global__

void

cuda_kernel(

char

*_msg) {

int

idx = blockIdx.x;

switch

( idx ) {

case

0: _msg[ idx ] =

'C'

;

break

;

case

1: _msg[ idx ] =

'z'

;

break

;

case

2: _msg[ idx ] =

'e'

;

break

;

case

3: _msg[ idx ] =

's'

;

break

;

case

4: _msg[ idx ] =

'c'

;

break

;

case

5: _msg[ idx ] =

'!'

;

break

;

case

6: _msg[ idx ] =

'!'

;

break

;

case

7: _msg[ idx ] =

'!'

;

break

;

case

8: _msg[ idx ] =

'!'

;

break

;

case

9: _msg[ idx ] =

'\0'

;

break

;

}

__syncthreads();

}

int

main(

int

argc,

char

*argv[]) {

host_msg =

new

char

[32];

cudaMalloc( (

void

**)&dev_msg, 32);

cuda_kernel<<<32, 1>>>( dev_msg );

cudaMemcpy( &host_msg[0], dev_msg, 32, cudaMemcpyDeviceToHost );

cout <<

"host_msg=["

<< host_msg <<

"]\n"

;

cudaFree( dev_msg);

delete

[] host_msg;

return

0;

}

W drugim przykładzie niezbędne są także operacje na pamięci. Upraszcza-
jąc, mamy dwa rodzaje pamięci: pamięć systemu operacyjnego, czyli pamięć
tradycyjnego procesora (nazywana także pamięcią hosta), oraz pamięć karty
graficznej. Pierwsza czynność to przydzielenie pamięci dla zmiennej

host_

msg. Do tej zmiennej zostanie skopiowany napis utworzony w pamięci urzą-
dzenia obliczeniowego (inaczej mówiąc, karty graficznej). Przydział pamięci
dla naszego napisu po stronie urządzenia realizujemy za pomocą linii:

cudaMalloc( (

void

**)&dev_msg, 32);

Następnie możemy uruchomić jądro obliczeniowe. Po zrealizowaniu obliczeń
należy wykonać operację kopiowania wyznaczonego ciągu znaków z pamięci
urządzenia do utworzonej przez nas wcześniej zmiennej

host_msg. Zadanie

to realizujemy w jednej linii kodu:

cudaMemcpy( &host_msg[0], dev_msg, 32, cudaMemcpyDeviceToHost );

Pierwszy parametr to wskaźnik do zmiennej, gdzie chcemy umieścić dane w
pamięci hosta (lub CPU). Drugi parametr to wskaźnik zmiennej urządzenia
GPU, z którego dane będziemy odczytywać. W kolejnym parametrze okre-
ślamy, ile bajtów należy skopiować (w naszym przypadku 32), oraz ostatni,
czwarty parametr określa kierunek kopiowania danych. Ponieważ chcemy
przenieść dane z pamięci urządzenia do pamięci hosta, to została podana sta-
ła o nazwie:

cudaMemcpyDeviceToHost.

Przykład kompilujemy podobnie jak poprzedni, wydając polecenie:

nvcc example2.cu

Nie ma potrzeby, aby wskazywać katalog, gdzie znajdują się dodatkowe

pliki nagłówkowe, gdyż ich nie używamy.

PRZYBLIŻENIE LICZBY PI OBLICZANE

W CUDA C/C++

Po dwóch przykładach i wcześniejszej wersji szeregowej mamy dość infor-
macji, aby zrealizować równoległą wersją programu podanego na Listingu
1. Najważniejsze fragmenty naszego pierwszego podejścia do wyznaczania
przybliżenia Pi przedstawia Listing 4.

Znajduje się na nim jądro obliczeniowe o nazwie

pi_estimation oraz

procedura wykonywana po stronie CPU sterująca procesem generowania
przybliżenia liczby Pi:

pi_estimation_host.

Nasze pierwsze podejście jest nieco inne niż implementacja podana na

Listingu 1. Generujemy dwa zestawy liczb losowych, które są przekazywane
do jądra obliczeniowego przez parametry

rnd_values1 oraz rnd_values2.

Pełnią one rolę wielkości

x oraz y. Oznacza to, iż odpowiednie tablice będą

przygotowane wcześniej, także przez kartę graficzną.

Treść jądra obliczeniowego jest tożsama z treścią pętli

for z Listingu 1,

numer punktu, jakim aktualnie się zajmujemy, jest wyznaczany w następu-
jący sposób:

int

id = blockDim.x * blockIdx.x + threadIdx.x;

Używamy wielkości bloku (możemy bowiem określać kształt bloku, w naszym
przypadku będzie to wektor), identyfikatora bloku, do którego jest dodawany
identyfikator wątku. Wymaga to naturalnie odpowiedniego wywołania jądra
obliczeniowego. Pozostałe czynności są podobne jak dla wersji szeregowej,
choć wynik testu przynależności jest przechowywany w kolejnej tablicy
wskazywanej przez parametr

rslt_data. Po zakończeniu działania proce-

dury obliczeniowej niezbędne będą jeszcze dodatkowe operacje, które bę-
dziemy wykonywać już za pomocą CPU.

Listing 4. Przybliżanie wartości liczby Pi w CUDA C/C++, najważ-
niejsze fragmenty

__global__

void

pi_estimation(

double

*

rnd_values1

,

double

*

rnd_

values2

,

int

*

rslt_data

) {

double

v1, v2;

int

id = blockDim.x * blockIdx.x + threadIdx.x;

v1 =

rnd_values1

[id];

v2 =

rnd_values2

[id];

if

( v1*v1 + v2 * v2 <= 1)

rslt_data

[id]=1;

else

rslt_data

[id]=0;

}

cudaError_t

pi_estimation_host() {

int

in_circ_points, i;

double

epi;

cudaError_t

cudaStatus;

int

cudaRngStatus;

cudaStatus = cudaSetDevice(0);

cudaMalloc((

void

**)&rnd_values1, NBlocks * NThreads *

sizeof

(

double

));

cudaMalloc((

void

**)&rnd_values2, NBlocks * NThreads *

sizeof

(

double

));

cudaRngStatus = curandCreateGenerator(

&generator, CURAND_RNG_PSEUDO_MRG32K3A);

cudaRngStatus |= curandSetPseudoRandomGeneratorSeed(

generator, 4294967296ULL^time(0));

cudaRngStatus |= curandGenerateUniformDouble(

generator, rnd_values1, (NBlock * NThreads));

cudaRngStatus |= curandGenerateUniformDouble(

generator, rnd_values2, (NBlock * NThreads));

cudaRngStatus |= curandDestroyGenerator(generator);

if

(cudaRngStatus !=

CURAND_STATUS_SUCCESS

) { . . . . }

cudaStatus = cudaMalloc((

void

**)&devData, NBlocks * NThreads *

sizeof

(

int

));

background image

48

/ 3

. 2014 . (22) /

PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE

hostData = (

int

*)malloc( NBlocks * NThreads *

sizeof

(

int

) );

pi_estimation<<< NBlocks, NThreads >>>(

rnd_values1, rnd_values2,

devData );

cudaDeviceSynchronize();

cudaStatus = cudaMemcpy(hostData, devData,

NBlocks * NThreads *

sizeof

(

int

),

cudaMemcpyDeviceToHost

);

in_circ_points = 0;

for

(i=0;i<NBlocks * Nthreads;i++) { in_circ_points +=

hostData[i]; }

epi = 4.0 * (

double

)in_circ_points / (

double

)(NBlocks *

NThreads);

printf(

"wartość pi = %lf\n"

,

M_PI

);

printf(

"przybliżona wartość pi = %lf\n"

, epi);

printf(

"błąd = %lf\n"

, fabs((

float

)

M_PI

- epi));

free( (

void

*) hostData );

cudaFree( devData );

return

cudaStatus;

}

Możemy teraz zająć się funkcją pomocniczą

pi_estimation_host. Pierw-

sze zadanie polega na alokacji pamięci dla zmiennych przechowujących
punkty oraz wyniki testu przynależności. Przy czym musimy naturalnie okre-
ślić ilość tych punktów. Obliczenia w jądrze zostaną rozdzielone na bloki oraz
wątki, więc całkowita liczby punktów to iloczyn liczby bloków oraz liczby wąt-
ków, jakie będą funkcjonować w ramach bloku. Przydział pamięci dla zmien-
nej

rnd_values1 jest następujący:

cudaMalloc((

void

**)&rnd_values1, NBlock * NThreads *

sizeof

(

double

));

Gdzie liczba punktów to iloczyn

NBlock * NThreads i dodatkowo jest on

przemnażany przez wielkość typu

double, bowiem nasze obliczenia podob-

nie jak dla CPU będziemy przeprowadzać na liczbach o podwójnej precyzji.
Wyniki przynależności poszczególnych wylosowanych punktów wymagają
alokacji po stronie GPU oraz CPU:

cudaStatus = cudaMalloc((

void

**)&devData,

NBlock * NThreads *

sizeof

(

int

));

hostData = (

int

*)malloc( NBlock * NThreads *

sizeof

(

int

) );

Przed wywołaniem jądra obliczeniowego trzeba wylosować współrzędne
punktów do testów. Skorzystamy z biblioteki o nazwie curand znajdującej w
się pakiecie CUDA Toolkit.

Na początku należy utworzyć generator liczb pseudolosowych, zastosuje-

my generator o skrócie MRG32K3A, autorstwa Pierre'a L'Ecuyera:

cudaRngStatus = curandCreateGenerator(&generator,

CURAND_RNG_PSEUDO_MRG32K3A);

Następnie dokonujemy jego inicjalizacji:

cudaRngStatus |= curandSetPseudoRandomGeneratorSeed(

generator, 4294967296ULL^time(0));

Gdzie liczba

4294967296 to inaczej 2 do 32 potęgi. Wypełnienie tablicy rnd_

values1 wartościami losowymi, która to tablica, przypomnijmy, znajduje się
w pamięci GPU, to tylko jedna linia kodu:

cudaRngStatus |= curandGenerateUniformDouble(generator,

rnd_values1, (NBlock * NThreads));

Pozostaje teraz wywołać jądro obliczeniowe, poprawnie określając liczbę blo-
ków oraz wątków, aby ich iloczyn był równy ilości punktów:

pi_estimation<<< NBlock, NThreads >>>( rnd_values1,

rnd_values2, devData );

W testowym programie mamy tysiąc bloków po tysiąc wątków, co łącznie
daje milion punktów. Niestety, CUDA ogranicza wielkość siatki obliczenio-

wej do wymiarów 65536 na 65535, więc należy tak dobrać liczbę bloków i
wątków, aby nie przekroczyć tej wielkości. Łatwo jednak przezwyciężyć ten
problem. W omówionym rozwiązaniu, pojedynczy wątek testuje tylko jeden
punkt. Zwiększając liczbę punktów przetwarzanych w jednym wątku, zwięk-
szymy również wydajność wyznaczania przybliżenia liczby Pi.

ALTERNATYWNE ROZWIĄZANIE

Wyznaczanie przybliżenia liczby Pi możemy zrealizować znacznie wydajniej,
eliminując konieczność wcześniejszego tworzenia tablicy z wartościami
współrzędnych punktów. Identycznie jak w naszym pierwszym przykładzie
dla pojedynczego procesora będziemy generować liczby pseudolosowe sa-
modzielnie w każdym wątku.

Należy jednak posiadać przystosowany do tego zadania generator, tj. taki

generator, który potrafi generować różne wartości pseudolosowe dla wielu
wątków jednocześnie. Pakiet CUDA Toolkit posiada taki generator, więc wy-
starczy wykorzystać gotowe rozwiązanie.

Wykorzystanie generatora PRNG w jądrze obliczeniowym wymaga dołą-

czenia pliku nagłówkowego:

#include

<curand_kernel.h>

oraz utworzenia zmiennej, gdzie będą zapamiętywane stany poszczególnych
generatorów:

curandState *devStates;

Dla tej zmiennej należy przydzielić odpowiednią ilość pamięci:

cudaStatus = cudaMalloc((

void

**)&devStates,

NBlocks * NThreads *

sizeof

(curandState));

W naszym przypadku aktywnych będzie

NBlocks * NThreads genera-

torów, bo tyle wątków będzie uczestniczyć w obliczeniach. Musimy jeszcze
dokonać inicjalizacji, co wykonuje funkcja obliczeniowa o nazwie

set-

up_kernel_for_curand. W momencie wywołania podajemy oczywiście
odpowiednią liczbę bloków oraz wątków na blok, zgodnie ze wcześniejszym
przydziałem pamięci:

setup_kernel_for_curand<<< NBlocks, NThreads >>>(

devStates, seedtime );

Spoglądając na Listing 5 oraz na funkcję

setup_kernel_for_curand,

można by powiedzieć, iż wszystkie wątki będą stosować ten sam zarodek
inicjalizacyjny

seed, jednak drugi parametr, czyli identyfikator wątku, to tzw.

podsekwencja. Oznacza to, iż choć zarodek jest jeden, mamy wiele różnych
podsekwencji, z których możemy odczytywać wartości pseudolosowe.

Po przygotowaniu generatora wartości pseudolosowych możemy już

przystąpić do głównej procedury obliczeniowej, choć trzeba jeszcze przy-
dzielić pamięć na wyniki testu przynależności punktu do wnętrza okręgu:

cudaStatus = cudaMalloc((

void

**)&devData,

NBlocks * NThreads *

sizeof

(

int

));

Podobnie jak poprzednio mamy tablicę z liczbami, ale tym razem nie są to
tylko wartości jeden oraz zero, ale ilości punktów, które przynależą do wnę-
trza okręgu. Każdy wątek nie sprawdza tylko jednego punktu, jak to było po-
przednio, ale testuje dokładnie

Npoints punktów. I tym zajmuje się funkcja

pi_estimation, którą wywołujemy w następujący sposób:

pi_estimation<<< NBlocks, NThreads >>>( devStates, devData );

Oznacza to, iż mamy dokładnie

NBlocks * Nthreads, a każdy z nich testu-

je

Npoints punktów. Wygenerowane wartości losowe znajdują się w zakresie

od zera do jedności. W naszej pierwszej wersji dla CPU generowaliśmy od

-1

background image

49

/ www.programistamag.pl /

CUDA Z LICZBĄ PI

do

1, ale pamiętajmy, że obliczamy kwadraty tych wielkości, więc informacja

o znaku liczby jest tracona.

Po zakończeniu obliczeń musimy skopiować dane z pamięci urządzenia

obliczeniowego do pamięci hosta:

cudaMemcpy(hostData, devData,

NBlocks * NThreads *

sizeof

(

int

),

cudaMemcpyDeviceToHost

);

Następnie za pomocą poniższej pętli

for obliczamy, ile ostatecznie punktów

trafiło do wnętrza okręgu:

in_circ_points = 0;

for

(i=0;i<NBlocks * Nthreads;i++) {

in_circ_points += hostData[i];

}

Pozostaje jeszcze dopowiedzieć, dlaczego stosowane jest polecenie
__syncthreads();. Jest to polecenie stawiające barierę. Wszystkie wątki
w danym bloku muszą dojść do miejsca wystąpienia bariery, aby dany wą-
tek mógł przejść przez barierę. Jest to przydatne np. podczas operacji zapisu
do pamięci, bowiem część wątków może wylosować punkty nienależące do
wnętrza okręgu, więc wykonają mniej operacji. W takim przypadku wątki,
które wykonały mniej operacji, muszą poczekać, aż pozostałe wątki zrealizują
swoje zadanie. W ten sposób zapis do pamięci globalnej będzie odbywał się
bardziej płynnie.

Listing 5. Alternatywne rozwiązanie o wyższej wydajności

__global__

void

setup_kernel_for_curand(

curandState

*

state

,

unsigned

long

long

seed

) {

int

id = (blockIdx.x * blockDim.x) + threadIdx.x;

curand_init(

seed

, id, 0, &

state

[id]);

}

__global__

void

pi_estimation(

curandState

*

state

,

int

*

data

) {

double

v, v1, v2;

int

id = (blockIdx.x * blockDim.x) + threadIdx.x;

int

i, _ncount = 0;

curandState

localState =

state

[ id ];

data

[ id ] = 0;

for

(i = 0; i < NPoints; i++) {

v1 = curand_uniform_double(&localState);

v2 = curand_uniform_double(&localState);

v = v1*v1 + v2*v2;

if

(v <= 1)

_ncount = _ncount + 1;

}

__syncthreads();

data

[ id ] = _ncount;

}

PODS UMOWANIE

Na zakończenie można postawić pytanie, czy istotnie wykorzystaliśmy już
wszystkie możliwości, aby otrzymać wydajny kod. Odpowiedź brzmi: prawie
wszystkie, bowiem możemy jeszcze skorzystać z pamięci dzielonej, aby szybciej
wyznaczyć sumy częściowe liczby punktów przynależących do wnętrza okrę-
gu. W ten sposób możemy do ostatecznej sumy wykonywanej po stronie pro-
cesora przekazać znacznie mniejszy zbiór wartości do obliczeń po stronie CPU.

Drugim zadaniem jest dobranie odpowiedniej ilości bloków oraz aktyw-

nych wątków, aby osiągnąć jak najwyższą wydajność. Jak widać, można jeszcze
poprawić podane przykłady, co pozwoli lepiej poznać możliwości CUDA C/C++.

W sieci

P Główna strona technologii CUDA:

http://developer.nvidia.com/

P Biblioteka w ANSI-C implementująca kilkanaście generatorów liczb

pseudolosowych:

http://statmath.wu.ac.at/prng/

P Generator liczb pseudolosowych MT19937:

http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/emt.html

P Generator liczb pseudolosowych TinyMT:

http://www.math.sci.hiroshima-u.ac.jp/~m-mat/MT/TINYMT/index.html

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.

reklama

background image

50

/ 3

. 2014 . (22) /

INŻYNIERIA OPROGRAMOWANIA

Wojciech Czabański

CO TO SĄ ZASADY SOLID I DLACZEGO

POWINIENEM SIĘ NIMI PRZEJMOWAĆ?

Próbę wyjaśnienia, czym są i czemu służą zasady SOLID, warto rozpocząć od
zdefiniowania środowiska, w którym mają one zastosowanie, czyli obiektowego
paradygmatu programowania. Czym zaś jest sam paradygmat? Najogólniej mó-
wiąc, jest to wzorzec programowania komputera, który definiuje sposób patrze-
nia programisty na to, w jaki sposób przepływa sterowanie w programie i w jaki
sposób jest wykonywany. Często projektanci języków programowania wybierają
dominujący paradygmat programowania, z wykorzystaniem którego definiują, z
czego „składa się” tworzony przez nich język i jakie konstrukcje można w nim sto-
sować. Do popularnych paradygmatów programowania należą: programowanie
strukturalne, programowanie funkcyjne i programowanie obiektowe.

Obecnie najczęściej spotykanym paradygmatem jest właśnie paradygmat

obiektowy. Sztandarowymi przykładami jego wdrożenia są takie języki jak C#
i Java. Nie ma jednomyślności w kwestii cech języka obiektowego, ale przy-
jętym konsensusem są następujące cechy: abstrakcja, enkapsulacja, polimor-
fizm i dziedziczenie.

Abstrakcja polega na tym, że obiekty w systemie służą jako modele, ukry-

wając szczegóły implementacji przed klientem interfejsu i pozwalając mu na
korzystanie z jego usług bez konieczności posiadania wiedzy o tym, w jaki
sposób i przez jakiego typu klasę dane operacje faktycznie są wykonywane.

Enkapsulacja to chronienie stanu obiektu przed bezpośrednią modyfika-

cją. Stan obiektu może zostać zmodyfikowany tylko poprzez wywoływanie
jego metod.

Polimorfizm, czyli „wielość postaci” w ujęciu obiektowym polega na repre-

zentowaniu za pomocą jednego interfejsu wielu typów pochodnych. Istnieje
wiele typów polimorfizmu, na przykład polimorfizm parametryczny, polimor-
fizm metod, polimorfizm ad hoc. W ramach obiektowości najczęściej mówi się
o polimorfizmie dynamicznym, czyli wykonywaniu metod obiektów, bazując
na sprawdzeniu ich typów podczas działania programu za pomocą mechani-
zmu późnego wiązania (ang. late binding).

Dziedziczenie natomiast porządkuje i realizuje polimorfizm, pozwalając

na rozszerzanie typów bazowych, czyli tworzenie wyspecjalizowanych wa-
riantów klas na podstawie bardziej ogólnych bez potrzeby ponownego defi-
niowania całej funkcjonalności.

Zasady SOLID są praktycznymi wskazówkami, które wypływają bezpo-

średnio z powyższych cech języków obiektowych. Pozwalają na tworzenie
kodu łatwiejszego do testowania i łatwiejszego do rozszerzania.

Dalej omówię każdą z zasad, przedstawię przykłady ich zastosowania i to,

w jaki sposób można diagnozować ich naruszenia oraz jak je poprawić.

S – ZASADA JEDNEJ ODPOWIEDZIALNOŚCI

Zasadę jednej odpowiedzialności można wyjaśnić jednym prostym zdaniem:
klasa powinna mieć tylko jeden powód do zmiany. Bardzo łatwo zdiagnozo-

wać naruszenie tej zasady, próbując opisać, co dana klasa powinna robić. Jeśli
nie jesteśmy w stanie za pomocą jednego prostego zdania opisać funkcjo-
nalności klasy, to znaczy, że najprawdopodobniej klasa wykonuje więcej niż
jedno zadanie.

Stosowanie zasady jednej odpowiedzialności ułatwia późniejsze testo-

wanie – lokalizowanie błędów jest łatwiejsze, jeśli klasy wykonują konkretne
zadania. Przede wszystkim jednak, w przypadku języków kompilowanych,
znacznie przyspiesza późniejsze wdrożenie modułów aplikacji.

Modelowymi przykładami naruszeń zasady jest tak zwany „zapach kodu”

(ang. code smell) – Boski Obiekt (ang. God object), czyli klasa, która wykonuje
zbyt wiele zadań i w związku z tym najczęściej zawiera też wiele metod w
swoim publicznym interfejsie.

Na poniższym listingu przedstawiony został interfejs klasy, która pełni

zbyt wiele ról: rysuje geometrię mapy (

Draw()), zapisuje mapę na dysk i ła-

duje ją (

SaveToFile(), LoadFromFile()) oraz zarządza logiką biznesową

mapy (

SetTilePassable(), ValidateMap()). Mieszana jest prezentacja,

logika biznesowa oraz przechowywanie danych.

Listing 1. Przykład klasy naruszającej zasadę jednej
odpowiedzialności

class

Map

{

public

void

CreateFlatMap(

int

width,

int

height, String

tilesetName) {

/* ... */

}

public

void

CreateFromHeightmap(String path, String tilesetName)

{

/* ... */

}

public

void

Draw() {

/* ... */

}

public

void

SaveToFile(

String

path,

bool

bExport) {

/* ... */

}

public

void

LoadFromFile(

String

path) {

/* ... */

/}

public

void

ValidateMap() {

/* ... */

}

public

void

AddLight(Light l) {

/* ... */

}

public

Light GetLight(

int

idx) {

/* ... */

}

public

void

SetLight(

int

idx, Light l) {

/* ... */

}

public

void

DeleteLight(

int

idx) {

/* ... */

}

public

void

UpdateLights() {

/* ... */

}

public

int

GetNumLights(ELightType type) {

/* ... */

}

public

void

RemoveObject(GameObjectDesc obj) {

/* ... */

}

public

void

RemoveObject(

int

logicalX,

int

logicalY) {

/* ... */

}

public

void

RotateObject(

float

degrees) {

/* ... */

}

public

void

Resize() {

/* ... */

}

public

void

SetTilePassable(

int

x,

int

y,

bool

p) {

/* ... */

}

}

Rozwiązaniem w tym przypadku będzie podzielenie klasy na kilka nowych.
Część graficzna mapy pozostaje w klasie

Map, natomiast część logiczna za-

wierająca walidację, zarządzanie obiektami i kaflami (

SetTilePassable(),

ValidateMap()) zostaje przeniesiona do klasy LogicalMap. Ładowanie i
wczytywanie mapy zostaje przeniesione do klasy wyspecjalizowanej w ope-
racjach wejścia/wyjścia -

MapPersistence. Zarządzanie światłami również

trafia do osobnej klasy –

LightManager.

Wykorzystanie zasad SOLID podczas

wytwarzania oprogramowania w pa-

radygmacie obiektowym

Artykuł przedstawia zbiór pięciu zasad dotyczących projektowania obiektowego
i na przykładach pokazane jest, w jaki sposób można je zastosować w swoim pro-
jekcie. Wspomniana jest także literatura dla zainteresowanych czytelników dla
pogłębienia wiedzy o przytoczonych zasadach i ograniczeniach ich stosowania.

background image

51

/ www.programistamag.pl /

WYKORZYSTANIE ZASAD SOLID…

Listing 2. Klasa po podzieleniu na kilka klas spełniających zasadę
jednej odpowiedzialności

class

Map {

public void

Draw() {

/* ... */

}

public void

Resize() {

/* ... */

}

private

LogicalMap logicalMap;

private

Minimap minimap;

private

MapLightManager lightManager;

}

class

MapPersistence {

public

Map CreateFlatMap(

int

width,

int

height, String

tilesetName) {

/* ... */

}

public

Map CreateMapFromHeightmap(String path, String

tilesetName) {

/* ... */

}

public void

SaveToFile(Map map, String path,

bool

bExport) {

/*

... */

}

public

Map LoadFromFile(String path) {

/* ... */

}

}

class

MapLightManager {

public void

AddLight(Light l) {

/* ... */

}

public

Light GetLight(

int

idx) {

/* ... */

}

public void

SetLight(

int

idx, Light l) {

/* ... */

}

public void

DeleteLight(

int

idx) {

/* ... */

}

public void

UpdateLights() {

/* ... */

}

public int

GetNumLights(ELightType type) {

/* ... */

}

}

class

LogicalMap {

public

void

SetTilePassable(

int

x,

int

y,

bool

p) {

/* ... */

}

public

void

RemoveObject(GameObjectDesc obj) {

/* ... */

}

public

void

RemoveObject(

int

logicalX,

int

logicalY) {

/* ... */

}

public

void

RotateObject(

float

degrees) {

/* ... */

}

public

bool

ValidateMap() {

/* ... */

}

}

Trudno jest zawsze poprawnie projektować klasy zgodnie z zasadą jednej od-
powiedzialności, jednak powinno w tym pomóc zastosowanie trzech kroków:
1. Zdefiniuj aktorów, którzy korzystać będą z systemu.
2. Zidentyfikuj zadania (odpowiedzialności), jakie stoją przed aktorami.
3. Dla każdego zadania (odpowiedzialności) przypisz dokładnie jedną klasę

lub funkcję.

Należy jednak uważać, aby nie próbować w fazie projektowania od razu zde-
finiować wszystkich aktorów biorących udział, ponieważ może prowadzić to
do przedwczesnej optymalizacji, a przez to zbytniego podzielenia systemu na
trudne do zrozumienia „rozproszone” moduły.

O – ZASADA OTWARTE–ZAMKNIĘTE

Zasada ta mówi, że kod powinien być zamknięty na zmiany, ale otwarty na
rozszerzanie. To znaczy, że dodawanie nowych funkcji powinno odbywać się
poprzez dopisywanie nowego kodu, a nie modyfikowanie starego, ponieważ
zwiększa to prawdopodobieństwo wprowadzenia błędu w już istniejących
funkcjach.

Analogicznie, przykładem naruszenia zasady jest zastosowanie instrukcji

switch zamiast wspólnego dla określonego zachowania interfejsu. W skraj-
nym przypadku, każde bezpośrednie użycie klasy, a nie jej interfejsu jest naru-
szeniem zasady otwarte-zamknięte, dlatego należy zawsze ocenić, czy warto
wprowadzać interfejs dla danej funkcjonalności. W poniższym przykładzie
SwapMove, RotateMove oraz ConvertMove są klasami z metodami statycz-
nymi

canPerform() oraz perform(). Aby dodać kolejny ruch, konieczne

jest zdefiniowanie nowej klasy i zmodyfikowanie instrukcji

switch wewnątrz

metody

performMove().

Listing 3. Przykład naruszenia zasady otwarte-zamknięte

public void

performMove(GraphVertex<Block> sourceVertex,

Direction direction) {

GraphVertex<Block> destVertex = sourceVertex.

getNeighbour(direction);

switch

(currMove) {

case

Swap:

if (SwapMove.canPerform(sourceVertex, destVertex)) {

SwapMove.perform(sourceVertex, destVertex);

}

break

;

case

Rotate:

if (RotateMove.canPerform(sourceVertex, destVertex)) {

RotateMove.perform(sourceVertex, destVertex);

}

break

;

case

Convert:

if (ConvertMove.canPerform(sourceVertex, destVertex)){

ConvertMove.perform(sourceVertex, destVertex);

}

break

;

}

}

Modelowym przykładem zastosowania zasady jest poniższy fragment kodu,
w którym wykonywany jest ruch w prostej grze. Ruch ma zostać wykonany z
określonego pola, wierzchołka grafu, w wybranym kierunku. Aby dodać nowy
rodzaj posunięcia, nie jest konieczne zmienianie metody

performMove(),

wystarczy dodać kolejną klasę realizująca interfejs

Move. Dzięki temu maleje

ryzyko wprowadzenia błędu do już istniejącego kodu.

Listing 4. Przykład zastosowania zasady otwarte-zamknięte

public void

performMove(GraphVertex<Block> sourceVertex,

Direction direction) {

GraphVertex<Block> destVertex = sourceVertex.

getNeighbour(direction);

if

(currMove.canPerform(sourceVertex, destVertex)) {

currMove.perform(sourceVertex, destVertex);

}

}

L – ZASADA ZASTĄPIENIA LISKOV

Zasada zastąpienia Liskov głosi, że klasy pochodne realizujące interfejs po-
winny być zamienne na klasy bazowe. To znaczy, że nie powinny naruszać
funkcjonalności dostarczanej przez klasy bazowe. Dobrym przykładem jest
paradoks prostokąta.

Klasa

Square zgodnie z intuicją jest podtypem klasy Rectangle (kwa-

drat to wszak szczególny przykład prostokąta). Problem natomiast pojawia
się w momencie, gdy klient chce skorzystać z klasy obiektu

Rectangle lub

pochodnej i ustawia wymiary na wartości 5 jako szerokość oraz 4 jako wyso-
kość. Naturalnym, oczekiwanym wynikiem byłoby tutaj 20, ale jak się okazuje,
dla kwadratu, który w naszej hierarchii jest również prostokątem, będzie to
16, ponieważ

Square w ramach operacji odziedziczonych po prostokącie

ustawia zarówno wysokość, jak i szerokość na taką samą. Kod nie zachowuje
się zatem tak jak oczekujemy: metoda zmieniająca wysokość zmienia także
szerokość i vice versa.

Listing 5. Przykład naruszenia zasady zastąpienia Liskov

class

Rectangle {

private

int

x, y, width, height;

public void

setHeight(int height) {

this

.height = height;

}

public int

getHeight() {

return

height;

}

public void

setWidth(int width) {

this

.width = width;

}

public int

getWidth() {

return

width;

}

public int

area() {

return

width * height;

}

}

class

Square

extends

Rectangle {

@Override

public void

setHeight(

int

height) {

background image

52

/ 3

. 2014 . (22) /

INŻYNIERIA OPROGRAMOWANIA

this

.width = height;

this

.height = height;

}

@Override

public void

setWidth(

int

width) {

this

.width = width;

this

.height = width;

}

}

class

Client {

bool

verifyArea(Rectangle r) {

r.setWidth(5);

r.setHeight(4);

if

(r.area() != 20) {

throw new

Exception(

”Bad area!”

);

}

return true

;

}

}

Powyższy przykład dowodzi również, że relacje pomiędzy obiektami w świecie
rzeczywistym nie zawsze zachodzą pomiędzy reprezentacjami tych obiektów
w programach obiektowych. Zatem, przez analogię, warto mieć na uwadze
to, że o ile pies jest rodzajem ssaka (pies jest podtypem ssaka), o tyle model
psa wcale nie musi być podtypem modelu ssaka – zależy to już od systemu, w
którym takie modele są definiowane i od celu takiej reprezentacji. Poprawienie
naruszeń zasady zastąpienia Liskov nie jest trywialne, ponieważ zwykle naru-
szona jest również zasada zamknięty-otwarty i wtedy warto zastanowić się, na
ile sensowna jest zastosowana reprezentacja danych i zachowań.

I – ZASADA SEGREGACJI

INTERFEJSÓW

Zasada segregacji interfejsów mówi, że klienci interfejsu nie powinni zależeć
od metod, których nie używają. Zasada dotyczy zatem komunikowania logiki
biznesowej klientom poprzez interfejsy.

W poniższym przykładzie klasa

Socket musi zaimplementować metodę

Seek(), mimo że taka operacja jest niemożliwa i zapewne służyłaby tylko do
zaraportowania błędu.

Listing 6. Przykład naruszenia zasady segregacji interfejsów

interface

IStream

{

abstract

void

Open();

abstract

void

Close();

abstract

byte

[] Read();

abstract

void

Write(

byte

[] data);

abstract

void

Seek(

int

offset, Direction direction);

}

class

File

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Implementacja */

}

}

class

Buffer

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Implementacja

*/

}

}

class

Socket

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Operacja

niewspierana - pusta implementacja */

}

}

Rozwiązaniem tego problemu jest podzielenie interfejsu na dwa. Eliminujemy
w taki sposób konieczność tworzenia pustej implementacji i łamanie zasady
zastąpienia Liskov, ponieważ funkcja

Seek() nie robiłaby nic, jeśli obiektem,

na którym wywoływana byłaby ta metoda, byłby typu

Socket. A to z kolei

jeszcze jeden „zapach kodu” – odrzucony spadek (ang. refused bequest).

Listing 7. Przykład zastosowania zasady segregacji interfejsów

interface

IStream

{

abstract

void

Open();

abstract

void

Close();

abstract

byte

[] Read();

abstract

void

Write(

byte

[] data);

abstract

void

Seek(

int

offset, Direction direction);

}

class

File

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Implementacja

*/

}

}

class

Buffer

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Implementacja

*/

}

}

class

Socket

extends

IStream

{

void

Open() {

/* Implementacja */

}

void

Close() {

/* Implementacja */

}

byte

[] Read() {

/* Implementacja */

}

void

Write(

byte

[] data) {

/* Implementacja */

}

void

Seek(

int

offset, Direction direction) {

/* Operacja

niewspierana - pusta implementacja */

}

}

D – ZASADA ODWRÓCENIA

ZALEŻNOŚCI

Zasada odwrócenia zależności mówi, że moduły wysokiego poziomu nie po-
winny zależeć od niskopoziomowych modułów, ale oba typy powinny zale-
żeć od abstrakcji. Abstrakcje natomiast nie powinny zależeć od szczegółów,
lecz odwrotnie – szczegóły powinny być zależne od abstrakcji.

Można to osiągnąć za pomocą techniki zwanej wstrzykiwaniem zależ-

ności (ang. dependency injection). Polega ona na przekazaniu implementacji
interfejsów wykonujących określone zadania do klasy, która korzysta z nich
tylko poprzez odniesienia do interfejsów. Pozwala na zmniejszenie zależności
między klasami poprzez usunięcie szczegółów implementacji z klasy korzy-
stającej z danego interfejsu. W konsekwencji pozwala także na wykonywanie
dokładniejszych testów, gdyż umożliwia sterowanie działaniem aplikacji po-
przez zastosowanie tak zwanych obiektów udawanych (ang. mock objects).
Przykładem biblioteki, która umożliwia testowanie z użyciem obiektów uda-
wanych, jest GoogleTest z frameworkiem GoogleMock.

Wstrzykiwanie zależności dobrze ilustruje poniższy listing:

Listing 8. Przykład zastosowania zasady wstrzykiwania zależności

interface

IFileSystem

{

List<String> ListFilesRecursively(String

rootDirectory);

// ...

}

class

RemoteFileSystem

extends

IFileSystem

{

// ...

}

background image

53

/ www.programistamag.pl /

WYKORZYSTANIE ZASAD SOLID…

class

LocalFileSystem

extends

IFileSystem

{

// ...

}

class

FileSystemListing

{

private

IFileSystem fileSystem;

FileSystemListing(IFileSystem fileSystem) {

this

.fileSystem = fileSystem;

}

public

List<String> CreateListing(String

rootDirectory) {

return

fileSystem.ListFilesRecursively(rootDirectory);

}

}

Warto wiedzieć, że wstrzykiwanie zależności może odbywać się zarówno
ręcznie (tak jak powyżej), jak i automatycznie, ale po szczegóły odsyłam
do dokumentacji frameworków Unity oraz Castle Windsor (oba dotyczą
języka C#).

OGRANICZENIA ZASAD SOLID

Należy przede wszystkim mieć na uwadze, że nie są to zawsze i bezwzględnie
poprawne reguły postępowania, tylko sugestie i wnioski wypływające z do-
świadczeń programistów i architektów systemów informatycznych, które się
sprawdziły. Podobnie wzorce oprogramowania są użytecznymi i przydatnymi
narzędziami, jednak zastosowane niepoprawnie potrafią uczynić strukturę sys-
temu o wiele bardziej skomplikowaną i trudniejszą w utrzymaniu i rozwijaniu.

Zasady SOLID są szczególnie przydatne w połączeniu z praktykami Test

Driven Development oraz nurtami zwinnego rozwijania oprogramowania.

Na zakończenie, zainteresowanych zgłębianiem tematu mogę odesłać do

trzech pozycji. Aby dowiedzieć się więcej o projektowaniu obiektowym, od-
syłam do książki Roberta C. Martina „Czysty kod. Podręcznik dobrego progra-
misty”. Uporządkowaną ewidencję „brzydkich zapachów” w kodzie i sposo-
bów radzenia sobie z nimi – refaktoryzacji można znaleźć natomiast w książce
Martina Fowlera „Refaktoryzacja. Ulepszenie struktury istniejącego kodu”.
Natomiast szczegółowo o paradygmacie obiektowym można przeczytać w
pozycji Bertranda Meyera „Object Oriented Software Construction”.

Wojciech Czabański

Na co dzień pracuje w firmie Nokia Solutions And Networks w dziale wytwarzania oprogra-
mowania na stacje bazowe działające w technologii LTE. Do jego zadań należy rozwijanie i
konserwacja kodu jednego z komponentów pracujących na stacji bazowej.

Jesteśmy dynamicznie rozwijającą się firmą w dziedzinie oprogramowania i zarządzania
infrastrukturą informatyczną dla dużych i średnich przedsiębiorstw.

Analityk biznesowy

Tester oprogramowania ERP

Starszy Programista PHP

Programista PHP

Specjalista ds. wdrożeń systemów ERP

Programista PL/pgSQL

Programista C/C++

Programista Java

biuro@nomino.pl

NOMINO Centrala

ul. Handlowa 2a

36-100 Kolbuszowa

tel. 17 250 08 41

Oddział Rzeszów

al. Wincentego Witosa 9b

35-115 Rzeszów

tel. 17 250 08 41 wew. 623

Oddział Kraków

ul. Malwowa 15/1

30-611 Kraków

tel. 12 350 22 00

Poszukiwani:

Dbamy o to, aby praca w naszej firmie dała nie tylko satysfakcję i możliwość uczestnictwa
w ambitnych projektach. Oferujemy również to, co bezcenne: miłą atmosferę i przyjazne środowisko pracy.

ZAINTERESOWALIŚMY CIĘ? SKONTAKTUJ SIĘ Z NAMI!

reklama

background image

54

/ 3

. 2014 . (22) /

PROGRAMOWANIE BAZ DANYCH

Jędrzej Czarnecki

ORM w PHP

z wykorzystaniem wzorca Active Record

Aby tworzyć aplikacje korzystające z baz danych, niekoniecznie trzeba pisać mnó-
stwo zapytań SQL. Programując obiektowo, dostęp do bazy danych można przed-
stawić w sposób zupełnie inny, niż tradycyjnie – za pomocą obiektów. W niniejszym
artykule wyjaśnię, czym jest ORM, jak wyglądają jego podstawowe wzorce projek-
towe oraz pokażę praktyczne zobrazowanie na przykładzie języka PHP.

CZYM JEST ORM?

ORM (ang. object-relational mapping, czyli mapowanie obiektowo-relacyjne)
jest to podejście, w którym struktura relacyjnej bazy danych (takiej jak np.
MySQL) jest odwzorowana za pomocą obiektów.

W aplikacji, zamiast pisać ręcznie zapytania SQL, używa się obiektów; dla

przykładu każda tabela jest instancją klasy, posiada predefiniowane metody,
za pomocą których można wykonywać na jej rekordach różne operacje (wy-
szukać, zmienić, usunąć). Kolumny tabeli są zdefiniowane jako właściwości
danej klasy. W prosty sposób można opisać relacje z innymi tabelami (jak
wiele do wielu, 1 do wielu, 1 do 1) czy zdefiniować walidację danych wejścio-
wych wg różnych kryteriów (typy danych, limity, wzorce itd.). Także kod staje
się niezależny od silnika bazy danych, więc przy ewentualnej migracji nie ma
problemu ewentualnych różnic składniowych w zapytaniach SQL pomiędzy
jedną bazą danych a drugą. Używanie systemów ORM wymusza na programi-
ście pisanie obiektowego kodu i patrzenie na dostęp do bazy danych w nieco
bardziej abstrakcyjny sposób.

Istnieje kilka wzorców projektowych, z czego najpopularniejszymi są Acti-

ve Record oraz Data Mapper, i na nich dzisiaj się skupię, przy czym praktycznie
wykorzystamy ten pierwszy. Jeśli chodzi o język PHP, to powstało wiele syste-
mów ORM – zarówno dla wymienionych wzorców, jak i innych. Używając po-
pularnych frameworków MVC, tworząc modele, używa się właśnie bibliotek
ORM; często można wybrać spośród kilku.

Zastosowanie Active Record znajdziemy w ORM-ach takich jak Propel,

php-activerecord, ADOdb Active Record, albo w dedykowanych implementa-
cjach we frameworkach Yii, CakePHP czy Kohana. Dobrym przykładem użycia
wzorca Data Mapper jest popularna biblioteka Doctrine2, która jest domyśl-
nie używana m.in. przez popularny framework Symfony2 (ciekawostką może
być fakt, iż Doctrine w wersji 1 było oparte o Active Record).

WZORZEC ACTIVE RECORD

Zdecydowanie najpopularniejszym wzorcem projektowym w mapowa-

niu obiektowo-relacyjnym jest Active Record. Został po raz pierwszy zapre-
zentowany przez Martina Fowlera w 2003 roku.

Interfejs klasy active record zawiera metody takie jak Insert

(wstawianie),

Update (modyfikacja) oraz Delete (usuwanie). We wzorcu tym każda tabela w
bazie danych reprezentowana jest za pomocą klasy dziedziczącej po active
record. Klasy te nazywane są potocznie modelami. Każda instancja takiej klasy
odpowiada jednemu rekordowi w tabeli. Tworząc nowy obiekt klasy, po wy-
wołaniu metody zapisu do bazy (

save), nowy wiersz (rekord) zostaje dodany

do tabeli. Każdy obiekt, który został „załadowany” (np. za pomocą wyszuki-
wania po kolumnie ID), pobiera informacje – typu kolumny i ich wartości – z
bazy danych, które automatycznie zostają przedstawione jako właściwości
klasy, bez konieczności ich wcześniejszego definiowania. Zmiana wartości
właściwości, np. poprzez przypisanie, powoduje aktualizację w bazie danych,
po wywołaniu metody zapisu.

No dobrze, ale jak to wygląda w praktyce? Dla przykładu, takie zapytanie SQL:

INSERT

INTO

ksiazki

(

tytul

,

cena

)

VALUES

(

'Gwiezdne wojny'

,

19.90

)

;

zapisane za pomocą wzorca Active Record wyglądałoby następująco (zakłada-
my, że klasa

Ksiazka implementuje owy wzorzec, wskazując na tabelę ksiazki):

$ksiazka

=

new

Ksiazka

;

$ksiazka

->

tytul

=

'Gwiezdne wojny'

;

$ksiazka

->

cena

=

19.90

;

$ksiazka

->

save

()

;

WZORZEC DATA MAPPER

Wzorzec ten został również zaprojektowany przez Martina Fowlera w 2003r.

Interfejs klasy data mapper realizuje model CRUD, czyli zawiera metody

takie jak Create (tworzenie), Read (pobieranie), Update (modyfikacja) oraz
Delete (usuwanie). Odpowiednikiem modeli z wzorca ActiveRecord są encje
(ang. entities). Data Mapper odpowiada za obsługę transferu danych pomię-
dzy bazą danych a obiektem i na odwrót. Występuje tu dodatkowe odsepa-
rowanie, w którym połączenie z bazą danych, metody typu wyszukiwanie
rekordów i zapis (mapper) są rozdzielone od samej struktury (będącej osobną
klasą) reprezentującej pojedynczy rekord (domain model). Oprócz tego istnie-
je też trzeci rodzaj klasy nazwany kolekcjami.

Wcześniej zaprezentowany kod, zaprojektowany wg wzorca Data Mapper,

wyglądałby mniej więcej następująco:

$ksiazka

=

new

Ksiazka

;

$ksiazka

->

tytul

=

'Gwiezdne wojny'

;

$ksiazka

->

cena

=

19.90

;

$mapper

=

new

KsiazkaMapper

(

$db

)

;

$mapper

->

save

(

$ksiazka

)

;

RZUT OKA NA PHP-ACTIVERECORD

Aby pokazać, jak wygląda w praktyce programowanie z użyciem mapowania
obiektowo-relacyjnego, stworzymy prostą aplikację. Skupię się na bibliotece
php-activerecord, jako że implementuje ona czysty wzorzec Active Record, od
którego zaczniemy. Można ją pobrać ze strony

http://phpactiverecord.org

.

Stwórzmy plik index.php i w tym samym miejscu rozpakujmy pobraną

paczkę biblioteki, tym samym powinniśmy od teraz posiadać katalog php-ac-
tiverecord
. Stwórzmy również folder models, w którym będziemy umieszczali
nasze modele. Zawartość pliku index.php:

<?php

require_once

'php-activerecord/ActiveRecord.php'

;

ActiveRecord\Config

::

initialize

(

function

(

$config

)

{

$config

->

set_model_directory

(

'models'

)

;

$config

->

set_connections

(

array

(

'development'

=>

'mysql://uzytkownik:haslo@localhost/nazwa_bazy'

))

;

})

;

background image

55

/ www.programistamag.pl /

ORM W PHP Z WYKORZYSTANIEM WZORCA ACTIVE RECORD

Zainicjowaliśmy tutaj, jak widać, połączenie z bazą MySQL (można oczy-

wiście użyć innej bazy, np. pgsql dla PostgreSQL, sqlite dla SQLite itd.). Zajmij-
my się teraz stworzeniem przykładowej, prostej struktury bazy:

CREATE

TABLE

ksiazki

(

id

INT

NOT

NULL

PRIMARY

KEY

AUTO_INCREMENT

,

tytul

VARCHAR

(

50

),

cena

FLOAT

,

autor_id

INT

)

;

Następnie napiszmy model reprezentujący ową tabelę. W tym celu w katalo-
gu models należy stworzyć plik o nazwie Ksiazka.php. Jego zawartość w naj-
prostszy sposób można zapisać tak:

<?php

class

Ksiazka

extends

ActiveRecord\Model

{

static

$table_name

=

'ksiazki'

;

}

Nazwa klasy musi mieć taką samą nazwę jak nazwa pliku. Poprzez statyczną
właściwość

$table_name ustawiamy nazwę tabeli w bazie danych, na którą

będzie wskazywał model. Jeśli byśmy tego nie zrobili, php-activerecord szu-
kałby tabeli o nazwie ksiazkas – sufix -s dodawany jest oczywiście wg gra-
matyki języka angielskiego, tworząc liczbę mnogą (zakładając, że nasz model
nazywa się Book, szukaną tabelą byłaby books, co ma więcej sensu).

Unikalne pole reprezentujące każdy rekord w tej tabeli, wg stworzonej przez

nas struktury bazy danych, ma nazwę id. Jeśli nazwa ta byłaby inna, np. KsiazkaID,
należałoby ten fakt analogicznie odnotować – poprzez właściwość

$primary_key.

WSTAWIANIE DANYCH

Wróćmy teraz do pliku index.php. Dodając na jego końcu:

$ksiazka

=

new

Ksiazka

;

$ksiazka

->

tytul

=

'Gwiezdne wojny'

;

$ksiazka

->

cena

=

19.90

;

if

(

$ksiazka

->

save

())

{

echo

'Książka została zapisana do bazy'

;

}

zostanie do naszej tabeli wstawiony nowy rekord. Można też to zapisać na
inne sposoby:

$atrybuty

=

array

(

'tytul'

=>

'Gwiezdne wojny'

,

'cena'

=>

19.90

)

;

$ksiazka

=

new

Ksiazka

(

$atrybuty

)

;

$ksiazka

->

save

()

;

// lub:

Ksiazka

::

create

(

$atrybuty

)

;

WYSZUKIWANIE DANYCH

Dostępnych jest wiele funkcji wyszukujących, czyli wykonujących (generują-
cych) zapytania

SELECT. Podstawową funkcją jest statyczna metoda find. Przy-

biera ona wiele wiariantów jako argumenty. Na przykład

find('all') zwróci

wszystkie rekordy. Alternatywnie do

find('all'), istnieje metoda all – tak

samo jak

first, tylko dla pierwszego rekordu i last dla ostatniego; zamiast więc

pisać

Ksiazka::find('last'), możemy skrócić to do Ksiazka::last().

Aby znaleźć tylko jeden rekord po naszym unikalnym polu id, należy prze-

kazać szukaną wartość id jako parametr, np.

Ksiazka::find(2) zwróci tylko

rekord o id=2 (a będąc bardziej dokładnym, chodzi oczywiście o wartość zde-
finiowaną przez

Ksiazka::$primary_key, która domyślnie ustawiona jest

na wartość id, o czym wspominałem już wcześniej). Aby znaleźć rekordy o id

2, 3 i 4, można napisać np.

Ksiazka::find(2, 3, 4), albo Ksiazka::find-

(array(2, 3, 4)).

Metoda

find posiada także drugi parametr, a są nim opcje – takie jak:

warunek (conditions), ograniczenie zwróconych wyników (limit), punkt star-
towy (offset), sortowanie (order), grupowanie (group), tryb tylko do odczytu
(readonly) itp. Dla przykładu taki kod zwróci nam trzy najdroższe książki, kosz-
tujące poniżej 20 zł, w kolejności alfabetycznej:

$ksiazki

=

Ksiazka

::

find

(

'all'

,

array

(

'conditions'

=>

array

(

'cena < ?'

,

20

)

,

'limit'

=>

3

,

'order'

=>

'cena DESC, tytul ASC'

))

;

foreach

(

$ksiazki

as

$ksiazka

)

{

echo

$ksiazka

->

tytul

.

'<br>'

;

}

Oprócz tego, możemy używać metod tworzonych dynamicznie:

Ksiazka

::

find_by_tytul

(

'Gwiezdne wojny'

)

;

// książka po tytule

MODYFIKACJA I USUWANIE DANYCH

Modyfikacja danych, czyli SQL-owe zapytanie

UPDATE, realizowane jest na

kilka sposobów. Pierwszym jest po prostu zmiana właściwości (a więc – ko-
lumn rekordu, na którym operujemy) obiektu i ich zapisanie do bazy danych
(metoda

save). Na przykład, aby zmienić cenę książki o id=1:

$ksiazka

=

Ksiazka

::

find

(

1

)

;

$ksiazka

->

cena

=

14.90

;

$ksiazka

->

save

()

;

Drugim sposobem jest funkcja

update_attributes:

$ksiazka

=

Ksiazka

::

find

(

1

)

;

$ksiazka

->

update_attributes

(

array

(

'cena'

=>

14.90

))

;

Aby masowo zmienić jakieś wpisy, można skorzystać z konstrukcji:

$what

=

array

(

'cena'

=>

3.90

)

;

$where

=

array

(

'id'

=>

array

(

2

,

3

))

;

Ksiazka

::

table

()

->

update

(

$what

,

$where

)

;

Zostaną zaaktualizowane rekordy o id=1 oraz 2. Do usuwania rekordów służy
– jak można się domyślić – metoda

delete:

Ksiazka

::

find

(

1

)

->

delete

()

;

Tak jak w przypadku funkcji

update, istnieje również możliwość masowego

usuwania. Aby usunąć rekordy o id=1 i 2:

Ksiazka

::

table

()

->

delete

(

array

(

'id'

=>

array

(

1

,

2

)))

;

PRZYKŁADOWA APLIKACJA

Absolutne wprowadzenie mamy już za sobą. Wiemy, jak połączyć się z bazą
danych i wykonywać najprostsze operacje. Jak mogłeś, drogi Czytelniku, za-
uważyć – na początku tworząc tabelę ksiazki, dodaliśmy kolumnę autor_id, z
której jeszcze nie korzystaliśmy. Teraz nam się przyda. Aby bardziej zobrazo-
wać w praktyce to, czego się nauczyliśmy, napiszmy jakiś skrypt – zarządzanie
książkami i autorami. Na początek dodajmy potrzebne tabele do bazy danych:

CREATE

TABLE

autorzy

(

id

INT

NOT

NULL

PRIMARY

KEY

AUTO_INCREMENT

,

imie

VARCHAR

(

50

),

nazwisko

VARCHAR

(

50

),

email

VARCHAR

(

150

)

)

;

background image

56

/ 3

. 2014 . (22) /

PROGRAMOWANIE BAZ DANYCH

Poniższy przykład pokaże również użycie dwóch innych elementów, z

których jeszcze nie korzystaliśmy, mianowicie tworzenie relacji między mo-
delami (tabelami) oraz walidację danych wejściowych. Jest to bardzo prowi-
zoryczna wersja (o wiele lepiej programuje się z użyciem frameworków MVC,
brakuje dokładniejszej obsługi sytuacji wyjątkowych itp.), jednak na potrzeby
tego artykułu myślę, że istota jest gdzie indziej.

Na samym początku stwórzmy jakąś separację warstw aplikacji. Utwórzmy

katalogi controllers oraz views. Następnie stwórzmy pliki tak jak na Rysunku 1.

Rysunek 1. Struktura plików tworzonej aplikacji

Plik index.php wypełnijmy następującą zawartością:

Listing 1. index.php

<?php

require_once

'php-activerecord/ActiveRecord.php'

;

ActiveRecord\Config

::

initialize

(

function

(

$config

){

$config

->

set_model_directory

(

'models'

)

;

$config

->

set_connections

(

array

(

'development'

=>

'mysql://uzytkownik:'

.

'haslo@localhost/nazwa_bazy'

))

;

})

;

abstract

class

PageProperties

{

public

static

$tytul

;

public

static

$content

;

public

static

$menu_ksiazki

;

}

abstract

class

PageBase

extends

PageProperties

{

protected

function

loadView

(

$view

,

$model

)

{

ob_start

()

;

require_once

'views/'

.

get_class

(

$this

)

.

$view

.

'.php'

;

return

ob_get_clean

()

;

}

}

interface

IPageMethods

{

public

function

dodaj

()

;

public

function

edytuj

(

$id

)

;

public

function

usun

(

$id

)

;

public

function

lista

()

;

}

class

Page

extends

PageProperties

{

public

static

function

run

(

IPageMethods

$page

)

{

try

{

if

(

!

empty

(

$_GET

[

'edytuj'

]))

$page

->

edytuj

(

$_GET

[

'edytuj'

])

;

elseif

(

!

empty

(

$_GET

[

'usun'

]))

$page

->

usun

(

$_GET

[

'usun'

])

;

else

$page

->

dodaj

()

;

$page

->

lista

()

;

}

catch

(

Exception

$e

)

{

self

::

$content

=

'Błędne żądanie'

;

}

}

}

require_once

'controllers/KsiazkiPage.php'

;

require_once

'controllers/AutorzyPage.php'

;

Page

::

run

(

isset

(

$_GET

[

'autorzy'

])

?

new

AutorzyPage

:

new

KsiazkiPage

)

;

require_once

'views/layout.php'

;

Teraz stwórzmy nasze kontrolery, pierwszy z nich to plik KsiazkiPage.php znaj-
dujący się w katalogu controllers:

Listing 2. KsiazkiPage.php

<?php

class

KsiazkiPage

extends

PageBase

implements

IPageMethods

{

public

function

__construct

()

{

self

::

$tytul

=

'Książki'

;

self

::

$menu_ksiazki

=

true

;

}

public

function

dodaj

()

{

if

(

!

empty

(

$_GET

[

'szukaj'

]))

{

self

::

$tytul

=

'Szukaj "'

.

htmlspecialchars

(

$_GET

[

'szukaj'

])

.

'"'

;

self

::

$content

=

'<h2>'

.

self

::

$tytul

.

'</h2>'

;

}

else

{

if

(

isset

(

$_POST

[

'submit'

]))

{

$ksiazka

=

new

Ksiazka

;

$ksiazka

->

tytul

=

$_POST

[

'tytul'

]

;

$ksiazka

->

cena

=

$_POST

[

'cena'

]

;

$ksiazka

->

autor_id

=

$_POST

[

'autor_id'

]

;

if

(

$ksiazka

->

is_valid

())

{

if

(

$ksiazka

->

save

())

{

self

::

$content

.=

'Dodano pomyślnie'

;

}

}

else

{

self

::

$content

.=

implode

(

'<br>'

,

$ksiazka

->

errors

->

full_messages

())

;

}

}

$this

->

formularz

(

array

(

'Dodaj książkę'

,

null

,

null)

,

null)

;

}

}

public

function

edytuj

(

$id

)

{

$ksiazka

=

Ksiazka

::

find

(

$id

)

;

if

(

isset

(

$_POST

[

'submit'

]))

{

$ksiazka

->

tytul

=

$_POST

[

'tytul'

]

;

$ksiazka

->

cena

=

$_POST

[

'cena'

]

;

$ksiazka

->

autor_id

=

$_POST

[

'autor_id'

]

;

if

(

$ksiazka

->

is_valid

())

{

if

(

$ksiazka

->

save

())

{

self

::

$content

.=

'Dodano pomyślnie'

;

}

}

else

{

self

::

$content

.=

implode

(

'<br>'

,

$ksiazka

->

errors

->

full_messages

())

;

$ksiazka

=

Ksiazka

::

find

(

$id

)

;

}

}

self

::

$content

.=

$this

->

formularz

(

array

(

htmlspecialchars

(

'Edytuj książkę "'

.

$ksiazka

->

tytul

.

'"'

)

,

$ksiazka

->

tytul

,

$ksiazka

->

cena

)

,

$ksiazka

->

autor_id

)

;

}

public

function

usun

(

$id

)

{

Ksiazka

::

find

(

$id

)

->

delete

()

;

self

::

$content

=

'Książka została usunięta'

;

}

public

function

lista

()

{

$atrybuty

=

array

(

'order'

=>

'tytul ASC'

)

;

// wyszukiwarka po tytule

background image

57

/ www.programistamag.pl /

ORM W PHP Z WYKORZYSTANIEM WZORCA ACTIVE RECORD

if

(

!

empty

(

$_GET

[

'szukaj'

]))

$atrybuty

[

'conditions'

]

=

array

(

'tytul LIKE CONCAT("%",?,"%")'

,

$_GET

[

'szukaj'

])

;

$ks

=

Ksiazka

::

all

(

$atrybuty

)

;

self

::

$content

.=

$this

->

loadView

(

'Lista'

,

$ks

)

;

}

protected

function

formularz

(

$model

,

$autor

=

null){

$model

[]

=

$autor

;

$model

[]

=

Autor

::

all

()

;

return

$this

->

loadView

(

'Form'

,

$model

)

;

}

}

Drugi kontroler – plik AutorzyPage.php:

Listing 3. AutorzyPage.php

<?php

class

AutorzyPage

extends

PageBase

implements

IPageMethods

{

public

function

__construct

()

{

self

::

$tytul

=

'Autorzy'

;

self

::

$menu_ksiazki

=

false

;

}

public

function

dodaj

()

{

if

(

isset

(

$_POST

[

'submit'

]))

{

$autor

=

new

Autor

;

$autor

->

imie

=

$_POST

[

'imie'

]

;

$autor

->

nazwisko

=

$_POST

[

'nazwisko'

]

;

$autor

->

email

=

$_POST

[

'email'

]

;

if

(

$autor

->

is_valid

())

{

if

(

$autor

->

save

())

self

::

$content

.=

'Dodano pomyślnie'

;

}

else

{

self

::

$content

.=

implode

(

'<br>'

,

$autor

->

errors

->

full_messages

())

;

}

}

self

::

$content

.=

$this

->

loadView

(

'Form'

,

array

(

'Dodaj autora'

,

null

,

null

,

null))

;

}

public

function

edytuj

(

$id

)

{

$autor

=

Autor

::

find

(

$id

)

;

if

(

isset

(

$_POST

[

'submit'

]))

{

$autor

->

imie

=

$_POST

[

'imie'

]

;

$autor

->

nazwisko

=

$_POST

[

'nazwisko'

]

;

$autor

->

email

=

$_POST

[

'email'

]

;

if

(

$autor

->

is_valid

())

{

if

(

$autor

->

save

())

self

::

$content

.=

'Dodano pomyślnie'

;

}

else

{

self

::

$content

.=

implode

(

'<br>'

,

$autor

->

errors

->

full_messages

())

;

$autor

=

Autor

::

find

(

$id

)

;

}

}

self

::

$content

.=

$this

->

loadView

(

'Form'

,

array

(

'Edytuj autora '

.

$autor

->

imie

.

' '

.

$autor

->

nazwisko

,

$autor

->

imie

,

$autor

->

nazwisko

,

$autor

->

email

))

;

}

public

function

usun

(

$id

)

{

Autor

::

find

(

$id

)

->

delete

()

;

self

::

$content

=

'Autor został usunięty'

;

}

public

function

lista

()

{

$a

=

Autor

::

all

(

array

(

'order'

=>

'nazwisko ASC, imie ASC'

))

;

self

::

$content

.=

$this

->

loadView

(

'Lista'

,

$a

)

;

}

}

Mając zdefiniowane kontrolery, możemy teraz napisać modele (znajdują się
one w katalogu models).

Listing 4. Ksiazka.php

<?php

class

Ksiazka

extends

ActiveRecord\Model

{

static

$table_name

=

'ksiazki'

;

// definiujemy relacje z modelem Autor

static

$belongs_to

=

array

(

array

(

'autor'

)

)

;

// walidacja - pola obowiazkowe

static

$validates_presence_of

=

array

(

array

(

'tytul'

,

'on'

=>

'save'

,

'message'

=>

'nie może być pusty'

)

)

;

}

Jak widać zdefiniowaliśmy w nim walidację danych wejściowych oraz opisaliśmy
relacje z modelem Autor, którego zawartość znajduje się w pliku models/Autor.php:

Listing 5. Autor.php

<?php

class

Autor

extends

ActiveRecord\Model

{

static

$table_name

=

'autorzy'

;

// definiujemy relacje z modelem Autor

static

$has_many

=

array

(

array

(

'ksiazki'

,

'class_name'

=>

'Ksiazka'

)

)

;

// walidacja - pola obowiazkowe

static

$validates_presence_of

=

array

(

array

(

'imie'

,

'on'

=>

'save'

,

'message'

=>

'nie może być puste'

)

,

array

(

'nazwisko'

,

'on'

=>

'save'

,

'message'

=>

'nie może być puste'

)

)

;

// walidacja wg wyrazen regularnych

static

$validates_format_of

=

array

(

array

(

'email'

,

'on'

=>

'save'

,

'message'

=>

'jest złego formatu'

,

'with'

=>

'/^[A-z0-9_\.]+@[A-z0-9_\.]+\.[A-z]{2,4}$/'

))

;

}

Oprócz walidacji pól obowiązkowych, zostało dodane wyrażenie regularne
do sprawdzania poprawności adresu e-mail. Ostatnim elementem brakują-
cym w naszej aplikacji są widoki (czyli szablony HTML).

Listing 6. KsiazkiPageForm.php

<?php

$select

=

'<select name="autor_id">

<option value="0">-- wybierz --</option>'

;

if

(

count

(

$model

[

4

]))

{

foreach

(

$model

[

4

]

as

$a

)

{

$sel

=

$a

->

id

===

$model

[

3

]

?

' selected="selected"'

:

''

;

$select

.=

'<option value="'

.

$a

->

id

.

'"'

.

$sel

.

'>'

.

$a

->

imie

.

' '

.

$a

->

nazwisko

.

'</option>'

;

}

}

$select

.=

'</select>'

;

?>

<

h3

>

<?=$model[0]?>

<

/

h3

>

<

form

action

=

""

method

=

"post"

>

<

table

class

=

"table table-striped table-bordered"

>

<

tr

>

<

td

width

=

"100"

>

Tytuł:

<

/

td

>

<

td

><

input

type

=

"text"

name

=

"tytul"

value

=

"<?=$model[1]?>

">

<

/

td

>

<

/

tr

>

<

tr

>

<

td

>

Cena:

<

/

td

>

<

td

><

input

type

=

"text"

name

=

"cena"

value

=

"<?=$model[2]?>

">

<

/

td

>

<

/

tr

>

<

tr

>

<

td

>

Autor:

<

/

td

>

<

td

>

<?=$select?>

<

/

td

>

<

/

tr

>

<

/

table

>

<

input

type

=

"submit"

name

=

"submit"

value

=

"<?=$model[0]?>

" class="btn btn-primary">

<

/

form

><

br

>

background image

58

/ 3

. 2014 . (22) /

PROGRAMOWANIE BAZ DANYCH

Listing 7. KsiazkiPageLista.php

<?php

if

(

count

(

$model

))

{

?>

<h3>Lista książek</h3>

<table class="table table-striped table-bordered">

<tr>

<th>Tytuł</th>

<th>Cena</th>

<th>Autor</th>

<th colspan="2">Akcje</th>

</tr>

<?php

foreach

(

$model

as

$ksiazka

)

{

if

(

$ksiazka

->

autor

)

{

$autor

=

$ksiazka

->

autor

->

imie

.

' '

.

$ksiazka

->

autor

->

nazwisko

;

}

else

{

$autor

=

'---'

;

}

$cena

=

number_format

(

$ksiazka

->

cena

,

2

)

;

?>

<tr>

<td>

<?=

$ksiazka

->

tytul

?>

</td>

<td>

<?=

$cena

?>

zł</td>

<td>

<?=

$autor

?>

</td>

<td><a href="?edytuj=

<?=

$ksiazka

->

id

?>

"

class="btn btn-warning">Edytuj</a></td>

<td><a href="?usun=

<?=

$ksiazka

->

id

?>

"

class="btn btn-danger">Usuń</a></td>

</tr>

<?php

}

echo

'</table>'

;

}

else

{

?>

<br><br>Brak wyników

<?php

}

?>

Listing 8. AutorzyPageForm.php

<

h3

>

<?=$model[0]?>

<

/

h3

>

<

form

action

=

""

method

=

"post"

>

<

table

class

=

"table table-striped table-bordered"

>

<

tr

>

<

td

width

=

"100"

>

Imię:

<

/

td

>

<

td

><

input

type

=

"text"

name

=

"imie"

value

=

"<?=$model[1]?>

">

<

/

td

>

<

/

tr

>

<

tr

>

<

td

>

Nazwisko:

<

/

td

>

<

td

><

input

type

=

"text"

name

=

"nazwisko"

value

=

"<?=$model[2]?>

">

<

/

td

>

<

/

tr

>

<

tr

>

<

td

>

E-mail:

<

/

td

>

<

td

><

input

type

=

"text"

name

=

"email"

value

=

"<?=$model[3]?>

">

<

/

td

>

<

/

tr

>

<

/

table

>

<

input

type

=

"submit"

name

=

"submit"

value

=

"<?=$model[0]?>

" class="btn btn-primary">

<

/

form

><

br

>

Listing 9. AutorzyPageLista.php

<?php

if

(

count

(

$model

))

{

?>

<h3>Lista autorów</h3>

<table class="table table-striped table-bordered">

<tr>

<th>Imię</th>

<th>Nazwisko</th>

<th>Lista książek</th>

<th colspan="2">Akcje</th>

</tr>

<?php

foreach

(

$model

as

$autor

)

{

if

(

$autor

->

ksiazki

)

{

$ksiazki

=

''

;

foreach

(

$autor

->

ksiazki

as

$k

)

{

$ksiazki

.=

$k

->

tytul

.

'<br>'

;

}

}

else

{

$ksiazki

=

'---'

;

}

?>

<tr>

<td>

<?=

$autor

->

imie

?>

</td>

<td>

<?=

$autor

->

nazwisko

?>

</td>

<td>

<?=

$ksiazki

?>

</td>

<td><a href="?autorzy&edytuj=

<?=

$autor

->

id

?>

"

class="btn btn-warning">Edytuj</a></td>

<td><a href="?autorzy&usun=

<?=

$autor

->

id

?>

"

class="btn btn-danger">Usuń</a></td>

</tr>

<?php

}

echo

'</table>'

;

}

else

{

?>

<br><br>Brak wyników

<?php

}

?>

I ostatni już widok, opisujący główną strukturę naszej strony – layout.php. Wy-
korzystuje on arkusze stylów CSS z biblioteki Bootstrap.

Listing 10. layout.php

<!DOCTYPE html>

<

html

>

<

head

>

<

meta

charset

=

"utf-8"

>

<

title

><?

=

Page::$tytul?><

/

title

>

<

link

href

=

"<?='//netdna.bootstrapcdn.com/'

. 'bootstrap/3.1.1/css/bootstrap.min.css'?>

"

rel="stylesheet">

<

/

head

>

<

body

>

<nav

class

=

"navbar navbar-default container-fluid"

>

<

ul

class

=

"nav navbar-nav"

>

<li

<?=

Page

::

$menu_ksiazki

?

' class="active"'

:

''

?>

>

<

a

href

=

"?"

>

Książki

<

/

a

><

/

li

>

<li

<?=

!

Page

::

$menu_ksiazki

?

' class="active"'

:

''

?>

>

<

a

href

=

"?autorzy"

>

Autorzy

<

/

a

><

/

li

>

<

/

ul

>

<

form

class

=

"navbar-form navbar-right"

action

=

"?"

method

=

"get"

>

<

input

type

=

"text"

class

=

"form-control"

placeholder

=

"Szukaj książki..."

name

=

"szukaj"

>

<

input

type

=

"submit"

class

=

"btn btn-default"

value

=

"Szukaj"

>

<

/

form

>

<

/

nav>

<

div

class

=

"container"

>

<?=

Page

::

$content

?>

<

/

div

>

<

/

body

>

<

/

html

>

W razie potrzeby pełnego kodu źródłowego, zachęcam do kontaktu mailo-
wego ze mną. Poniżej znajdują się zrzuty prezentujące aplikację w działaniu:

Rysunek 2. Dodawanie i lista książek

background image

59

/ www.programistamag.pl /

ORM W PHP Z WYKORZYSTANIEM WZORCA ACTIVE RECORD

Rysunek 3. Wyszukiwarka książek

Jędrzej Czarnecki

jedrzej@czarnecki.org.pl

Autor obecnie mieszka w Londynie i pracuje jako programista/web developer. Oprócz
tego, początkujący przedsiębiorca. Pasjonat podróżowania po świecie, psychologii
i rozwoju osobistego. Lubi ciekawie spędzać czas. Więcej informacji na stronie domowej

http://czarnecki.org.pl

.

Rysunek 4. Edycja i lista autorów

PODSUMOWANIE

Niniejszym wprowadzenie do mapowania obiektowo-relacyjnego dobiegło
końca. Jest to na pewno podejście warte uwagi, tym bardziej że jest używa-
ne praktycznie przez każdy współczesny framework MVC. Systemy ORM nie
są oczywiście pozbawione wad – poprzez kolejną warstwę są mniej wydajne
(ale od czego jest cache), czasami ciężko osiągnąć oczekiwany rezultat przy
bardzo skomplikowanych zapytaniach (niemniej w większości bibliotek ORM
istnieje alternatywa tworzenia zapytań SQL ręcznie tam, gdzie to potrzebne
– szczególnie również dla „wąskich gardeł”), pomimo zaimplementowanego
w większości bibliotek ORM mechanizmu lazy loading, polegającego na po-
bieraniu z bazy żądanych części danych, tylko jeśli się o nie „upomnimy” (w
kodzie) – np. do jakiejś relacji z innymi tabelami – i tak ORM będzie wolniejszy

od tradycyjnych zapytań, a czasami i stwarza niebezpieczeństwo generowa-
nia bardzo nadmiarowej ilości zapytań SQL. Uważam jednak, że ilość plusów
przeważa – prostota i efektywność, produkuje się o wiele mniej kodu (być
może na prostych przykładach tego szczególnie nie widać), kod jest bardziej
uniwersalny (można wykonywać dziedziczenia modeli itp.), jest to w pełni
obiektowe podejście, typowe zapytania SQL można zupełnie wyeliminować
za pomocą mnóstwa wbudowanych funkcji.

Bezpieczeństwo danych mobilnych dzięki Samsung KNOX

System Samsung KNOX to kompleksowe rozwiązanie zwiększające bez-
pieczeństwo danych w urządzeniach mobilnych, dzięki któremu firmy
oraz indywidualni użytkownicy nie muszą obawiać się prób kradzieży
poufnych informacji lub włamań do telefonów i tabletów. KNOX łączy w
sobie system Android w wersji SE (Security Enhanced) o podwyższonym
poziomie bezpieczeństwa oraz rozwiązania do zarządzania bezpieczeń-
stwem, implementowane zarówno w warstwie sprzętowej, jak i w ra-
mach systemu operacyjnego. W połączeniu z oferowanymi przez partne-
rów Samsung rozwiązaniami Mobile Device Management KNOX pozwala
ponadto działom IT przedsiębiorstw na skuteczne wdrożenie zaawanso-
wanych polityk BYOD, zgodnie z którymi pracownicy mogą korzystać z
prywatnych urządzeń GALAXY do celów służbowych.

KNOX wykorzystuje wydzielone z zasobów systemowych bezpieczne

szyfrowane środowisko, w którym są przechowywane dane dla zatwierdzo-
nych aplikacji (np. biznesowych, jak program pocztowy, kontakty, kalenda-
rze, udostępnianie plików, CRM i business intelligence). Są one w ten sposób
chronione przed złośliwym oprogramowaniem, phishingiem oraz utratą
danych w przypadku kradzieży lub zgubienia urządzenia. W ten sposób za-
pewniona zostaje całkowita separacja łatwo dostępnej od zabezpieczonej,
szyfrowanej części urządzenia.

Samsung KNOX to idealne rozwiązanie nie tylko dla użytkowników pry-

watnych chcących chronić swoje dane przed niepowołanym dostępem, ale

również dla działów IT przedsiębiorstw, które chcą ograniczyć koszty infra-
struktury przez zastosowanie coraz bardziej popularnego modelu BYOD
(Bring Your Own Device). Dzięki KNOX pracownicy firm mogą w sposób
bezpieczny korzystać z aplikacji biznesowych nawet na swoich prywatnych
urządzeniach mobilnych, co zdecydowanie zwiększa wygodę (nie ma po-
trzeby zabierania do domu służbowych telefonów lub tabletów) i jednocze-
śnie umożliwia znaczne zmniejszenie kosztów, a nawet rezygnację z zaku-
pu firmowego sprzętu. Najwyższy poziom bezpieczeństwa gwarantowany
przez Samsung KNOX docenił m.in. Departament Obrony Stanów Zjedno-
czonych, który dopuścił do użytku w swoich sieciach urządzenia mobilne
wykorzystujące to rozwiązanie.

Działy IT otrzymują przy tym możliwość zarządzania aplikacjami bizne-

sowymi za pośrednictwem usługi EAS (Exchange Active Server). Rozwiąza-
nie KNOX jest ponadto kompatybilne z powszechnie używanymi modelami
infrastruktury korporacyjnej i obsługuje m.in. sieci VPN zgodne z normą
FIPS, szyfrowanie na urządzeniu, zabezpieczenie przed wyciekiem danych,
jednokrotne logowanie firmowe (Enterprise Single Sign ON – SSO), usługi
katalogowe Active Directory czy uwierzytelnianie wieloskładnikowe z wyko-
rzystaniem kart Smart Card.

Samsung KNOX jest dostępny na wszystkich najnowszych smartfonach

i tabletach Samsung przeznaczonych dla najbardziej wymagających użyt-
kowników, w tym na GALAXY S4, GALAXY Note 3, GALAXY Note 10.1 (Edy-
cja 2014), GALAXY NotePRO oraz GALAXY TabPRO.

nowości technologiczne

background image

60

/ 3

. 2014 . (22) /

ANKIETA

Ankieta magazynu Programista:

„Proces wytwarzania oprogramowa-

nia w Twojej firmie”

W ostatnim kwartale 2013 roku magazyn Programista przeprowadził badanie pt.
„Proces wytwarzania oprogramowania w Twojej firmie” przy współpracy z IBM Pol-
ska i firmą Arrow ECS. Sondaż dotyczył małych i średnich przedsiębiorstw, a jego
efektem miało być wykazanie, jak zbudowane są zespoły programistyczne, jakie
mają metodyki pracy oraz jakich narzędzi używają. Badanie miało również na celu
dostosowanie oferty IBM związanej z oprogramowaniem wspierającym wytwarzanie
aplikacji (narzędzia IBM Rational) do wymogów polskiego rynku w sektorze MŚP.

» implementacje zmian,

» uzyskiwanie informacji/danych potrzebnych do pracy,

» spotkania, ustalenia,

» analiza kodu,

» analiza przepływu danych,

» testy regresyjne,

» tworzenie harmonogramów prac,

» tworzenie raportów z wykonanych prac/postępów.

TESTOWANIE I INTEGRACJA

Wykorzystanie narzędzi CI (ang. Continuous Integration) deklaruje 27.6% bada-
nych. Najpopularniejszym narzędziem tego typu jest Jenkins (58.9%). Poza tym
wskazano również TeamCity firmy JetBrains (16.7%), a wśród użytkowników ze-
stawu narzędzi firmy Microsoft popularny jest Team Foundation Server.

38% osób biorących udział w ankiecie deklaruje, że w ich firmie jest dział, któ-

rego głównym zadaniem jest badanie jakości kodu. 22.1% ankietowanych stosu-
je narzędzia do badania jakości kodu (uwaga: respondentami byli developerzy z
różnych działów, niekoniecznie ci zajmujący się kontrolą jakości oprogramowania).

W

ankiecie wzięło udział 326 osób związanych
z branżą IT, z ponad 300 firm zaliczanych
do małych i średnich przedsiębiorstw. Naj-

częściej wymieniane przez badanych języki progra-
mowania to kolejno: C#, Java, PHP i C++. Najrzadziej
występują języki wykorzystywane do zastosowań
specjalistycznych (z naciskiem na Assemblera). 69.9%
ankietowanych deklaruje, iż używa środowisk na licen-
cjach otwartych, a najpopularniejszym środowiskiem
na tego typu licencji jest Eclipse. 62.6% badanych de-
klaruje użycie środowisk komercyjnych. W tej kategorii
najpopularniejszym produktem jest Microsoft Visual
Studio. 32.2% badanych używa jednocześnie otwar-
tych, jak i komercyjnych rozwiązań.

BUDOWA ZESPOŁÓW

PROGRAMISTYCZNYCH

Wśród firm, które wzięły udział w ankiecie, zdecydowa-
nie najpopularniejsze są małe zespoły, składające się z
od jednego do pięciu programistów (65.9% odpowiedzi)
oraz jednego do pięciu analityków (85.6% odpowiedzi).

Poniżej znajduje się ranking technologii stworzony

na podstawie danych wpisywanych przez ankietowanych w kolumnach „na-
zwy frameworków” oraz „nazwy narzędzi”. Technologie w tabeli są wymienio-
ne według popularności (od najbardziej popularnej do najmniej popularnej).

Technologia Najpopularniejszy framework Najpopularniejszy ORM
.NET

ASP.NET MVC

Entity Framework

Java

Spring

Hibernate

PHP

Zend Framework

Doctrine 2

Python

Django

(wbudowany)

Ruby

Ruby on Rails

(wbudowany)

Niezależnie od używanego języka, dużą popularność mają własne rozwiąza-
nia w zakresie tzw. middleware rozwijane wewnątrz konkretnych firm.

ZADANIA CZASOCHŁONNE

Statystyczna klasyfikacja czasochłonności określonych zadań dokonana przez
ankietowanych przedstawia się następująco (od najbardziej czasochłonnego
zadania do najmniej czasochłonnego):

background image

61

/ www.programistamag.pl /

Stosowane narzędzia różnią się diametralnie w zależności od wykorzy-

stywanych języków programowania - nie ma w tym wypadku swego rodzaju
panaceum na wszystkie problemy. Największe wykorzystanie tego typu na-
rzędzi jest zauważalne wśród zespołów używających języka Java.

WYTWARZANIE OPROGRAMOWANIA

28.2% badanych deklaruje użycie narzędzi służących do modelowania aplika-
cji. 70.6% z nich wykorzystuje w tym celu narzędzia komercyjne (najpopular-
niejszym jest Enterprise Architect). W przypadku metodyk wytwarzania opro-
gramowania nie zaskakuje duża popularność SCRUM (39.2%) i Agile (37.1%).
Inne wskazywane metodyki to kolejno: Kanban, Waterfall i Rational Unified
Process. Niektóre firmy próbują dążyć do wdrażania własnych rozwiązań, o
czym mogą świadczyć pojawiające się niekiedy odpowiedzi „metodyka nie-
formalna” lub „własne”.

Najpopularniejszym wskazanym VCS jest SVN. Może to wynikać z dużej ilo-

ści projektów legacy, które od wstępnej fazy projektu zaczęto wersjonować z
użyciem tego narzędzia. Co piąty zespół używający SVN deklaruje równoległe
użycie przynajmniej jednego innego systemu kontroli wersji. Najczęściej obok
SVNa używany jest Git, drugi zdecydowanie najpopularniejszy obecnie system
kontroli wersji - może to wskazywać na migrację z jednego systemu na drugi.

Poniżej znajduje się zestawienie poszczególnych pakietów służących do

wersjonowania kodu. Procentowy wskaźnik popularności został wyliczony in-
dywidualnie dla każdej pozycji zestawienia z uwagi na to, że część zespołów
używa kilku systemów kontroli wersji.

Nazwa VCS

Popularność

SVN

60.1%

Git

38.3%

Mercurial

7.7%

CVS

6.7%

TFS

4.9%

SourceSafe

2.7%

Bazaar

1.3%

Najczęściej wymieniane w tym miejscu pożądane cechy to:

» łatwość integracji z innymi narzędziami,

» możliwość bezproblemowej migracji rozbudowanych projektów rozpo-

czętych w innych środowiskach,

» wsparcie producenta i oferta szkoleń,

» stosunek ceny do jakości.

WYDATKI NA OPROGRAMOWANIE

31.6% zespołów deklaruje, że skorzystałoby z możliwości zakupu wszystkich
narzędzi wspierających wytwarzanie oprogramowania w ramach jednego
pakietu, który łatwo się ze sobą integruje. Przykładem podobnego pakietu
obecnego już na rynku jest zestaw narzędzi firmy Microsoft, który cechuje
się stosunkowo wysoką ceną. To właśnie użytkownicy tego zestawu oraz
użytkownicy oprogramowania specjalistycznego ponieśli największe wydat-
ki. Narzędzia oferowane przez Microsoft są również najczęściej wybierane
przez większe zespoły (powyżej 5 programistów). W przypadku tego pyta-
nia, wystąpiło wyjątkowo dużo odpowiedzi „nie wiem” - aż 53.1%. W sumie
tylko 15.3% zespołów zadeklarowało się w tej sprawie na „tak”. To pokazuje,
że łatwość integracji nie stanowi bardzo istotnej zalety, a narzędzia są kupo-
wane pojedynczo i łączone za pomocą własnego middleware działającego
pomiędzy różnymi pakietami. Zjawisko to udowadnia, że ceny pakietów są
zbyt wysokie z punktu widzenia zespołu, który zamierza wykorzystać jedynie
jego część. Suma wydatków poszczególnych zespołów jest zdecydowanie
mniejsza niż suma cen pojedynczych licencji dla końcowego użytkownika.
Olbrzymi wpływ na obniżenie ceny końcowej ma więc zastosowanie licencji
zbiorczych.

PODSUMOWANIE

Jak wynika z przeprowadzonej przez nasz magazyn ankiety, najpopularniej-
szymi językami programowania są: C#, Java, PHP i C++. 70% badanych uży-
wa środowisk open source, a statystyczny zespół składa się z maksymalnie
5 programistów i analityków. Jako najbardziej czasochłonne zadanie badani
wymieniają implementację zmian w specyfikacji. Co czwarty ankietowany
deklaruje użycie narzędzi CI, a 2/5 uczestników badania posiada w firmie
dział kontrolujący jakość kodu. Narzędzia do modelowania aplikacji cieszą
się zainteresowaniem na poziomie 28%, a najpopularniejszymi metodykami
są SCRUM (40%) i Agile (37%). W przypadku systemów kontroli wersji najpo-
pularniejszy jest SVN, a Git najszybciej zyskuje na popularności. Najbardziej
ceniona cecha oprogramowwania to łatwość integracji z innymi narzędziami,
niekoniecznie tej samej firmy, a ważną rolę odgrywają licencje zbiorcze, które
są najczęściej wybierane.

Opracowanie: redakcja

background image

62

/ 3

. 2014 . (22) /

LABORATORIUM BOTTEGA

Paweł Badeński

W

poprzednim odcinku serii opowiedziałem, jak efektywnie dawać
feedback innym. Feedback stanowi elementarną część metody-
ki Agile: umożliwia lepszą komunikację z klientem, zwiększanie

efektywności zespołu, ulepszanie procesu, iteracyjne tworzenie produktu
spełniającego w pełni potrzeby użytkownika. Obecność kultury dawania/
otrzymywania feedbacku w zespolu to ważny pierwszy krok na drodze do
lepszej współpracy. Po wykonaniu tego kroku stajemy przed wyzwaniem –
jak otrzymany feedback wprowadzać w życie?. Przykładowo: co ma zrobić Szy-
mon (młodszy programista), jeśli Kasia (doświadczony tester) powie mu, że
oddaje funkcjonalności do testów zbyt szybko – podając jako przykład 8 z 10
historyjek, które wróciły do Szymona ze zgłoszeniem ewidentnych bugów?
Szymon może spróbować testować historyjki trochę dokładniej, jednak brak
doświadczenia będzie utrudniał mu poprawę. Korzyść, jaka z tego wyniknie
dla Kasi, to spokój ducha – Kasia może sobie powiedzieć w myślach: przynaj-
mniej próbowałam
. Wyzwaniem dla Kasi jest odpowiedzieć na pytanie – jak
może pomóc Szymonowi efektywniej testować funkcjonalności przed od-
daniem jej do QA. Dla Szymona będzie to przecież nie lada problem – jest
jeszcze niedoświadczony i trzeba czasu, żeby osiągnął większą skuteczność.

JAKOŚĆ FEEDBACKU

Feedback, który otrzymasz od współpracowników, klientów czy nawet part-
nera życiowego, będzie jakościowo bardzo zróżnicowany. Czasem dostaniesz
konkretną informację, co robisz nieefektywnie i jak to poprawić: podczas re-
trospektyw często komentujesz na temat błędów Jacka. Czy mógłbyś zamiast
tego porozmawiać z nim osobiście?
. Często jednak feedback będzie dużo bar-
dziej „mętny”: czuję, że podczas niektórych spotkań jesteś jakby nieobecny.

Na własnym przykładzie wiem, że kiedy otrzymuję feedback o kiepskiej

jakości, moją pierwszą reakcją jest frustracja. Jeśli próba dopytania drugiej
osoby o szczegóły nie pomaga, frustracja staje się jeszcze większa. Natural-
nym odruchem wydaje się wtedy zignorowanie takiego feedbacku. Wiem
jednak na bazie doświadczeń, że każdy feedback może być pouczający i roz-
wijać. Dlatego staram się nawet taki feedback wykorzystać i znaleźć metody,
które pomogą mi go implementować. Jedną z technik, którą stosuję w takich
trudnych sytuacjach, jest „skupianie uwagi”.

SKUPIANIE UWAGI

Zdarza się, żę nie zgadzasz się z feedbackiem, który otrzymujesz, np. mało
udzielasz się w dyskusjach
. Czasem dotyczy on kwestii, nad którą nigdy się
nie zastanawiałeś „kiedy rozmawiasz z innymi, nie odnosisz się do ich emo-
cji”. Wbrew pozorom, to jeden z lepszych rodzajów feedbacku, jaki możesz
od kogoś dostać. Dotyczy on cech bądź zachowań, które znajdują się w Two-
im „martwym polu”. Są to aspekty, które z Twojej perspektywy uznajesz za
nieistotne i tego typu feedback daje szansę na rozwinięcie zupełnie nowej
umiejętności. Warto pamiętać, że ciągłe ulepszanie (ang. continuous improve-
ment
) w metodykach Agile dotyczy procesu, produktu, jak również członków
zespołu.

Z takim feedbackiem należy się najpierw oswoić. Do tego celu stosuję

technikę „skupiania uwagi”. Polega ona na przyciągnięciu uwagi Twojego
umysłu do problemu, starając się zachować podejście bezkrytyczne. Ta me-
toda opiera się na założeniu, że Twoje myśli lubią podróżować w mózgu po-
pularnymi drogami. Skupienie uwagi pozwala „zachęcić” sygnały do podróży
nowymi, nieznanymi ścieżkami. Z podobnej przyczyny potrzebne jest zacho-
wanie bezkrytycznego podejścia. Przyjmowanie istniejących opini kieruje
Twoje myśli na znane szlaki i powstrzymuje od tworzenia się nowych struktur
w mózgu. Innymi słowy – powstrzymuje proces uczenia się.

Załóżmy, że feedback, na którym chcę się skupić, odnosi się do tego, jak

rzadko wyrażam swoją opinię podczas spotkań. Skupienie uwagi najłatwiej
osiągnąć poprzez zadanie pytania w stylu: czy ja rzeczywiście za mało się udzie-
lam w dyskusjach
. Podejście bezkrytyczne polega na tym, że unikam ocena-
nia, tj. czy jestem lepszy, bo to robię albo czy jestem gorszy, bo tego nie robię.

Dobrym sposobem jest zapisywanie każdego pytania – najlepiej w miejscu,

na które często zwracasz uwagę. Wtedy częściej utrwalasz nowe ścieżki i skupiasz
uwagę umysłu na interesującej Cię kwestii. W rozważanym przypadku należy co
najmniej przypominać sobie je przed każdym spotkaniem, aby zainicjować w Two-
im mózgu proces zbierania informacji oraz uczenia się. Natomiast po każdym spo-
tkaniu warto, byś zastanowił się, czego nowego się nauczyłeś, lub nawet utrwalić
swoje myśli w formie pisemnej (o której wspomnę jeszcze w sekcji „journalling”).

SAMOŚWIADOMOŚĆ

Częstym błędem popełnianym przy implementacji feedbacku jest przyjęcie za-
łożenia, że albo dokonamy zmiany, albo zostaniemy przy obecnym zachowaniu.
Tymczasem istnieje jeszcze trzecia droga, czyli bycie świadomym swojego za-
chowania. Najlepiej wytłumaczyć to przez przykład. Kierownikiem projektu w
jednym z zespołów, gdzie pracowałem, był Marcin. Już pierwszego dnia po dołą-
czeniu do zespołu Marcin poinformował nas, czego możemy się po nim spodzie-
wać. Powiedział, że często wygląda, jakby nadmiernie się przejmował sytuacją w
projekcie, i podkreślił, że wynika to z jego osobowości. Taka informacja jest bar-
dzo cenna i pomogła nam zachować spokój, kiedy Marcin wyglądał na mocno
przejętego. Zrozumienie i dobra komunikacja jest fundamentem Agile, zwłaszcza
między kierownikiem projektu i członkami zespołu programistycznego.

Marcin z pewnością zebrał wiele razy feedback dotyczący tego zacho-

wania. Wiedział również o trzeciej drodze, czyli jak może pomóc mu samo-
świadomość. Jest to cecha, którą bardzo cenimy u innych. Pozwala uniknąć
konfliktów przez błędną interpretację czyjegoś zachowania. Ludzie mający
większą samoświadomość cieszą się również większym zaufaniem. Znajo-
mość swoich wad i zalet pozwala im na większą efektywność.

Po otrzymaniu feedbacku w niektórych sytuacjach możesz zastanowić

się, czy możesz wybrać „trzecią drogę”. Powinieneś wtedy wykorzystać świa-
domość swojego zachowania, aby zwiększyć efektywność. Jedną z technik
zaprezentowałem już na przykładzie historii Marcina. Polega ona na poinfor-
mowaniu innych o swoim wzorcu zachowania, aby byli lepiej przygotowani.
Jestem pewien, że pracując z feedbackiem, znajdziesz wiele innych metod,
które lepiej umożliwią Ci wykorzystanie samoświadomości.

Brakujący element Agile

Część 2: Wprowadzanie feedbacku w życie

W tym artykule przedstawię wyzwania, które napotykamy przy wprowadzaniu feedbacku
w życie. Opiszę skuteczne metody jego implementacji i sposoby radzenia sobie z często
pojawiającymi się problemami. Pokażę przede wszystkim propozycję frameworka, który
pomoże Ci podejść do feedbacku „po inżyniersku”, czyli profesjonalnie i ze strukturą.

background image

63

/ www.programistamag.pl /

BRAKUJĄCY ELEMENT AGILE CZĘŚĆ 2: WPROWADZANIE FEEDBACKU W ŻYCIE

WPROWADZANIE ZMIANY

Trzecim sposobem, któremu również poświęcę najwięcej miejsca w artykule,
jest wprowadzenie zmiany na bazie otrzymanego feedbacku. Bardzo często
wprowadzanie zmiany odbywa się w sposób chaotyczny. Równie często, z
tego właśnie powodu, utrudnia osiąnięcie celu. To może generować znudze-
nie, frustrację, a nawet złość. Wielokrotnie zdarzyło mi się widzieć osoby, które
próbują przeprowadzać kilkanaście zmian dotyczących swojego zachowania
i poddają się przytłoczone ich ogromem. Wprowadzanie zmian w życie zawo-
dowe, zmian dotyczących swojego zachowania, reakcji, wzorców komunika-
cji jest takim samym projektem jak projekty programistyczne, które prowa-
dzimy na co dzień. Dlatego proponuję framework oparty o uproszczony cykl
życia projektu w metodyce Agile.

budowanie backlogu feedbacku

Kiedy zaczynasz zbierać feedback, bardzo szybko okazuje się, że liczba rzeczy,
nad którymi mógłbyś pracować, przerasta Twoje możliwości. Czasem feed-
back od jednej osoby to już za dużo, żeby wdrożyć wszystkie zmiany naraz.
Dlatego listę rzeczy, które rozważasz do zmiany, warto przechowywać w jed-
nym miejscu jako „backlog” do implementacji. Oczywista zaleta to posiadanie
explicite listy rzeczy, które mogą poprawić Twoją efektywność. Ponadto jest
ważną deklaracją: wiem, że są aspekty, gdzie jestem mniej efektywny, ale w tym
momencie skupiam się na tych istotniejszych
. Na etapie budowania backlogu
feedbacku możesz dopisywać feedback o różnej „szczegółowości” „słuchać
uważniej na standupie” oraz „poszerzyć wiedzę na temat baz noSQL”. Warto
jednak zapisać go w sposób wystarczająco zrozumiały – możesz przecież zde-
cydować o wdrożeniu go w życie po kilku miesiącach od wpisania na listę.

iteracje, eposy

1

i historyjki

Z doświadczenia wiem, że zarówno od siebie, jak i od innych oczekujemy
błyskawicznych zmian. W rzeczywistości zmiany są jednak powolne i wy-
magają wykonywania małych kroków. Jest bardziej niż prawdopodobne,
że większość „historyjek” w Twoim backlogu feedbacku to w rzeczywisto-
ści eposy – feedback, wymagający dużych zmian, które mogą trwać wiele
miesięcy. Próba zaadresowania zmiany sformułowanej w postaci eposu
może być bardzo trudna i powodować frustrację. Dlatego zmiany warto
wprowadzać w iteracjach o określonej długości, np. 2 tygodnie. W prze-
ciwności do metodyk Agile w przypadku feedbacku nie potrzeba rozbi-
jać eposu na historyjki. Zamiast tego warto się zastanowić, jaki cel jest
osiągalny w ciągu kolejnych dwóch tygodni, który jednocześnie przybliży
Cię do zrealizowania eposu. W ten sposób za każdym razem projektujesz
historyjkę na jedną iterację, a w perspektywie czasu przybliżasz się do re-
alizacji swojego celu

2

.

planowanie iteracji

Dobry plan iteracji feedbacku wymaga efektywnie zaprojektowanych histo-
ryjek. Bardzo przydatnym narzędziem jest technika GROW. Akronim GROW
rozwija się jako Goals (pol. cele), Reality (pol. stan obecny), Options/Obstac-
les (pol. opcje/ograniczenia), Way forward (pol. dalsze kroki). W dalszej części
przedstawię, jak zaplanować przykładową historyjkę feedbacku w oparciu o
tę technikę. Jako przykład historyjki posłuży mi wspomniany wcześniej we
wstępie przykład feedbacku. Załóżmy, że w backlogu Szymona znajduje się
epos „zgłaszać do testów funkcjonalności pozbawione błędów ocenianych
przez testerów jako ewidentne”

3

.

1 ang.

epics

2 Przedstawiony sposób jest bazowany na koncepcji Solutions Focus opracowanej przez Marka

McKergow. Więcej informacji na blogu guru Agile Alistaira Cockburna:

http://alistair.cockburn.us/Solut

ions+Focus+aka+Delta+Method

3 Szymon zastosował dobrą praktykę wprowadzania zmian, czyli formułowanie ich w sposób pozyt-

ywny, tj.“odnosić się w sposób kulturalny do klienta” zamiast “nie odnosić się arogancko do klienta”

stan obecny

Celem tego kroku jest dokładne zastanowienie się, jaki jest punkt starto-

wy. Bardzo często zdarza się, że podejmujemy się zmiany bez pełnego zrozu-
mienia, z jakimi problemami zmagamy się teraz, jak daleko od celu się znajdu-
jemy. Wprowadzanie zmiany w ten sposób przypomina „ruchy Browna”

4

– jest

chaotyczne i nie ma określonego celu.

W sytuacji Szymona wartościowa informacja znajduje się w feedbacku od

Kasi – 8 na 10 historyjek w ostatniej iteracji zawierało ewidentne błędy. Szy-
mon zna liczbę historyjek, co pozwala mu na lepsze zaplanowanie celu. Kasia
wspomniała również o ewidentnych błędach – pomocne byłoby dopytać ją,
co przez to rozumie. Szymon powinien również zastanowić się, co według
niego może być przyczyną takiego stanu rzeczy, np. zmiany w ostatniej chwi-
li albo mało testów jednostkowych. Załóżmy, że Kasia podała jako przykład
ewidentnych błędów wyjątki rzucane przez aplikację. Szymon natomiast oce-
nił, że zaniedbuje testy jednostkowe negatywnych ścieżek.

cel

Mając zdefiniowany stan obecny, możesz przejść do określenia, co jest ce-

lem zmiany. W przygotowaniu efektywnych celów pomoże Ci SMART – jedna
z nielicznych koncepcji reprezentowanych przez akronim, które są przydatne.
Ich spełnienie pozwala Ci budować motywację potrzebną do długotermi-
nowej zmiany. Jej szczegółowy opis wraz z wytłumaczeniem przeznaczenia
znajdziesz poniżej.

» Specific (precyzyjny)

Cel powinien skupiać się na konkretach, np. opanuję techniki proponowa-

ne w książce Kenta Becka o TDD zamiast nauczę się TDD. Z dobrze zdefiniowa-
nym celem łatwiej wymyślić kolejne kroki prowadzące do niego oraz utrzy-
mać motywację. Trudniej też oszukać samego siebie, mówiąc np. No tak, uczę
się TDD, wczoraj w kuchni uważnie słuchałem, jak Maciek opowiada o TDD.

» Measurable (mierzalny)

Pownieneś zdefiniować cel tak, abyś mógł mierzyć postępy w jego realizacji.

„Będę mniej arogancki wobec klienta ” jest przykładem celu, który jest mierzal-
ny, pod warunkiem że uda Ci się uzyskać po każdym spotkaniu feedback odno-
śnie Twojego zachowania (niekoniecznie musisz podawać mu do wypełnienia
profesjonalny kwestionariusz z pytaniem: Jak arogancki byłem w skali 1 do 5?).

» Achievable (osiągalny)

Łatwo się zniechęcić, jeśli przez kilka kolejnych iteracji nie osiągniesz za-

łożonego celu.

Cel powinien być zdefiniowany tak, aby można go było osiągnąć w ciągu

jednej iteracji. Zamiast nauczę się GITa lepiej postawić sobie cel: zrozumiem,
które komendy GITa są odpowiednikami komend SVNa
.

» Relevant (mający znaczenie)

Cel musi mieć znaczenie dla Ciebie i w kontekście Twojej pracy. Bez speł-

nienia tego warunku trudno o motywację. Załóżmy, że kierownik projektu
prosi Cię, abyś rzadziej jadł obiady przy biurku. Jeśli się z nim nie zgadzasz,
powinieneś najpierw zrozumieć jego potrzeby i zdefiniować taki cel, który
zadowoli potrzeby obu z Was.

» Time-related (ograniczony czasowo)

Trudno będzie Ci utrzymać motywację, jeśli Twoim celem będzie nauczę

się Emacsa bez określania ram czasowych. Stwierdzenie nauczę się Emacsa w
ciągu 3 miesięcy
pomaga Ci podkreślić, że ten cel jest dla Ciebie istotny. Za-
mknięcie go w ramach czasowych zwiększa motywację i poczucie pilności w
jego realizacji.

Wróćmy więc do Szymona. Na podstawie wcześniej zdefiniowanego sta-

nu obecnego może postawić sobie jako cel „zapewnić, że funkcjonalności od-
dane do testowania nie rzucają nieoczekiwanych wyjątków”.

4 ruchy Browna – chaotyczne ruchy cząstek w płynie

background image

64

/ 3

. 2014 . (22) /

LABORATORIUM BOTTEGA

ograniczenia

Następny krok polega na zidentyfikowaniu, jakie przeszkody oraz trud-

ności oddzielają Cię od celu. Dopiero kolejnym etapem jest opracowanie
opcji, które pozwolą je pokonać. Od zdefinowania ograniczeń warto zacząć
z dwóch powodów. Po pierwsze, jest to krok łatwiejszy – ludziom łatwiej my-
śleć negatywnie, do unikania zagrożeń przystosowała nas ewolucja (konse-
kwencje ugryzienia przez węża chowającego się w krzaku jeżyn są dużo więk-
sze niż korzyść z zebrania jeżyn). Po drugie, problemy to coś, co już istnieje
– wystarczy na ogół przywołać ich przykłady, podczas gdy rozwiązania to coś,
co musisz dopiero wymyślić.

Lista ograniczeń z perspektywy Szymona to:

» kod, który oddaje do testów, rzuca niespodziewane wyjątki

» zarządzanie wyjątkami w moim kodzie jest podatne na błędy

opcje

Z perspektywy GROW ograniczenia są przeszkodami, które oddzielają Cię

od celu, natomiast opcje sposobami na ich pokonanie. Po wypisaniu wszyst-
kich ograniczeń możesz zastanowić się, czy jeśli usuniesz je wszystkie, cel
zostanie osiągnięty. GROW zakłada, że po pokonaniu wszystkich ograniczeń
powinieneś osiągnąć cel.

Lista opcji w sytuacji Szymona:

» zapewnić, że wyjątki są obsłużone

» ulepszyć sposób zarządzania wyjątkami w kodzie

dalsze kroki

Opcje mogą mieć formę abstrakcyjnych koncepcji, dlatego musisz je „prze-

konwertować” do konkretnych kroków. Na przykładzie Szymona będą to:

» przed oddaniem kodu do testów sprawdzić pokrycie kodu z wykorzysta-

niem Cobertury

» poprosić jednego ze starszych programistów o ocenę mechanizmu zarzą-

dzania wyjątkami i propozycję ulepszeń

WZORCE IMPLEMENTACJI

FEEDBACKU

Podobnie jak w przypadku kodu, również w implementacji feedbacku poja-
wiają się wzorce. Ich znajomość ułatwia wprowadzanie feedbacku w życie i
pozwala uniknąć „odkrywania koła na nowo”. Poniżej przedstawiam kilka z
tych metod, które uważam za najbardziej przydatne.

journalling

Journalling wymaga od Ciebie prowadzenia notatek na temat wprowadzanej
zmiany. Może przybierać wiele form; trzy z nich, które ja uważam za najcen-
niejsze, to:

» styl wolny – notujesz dowolne wrażenia z procesu wprowadzanej zmiany

w takiej formie, jaka jest dla Ciebie najwygodniejsza.

» meta-feedback – czyli feedback na temat implementacji feedbacku. Wy-

maga od Ciebie ewaluacji wprowadzanych zmian, oceniając, co robisz do-
brze, a gdzie powinieneś poświęcić więcej czasu i pracy.

» zapisywanie sukcesów – zbliżony do meta-feedbacku, ale ograniczasz się wy-

łącznie do zapisywania rzeczy, które idą dobrze. Uważam ten sposób za przy-
datny, kiedy dopiero zaczynasz zmianę. Dostarcza Ci potrzebnego wsparcia i
pomaga wytrwać w początkowych etapach wprowadzania zmiany.

Przydatną techniką przy prowadzeniu notatek jest wprowadzenie ograni-

czenia czasowego. Ograniczenie czasowe ułatwia Ci zmotywować się do ich
częstszego prowadzenia, zgodnie z myślą „to tylko 3 minuty, na tyle przecież
mogę znaleźć czas”.

metoda bardzo małych kroków

Bardzo często widzę, jak ludzie wstrzymują się przed implementacją feed-
backu, zakładając, że nie są w stanie nic zrobić. Po krótkiej rozmowie okazuje
się, że stawiają przed sobą bardzo duże oczekiwania. Warto w takiej sytuacji
zastosować „metodę bardzo małych kroków” i zadać sobie pytanie: Jaka jest
najprostsza możliwa rzecz, którą mogę zrobić, żeby znaleźć się bliżej celu
. Załóż-
my na przykład, że moim problemem jest gadatliwość podczas spotkań. Naj-
mniejszym możliwym krokiem może być poproszenie jednego z uczestników
o zasygnalizowanie, kiedy uzna, że rozmowa przerodziła się w mój monolog.
Ten pierwszy krok pozwoli mi w pełni zdać sobie sprawę z tego, jak często mi
się taka sytuacja zdarza, i być może z czasem lepiej kontrolować się samemu.
Zgodnie z tym, co powiedział G.K. Chesterton: „rzeczy warte zrobienia są war-
te zrobienia nawet źle”

5

.

quotas

„Quotas” polega na wprowadzeniu określonych minimów dla czynności, któ-
re zidentyfikowałeś przy etapie „dalsze kroki”. Załóżmy, że postanowiłeś rza-
dziej przerywać wypowiedzi innych podczas spotkań. Możesz wtedy określić,
że możesz przerwać tylko trzy razy podczas każdego spotkania. Podobnie
jeśli uznałeś, że powinieneś częściej się odzywać podczas spotkań, mógłbyś
oczekiwać od siebie, że odezwiesz się co najmniej raz. Alternatywą jest wpro-
wadzenie „zliczania”, kiedy notujesz, ile razy odezwałeś się podczas spotkania.
Mając te dane, możesz przyjąć oczekiwanie, że ich średnia powinna urosnąć z
czasem do zadowolającego Cię poziomu.

PODSUMOWANIE

W artykule opisałem trzy sposoby implementacji feedbacku: skupianie uwa-
gi, samoświadomość, oraz wprowadzanie zmiany. W zależności od konkretnej
sytuacji możesz zastosować jeden z nich, albo wszystkie naraz. Warto pamię-
tać, że zmienianie zachowania jest trudne i pozostałe techniki implementacji
feedbacku mogą być równie korzystne. Dla przypadków, kiedy zdecydujesz
się na wprowadzenie zmiany, przedstawiłem framework, który pozwoli Ci
zrobić to efektywniej. Chcę zaznaczyć, że nie musisz od razu podejmować
się wdrożenia go w pełni. Zachęcam do wybrania fragmentów, które w tym
momencie uznasz za przydatne. W podobnym celu przedstawiłem wzorce
implementacji feedbacku – są to techniki, które sprawdziłem sam, i uznałem
jako przydatne.

Wprowadzanie feedbacku w życie jest procesem i z pewnością napotkasz

trudności. Informacje, które udzielają Ci o Tobie inni ludzie, są na ogół bez-
cenne. Sprawne wykorzystanie rad, opinii i uwag od innych z czasem mocno
zaprocentuje.

5 ang. “Things that are worth doing are worth doing badly”

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

66

/ 3

. 2014 . (22) /

WYWIAD

Jakie będą parametry sieci 5G w Polsce?

5G to technologia przyszłości – „następczyni” wykorzystywanych obecnie
standardów trzeciej (3G – WDCMA) i czwartej (4G – LTE/LTE Advanced) gene-
racji telefonii komórkowej. Technologia piątej generacji to integracja dostęp-
nych rozwiązań (3G, 4G, Wi-Fi, mobilnych sieci sensorycznych itp.) z nowymi,
aktualnie rozwijanymi technologiami, które są obecnie w fazie badań. Firmy
zainteresowane inwestowaniem w technologie przyszłości analizują poten-
cjalne zastosowania nowego systemu, stawiane mu wymagania i rozwiązania
techniczne pozwalające je spełnić. Tak naprawdę jest jeszcze za wcześnie,
żeby dokładnie sprecyzować parametry z rozbiciem na konkretne kraje czy
poszczególnych operatorów, ale wiemy, że w sieci 5G opóźnienie w łączu ra-
diowym zmniejszy się nawet do mniej niż 1 milisekundy, a maksymalne prze-
pływności mogą być większe niż 10 gigabitów na sekundę.

Czy kiedyś możemy się spodziewać, że znikną upor­
czywe limity przesyłu danych w mobilnych pakietach
internetowych?

Posiadanie wyłącznie telefonu komórkowego, wykorzystywanego głównie
do rozmów głosowych, przestało być obowiązującym standardem. Użyt-
kownicy sieci telekomunikacyjnych coraz częściej sięgają po urządzenia typu
smartfon czy tablet. Efektem są: gwałtowny wzrost abonentów mobilnej sieci
internetowej (ok. 2,5-krotny w ciągu najbliższych 3 lat), rosnący rynek aplika-
cji oraz szeroka gama funkcjonalności wykorzystujących technologie mobil-
ne, co spowoduje niespotykany wzrost ilości transferowanych danych, nawet
do 1GB na użytkownika dziennie w roku 2020.

Jeszcze kilka lat temu nikt nie myślał o abonamencie z nielimitowanymi

rozmowami czy SMS-ami. Dziś takie oferty już nikogo nie dziwią, a operatorzy
wręcz prześcigają się w promocjach. Moim zdaniem zniesienie praktycznych
limitów przesyłu danych w mobilnych pakietach internetowych jest tylko
kwestią czasu i będzie możliwe dzięki znacznie zwiększonej pojemności sieci
telekomunikacyjnych i zmniejszeniu kosztu dla operatorów w przeliczeniu na
jeden bit informacji.

Co bezpośrednio będzie oznaczać rosnący zasięg 5G
z punktu widzenia programistów i architektów syste­
mów wykorzystujących technologie mobilne?

W ciągu obecnej dekady będziemy świadkami tysiąckrotnego wzrostu zapo-
trzebowania na wysokoprzepustowe łącza bezprzewodowe, w dużej mierze
spowodowane rosnącą popularnością dostępu do usług wideo. 5G jako połą-
czenie różnych rozwiązań technologicznych i innowacji otwiera nowe spektrum
możliwości, pozwalając sprostać kolejnym wyzwaniom. W ciągu następnej de-
kady znacznie zwiększy się ilość urządzeń podłączonych do sieci, w wielu przy-
padkach będą one komunikować się ze sobą bez pośrednictwa użytkownika
– zobaczymy w praktyce „Internet of Things”. Wraz z wprowadzeniem sieci 5G
znacznie zwiększone będą szybkości przesyłu danych, a zmniejszony potrzebny
na to czas, co będzie miało bezpośrednie przełożenie na powstanie nowator-
skich aplikacji. Reasumując, programiści będą mieli większe pole do popisu.

Jakie zastosowania praktyczne będzie miała nowa sieć 5G?

5G to pokonanie ograniczeń obecnie stosowanych komercyjnie technologii
mobilnych, takich jak 3G czy 4G. Dla użytkowników, obok lepszego zasięgu,
oznacza to dużo lepszą jakość oferowanych usług mobilnych. Transmisja wi-

deo w dużych rozdzielczościach stanie się normą. Zwykły użytkownik będzie
mógł to odczuć, np. ściągając na swój smartfon czy tablet filmy niemal w
ułamku sekundy. Również wideokonferencje staną się bardziej popularne za
sprawą wprowadzenia nowych rozwiązań.

Duże przepływności i małe opóźnienia umożliwią realizację nowych

usług, np. Augmented Reality, czyli systemu łączącego świat rzeczywisty z
generowanym komputerowo. Jednym z już testowanych urządzeń z tego
obszaru są okulary firmy Google, pozwalające na jednoczesne oglądanie rze-
czywistych i wirtualnych obrazów. Będziemy mogli również lepiej wykorzy-
stać usługi mobilne w chmurze.

5G zdecydowanie rozwinie technologie M2M, czyli Machine to Machine.

Co to oznacza? Proszę sobie wyobrazić, że nasz smartfon w czasie rzeczywi-
stym monitoruje nasz stan zdrowia i w razie konieczności powiadamia służby
ratunkowe. Innym przykładem może być samochód, który automatycznie
przesyła informacje o korkach czy uszkodzonej nawierzchni innym pojazdom
zmierzającym w tym kierunku.

To wszystko, o czym Pan powiedział, dotyczy zwy­
kłego „zjadacza chleba”, co zmieni się w „tle”?

Sieć będzie automatycznie dostrajana do indywidualnych potrzeb użytkow-
nika i znacznie zwiększy się jej pojemność, m.in. za sprawą tzw. small cells,
czyli punktów dostępowych o małej mocy. Ich zadaniem będzie pokrycie za-

5G made in Wrocław

Rozmowa z Bartoszem Ciepluchem, Dyre-

ktorem Europejskiego Centrum Inżynierii

i Oprogramowania NSN we Wrocławiu

background image

67

/ www.programistamag.pl /

sięgiem małego obszaru, dzięki czemu duża ilość zasobów sieci będzie do
dyspozycji relatywnie niewielkiej grupy użytkowników objętych zasięgiem
takiej małej komórki. Specjaliści z wrocławskiego centrum technologicznego
NSN obecnie pracują nad standaryzacją tych rozwiązań.

Smartfony i tablety stopniowo powodują ogromny
wzrost transferu danych. Czy nowa generacja sieci 5G
będzie przeznaczona tylko dla nich czy również inne
urządzenia będą mogły z niej korzystać? Jeśli tak, to w
jakich obszarach można się będzie tego spodziewać?

Sieć 5G jest projektowana nie tylko dla smartfonów i tabletów, lecz również
dla wspomnianej już technologii M2M. Jedną z podstawowych zmian między
siecią 4G i 5G będzie ilość podłączonych obiektów nie kontrolowanych bez-
pośrednio przez człowieka. W języku angielskim mamy na to wiele terminów:
„Internet of Things”, „Smart Society”, „Connected Cities/Homes” i inne. W prak-
tyce oznacza to lawinowy wzrost ilości obiektów transmitujących bezprzewo-
dowo dane nie tylko do użytkowników, ale także między sobą. Reasumując:
we wszystkich tych obszarach, gdzie niezbędna jest komunikacja i przesył
danych, możemy liczyć na znaczą poprawę świadczonych usług.

Czy wrocławskie centrum technologiczne NSN jest
pionierem we wdrażaniu technologii 5G?

Wrocławskie centrum technologiczne NSN już od kilku lat zajmuje się techno-
logią 5G, jednak te prace nie odbywają się tylko w naszym oddziale. Obecnie
na całym świecie prowadzona jest standaryzacja tej nowej technologii, w któ-
rej, podobnie jak w pracach nad wcześniejszymi standardami, uczestniczy fir-

ma Nokia Solutions and Networks. Sam program jest niezwykle innowacyjny
i jestem dumny z tego, że Polacy mogą brać czynny udział w jego tworzeniu.

Kiedy jest planowane wejście technologii 5G na rynek
i jakie to będzie miało konsekwencje w istniejącej in­
frastrukturze? Czy potrzeba będzie zmienić posiadany
sprzęt taki jak smartfony czy tablety?

Początek implementacji sieci 5G przewidywany jest około roku 2020. Patrząc
na nasze zaangażowanie przy tym projekcie, podany termin jest całkiem re-
alny. Jeśli chodzi o urządzenia mobilne, to niestety, aby w pełni cieszyć się z
możliwości nowych rozwiązań, musimy liczyć się ze zmianą sprzętu.

Wiadomo, że 5G dostarcza większe ilości danych,
dlatego też potrzebuje więcej stacji bazowych w
infrastrukturze. Czy planowana jest rozbudowa tej
warstwy w Polsce?

W przypadku 5G planowana jest integracja z już istniejącymi technologia-
mi, jednak ze względu na wykorzystywanie nowych pasm częstotliwości,
niezbędna będzie rozbudowa obecnej infrastruktury o nowe stacje bazowe
– szczególnie małej mocy. Pierwsze stacje 5G prawdopodobnie będą bardziej
przypominać – mocą i zasięgiem – access pointy WiFi, ale będą zapewniały
większe przepływności, niższe opóźnienia, przede wszystkim jednak znacznie
większą niezawodność i znakomitą integrację z istniejącą infrastrukturą sie-
ci komórkowych. Polskę, podobnie jak inne kraje, czeka duża modernizacja.
Nasze prace mają na celu także sprawienie, że będzie ona dla operatorów jak
najmniej skomplikowana i kosztowna.

Informacje o Nokia Solutions and Networks

Nokia Solutions and Networks to największy na świecie specjalista w
dziedzinie transmisji szerokopasmowej w sieciach komórkowych. Firma
działa w czołówce każdej generacji technologii mobilnych, od pierwsze-
go połączenia w sieci GSM, po pierwsze połączenie w sieci LTE. Eksperci
na całym świecie tworzą nowe rozwiązania, których poszukują opera-

torzy dla swoich sieci. NSN dostarcza najbardziej wydajne sieci komór-
kowe na świecie, wiedzę umożliwiającą maksymalne zwiększenie ich
wartości oraz usługi, dzięki którym wszystkie te elementy perfekcyjnie
ze sobą współpracują.

Główna siedziba mieści się w Espoo w Finlandii. NSN działa w ponad 120

krajach. W 2013 roku przychody netto firmy wyniosły 11,3 mld euro. NSN na-
leży w całości do Nokia Corporation. Więcej informacji:

http://www.nsn.com/

background image

68

/ 3

. 2014 . (22) /

STREFA CTF

Gynvael Coldwind

CTF

RuCTF Quals 2014

http://ructf.org/2014/en/

Waga CTFtime.org

60 (

https://ctftime.org/event/122

)

Liczba drużyn
(z niezerową liczbą
punktów)

249

System punktacji
zadań

Od 10 (proste) do 500 (trudne) punktów.

Liczba zadań

50

Podium

1. Leet More (Rosja) – 9210 pkt.
2. Dragon Sector (Polska) – 9110 pkt.
3. StratumAuhuur (Niemcy) – 9010 pkt.

Zadania

Nyan-task (300 pkt.)

RuCTF

to coroczne, odbywające się od sześciu
lat, zawody przeznaczone przede wszyst-
kim dla drużyn uniwersyteckich. Runda

kwalifikacyjna jest jednak otwarta dla wszystkich i zazwyczaj bierze w niej
udział większość najlepszych zespołów z rankingu CTFTime.org.

W tym roku część otwarta była wyjątkowo bogata w różnorodne zadania

z wielu kategorii, takich jak:

» admin (kategoria, w której najlepiej czuli się administratorzy),

» crypto (kryptografia),

» forensics (informatyka śledcza),

» hardware (coś dla elektroników),

» ppc (programowanie/algorytmika),

» recon (szukanie informacji zaszytych w czeluściach Internetu),

» reverse (inżynieria wsteczna),

» stegano (steganografia),

» vuln (eksploitacja błędów niskopoziomowych),

» web (bezpieczeństwo aplikacji webowych),

» oraz misc (różności).

Zdobyć flagę...

RuCTF Quals 2014 – Nyan-task

Ś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.

background image

69

/ www.programistamag.pl /

ZDOBYĆ FLAGĘ...

W sumie runda kwalifikacyjna oferowała 50 zadań do rozwiązania, z czego

naszej drużynie udało się rozwiązać 38.

NYAN-TASK

Zadanie z kategorii stegano za 300 pkt., czyli właśnie „Nyan-task", polegało na
znalezieniu flagi ukrytej w pliku PNG i ostatecznie zostało rozwiązane przez
19 drużyn. Sam obraz przedstawiał sławnego Nyan Cat (patrz: zrzut ekranu na
początku artykułu) i pomimo niewielkich rozmiarów pliku – jedynie 13 kB –
jego rozdzielczość była znaczna – 2880x1800x8bpp.

Zadania steganograficzne z udziałem bitmap można podzielić na dwie

nieformalne kategorie:

» „data stegano" – w których flaga jest ukryta w samym obrazie,

» „format stegano" – w których flaga została zaszyta w specyficznych dla

danego formatu nagłówkach, metadanych lub została po prostu dokle-
jona na koniec pliku.

Oczywiście rozpoczynając prace nad zadaniem, nie wiadomo, z którym przy-
padkiem mamy do czynienia, więc należy sprawdzić wszystkie możliwości –
tak było również w tym wypadku. Poszukiwania zaczęliśmy od metod charak-
terystycznych dla bitmap ze zdefiniowaną paletą barw.

REKONESANS

Pierwszą rzeczą, którą zazwyczaj sprawdzamy, jest użycie podobnego (w
przypadku RGB) lub tego samego (w przypadku oddzielnie określonej palety
barw) koloru do zaszycia wizua lnej wiadomości w obrazie, podobnie jak zro-
bił to Blizzard w instalatorze do pierwszej części Diablo:

http://diablo2.diablowiki.net/Diablo_I_Easter_Eggs

.

W naszym przypadku obraz był 8-bitowy i posiadał zdefiniowaną paletę

(jest to 256 wpisów definiujących barwę czerwoną, zieloną i niebieską dla da-
nego koloru), więc zaczęliśmy od jej wyekstraktowania za pomocą darmowej
przeglądarki plików graficznych IrfanView (opcja Image / Palette / Export pa-
lette...), otrzymując tekstowy plik PAL z następującymi wpisami:

JASC-PAL

0100

256

11 65 119

255 177 162

11 65 119

255 177 162

11 65 119

255 91 179

11 65 119

255 91 179

11 65 119

255 91 179

11 65 119

255 210 158

...

Patrząc na powyższy wycinek palety, od razu widać powtarzające się kolory
(np. kolory na indeksach 0, 2, 4, 6, 8, oraz 10 mają RGB równe 11, 65, 119).

REDUNDANTNA PALETA

Chcąc potwierdzić, że faktycznie jakieś dane są ukryte tą metodą w obrazie, po-
stanowiliśmy podmienić paletę kolorów na typową skalę szarości. Celem takie-
go zabiegu jest sprawienie, aby powtarzające się, identyczne, barwy zaczęły się
wizualnie różnić – dzięki temu fakt użycia różnych wpisów w palecie do określe-
nia tego samego koloru na obrazie będzie widoczny gołym okiem, zazwyczaj w
postaci przypominającej szumy (a więc ogólna entropia obrazu się zwiększy).

Odpowiedni plik PAL wygenerowaliśmy następującym, trywialnym skryp-

tem (Python):

print

"JASC-PAL"

print

"0100"

print

"256"

for

i

in

xrange

(

256

):

print

i

,

i

,

i

Wygenerowaną paletę zaimportowaliśmy w IrfanView (opcja Image /

Palette / Import palette...) i spodziewaliśmy się ujrzeć obraz w skali szarości
o podwyższonej entropii, jednak obraz, który ukazał się naszym oczom, był
krystalicznie czysty (patrz: Rysunek 1).

Rysunek 1. Efekt podmienionej palety

Ponieważ nie do końca wierzyliśmy, że IrfanView w sposób bezpośredni pod-
mienił paletę, postanowiliśmy wyekstraktować sekcję danych (IDAT) z pliku
PNG i ją zdekompresować (PNG używa zlib, czyli de facto DEFLATE). W tym
celu posłużyliśmy się poniższym skryptem:

from

struct

import

unpack

d

=

open

(

"nyan-task.png"

,

"rb"

).

read

()

# Znajdz magic chunku IDAT.

i

=

d

.

find

(

"IDAT"

)

# Budowa chunku:

# [4 bajty: dlugosc]

# i – -> [4 bajty: magic ]

# ... dane ...

# [4 bajty: crc ]

# Odczytaj wielkosc.

sz

=

unpack

(

">I"

,

d

[

i

-

4

:

i

])[

0

]

# Rozpakuj i zapisz dane.

d

=

d

[

i

+

4

:

i

+

4

+

sz

].

decode

(

"zlib"

)

open

(

"out.raw"

,

"wb"

).

write

(

d

)

Wyjściowe dane to oczywiście nie końcowa bitmapa, a tablica par: rodzaj
funkcji filtrującej PNG (jeden bajt), oraz dane linii z naniesionym filtrem. Nie-
mniej jednak dla podwyższonej entropii na końcowej bitmapie „przefiltrowa-
ny“ obraz również miałby podwyższoną entropię, a więc już na tym etapie
powinno być widać szum.

Otrzymany plik out.raw otworzyliśmy w IrfanView, podając jako rozdziel-

czość 2881x1800 (dodatkowy pixel z uwagi na bajt definiujący rodzaj funkcji
filtrującej dla danej linii) w skali szarości, i naszym oczom ukazał się ponownie
czysty, niezaszumiony, obraz (patrz: Rysunek 2).

Rysunek 2. Przefiltrowany obraz wyekstraktowany z PNG

background image

70

/ 3

. 2014 . (22) /

STREFA CTF

Bazując na powyższych obrazach, w zasadzie musieliśmy wyeliminować bit-

mapę jako miejsce ukrycia wiadomości (pomijając elementy wizualne, jak np.
układ plamek na tułowiu Nyan Cata, ale te okazały się być zgodne z oryginałem).

STRUKTURA PNG

Korzystając z narzędzia pngcheck (

http://www.libpng.org/pub/png/apps/

pngcheck.html

), wygenerowaliśmy raport na temat dokładnej budowy otrzy-

manego pliku PNG:

File: nyan-task.png (13105 bytes)

chunk IHDR at offset 0x0000c, length 13

2880 x 1800 image, 8-bit palette, non-interlaced

chunk PLTE at offset 0x00025, length 768: 256 palette entries

chunk IDAT at offset 0x00331, length 12268

zlib: deflated, 32K window, maximum compression

chunk IEND at offset 0x03329, length 0

No errors detected in nyan-task.png (4 chunks, 99.7%

compression).

Z raportu można wywnioskować, że:

» Plik PNG posiada jedynie podstawowe i spodziewane sekcje, tj. IHDR (na-

główki), PLTE (paleta kolorów), IDAT (dane obrazu) oraz IEND (zakończenie
pliku PNG).

» Nie ma nic doklejonego na koniec pliku – wielkość to 13105 bajtów, nato-

miast magic IEND został znaleziony na offsecie 13097; ponieważ nagłówki
tej sekcji (magic, oraz suma CRC) zajmują dokładnie 8 bajtów, nie pozosta-
wia to żadnego miejsca na dodatkowe dane.

» IHDR ma standardową wielkość.

» PLTE ma również standardową wielkość (256 wpisów po 3 bajty, razem 768).

Istniało pewne prawdopodobieństwo, że pewne dane zostały doklejone na
koniec skompresowanych danych w sekcji IDAT, jednak uznaliśmy to za mało
prawdopodobne i odłożyliśmy do sprawdzenia później.

Oczywiście, mogło być po prostu więcej skompresowanych danych, które

zostały poprawnie zdekompresowane, ale były poza zadeklarowaną bitmapą
(2880x1800), a więc nigdy nie zostały wyświetlone na ekranie. Przeczyła temu
wielkość zdekompresowanych danych: 5185800 bajtów – czyli dokładnie
2881 (należy pamiętać o bajcie definiującym funkcję filtrującą) pomnożone
przez ilość linii (1800), a więc to, czego się spodziewaliśmy.

Po wyeliminowaniu powyższych możliwości, najbardziej prawdopodob-

ną z pozostałych opcji została ponownie paleta kolorów.

DUCH W KOLORACH

Postanowiliśmy wyświetlić paletę barw, tym razem faktycznie w postaci ko-
lorów, a nie liczb. W tym celu ponownie użyliśmy IrfanView i jego opcji Edit
palette (patrz: Rysunek 3).

Rysunek 3. Paleta kolorów wyświetlona przez IrfanView

W oczy rzuciły nam się dwie rzeczy: umieszczenie podobnych kolorów bli-

sko siebie i specyficzny wzór w górnym wierszu oraz prawej kolumnie, który
wspólnie z jednolitym dolnym wierszem i lewą kolumną do złudzenia przypo-
minał wzorzec wyszukiwania w kodach typu Data Matrix (kody 2D służące do
zapisu niewielkich ilości informacji, podobne do kodów QR).

Z uwagi na ten trop wyekstraktowaliśmy dane sekcji PLTE (używając

skryptu analogicznego do tego, którego użyliśmy przy ekstrakcji IDAT, pomi-
jając dekompresję) oraz przekonwertowaliśmy je do czarno-białej bitmapy,
używając następującego skryptu:

d

=

open

(

"plte.raw"

,

"rb"

).

read

()

o

=

""

k

=

0

for

j

in

xrange

(

0

,

16

):

for

i

in

xrange

(

0

,

16

):

p

=

d

[

k

:

k

+

3

]

k

+=

3

# Czy kolor tla?

if

p

==

"\x0b\x41\x77"

:

o

+=

"\0"

# Czarny

else

:

o

+=

"\xff"

# Bialy

open

(

"plte_bw.raw"

,

"wb"

).

write

(

o

)

Następnie otworzyliśmy w IrfanView plik wyjściowy (16x16x8bpp), powięk-
szyliśmy (bez resamplingu) do rozsądnych rozmiarów (patrz: Rysunek 4) i za-
pisaliśmy jako PNG.

Rysunek 4. Wyjściowy kod Data Matrix

Tak wygenerowany PNG wysłaliśmy na kilka serwisów dekodujących kody tego
typu i jeden z nich (

http://online-barcode-reader.inliteresearch.com/

) popraw-

nie odczytał Data Matrix, zwracając następujący krótki link: u.to/P4JUBg.

Wchodząc na powyższy adres, zostaliśmy przekierowani do wyszukiwarki

Google z określonym zapytaniem:

https://www.google.ru/search?q=RUCTF_ca8250c2b4b50581afc9ffd1f403f3f2

Treść zapytania była poszukiwaną przez nas flagą :)

PODSUMOWANIE

Koniec końców zadanie okazało się nie być trudne, natomiast zanim dotar-
liśmy do faktycznego rozwiązania, trochę czasu zajęło nam przeglądanie
różnych możliwych miejsc ukrycia flagi. Ostatecznie pomógł nieco fakt, że
IrfanView wyświetlił paletę w formie kolorowej bitmapy 16x16, a nie np. jako
listę 256 pasków – w tym wypadku kod Data Matrix pozostałby jeszcze przez
pewien czas przez nas niezauważony. W ramach Dragon Sector zadanie zosta-
ło rozwiązane przez autora tego tekstu oraz tkd.

Rozwiązanie zadania Nyan-task zostały nadesłane przez Dragon Sector

– jedną z polskich drużyn CTFowych.

http://dragonsector.pl/

background image
background image

72

/ 3

. 2014 . (22) /

KLUB LIDERA IT

Mariusz Sieraczkiewicz

Jak całkowicie odmienić sposób pro-

gramowania, używając refaktoryza-

cji (część 7)

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.

+ foundWord +

"=>"

);

polishWord

=

new

String (foundWord.getBytes(),

"UTF8"

);

polish =

false

;

}

else

{

System.out.println(foundWord);
polish =

true

;

englishWord

=

new

String(foundWord.getBytes(),

"UTF8"

);

lastSearchWords.add(

new

DictionaryWord(polishWord,

englishWord,

new

Date()));

counter ++;

}

}

6

line = bufferedReader.readLine();

}

}

catch

(MalformedURLException ex) {

ex. printStackTrace ();

}

catch

( IOException ex) {

ex. printStackTrace ();

}

finally

{

try

{

if

(bufferedReader !=

null

) {

bufferedReader.close();

}

}

catch

( IOException ex) {

ex. printStackTrace ();

}

}

}

</java>

Refaktoryzacja: Zastąpienie metody przez
obiekt reprezentujący metodę

Metoda

searchWord składa się z kilku podczynności, takich jak inicjacja zmien-

nych, odczyt strony, analiza pojedynczego wiersza, zapamiętanie znalezionych
tłumaczeń. Podczynności te współdzielą wspólny stan – jest to obiekt klasy
BufferedReader dający dostęp do przetwarzanej strony HTML. Ponadto me-
toda

searchWord zawiera wiele niezależnych zmiennych lokalnych, co utrud-

nia, a w zasadzie uniemożliwia prostą refaktoryzację typu Wydzielenie metody.

Z drugiej strony funkcjonalność wyszukiwania wyrazów daleko wykracza

poza odpowiedzialność klasy

WebDictionary, która zajmuje się obsługą

użytkownika aplikacji. Warto by wydzielić ją do osobnej klasy. Nazwijmy ją
SearchWordService. Posunięcie polegające na wydzieleniu zawartości
metody realizującej złożone przetwarzanie do osobnego obiektu nazywamy
Zastąpieniem metody przez obiekt reprezentujący metodę. Po tym kroku kod
metody

searchWord będzie wyglądał następująco:

JAK UŻYWAĆ REFAKTORYZACJI DO

TWORZENIA W PEŁNI OBIEKTOWYCH

APLIKACJI

W tej części dokładniej przedstawię proces pisania w pełni obiektowych apli-
kacji z wykorzystaniem refaktoryzacji.

Długie metody nie są wcale dobre

Wróćmy do realizowanego wcześniej przykładu – aplikacji do znajdowania
tłumaczeń słów za pomocą słownika internetowego. Mamy już stworzony
szkielet i kilka ważniejszych refaktoryzacji za sobą. Wróćmy do metody

Web-

Dictionary.searchWord, która pełni jedno z najważniejszych zadań – re-
alizuje wyszukiwanie tłumaczonych słów. Jak już wspomniałem, obecne roz-
wiązanie ma poważną wadę – metoda jest bardzo długa.

Pierwszym pomysłem może być próba wydzielenia mniejszych metod na

bazie metody

searchWord. Jeśli jednak przyjrzymy się jej bliżej, to zauważy-

my, że nie jest to takie proste zadanie.

Listing 1. Metoda searchWord

<java>

private void

searchWord (String command) {

lastSearchWords.clear ();

BufferedReader bufferedReader =

null

;

String polishWord =

null

;

String englishWord =

null

;

int

counter = 1;

try

{

String[] commandParts = command.split (

" "

);

String wordToFind = commandParts [1];

String urlString =

"http://www.dict.pl/dict?word="

+ wordToFind +

"&words=&lang=PL"

;

bufferedReader =

new

BufferedReader (

new

InputStreamReader (

new

URL(urlString).openStream()));

boolean

polish =

true

;

String line = bufferedReader.readLine();

while

(hasNextLine(line)) {

Pattern pat = Pattern

.compile(

".*<a href=\"dict\\?words?=(.*)&lang.*"

);

Matcher matcher = pat.matcher(line);

if

(matcher.find()) {

String foundWord

= matcher.group(matcher.groupCount());

if

(polish) {

System.out.print(counter +

")"

background image

73

/ www.programistamag.pl /

JAK CAŁKOWICIE ODMIENIĆ SPOSÓB PROGRAMOWANIA…

Listing 2. Klasa SearchWordService

<java>

private void

searchWord(String command) {

SearchWordService searchWordService

=

new

SearchWordService(command);

lastSearchWords = searchWordService.search();

}
</java>

Klasa SearchWordService będzie wyglądać następująco:

<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.Date;

import

java.util.List;

import

java.util.regex.Matcher;

import

java.util.regex.Pattern;

public class

SearchWordService {

private

String command =

null

;

public

SearchWordService(String command) {

this

.command = command;

}

public

List <DictionaryWord> search () {

List <DictionaryWord> result=

new

ArrayList <DictionaryWord>();

// ... ta część kodu bez zmian

result.add(

new

DictionaryWord(polishWord,

englishWord,

new

Date()));

// ... ta część kodu bez zmian

return

result ;

}

private boolean

hasNextLine(String line) {

return

( line !=

null

);

}

}

</java>

Refaktoryzacja: Zmiana algorytmu na pisany
ludzkim językiem

Algorytm poszukiwania słowa można zapisać z użyciem wymienionych po-
niżej kroków:
1. inicjacja zmiennych i pobranie zawartości strony z tłumaczeniem
2. dla każdego znalezionego słowa będącego częścią tłumaczenia:
a. znajdź polski odpowiednik
b. znajdź angielski odpowiednik
c. zapamiętaj tłumaczenie
d. wypisz tłumaczenie na ekranie

Powyższy zapis algorytmu odpowiada ludzkiemu rozumowaniu, zaś obecna
wersja kodu jest zapisem programistycznego myślenia. Spróbujmy zatem zre-
faktoryzować kod, aby jego struktura odzwierciedlała przebieg opisany po-
wyżej. Pierwszy krok, który zrobimy w tym kierunku, to wydzielenie metody
odpowiedzialnej za część inicjacja zmiennych i pobranie zawartości strony z tłu-
maczeniem
. Wynikiem tej metody będzie strumień

BufferedReader repre-

zentujący analizowaną stronę HTML. Strumień jest obiektem potrzebnym w
całym algorytmie, zatem będzie polem w klasie

SearchWordService. Kod

po refaktoryzacji wygląda następująco:

Listing 3. Klasa SearchWordService

<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.Date;

import

java.util.List;

import

java.util.regex.Matcher;

import

java.util.regex.Pattern;

public class

SearchWordService {

private

String command =

null

;

private

BufferedReader bufferedReader =

null

;

public

SearchWordService (String command) {

this

.command = command ;

}

public

List <DictionaryWord> search() {

List <DictionaryWord> result =

new

ArrayList

<DictionaryWord>();

String polishWord =

null

;

String englishWord =

null

;

int

counter = 1;

try

{

bufferedReader = prepareBufferedReader();

boolean

polish =

true

;

String line = bufferedReader.readLine();

// .. dalej kod bez zmian

return

result ;

}

private

BufferedReader prepareBufferedReader ()

throws

IOException,

MalformedURLException {

String [] commandParts = command.split(

" "

);

String wordToFind = commandParts[1];

String urlString =

"http://www.dict.pl/dict?word="

+ wordToFind +

"&words=&lang=PL"

;

return new

BufferedReader (

new

InputStreamReader(

new

URL(urlString).openStream()));

}

private boolean

hasNextLine (String line) {

return

(line !=

null

);

}

}

</java>

W ten sposób złożona metoda

search uprościła się nieco poprzez wydzie-

lenie metody składowej

prepareBufferedReader. Metoda ta zwraca jako

wynik stworzony strumień do odczytu zawartości strony, jednocześnie stru-
mień ten jest polem klasy. Może zatem paść propozycja, aby metoda ta nie
zwracała żadnego wyniku, tylko ustawiała to pole. Jest to opcja poprawna,
natomiast ja osobiście preferuję zwracanie wyniku, gdyż dzięki temu uzysku-
jemy jawną informację o efekcie działania metody. Przyjrzyjmy się wersji z
niejawnym ustawieniem pola

bufferedReader.

Listing 4. Pole bufferedReader

<java>

// ...

public

List <DictionaryWord> search() {

List <DictionaryWord> result =

new

ArrayList <DictionaryWord>();

String polishWord =

null

;

String englishWord =

null

;

int

counter = 1;

try

{

init();

// ...

}

private void

init ()

throws

IOException,

MalformedURLException {

String[] commandParts = command.split(

" "

);

String wordToFind = commandParts[1];

String urlString =

"http://www.dict.pl/dict?word="

+ wordToFind +

"&words=&lang=PL"

;

bufferedReader =

new

BufferedReader(

new

InputStreamReader(

new

URL(urlString ).openStream()));

}

</java>

background image

74

/ 3

. 2014 . (22) /

KLUB LIDERA IT

Wywołanie

init() nie daje żadnych informacji o tym, że w ciele tej me-

tody następuje zmiana stanu, co wyraźnie widać w poprzednim przykładzie
bufferedReader = prepareBufferedReader(). Jest to dosłowny zapis
celu działania metody, którym jest przygotowanie obiektu typu

Buffere-

dReader, a ten z kolei zostanie zapamiętany jako pole w klasie. Zajmiemy się
główną częścią algorytmu, która teraz przedstawia się następująco:

Listing 5. Główna część algorytmu przed refaktoryzacją

<java>

// ...

boolean

polish =

true

;

String line = bufferedReader.readLine();

while

(hasNextLine(line)) {

Pattern pat = Pattern

.compile(

".*<a href=\"dict\\?words?=(.*)&lang.*"

);

Matcher matcher = pat.matcher(line);

if

(matcher.find()) {

String foundWord

= matcher.group(matcher.groupCount());

if

(polish) {

System.out.print(counter +

") "

+ foundWord +

" => "

);

polishWord

=

new

String (foundWord.getBytes(),

"UTF8"

);

polish =

false

;

}

else

{

System.out.println(foundWord);

polish =

true

;

englishWord

=

new

String(foundWord.getBytes(),

"UTF8"

);

result.add(

new

DictionaryWord(polishWord,

englishWord,

new

Date()));

counter ++;

}

}

line = bufferedReader.readLine();

}

}

catch

(MalformedURLException ex) {

// ...

</java>

Taki zapis jest bardzo zawiły, jest sporo zmiennych tymczasowych:

polish

– do przechowywania informacji o bieżącej wersji językowej,

polishWord,

englishWord – zmienne tymczasowo przechowujące słowo w wersji pol-
skiej i angielskiej.

Zatrzymajmy się na chwilę. Jeśli przyjrzymy się jeszcze raz strukturze

strony HTML, to zauważymy, że słowo polskie i angielskie podlega analizie
w identyczny sposób. Słowa te występują naprzemiennie – najpierw słowo
polskie, później słowo angielskie. Dlaczegóż by zatem nie uprościć nieco al-
gorytmu? Wydzielmy metodę, która będzie potrafiła znaleźć kolejny wyraz
(polski lub angielski). Użyjemy jej do naprzemiennego znajdowania wyrazów
polskiego i angielskiego.

Wyszukiwanie kolejnego słowa oprzemy na istniejącym już algo-

rytmie z powyższego kodu źródłowego. Wydzieloną metodę nazwijmy
moveToNextWord, będzie zwracać kolejny znaczący wyraz polski lub an-
gielski znajdujący się na stronie HTML lub

null, jeśli wszystkie słowa zostały

odnalezione. Głównym celem tej metody jest stworzenie mechanizmu do od-
najdywania kolejnych znalezionych wyrazów na stronie HTML.

Listing 5. Metoda moveToNextWord

<java>

private

String moveToNextWord () {

try

{

String line = bufferedReader.readLine();

while

(hasNextLine(line)) {

Pattern pattern = Pattern

.compile(

".*<a href=\"dict\\?words?=(.*)&lang.*"

);

Matcher matcher = pattern.matcher(line);

if

(matcher.find()) {

String foundWord = matcher.group(matcher.

groupCount());

return new

String(foundWord.getBytes(),

"UTF8"

);

}

else

{

line = bufferedReader.readLine();

}

}

}

catch

(IOException e) {

// TODO obsluzyc blad

}

return null

;

}

</java>

Refaktoryzacja: Wprowadzenie klarownej
obsługi wyjątków

Na chwilę chciałbym się zatrzymać nad obsługą wyjątków. Do tej pory nie po-
święcaliśmy temu tematowi zbyt wiele czasu. Odruchowo stosowaliśmy schemat:

// ...

}

catch

( IOException ex) {

ex. printStackTrace ();

}

// ...

Dławienie wyjątków

W przypadku wystąpienia sytuacji wyjątkowej, program wprawdzie wypisze
informacje o stosie wywołań w momencie wystąpienia wyjątku, jednak nie
jest w żaden sposób przygotowany na jego obsługę. Powyższe rozwiązanie
to nieco lepsza odmiana techniki zwanej dławieniem wyjątków, która w 99%
przypadków jest niedopuszczalna. Wygląda ona mniej więcej tak:

// ...

}

catch

( IOException ex) {

// nic nie robie

}

// ...

Takie posunięcie spowoduje, że w momencie wystąpienia błędu nic się nie
stanie z aplikacją, a nie jest to pożądany efekt. Błąd będzie niezauważony. Na
sytuację wyjątkową w jakiś sposób należy zareagować, być może przedstawić
jakiś komunikat użytkownikowi, być może dokonać próby wznowienia ope-
racji. Coś należy zrobić.

Jeśli wystąpi wyjątek, nie należy udawać, że nic się nie stało. Należy zare-
agować na sytuację wyjątkową. Inaczej w systemie zginie ważna informacja,
która jest trudna do odtworzenia w przypadku analizy lub naprawy systemu.

Jest kilka możliwości reakcji.
1. Zareagować natychmiast w bloku

catch.

Jeśli tylko jesteś w stanie sensownie zareagować w miejscu wystąpienia wy-
jątku, zrób to.

2. Przerzucić wyjątek do metody wywołującej (

throws w sygnaturze).

Jeśli nie jesteś w stanie obsłużyć wyjątku lub z pewnego powodu nie chcesz
tego zrobić, możesz przerzucić wyjątek do metody wywołującej daną metodę;
opcja ta może mieć sens przy stosowaniu refaktoryzacji Wydzielenie metody.

3. Opakować wyjątek lub wygenerować własny wyjątek kontrolowany (ang.

checked) dziedziczący po

java.lang.Exception.

Jeśli wyjątek powinien mieć wpływ na inną część systemu (np. na interfejs
użytkownika) i ta część systemu powinna być przygotowana na jego obsługę,
rzuć własny wyjątek lub opakuj nim wyjątek źródłowy.

4. Opakować wyjątek lub wygenerować własny wyjątek niekontrolowany

(ang. unchecked) dziedziczący po

java.lang.RuntimeException.

Jeśli wyjątek jest błędem, na który system w sposób bezpośredni nie będzie

background image

75

/ www.programistamag.pl /

JAK CAŁKOWICIE ODMIENIĆ SPOSÓB PROGRAMOWANIA…

reagować lub nie jest w stanie reagować, rzuć wyjątek niekontrolowany lub
opakuj nim wyjątek źródłowy.

Wróćmy do przykładu. W metodzie

moveToNextWord wyjątek wystąpi wtedy,

kiedy pojawią się problemy podczas pracy ze strumieniem. Jest to sytuacja, co
do której nie przewidujemy bezpośredniej obsługi w naszym przypadku, dla-
tego zastosujemy czwartą opcję obsługi wyjątku – stworzymy własną klasę
wyjątku o nazwie

WebDictionaryException dziedziczącą z java.lang.

RuntimeException. W przypadku gdy błąd wystąpi, program zostanie prze-
rwany. Jeśli chcemy reagować na tę sytuację po stronie interfejsu użytkowni-
ka, w metodzie

WebDictionary.main, WebDictionary.processMenu lub

WebDictionary.searchWord możemy dodać obsługę tego wyjątku. Przypo-
mnę tylko, iż obsługa wyjątków dziedziczących po

RuntimeException nie jest

wymuszana przez kompilator – nie ma konieczności ich deklaracji w

throws.

Klasa wyjątku będzie wyglądać następująco:

package

pl.bnsit.webdictionary;

public class

WebDictionaryException

extends

RuntimeException {

public

WebDictionaryException() {

}

public

WebDictionaryException(String arg0) {

super

(arg0);

}

public

WebDictionaryException (Throwable arg0) {

super

(arg0);

}

public

WebDictionaryException (String arg0, Throwable arg1) {

super

(arg0, arg1);

}

}

Zaś obsługa wyjątku wygląda następująco:

}

catch

( IOException e) {

throw new

WebDictionaryException (e);

}

Niby nic wielkiego się nie stało, jednak wyjątek został odpowiednio przygoto-
wany – jeśli zdarzy się w systemie coś niepożądanego, na pewno pojawi się o
tym odpowiednia informacja.

Zapraszam do kolejnych części artykułu, gdzie będziemy kontynuować

wprowadzanie refaktoryzacji do powyższego przykładu.

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.

reklama

background image

76

/ 3

. 2014 . (22) /

KLUB DOBREJ KSIĄŻKI

Rafał Kocisz

Scrum. Praktyczny przewodnik po

najpopularniejszej metodyce Agile

S

crum opanował świat. Gdzie
się nie obejrzę, słyszę: „w na-
szej firmie projekty prowadzi

się scrumowo”, „pracuję w zespole
scrumowym” albo „porozmawiamy
później, bo lecę na daily standup”.
Faktem jest, że zmiana metodolo-
gii prowadzenia projektu z kaska-
dowej na zwinną to olbrzymi skok
jakościowy w zakresie zarządzania
i coraz więcej organizacji dostrzega
wynikające z tego korzyści. Jednak-
że Scrum ma jeszcze jedną, wielką
zaletę: jest prosty. Przynajmniej na
pierwszy rzut oka. Teoretycznie, na-

wet po pobieżnym zapoznaniu się z jego zasadami można rozpocząć pracę
w oparciu o ten proces. Z drugiej strony do metodologii scrumowej jak ulał
pasuje znane amerykańskie powiedzenie „easy to learn, hard to master”. W
związku z tym często widzi się projekty prowadzone w metodologii quasi-
-scrumowej. I może nie ma w tym nic złego (dopóki jest to robione świadomie
i nie wpływa to negatywnie na projekt), jednakże czasami warto sięgnąć po
sprawdzone materiały i pogłębić swoją wiedzę w tej dziedzinie.

Jakiś czas temu sam znalazłem się w sytuacji podobnej nieco do tego, co

powyżej opisałem, i poszukując wiedzy, trafiłem na książkę Scrum. Praktyczny
przewodnik po najpopularniejszej metodyce Agile
. Chciałbym podzielić się dziś
moją refleksją na temat tej pozycji. Ta średniej wielkości (456 stron) książka
autorstwa Kenneth'a S. Rubin'a podzielona jest na pięć części:

» Wstęp i wprowadzenie,

» Pojęcia podstawowe,

» Role,

» Planowanie,

» Wykonywanie sprintów.

Podział ten wydaje się być logiczny i autor bardzo zwinnie prowadzi czytelni-
ka przez omawianą tematykę. Ksiażkę tę należy (przynajmniej za pierwszym
razem) przeczytać od deski do deski.

Króciutka, pierwsza część książki jest całkiem udaną próbą wprowadzenia

czytelnika w tematykę Scruma. Autor daje tutaj bardzo ogólne odpowiedzi
na fundamentalne pytania pokroju: Czym jest Scrum?, Skąd się wziął Scrum?,
Dlaczego Scrum? i wreszcie: Czy Scrum może pomóc Tobie?

Dalej mamy rozległą część prezentującą pojęcia podstawowe związane

z opisywaną metodologią. Tutaj autor przeprowadza nas praktycznie przez
cały proces scrumowy. Na początek przedstawiony jest opis podstawowych
ról (właściciel produktu, mistrz młyna, zespół developerski), aktywności i ar-
tefaktów (rejestr produktu, sprinty, planowanie sprintu, wykonanie sprintu,
retrospekcja sprintu itd.). Następnie autor omawia zasady zwinności, odno-
sząc się do takich aspektów jak: zmienność i niepewność, przewidywanie i
adaptacja, wiedza potwierdzona, praca cząstkowa oraz postęp i wydajność.
Dalej pojawiają się szczegółowe opisy kluczowych elementów Scruma: sprin-
tów, gromadzenia wymagań i historyjek użytkownika, rejestru produktu,
nadawania ocen i mierzenia prędkości pracy zespołu oraz zarządzania dłu-
giem technologicznym.

Kolejna część książki skupia się na rolach występujących w procesie scru-

mowym. W tym miejscu autor pochyla się kolejno nad właścicielem produk-
tu, mistrzem młyna oraz zespołem developerskim. Ponadto zaprezentowane
są tutaj zagadnienia dotyczące budowania zespołów scrumowych oraz roli
menedżerów.

Następna część: Planowanie, omawia ten jakże istotny w procesie zarzą-

dzania projektem proces na poziomie portfela, produktu, wersji dystrybucyj-
nej, sprintu oraz codziennej pracy. I wreszcie część ostatnia książki zawiera
opis procesu wykonywania sprintów. W trakcie lektury tej sekcji czytelnik
dowie się, na czym polega planowanie sprintu, przegląd sprintu i wreszcie:
retrospekcja.

Na samym końcu, w rozdziale zatytułowanym: Co dalej?, autor daje szereg

wskazówek odnośnie wdrożenia w życie zaprezentowanego materiału i na
tym przygoda z książką się kończy.

I tu zaczyna się prawdziwa przygoda ze Scrumem. Patrząc z mojej per-

spektywy, lektura książki Scrum. Praktyczny przewodnik po najpopularniejszej
metodyce Agile
to dobra inwestycja. Książka wydaje się być idealna jako po-
most pomiędzy stosowaniem Scruma w trybie ad-hoc do pełnego zrozumie-
nia elementów tego procesu i rozpoczęciem stosowania ich w pełni świado-
mie. Zdecydowanie polecam tę pozycję osobom, które chciałyby poczynić
taki krok.

Mnie osobiście najbardziej do gustu przypadła część opisująca podział ról,

obowiązków i procesów scrumowych: jest to bardzo profesjonalnie i przystęp-
nie opisany kawałek wiedzy. Na plus działa również to, że przy okazji omawiania
Scruma autor przemyca też bardziej ogólną wiedzę na temat ruchu Agile.

Z kolei to, co mniej mi się spodobało, to tłumaczenie. Kwiatki w stylu

mistrz młyna (ang. scrum master) są oderwane od rzeczywistości i sprawia-
ją, że zawartość książki brzmi miejscami nienaturalnie. Nie wszystkim mogą
również przypaść do gustu częste porównywanie Scruma do metodologii ka-
skadowych; podejrzewam, że dla wielu czytelników fragmenty tę będą nad-
miarowe. Trzeba też jasno podkreślić, że nie jest to zaawansowany podręcz-
nik metodologii (autor nie pisze prawie nic na temat prowadzenia projektów
złożonych z wielu zespołów scrumowych ani na temat integracji zespołów
działających w Scrumie z zespołami zarządzanymi metodami kaskadowymi);
wydaje się jednak, iż nie jest to defekt, a raczej zamierzony efekt.

Podsumowując: Scrum. Praktyczny przewodnik po najpopularniejszej me-

todyce Agile to solidny podręcznik Scruma, który pomoże Ci uporządkować
i utrwalić wiedzę na temat tej popularnej metodologii zwinnego zarządza-
nia projektami. Z czystym sercem polecam ją wszystkim początkującym, jak i
średnio-zaawansowanym adeptom zwinnego zarządzania projektami.

Scrum. Praktyczny przewodnik po najpopularniejszej metodyce Agile

Autor: Kenneth S. Rubin

Stron: 456

Wydawnictwo: Helion

Data wydania: 2013/12/12

background image
background image

Wyszukiwarka

Podobne podstrony:
Programista 06 2014 iNTERnet
Interna 26.03.2014, weterynaria, 4 rok, notatki 2014
Sprawko programowanie robotów 03 2014
Interna 26.03.2014, weterynaria, 4 rok, notatki 2014
Analiza cen usług internetu stacjonarnego UKE 03 2014
Ekonomia ćwiczenia program PS1 2014 2015 (1)
Wykład 3 03 2014
Laborki 03 2014
Wykład 3  03 2014
6  03 2014 Ogólna charakterystyka polskiej poezji XVIII w
Prawo konstytucyjne& 03 2014
Makroekonomia 03 2014
Postępowanie?m wykład  03 2014
Wykład 2 03 2014
9 & 03 2014 Utopia
Wykład 4 03 2014
17 03 2014 Jaskowskaid 17194 Nieznany (2)
wykład 4 03 2014 anatomia wyk 4

więcej podobnych podstron