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

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 

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

 

[1] 

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

 

[2] 

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

 

[3] 

http://www.beaengine.org/

 

[4] 

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

 

P [5] 

http://udis86.sourceforge.net/

 

[6] 

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

 

[7] 

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

 

[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).

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

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

=

 

open

(

"nyan-task.png"

,

 

"rb"

).

read

()

# Znajdz magic chunku IDAT.

=

 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

[

+

 

4

:

+

 

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:

=

 

open

(

"plte.raw"

,

"rb"

).

read

()

o

=

""

=

 

0

for

 j 

in

 

xrange

(

0

,

16

):

for

 i 

in

 

xrange

(

0

,

16

):

=

 d

[

k

:

k

+

3

]

+=

 

3

# Czy kolor tla?

if

 p 

==

 

"\x0b\x41\x77"

:

+=

 

"\0"

 

# Czarny

else

:

+=

 

"\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