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
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).
5
/ www.programistamag.pl /
SPIS TREŚCI
BIBLIOTEKI I NARZĘDZIA
Dawid Borycki
6
Aleksander Kania
14
Wojciech Sura
20
PROGRAMOWANIE APLIKACJI WEBOWYCH
Piotr Tołłoczko
24
PROGRAMOWANIE SYSTEMOWE
Mateusz “j
00ru” Jurczyk
28
PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE
Tomasz Nurkiewicz
36
Marek Sawerwain
44
INŻYNIERIA OPROGRAMOWANIA
Wykorzystanie zasad SOLID podczas wytwarzania oprogramowania w paradygmacie obiektowym....
Wojciech Czabański
50
PROGRAMOWANIE BAZ DANYCH
Jędrzej Czarnecki
54
ANKIETA
Ankieta magazynu Programista: „Proces wytwarzania oprogramowania w Twojej firmie”........
60
LABORATORIUM BOTTEGA
Paweł Badeński
62
WYWIAD
66
STREFA CTF
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
Rafał Kocisz
76
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.
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.
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.
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.
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.
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
MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW
NOWY HO STING
MAPPL1403C1P_420x297+5_KB_46L.indd 1
26.02.14 17:04
MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW
MAPPL1403C1P_420x297+5_KB_46L.indd 1
26.02.14 17:04
MAKSYMALNA ELASTYCZNOŚĆ I WYDAJNOŚĆ DLA TWOICH PROJEKTÓW
Maksymalna dostępność dzięki georedundancji
Ponad 300 Gbit/s przepustowości
Skan bezpieczeństwa 1&1 SiteLock
Ponad 140 popularnych aplikacji (Drupal™,
WordPress , Joomla!™, TYPO3, Magento
Wsparcie eksperta od aplikacji
Nielimitowana powierzchnia, transfer,
konta e-mail i bazy danych MySQL
MAPPL1403C1P_420x297+5_KB_46L.indd 2
26.02.14 17:04
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.
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);
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);
}
}
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;
}
}
}
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
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).
Samsung_Knox_TAB_210x297Programista.indd 1
3/24/14 3:00 PM
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...
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:
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
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.
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
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 „<”)
» {{{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.
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
}}
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).
28
/ 3
. 2014 . (22) /
PROGRAMOWANIE SYSTEMOWE
Mateusz “j00ru” Jurczyk
W
pierwszym artykule serii „Jak napisać własny debugger w syste-
mie Windows" opublikowanym w numerze 02/2014 magazynu
Programista omówiliśmy podstawowy schemat działania debug-
gera oraz przedstawiliśmy prosty szablon debuggera napisany w języku C++.
W niniejszym artykule przedstawiamy bardziej zaawansowane aspekty budo-
wy debuggera, skupiając się na operowaniu na pamięci wirtualnej, obsłudze
punktów wstrzymania, odczytywaniu i zapisywaniu kontekstu procesora, ob-
słudze wyjątków oraz praktycznym zastosowaniu wszystkich wymienionych
wcześniej operacji. Zapraszamy do lektury.
AKTYWNE INTERAKCJE
Z PROCESAMI
Jak wspomnieliśmy w poprzednim artykule, debuggery w systemie Windows
działają na zasadzie obsługi zdarzeń – kiedy w kontekście monitorowanego
procesu wydarzy się coś, co zasługuje na uwagę debuggera, system wysyła
do niego odpowiednią notyfikację, wstrzymując w tym samym czasie dzia-
łanie obserwowanego procesu, dając w ten sposób debuggerowi czas na
odpowiednią reakcję. Jeśli ów debugger wyłącznie odbiera informacje o zda-
rzeniach, nie ingerując w żaden sposób w tok działania aplikacji, działa on
w sposób pasywny. Aby funkcjonalny debugger mógł osiągnąć jakikolwiek
znaczący cel, musi on conajmniej odczytywać stan programu wykraczający
poza domyślne informacje przekazane przez system w strukturach opisują-
cych poszczególne zdarzenia, a najczęściej zachowywać się wręcz w sposób
inwazyjny – modyfikować zawartość pamięci, rejestrów, flag oraz innych ele-
mentów stanowiących ogół stanu procesu (np. otwarte zasoby). Debug API
oddaje programistom do dyspozycji wiele funkcji umożliwiających operowa-
nie na poszczególnych częściach stanu aplikacji – najważniejsze z nich zosta-
ną omówione w kolejnych sekcjach.
Jako że system Windows aż do roku 2012 wspierał wyłącznie procesory o
architekturze IA-32, IA-64 (Itanium) oraz x86-64, artykuł ten został napisany z
myślą właśnie o tych platformach. Wszystkie przytoczone przykłady urucha-
miane były na 32-bitowych procesorach i systemach operacyjnych.
Pamięć i wirtualna przestrzeń adresowa
Pamięć operacyjna – niezbędny element działania dowolnego programu
komputerowego – może być adresowana na wiele sposobów, w zależności
od architektury procesora. Wszystkie procesory z rodziny Intel korzystają z
tzw. architektury von Neumanna, w której dane aplikacji są przechowywane
wspólnie z instrukcjami, a adresowanie obu typów pamięci wykonuje się w
ten sam sposób. Jest to istotne spostrzeżenie, ponieważ oznacza to, że nieza-
leżnie od typu pamięci, na której nasz debugger będzie operował, będziemy
używali tego samego interfejsu i wyłącznie od nas zależało będzie, w jaki spo-
sób będziemy interpretowali przetwarzane dane.
Adresowanie pamięci na 32-bitowych procesorach z rodziny x86 w śro-
dowisku Windows odbywa się przy pomocy pary: 16-bitowego selektora
segmentu oraz 32-bitowego przesunięcia wewnątrz owego segmentu. W
momencie odwołania do pamięci procesor tłumaczy adres wirtualny na
32-bitowy adres liniowy poprzez dodanie 32-bitowego adresu bazowego
segmentu do przesunięcia. Ze względu na fakt, że system Windows używa
tzw. płaskiego modelu pamięci, adres bazowy większości segmentów jest
równy zero. Wyjątek stanowi rejestr o nazwie
fs – adres początkowy segmen-
tu, na który ów rejestr wskazuje, to początek systemowego regionu opisują-
cego aktualny wątek; jedną z przechowywanych tam informacji jest wskaźnik
na funkcję obsługi wyjątków, stąd w kodzie binarnym natywnych aplikacji dla
systemu Windows często spotkać można odwołania do pamięci pod adre-
sami
fs:0 i fs:4. W przypadku pozostałych rejestrów segmentowych seg-
mentacja jest mechanizmem transparentnym, tj. odwołania takie jak
cs:N
czy
ds:N tłumaczone są na adres liniowy o wartości N. W drugiej kolejności
adres liniowy tłumaczony jest na adres fizyczny przy pomocy tzw. tabeli stron
(ang. page table) – systemowej struktury danych zarządzanej przez menadżer
pamięci będący częścią jądra.
Platformy 64-bitowe o architekturze AMD64 prawie całkowicie porzuciły
wsparcie dla mechanizmu segmentacji, a operacje na pamięci odbywają się
bezpośrednio przy użyciu 64-bitowych adresów liniowych. W dalszej części
artykułu używamy terminu „adres" w rozumieniu „adres liniowy", 32-bitowy
dla architektury x86 lub 64-bitowy dla architektury x86-64.
Każdy proces działający w środowisku Windows posiada swoją własną
przestrzeń adresową. Jedną z konsekwencji takiego modelu jest fakt, że po-
prawny wskaźnik w kontekście jednego procesu nie może zostać bezmyślnie
użyty w kontekście innej aplikacji. Aby odczytać lub zmodyfikować pamięć
zewnętrznego procesu, konieczne jest użycie odpowiednich funkcji API, któ-
rych deklaracje przedstawiono na Listingu 1. Funkcje te jako parametry przyj-
mują kolejno: uchwyt na proces, którego dotyczy operacja, adres w kontek-
ście owego procesu, wskaźnik na bufor, do którego mają zostać odczytane lub
Jak napisać własny debugger
w systemie Windows – część 2
Interfejs Debug API dostępny w systemie Windows co najmniej od czasów Win-
dowsa XP posiada ogromny potencjał, który może być wykorzystany do różnora-
kich celów przez dowolnego pasjonata niskopoziomowych aspektów informatyki,
zainteresowanego badaniem i monitorowaniem przebiegu działania aplikacji, two-
rzeniem zabezpieczeń antypirackich czy rozpakowywaniem plików wykonywalnych
zabezpieczonych tzw. protectorami lub packerami plików PE. Wchodzenie w inte-
rakcję z zewnętrznymi procesami poprzez odczytywanie i zapisywanie wartości re-
jestrów, pamięci operacyjnej oraz obsługiwanie pochodzących z nich zdarzeń może
służyć wielu celom, jednak jedno jest pewne - płynna znajomość tej części WinAPI
jest istotnym elementem wiedzy o działaniu systemu Windows, w szczególności
zaś wiedzy o fundamentalnych prawach, jakimi rządzi się wykonywanie programów
na linii system operacyjny – procesor.
30
/ 3
. 2014 . (22) /
PROGRAMOWANIE SYSTEMOWE
pobrane do zapisu dane, liczbę bajtów oraz wskaźnik na zmienną, do której
trafia informacja o liczbie bajtów, które udało się przetworzyć. W przypadku
pomyślnego wykonania operacji funkcje zwracają wartość
TRUE; w przeciw-
nym wypadku wartość
FALSE.
Listing 1. Deklaracje funkcji systemowych odpowiedzialnych za
operacje na pamięci zewnętrznych procesów
BOOL
WINAPI
ReadProcessMemory
(
_In_
HANDLE
hProcess
,
_In_
LPCVOID
lpBaseAddress
,
_Out_
LPVOID
lpBuffer
,
_In_ SIZE_T nSize
,
_Out_ SIZE_T
*
lpNumberOfBytesRead
)
;
BOOL
WINAPI
WriteProcessMemory
(
_In_
HANDLE
hProcess
,
_In_
LPVOID
lpBaseAddress
,
_In_
LPCVOID
lpBuffer
,
_In_ SIZE_T nSize
,
_Out_ SIZE_T
*
lpNumberOfBytesWritten
)
;
Podczas modyfikowania pamięci nie należącej do lokalnego procesu należy
zachować szczególną ostrożność – musimy pamiętać, że działanie takie jest
swego rodzaju „operowaniem na żywym organizmie", a zagrożeniem wyni-
kającym z niesynchronizowanego dostępu do pamięci są sytuacje wyścigu,
które mogą prowadzić do nieoczekiwanego zachowania lub niestabilno-
ści aplikacji. W zależności od natury danych, które zapisujemy, prawidłowa
synchronizacja może okazać się nietrywialnym zadaniem; na przykład, jeśli
modyfikujemy dwa lub więcej bajtów w sekcji kodu, nie wystarczy wyłącznie
wstrzymać działania wszystkich wątków w procesie (co dzieje się automa-
tycznie w momencie obsługi zdarzenia debuggera), gdyż wciąż możliwa by-
łaby sytuacja, w której jeden z wątków zostaje zatrzymany wewnątrz modyfi-
kowanego regionu. Wątek taki w momencie wznowienia wylądowałby albo w
środku skopiowanej w to miejsce instrukcji, albo rozpocząłby wykonanie od
instrukcji nieprzystającej do stanu, w którym obecnie znajduje się procesor.
W obu przypadkach niemożliwe byłoby poprawne działanie programu, który
zostałby natychmiastowo zamknięty w trybie awaryjnym.
Kontekst procesora
Kontekst procesora każdego z wątków jest obok pamięci operacyjnej naj-
ważniejszym elementem stanu procesu. Przy jego pomocy debugger może
osiągać rozmaite, ciekawe efekty, jak pokazane zostało w dalszej części arty-
kułu. Obok informacji o kontekście procesora (wartości rejestrów ogólnego
przeznaczenia, segmentowych, FPU i pozostałych, a także wartości flag) w
strukturze
CONTEXT znajduje się pole ContextFlags, którego wartość mówi
o tym, którymi elementami kontekstu zainteresowany jest użytkownik. Pole
to jest maską bitową flag takich jak
CONTEXT_CONTROL, CONTEXT_INTEGER
czy
CONTEXT_SEGMENTS.
Wskaźnik na strukturę
CONTEXT przekazuje się funkcjom GetThreadCon-
text oraz SetThreadContext, których nazwy w zupełności opisują oferowa-
ną przez nie funkcjonalność. Listing 2 przedstawia przykładowy kod modyfiku-
jący wartość rejestru
EAX w wątku identyfikowanym przez uchwyt hThread.
Listing 2. Przykładowy kod zmieniający część kontekstu procesora
wybranego wątku
CONTEXT
ctx
;
memset
(&
ctx
,
0
,
sizeof
(
ctx
))
;
ctx
.
ContextFlags
=
CONTEXT_INTEGER
;
if
(!
GetThreadContext
(
hThread
,
&
ctx
))
{
// Wystapil blad.
}
ctx
.
Eax
=
0x0badbeef
;
if
(!
SetThreadContext
(
hThread
,
&
ctx
))
{
// Wystapil blad.
}
Większość rejestrów przechowuje istotne dane w bezpośredniej formie
– wyjątek stanowią wyłącznie rejestry segmentowe na 32-bitowych proce-
sorach z rodziny x86, których wartości są jedynie selektorami – indeksami
w systemowej strukturze Global Descriptor Table. W większości przypadków
debugger nie jest zainteresowany szczegółami dotyczącymi konfiguracji seg-
mentów, gdyż ich domyślne wartości są dobrze znane (adres bazowy równy
0
i limit równy
0xffffffff), jednak w pewnych przypadkach może potrzebo-
wać on informacji na temat konkretnego segmentu, najczęściej wspomniane-
go wcześniej segmentu
fs. W tej sytuacji z pomocą przychodzi nam funkcja
GetThreadSelectorEntry, która tłumaczy wskazany selektor na strukturę
LDT_ENTRY zawierającą wszystkie informacje opisujące dany segment. Po-
nieważ jest to w zasadzie struktura procesora (dokładny opis znaczenia po-
szczególnych pól można znaleźć np. w tomie „Intel 64 and IA-32 Architectures
Software Developer's Manual, Volume 3A: System Programming Guide, Part
1", w rozdziale 3.4.5 zatytułowanym „Segment Descriptors"), jej format jest
dość skomplikowany, a wiele pól jest rozbitych na mniejsze, kilkubitowe czę-
ści. Przykładowy kod odczytujący i wypisujący adres bazowy segmentu
fs
aktualnego wątku został przedstawiony na Listingu 3.
Listing 3. Przykład wykorzystania funkcji
GetThreadSelectorEntry
do pobrania adresu obszaru Thread Environment Block
CONTEXT
ctx
;
memset
(&
ctx
,
0
,
sizeof
(
ctx
))
;
ctx
.
ContextFlags
=
CONTEXT_SEGMENTS
;
if
(!
GetThreadContext
(
GetCurrentThread
(),
&
ctx
))
{
return
EXIT_FAILURE
;
}
LDT_ENTRY
ldt_entry
;
if
(!
GetThreadSelectorEntry
(
GetCurrentThread
(),
ctx
.
SegFs
,
&
ldt_
entry
))
{
return
EXIT_FAILURE
;
}
DWORD
base
=
(
ldt_entry
.
HighWord
.
Bytes
.
BaseHi
<<
24
)
|
(
ldt_entry
.
HighWord
.
Bytes
.
BaseMid
<<
16
)
|
ldt_entry
.
BaseLow
;
printf
(
"
fs: segment base:
%x\n
"
,
base
)
;
W ramach ciekawostki można wspomnieć tutaj, że o ile dedykowany debug-
ger zaprojektowany do jednego, konkretnego celu może pozwolić sobie na
ignorowanie faktu istnienia mechanizmu segmentacji na 32-bitowych plat-
formach, debuggery ogólnego użytku powinny zapewniać ich pełne wspar-
cie, ponieważ debugowany program może tworzyć własne segmenty o nie-
zerowym adresie bazowym i używać ich do adresowania kodu, danych lub
stosu. Jak się jednak okazuje, nie wszystkie z powszechnie używanych debug-
gerów przestrzegają tej zasady, w związku z czym segmentacja może zostać
użyta jako efektywna metoda utrudniania analizy działania programu. Więcej
informacji na ten temat znaleźć można w poście „Protected Mode Segmenta-
tion as a powerful anti-debugging measure" na blogu autora [1].
MECHANIZMY DEBUGGERA
Dysponując już podstawowymi, niskopoziomowymi narzędziami do analizy i
modyfikacji stanu procesów, możemy na ich podstawie budować bardziej skom-
plikowane mechanizmy, takie jak deasemblacja kodu, monitorowanie przebiegu
wykonywania programu za pomocą tzw. single steppingu czy ustawianie i zarzą-
dzanie punktami wstrzymania. Szczegóły implementacyjne wymienionych funk-
cjonalności zostały opisane w poszczególnych sekcjach poniżej.
Deasemblacja kodu
Jedną z ważniejszych części debuggerów, niezależnie od pełnionej przez nie
funkcji, jest możliwość tłumaczenia kodu maszynowego (binarnej reprezen-
tacji kodu wykonywalnego) na zrozumiałe dla człowieka mnemoniki. System
Windows nie udostępnia interfejsów umożliwiających deasemblację kodu, a
własna implementacja owej funkcjonalności znacznie wykracza poza zakres
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
;
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++.
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.
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.
JAK NAPISAĆ WŁASNY DEBUGGER W SYSTEMIE WINDOWS – CZĘŚĆ 2
Po uruchomieniu tak skonstruowanego debuggera na prostej aplikacji
przedstawionej na Listingu 13, naszym oczom powinna ukazać się lista około
450 linii, podobna do tej przedstawionej w skróconej formie poniżej:
[00401283] mov dword [esp], 0x1
[0040128a] call dword [0x4060f4]
[00401290] call 0xfffffd70
[00401000] push ebx
[00401001] sub esp, 0x38
...
[00401b73] mov eax, 0x1
[00401b78] add esp, 0x1c
[00401b7b] ret
[00401448] mov eax, 0x1
[0040144d] add esp, 0x1c
[00401450] ret 0xc
Listing 13. Prosty program testowy, na którym uruchamiany jest
nasz debugger
#include
<
cstdio
>
#include
<
cstdlib
>
#include
<
string
>
using
namespace
std
;
int
main
()
{
for
(
unsigned
int
i
=
0
;
i
<
10
;
i
++)
{
fprintf
(
stderr
,
"
Hello, world!
\n
"
)
;
}
return
0
;
}
Na podstawie tak skonstruowanego pliku możemy wyznaczyć najczęściej
wykonywane w programie regiony poprzez proste przetwarzanie przy użyciu
ciągu komend „
debugger.exe test.exe | sort | uniq – -count |
sort – n – r”:
11 [004013fe] jnz 0xffffffca
11 [004013fc] test al, al
11 [004013f9] setbe al
11 [004013f4] cmp dword [esp+0x1c], 0x9
10 [00401c30] jmp dword [0x406118]
10 [004013f0] inc dword [esp+0x1c]
10 [004013eb] call 0x845
10 [004013e4] mov dword [esp], 0x403064
10 [004013dc] mov dword [esp+0x4], 0x1
10 [004013d4] mov dword [esp+0x8], 0xe
10 [004013d0] mov [esp+0xc], eax
10 [004013cd] add eax, 0x40
10 [004013c8] mov eax, [0x4060fc]
2 [00401c70] jmp dword [0x4060c4]
2 [004019c8] jz 0x8
2 [004019c6] test ecx, ecx
2 [004019c0] mov ecx, [0x40503c]
Nasz debugger w obecnej formie jest dobrym punktem wyjściowym do bar-
dziej zaawansowanych interakcji z zewnętrzną aplikacją: reagowanie wyłącz-
nie na konkretny typ instrukcji, zmiana semantyki lub wręcz całkowita emu-
lacja niektórych instrukcji, monitorowanie częstotliwości i rodzaju dostępu
do pamięci, zrzucanie pamięci w wybranych momentach działania aplikacji
itp. Jak przekonamy się w kolejnych artykułach z serii, opisane w tym nume-
rze mechanizmy znajdują zastosowanie w debuggerach implementujących
właściwie dowolną funkcjonalność, lecz jest to dopiero czubek góry lodowej
– na opisanie wciąż oczekują inne typy punktów wstrzymania, pomocnicza
biblioteka DbgHelp i oferowane przez nią możliwości, a także kolejne przy-
kłady zastosowań dedykowanych debuggerów w codziennej pracy progra-
mistów języków natywnych. To wszystko znajdzie swoje miejsce w tekstach
publikowanych w nadchodzących miesiącach – tymczasem już teraz serdecz-
nie zachęcamy czytelników do samodzielnych eksperymentów z Debug API.
Miłej zabawy!
W sieci
P [1]
http://j00ru.vexillium.org/?p=866
P [2]
https://code.google.com/p/distorm/
P [3]
P [4]
http://bastard.sourceforge.net/libdisasm.html
P [5]
http://udis86.sourceforge.net/
P [6]
http://www.theregister.co.uk/2010/11/15/amd_secret_debugger/
P [7]
http://hardware.slashdot.org/story/10/11/12/047243/hidden-debug-mode-found-in-amd-processors
P [8]
http://msdn.microsoft.com/en-us/library/windows/desktop/ms684884%28v=vs.85%29.aspx
Mateusz “j00ru” Jurczyk
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 (
reklama
36
/ 3
. 2014 . (22) /
PRZETWARZANIE RÓWNOLEGŁE I ROZPROSZONE
Tomasz Nurkiewicz
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.
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)
}
}
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
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-
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:
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]
}
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.
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
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 (
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”.
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 );
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
));
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
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:
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
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.
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) {
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
{
// ...
}
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.
Specjalista ds. wdrożeń systemów ERP
ZAINTERESOWALIŚMY CIĘ? SKONTAKTUJ SIĘ Z NAMI!
reklama
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'
))
;
})
;
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
),
VARCHAR
(
150
)
)
;
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
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
->
=
$_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
->
=
$_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
->
))
;
}
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
>
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
59
/ www.programistamag.pl /
ORM W PHP Z WYKORZYSTANIEM WZORCA ACTIVE RECORD
Rysunek 3. Wyszukiwarka książek
Jędrzej Czarnecki
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
.
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
60
/ 3
. 2014 . (22) /
ANKIETA
Ankieta magazynu Programista:
„Proces wytwarzania oprogramowa-
nia w Twojej firmie”
W ostatnim kwartale 2013 roku magazyn Programista przeprowadził badanie pt.
„Proces wytwarzania oprogramowania w Twojej firmie” przy współpracy z IBM Pol-
ska i firmą Arrow ECS. Sondaż dotyczył małych i średnich przedsiębiorstw, a jego
efektem miało być wykazanie, jak zbudowane są zespoły programistyczne, jakie
mają metodyki pracy oraz jakich narzędzi używają. Badanie miało również na celu
dostosowanie oferty IBM związanej z oprogramowaniem wspierającym wytwarzanie
aplikacji (narzędzia IBM Rational) do wymogów polskiego rynku w sektorze MŚP.
» implementacje zmian,
» uzyskiwanie informacji/danych potrzebnych do pracy,
» spotkania, ustalenia,
» analiza kodu,
» analiza przepływu danych,
» testy regresyjne,
» tworzenie harmonogramów prac,
» tworzenie raportów z wykonanych prac/postępów.
TESTOWANIE I INTEGRACJA
Wykorzystanie narzędzi CI (ang. Continuous Integration) deklaruje 27.6% bada-
nych. Najpopularniejszym narzędziem tego typu jest Jenkins (58.9%). Poza tym
wskazano również TeamCity firmy JetBrains (16.7%), a wśród użytkowników ze-
stawu narzędzi firmy Microsoft popularny jest Team Foundation Server.
38% osób biorących udział w ankiecie deklaruje, że w ich firmie jest dział, któ-
rego głównym zadaniem jest badanie jakości kodu. 22.1% ankietowanych stosu-
je narzędzia do badania jakości kodu (uwaga: respondentami byli developerzy z
różnych działów, niekoniecznie ci zajmujący się kontrolą jakości oprogramowania).
W
ankiecie wzięło udział 326 osób związanych
z branżą IT, z ponad 300 firm zaliczanych
do małych i średnich przedsiębiorstw. Naj-
częściej wymieniane przez badanych języki progra-
mowania to kolejno: C#, Java, PHP i C++. Najrzadziej
występują języki wykorzystywane do zastosowań
specjalistycznych (z naciskiem na Assemblera). 69.9%
ankietowanych deklaruje, iż używa środowisk na licen-
cjach otwartych, a najpopularniejszym środowiskiem
na tego typu licencji jest Eclipse. 62.6% badanych de-
klaruje użycie środowisk komercyjnych. W tej kategorii
najpopularniejszym produktem jest Microsoft Visual
Studio. 32.2% badanych używa jednocześnie otwar-
tych, jak i komercyjnych rozwiązań.
BUDOWA ZESPOŁÓW
PROGRAMISTYCZNYCH
Wśród firm, które wzięły udział w ankiecie, zdecydowa-
nie najpopularniejsze są małe zespoły, składające się z
od jednego do pięciu programistów (65.9% odpowiedzi)
oraz jednego do pięciu analityków (85.6% odpowiedzi).
Poniżej znajduje się ranking technologii stworzony
na podstawie danych wpisywanych przez ankietowanych w kolumnach „na-
zwy frameworków” oraz „nazwy narzędzi”. Technologie w tabeli są wymienio-
ne według popularności (od najbardziej popularnej do najmniej popularnej).
Technologia Najpopularniejszy framework Najpopularniejszy ORM
.NET
ASP.NET MVC
Entity Framework
Java
Spring
Hibernate
PHP
Zend Framework
Doctrine 2
Python
Django
(wbudowany)
Ruby
Ruby on Rails
(wbudowany)
Niezależnie od używanego języka, dużą popularność mają własne rozwiąza-
nia w zakresie tzw. middleware rozwijane wewnątrz konkretnych firm.
ZADANIA CZASOCHŁONNE
Statystyczna klasyfikacja czasochłonności określonych zadań dokonana przez
ankietowanych przedstawia się następująco (od najbardziej czasochłonnego
zadania do najmniej czasochłonnego):
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
62
/ 3
. 2014 . (22) /
LABORATORIUM BOTTEGA
Paweł Badeński
W
poprzednim odcinku serii opowiedziałem, jak efektywnie dawać
feedback innym. Feedback stanowi elementarną część metody-
ki Agile: umożliwia lepszą komunikację z klientem, zwiększanie
efektywności zespołu, ulepszanie procesu, iteracyjne tworzenie produktu
spełniającego w pełni potrzeby użytkownika. Obecność kultury dawania/
otrzymywania feedbacku w zespolu to ważny pierwszy krok na drodze do
lepszej współpracy. Po wykonaniu tego kroku stajemy przed wyzwaniem –
jak otrzymany feedback wprowadzać w życie?. Przykładowo: co ma zrobić Szy-
mon (młodszy programista), jeśli Kasia (doświadczony tester) powie mu, że
oddaje funkcjonalności do testów zbyt szybko – podając jako przykład 8 z 10
historyjek, które wróciły do Szymona ze zgłoszeniem ewidentnych bugów?
Szymon może spróbować testować historyjki trochę dokładniej, jednak brak
doświadczenia będzie utrudniał mu poprawę. Korzyść, jaka z tego wyniknie
dla Kasi, to spokój ducha – Kasia może sobie powiedzieć w myślach: przynaj-
mniej próbowałam. Wyzwaniem dla Kasi jest odpowiedzieć na pytanie – jak
może pomóc Szymonowi efektywniej testować funkcjonalności przed od-
daniem jej do QA. Dla Szymona będzie to przecież nie lada problem – jest
jeszcze niedoświadczony i trzeba czasu, żeby osiągnął większą skuteczność.
JAKOŚĆ FEEDBACKU
Feedback, który otrzymasz od współpracowników, klientów czy nawet part-
nera życiowego, będzie jakościowo bardzo zróżnicowany. Czasem dostaniesz
konkretną informację, co robisz nieefektywnie i jak to poprawić: podczas re-
trospektyw często komentujesz na temat błędów Jacka. Czy mógłbyś zamiast
tego porozmawiać z nim osobiście?. Często jednak feedback będzie dużo bar-
dziej „mętny”: czuję, że podczas niektórych spotkań jesteś jakby nieobecny.
Na własnym przykładzie wiem, że kiedy otrzymuję feedback o kiepskiej
jakości, moją pierwszą reakcją jest frustracja. Jeśli próba dopytania drugiej
osoby o szczegóły nie pomaga, frustracja staje się jeszcze większa. Natural-
nym odruchem wydaje się wtedy zignorowanie takiego feedbacku. Wiem
jednak na bazie doświadczeń, że każdy feedback może być pouczający i roz-
wijać. Dlatego staram się nawet taki feedback wykorzystać i znaleźć metody,
które pomogą mi go implementować. Jedną z technik, którą stosuję w takich
trudnych sytuacjach, jest „skupianie uwagi”.
SKUPIANIE UWAGI
Zdarza się, żę nie zgadzasz się z feedbackiem, który otrzymujesz, np. mało
udzielasz się w dyskusjach. Czasem dotyczy on kwestii, nad którą nigdy się
nie zastanawiałeś „kiedy rozmawiasz z innymi, nie odnosisz się do ich emo-
cji”. Wbrew pozorom, to jeden z lepszych rodzajów feedbacku, jaki możesz
od kogoś dostać. Dotyczy on cech bądź zachowań, które znajdują się w Two-
im „martwym polu”. Są to aspekty, które z Twojej perspektywy uznajesz za
nieistotne i tego typu feedback daje szansę na rozwinięcie zupełnie nowej
umiejętności. Warto pamiętać, że ciągłe ulepszanie (ang. continuous improve-
ment) w metodykach Agile dotyczy procesu, produktu, jak również członków
zespołu.
Z takim feedbackiem należy się najpierw oswoić. Do tego celu stosuję
technikę „skupiania uwagi”. Polega ona na przyciągnięciu uwagi Twojego
umysłu do problemu, starając się zachować podejście bezkrytyczne. Ta me-
toda opiera się na założeniu, że Twoje myśli lubią podróżować w mózgu po-
pularnymi drogami. Skupienie uwagi pozwala „zachęcić” sygnały do podróży
nowymi, nieznanymi ścieżkami. Z podobnej przyczyny potrzebne jest zacho-
wanie bezkrytycznego podejścia. Przyjmowanie istniejących opini kieruje
Twoje myśli na znane szlaki i powstrzymuje od tworzenia się nowych struktur
w mózgu. Innymi słowy – powstrzymuje proces uczenia się.
Załóżmy, że feedback, na którym chcę się skupić, odnosi się do tego, jak
rzadko wyrażam swoją opinię podczas spotkań. Skupienie uwagi najłatwiej
osiągnąć poprzez zadanie pytania w stylu: czy ja rzeczywiście za mało się udzie-
lam w dyskusjach. Podejście bezkrytyczne polega na tym, że unikam ocena-
nia, tj. czy jestem lepszy, bo to robię albo czy jestem gorszy, bo tego nie robię.
Dobrym sposobem jest zapisywanie każdego pytania – najlepiej w miejscu,
na które często zwracasz uwagę. Wtedy częściej utrwalasz nowe ścieżki i skupiasz
uwagę umysłu na interesującej Cię kwestii. W rozważanym przypadku należy co
najmniej przypominać sobie je przed każdym spotkaniem, aby zainicjować w Two-
im mózgu proces zbierania informacji oraz uczenia się. Natomiast po każdym spo-
tkaniu warto, byś zastanowił się, czego nowego się nauczyłeś, lub nawet utrwalić
swoje myśli w formie pisemnej (o której wspomnę jeszcze w sekcji „journalling”).
SAMOŚWIADOMOŚĆ
Częstym błędem popełnianym przy implementacji feedbacku jest przyjęcie za-
łożenia, że albo dokonamy zmiany, albo zostaniemy przy obecnym zachowaniu.
Tymczasem istnieje jeszcze trzecia droga, czyli bycie świadomym swojego za-
chowania. Najlepiej wytłumaczyć to przez przykład. Kierownikiem projektu w
jednym z zespołów, gdzie pracowałem, był Marcin. Już pierwszego dnia po dołą-
czeniu do zespołu Marcin poinformował nas, czego możemy się po nim spodzie-
wać. Powiedział, że często wygląda, jakby nadmiernie się przejmował sytuacją w
projekcie, i podkreślił, że wynika to z jego osobowości. Taka informacja jest bar-
dzo cenna i pomogła nam zachować spokój, kiedy Marcin wyglądał na mocno
przejętego. Zrozumienie i dobra komunikacja jest fundamentem Agile, zwłaszcza
między kierownikiem projektu i członkami zespołu programistycznego.
Marcin z pewnością zebrał wiele razy feedback dotyczący tego zacho-
wania. Wiedział również o trzeciej drodze, czyli jak może pomóc mu samo-
świadomość. Jest to cecha, którą bardzo cenimy u innych. Pozwala uniknąć
konfliktów przez błędną interpretację czyjegoś zachowania. Ludzie mający
większą samoświadomość cieszą się również większym zaufaniem. Znajo-
mość swoich wad i zalet pozwala im na większą efektywność.
Po otrzymaniu feedbacku w niektórych sytuacjach możesz zastanowić
się, czy możesz wybrać „trzecią drogę”. Powinieneś wtedy wykorzystać świa-
domość swojego zachowania, aby zwiększyć efektywność. Jedną z technik
zaprezentowałem już na przykładzie historii Marcina. Polega ona na poinfor-
mowaniu innych o swoim wzorcu zachowania, aby byli lepiej przygotowani.
Jestem pewien, że pracując z feedbackiem, znajdziesz wiele innych metod,
które lepiej umożliwią Ci wykorzystanie samoświadomości.
Brakujący element Agile
Część 2: Wprowadzanie feedbacku w życie
W tym artykule przedstawię wyzwania, które napotykamy przy wprowadzaniu feedbacku
w życie. Opiszę skuteczne metody jego implementacji i sposoby radzenia sobie z często
pojawiającymi się problemami. Pokażę przede wszystkim propozycję frameworka, który
pomoże Ci podejść do feedbacku „po inżyniersku”, czyli profesjonalnie i ze strukturą.
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
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
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
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.
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
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:
68
/ 3
. 2014 . (22) /
STREFA CTF
Gynvael Coldwind
CTF
RuCTF Quals 2014
Waga CTFtime.org
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.
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):
"JASC-PAL"
"0100"
"256"
for
i
in
xrange
(
256
):
i
,
i
,
i
Wygenerowaną paletę zaimportowaliśmy w IrfanView (opcja Image /
Palette / Import palette...) i spodziewaliśmy się ujrzeć obraz w skali szarości
o podwyższonej entropii, jednak obraz, który ukazał się naszym oczom, był
krystalicznie czysty (patrz: Rysunek 1).
Rysunek 1. Efekt podmienionej palety
Ponieważ nie do końca wierzyliśmy, że IrfanView w sposób bezpośredni pod-
mienił paletę, postanowiliśmy wyekstraktować sekcję danych (IDAT) z pliku
PNG i ją zdekompresować (PNG używa zlib, czyli de facto DEFLATE). W tym
celu posłużyliśmy się poniższym skryptem:
from
struct
import
unpack
d
=
open
(
"nyan-task.png"
,
"rb"
).
read
()
# Znajdz magic chunku IDAT.
i
=
d
.
find
(
"IDAT"
)
# Budowa chunku:
# [4 bajty: dlugosc]
# i – -> [4 bajty: magic ]
# ... dane ...
# [4 bajty: crc ]
# Odczytaj wielkosc.
sz
=
unpack
(
">I"
,
d
[
i
-
4
:
i
])[
0
]
# Rozpakuj i zapisz dane.
d
=
d
[
i
+
4
:
i
+
4
+
sz
].
decode
(
"zlib"
)
open
(
"out.raw"
,
"wb"
).
write
(
d
)
Wyjściowe dane to oczywiście nie końcowa bitmapa, a tablica par: rodzaj
funkcji filtrującej PNG (jeden bajt), oraz dane linii z naniesionym filtrem. Nie-
mniej jednak dla podwyższonej entropii na końcowej bitmapie „przefiltrowa-
ny“ obraz również miałby podwyższoną entropię, a więc już na tym etapie
powinno być widać szum.
Otrzymany plik out.raw otworzyliśmy w IrfanView, podając jako rozdziel-
czość 2881x1800 (dodatkowy pixel z uwagi na bajt definiujący rodzaj funkcji
filtrującej dla danej linii) w skali szarości, i naszym oczom ukazał się ponownie
czysty, niezaszumiony, obraz (patrz: Rysunek 2).
Rysunek 2. Przefiltrowany obraz wyekstraktowany z PNG
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/
), wygenerowaliśmy raport na temat dokładnej budowy otrzy-
manego pliku PNG:
File: nyan-task.png (13105 bytes)
chunk IHDR at offset 0x0000c, length 13
2880 x 1800 image, 8-bit palette, non-interlaced
chunk PLTE at offset 0x00025, length 768: 256 palette entries
chunk IDAT at offset 0x00331, length 12268
zlib: deflated, 32K window, maximum compression
chunk IEND at offset 0x03329, length 0
No errors detected in nyan-task.png (4 chunks, 99.7%
compression).
Z raportu można wywnioskować, że:
» Plik PNG posiada jedynie podstawowe i spodziewane sekcje, tj. IHDR (na-
główki), PLTE (paleta kolorów), IDAT (dane obrazu) oraz IEND (zakończenie
pliku PNG).
» Nie ma nic doklejonego na koniec pliku – wielkość to 13105 bajtów, nato-
miast magic IEND został znaleziony na offsecie 13097; ponieważ nagłówki
tej sekcji (magic, oraz suma CRC) zajmują dokładnie 8 bajtów, nie pozosta-
wia to żadnego miejsca na dodatkowe dane.
» IHDR ma standardową wielkość.
» PLTE ma również standardową wielkość (256 wpisów po 3 bajty, razem 768).
Istniało pewne prawdopodobieństwo, że pewne dane zostały doklejone na
koniec skompresowanych danych w sekcji IDAT, jednak uznaliśmy to za mało
prawdopodobne i odłożyliśmy do sprawdzenia później.
Oczywiście, mogło być po prostu więcej skompresowanych danych, które
zostały poprawnie zdekompresowane, ale były poza zadeklarowaną bitmapą
(2880x1800), a więc nigdy nie zostały wyświetlone na ekranie. Przeczyła temu
wielkość zdekompresowanych danych: 5185800 bajtów – czyli dokładnie
2881 (należy pamiętać o bajcie definiującym funkcję filtrującą) pomnożone
przez ilość linii (1800), a więc to, czego się spodziewaliśmy.
Po wyeliminowaniu powyższych możliwości, najbardziej prawdopodob-
ną z pozostałych opcji została ponownie paleta kolorów.
DUCH W KOLORACH
Postanowiliśmy wyświetlić paletę barw, tym razem faktycznie w postaci ko-
lorów, a nie liczb. W tym celu ponownie użyliśmy IrfanView i jego opcji Edit
palette (patrz: Rysunek 3).
Rysunek 3. Paleta kolorów wyświetlona przez IrfanView
W oczy rzuciły nam się dwie rzeczy: umieszczenie podobnych kolorów bli-
sko siebie i specyficzny wzór w górnym wierszu oraz prawej kolumnie, który
wspólnie z jednolitym dolnym wierszem i lewą kolumną do złudzenia przypo-
minał wzorzec wyszukiwania w kodach typu Data Matrix (kody 2D służące do
zapisu niewielkich ilości informacji, podobne do kodów QR).
Z uwagi na ten trop wyekstraktowaliśmy dane sekcji PLTE (używając
skryptu analogicznego do tego, którego użyliśmy przy ekstrakcji IDAT, pomi-
jając dekompresję) oraz przekonwertowaliśmy je do czarno-białej bitmapy,
używając następującego skryptu:
d
=
open
(
"plte.raw"
,
"rb"
).
read
()
o
=
""
k
=
0
for
j
in
xrange
(
0
,
16
):
for
i
in
xrange
(
0
,
16
):
p
=
d
[
k
:
k
+
3
]
k
+=
3
# Czy kolor tla?
if
p
==
"\x0b\x41\x77"
:
o
+=
"\0"
# Czarny
else
:
o
+=
"\xff"
# Bialy
open
(
"plte_bw.raw"
,
"wb"
).
write
(
o
)
Następnie otworzyliśmy w IrfanView plik wyjściowy (16x16x8bpp), powięk-
szyliśmy (bez resamplingu) do rozsądnych rozmiarów (patrz: Rysunek 4) i za-
pisaliśmy jako PNG.
Rysunek 4. Wyjściowy kod Data Matrix
Tak wygenerowany PNG wysłaliśmy na kilka serwisów dekodujących kody tego
typu i jeden z nich (
http://online-barcode-reader.inliteresearch.com/
) popraw-
nie odczytał Data Matrix, zwracając następujący krótki link: u.to/P4JUBg.
Wchodząc na powyższy adres, zostaliśmy przekierowani do wyszukiwarki
Google z określonym zapytaniem:
https://www.google.ru/search?q=RUCTF_ca8250c2b4b50581afc9ffd1f403f3f2
Treść zapytania była poszukiwaną przez nas flagą :)
PODSUMOWANIE
Koniec końców zadanie okazało się nie być trudne, natomiast zanim dotar-
liśmy do faktycznego rozwiązania, trochę czasu zajęło nam przeglądanie
różnych możliwych miejsc ukrycia flagi. Ostatecznie pomógł nieco fakt, że
IrfanView wyświetlił paletę w formie kolorowej bitmapy 16x16, a nie np. jako
listę 256 pasków – w tym wypadku kod Data Matrix pozostałby jeszcze przez
pewien czas przez nas niezauważony. W ramach Dragon Sector zadanie zosta-
ło rozwiązane przez autora tego tekstu oraz tkd.
Rozwiązanie zadania Nyan-task zostały nadesłane przez Dragon Sector
– jedną z polskich drużyn CTFowych.
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 +
")"
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>
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
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
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
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