��������������������������������� ����������������������������������� ����������������������������������������������������������������������������� ����������������������������������������������������������������������������� ������������������������������������������������������������������������������� ���������������������������� ��������������������������������������������������������������������������� �������������������������������������������������������������������������������� ������������������ ����������������������������������������������������������������������������������� �������������������������������������������������������������������������������� ��������������������������������������������������������������������������������� ������������������������������������������������������������������������������ ���������������������������������������������
�����������������������������������������������������������������������������������������������
8/2010 (188)
SPIS TREŚCI
BIBLIOTEKA MIESIĄCA
PROGRAMOWANIE JAVA
6 Google SparseHash – Wyspecjalizowane kontenery haszujące w języku C++
30 Przewodnik po SCJP – Czyli certyfikat z Javy cz.V
Rafał Kocisz Standardowa Biblioteka Wzorców (STL) języka C++ to wspaniałe i potężne narzędzie. Niestety – ewidentnie doskwiera w niej brak wsparcia dla kontenerów mieszających. Nowy standard C++ ma rozwiązać ten problem, ale póki co trzeba szukać innych alternatyw w postaci zewnętrznych bibliotek. W niniejszym artykule przedstawię implementację tablic haszujących rodem z Google.
KLUB TECHNICZNY 16 Technologie Progress OpenEdge – Część 9. OpenEdge SQL
Piotr Tucholski OpenEdge SQL stworzony przez Progress Software Corporation jest implementacją powszechnie znanych standardów, włączając SQL-92, SQL-99 i SQL-2003. Jest częścią otwartego, elastycznego interfejsu i pełni niezwykle ważną rolę w procesie rozwoju nowoczesnych aplikacji biznesowych OpenEdge.
PROGRAMOWANIE PYTHON 20 Kurs Pythona. Cz.II – Struktury danych, funkcje i moduły
Łukasz Langa W odcinku wprowadzającym zainstalowaliśmy Pythona i trochę pobawiliśmy się różnymi jego cechami. Po nabraniu swobody w wykorzystaniu linii poleceń możemy zabrać się za bardziej metodyczny przegląd tego, co oferuje nam język spod znaku węża.
4
Krzysztof Rychlicki - Kicior Proces zdobywania certyfikatów, potwierdzających umiejętności z różnych dziedzin wiedzy, stał się jednym z ważniejszych elementów osobistego rozwoju. Proces ten ma miejsce również w branży IT; certyfikaty dla programistów (Java lub .NET), administratorów czy sieciowców (Cisco) można coraz częściej odnaleźć w CV osób starających się o pracę.
36 Java na BlackBerry – Podstawy pisania aplikacji
Tomasz Milczarek Artykuł przedstawia podstawy programowania aplikacji w języku Java pod system BlackBerry. W podstawowym zakresie omówione zostały cztery tematy:ogólne sposoby tworzenia aplikacji, budowanie interfejsu użytkownika, programowanie menu telefonu oraz zagadnienie utrwalania danych.
42 Wiosna z drugą twarzą w chmurach – Część I
Paweł Nieścioruk Kompletny przykład procesu wytwarzania aplikacji Java Server Faces 2.0 z użyciem Spring Framework i Hibernate wraz z jej końcowym wdrożeniem na chmurę obliczeniową Amazon Elastic Compute Cloud (EC2).
NIEZAWODNE OPROGRAMOWANIE 50 Testowanie gier na urządzenia mobilne
Grzegorz Tarczyński Testowanie gier jest niezwykle złożoną materią, zwłaszcza przy dużych, wysokobudżetowych produkcjach. Gry na urządzenia mobilne zwykle do takich nie należą, co nie oznacza, że proces testowania jest tu mało istotny czy możliwy do pominięcia. Jaka jest specyfika tego procesu, dowiecie się z niniejszego artykułu.
8/2010
SPIS TREŚCI
SPIS TREŚCI
Z ŻYCIA ITOLOGA 58 CMMI – Dlaczego powinno Cię to obchodzić?
Mariusz Chrapko Pamiętam jak zaczynałem swoją przygodę z rozwiązywaniem sudoku. Na początku było niemiłosiernie trudno, potem stopniowo łapałem „wiatr w żagle”. Podobnie jest z modelem CMMI. Na pierwszy rzut oka wydaje się bardzo skomplikowany. Później, w miarę jak stopniowo go poznajemy, zaczynamy dostrzegać jego wewnętrzne „piękno” i logikę, widzimy że jego praktyki naprawdę mają sens i mogą nam się przydać. Tak było ze mną, i tak – jestem o tym przekonany – będzie również z Wami!
WYWIAD 64 Mariusz Chrapko – Wywiad z autorem pierwszej w Polsce książki na temat modelu CMMI oraz jego praktycznego zastosowania.
Mariusz Chrapko jest wieloletnim praktykiem w zakresie doskonalenia procesów tworzenia oprogramowania w oparciu o model CMMI®. Wspiera firmy informatyczne na terenie całej Europy, prowadząc coaching zespołów projektowych oraz szkolenia na różnych szczeblach organizacyjnych. Dodatkowo jest praktykiem we wdrażaniu i adaptacji metod Agile Software Development (wcześniej Agile Coach/ Centrum Oprogramowania Motoroli w Krakowie), Programu Metryk Organizacyjnych, a także procesu Peer Review. Reklama
Miesięcznik Software Developer’s Journal (12 numerów w roku) jest wydawany przez Software Press Sp. z o.o. SK Redaktor naczelny: Łukasz Łopuszański lukasz.lopuszanski@software.com.pl Projekt okładki: Agnieszka Marchocka Skład i łamanie: Tomasz Kostro www.studiopoligraficzne.com Kierownik produkcji: Andrzej Kuca andrzej.kuca@software.com.pl Adres korespondencyjny: Software Press Sp. z o.o. SK, ul. Bokserska 1, 02-682 Warszawa, Polska tel. +48 22 427 36 91, fax +48 22 224 24 59 www.sdjournal.org cooperation@software.com.pl
Dział reklamy: adv@software.com.pl Redakcja dokłada wszelkich starań, by publikowane w piśmie i na towarzyszących mu nośnikach informacje i programy były poprawne, jednakże nie bierze odpowiedzialności za efekty wykorzystania ich; nie gwarantuje także poprawnego działania programów shareware, freeware i public domain. Wszystkie znaki firmowe zawarte w piśmie są własności odpowiednich firm. Zostały użyte wyłącznie w celach informacyjnych. Osoby zainteresowane współpracą prosimy o kontakt: cooperation@software.com.pl
BIBLIOTEKA MIESIĄCA
Google SparseHash Wyspecjalizowane kontenery haszujące w języku C++ Standardowa Biblioteka Wzorców (STL) języka C++ to wspaniałe i potężne narzędzie. Niestety – ewidentnie doskwiera w niej brak wsparcia dla kontenerów mieszających. Nowy standard C++ ma rozwiązać ten problem, ale póki co trzeba szukać innych alternatyw w postaci zewnętrznych bibliotek. W niniejszym artykule przedstawię implementację tablic haszujących rodem z Google. Dowiesz się:
Powinieneś wiedzieć:
• • • •
• Solidna znajomość języka C++; • Solidna znajomość biblioteki STL.
Do czego służy biblioteka Google SparseHash; Jak rozpocząć z nią pracę; Jak zastosować ją w praktyce; Jak Google SparseHash integruje się z biblioteką STL.
S
truktury danych to niewątpliwie jeden z najbardziej elementarnych, a zarazem kluczowych komponentów z których zbudowane są nasze programy. Posiadanie wydajnych struktur danych to jeden z wielu kluczy do osiągnięcia sukcesu w inżynierii oprogramowania. Dziś, dzięki bibliotekom standardowym, które wyposażone są w cały szereg przydatnych, a zarazem łatwych w użyciu i – co ważne – bardzo wydajnych kontenerów, problem niskopoziomowych struktur danych w pewnym stopniu został usunięty ze świadomości programistów. Nie wierzysz? Więc zapytaj sam siebie: kiedy implementowałeś ostatnio własny, wyspecjalizowany kontener? A w następnej kolejności zadaj takie samo pytanie kilku swoim znajomym programistom... Wysokiej jakości biblioteka standardowa to oczywiście Bardzo Dobra Rzecz. Dzięki takim bibliotekom jak STL czy Java Collection Framework, lista czy algorytm sortowania stały się podstawowymi klockami, z których mogę niejako składać swoje programy. Dzięki temu jestem w stanie skupić się bardziej na ich właściwej funkcjonalności. Innym słowy: tworząc moje aplikacje mogę myśleć na wyższym poziomie abstrakcji. W zasadzie powinienem być zadowolony... Jednakże, operowanie na wysokim poziomie abstrakcji ma zarówno swoje dobre jak i złe strony. Negatywnym czynnikiem może być w tym przypadku zjawisko polegające na tym, że tworząc programy w ten sposób przestajemy dostrzegać szczegóły.
6
W językach wysokiego poziomu (Perl, Python, Ruby itd...) jest to efekt jak najbardziej pożądany. Jednakże w językach niższego poziomu (np. C, bądź C++, który dziś de facto jest językiem zdecydowanie niskopoziomowym) efekt taki może być w wielu przypadkach nie do zaakceptowania. Ciekawy casus stanowi wspomniana wcześniej biblioteka STL (a w zasadzie część tej biblioteki wchodząca w skład standardu języka C++). Przez wielu (również przez autora niniejszego tekstu) uważana za arcydzieło inżynierii oprogramowania, posiada niestety pewne wady. Jedną z nich jest brak kontenerów zbudowanych w oparciu o tablice mieszające (ang. hash tables). Co ciekawe – tablice te nie zostały wprowadzone do biblioteki standardowej C++ z bardzo prozaicznego, czysto ludzkiego powodu: Komisji opracowującej standard C++98 po prostu zabrakło czasu. W efekcie, cała rzesza programistów języka C++ wykorzystuje na co dzień kontenery asocjacyjne oparte na zrównoważonych drzewach. Co gorsza, młodsi, tudzież słabiej wyedukowani programiści mogą wręcz nie zdawać sobie sprawy z istnienia rozwiązań alternatywnych, często o wiele lepiej dostosowanych do wymagań naszych programów. W tym kontekście można powiedzieć, że biblioteka standardowa języka C++ w pewnym sensie ogranicza nasze umysły... Cóż – jest to niewątpliwie cena postępu. W końcu po to budujemy kolejne warstwy abstrakcji aby nie zaprzątać sobie głowy zbęd-
8/2010
Tablice haszujące w języku C++
nymi szczegółami. Pozostaje tylko pytanie: czy aby zawsze zbędnymi...? Wiele znanych i cenionych autorytetów z dziedziny programowania w języku C++ (między innymi Scott Meyers) wskazuje na to, że standardowe kontenery asocjacyjne pozostawiają wiele do życzenia pod względem wydajności oraz fragmentacji pamięci. Są za to bardzo uniwersalne. Jednakże nie zawsze tej uniwersalności potrzebujemy. A jeśli już decydujemy się pisać aplikację w stosunkowo trudnym języku niskopoziomowym, to zapewne zależy nam na wydajności. Cóż więc zrobić?
Google SparseHash przybywa na ratunek!
Odpowiedzi na zadane wyżej pytanie jest klika. Można oczywiście napisać własny, wyspecjalizowany kontener. Jednakże zanim zdecydujesz się na ten desperacki krok, pamiętaj że pisanie bezbłędnych, przenośnych i wydajnych kontenerów STL jest zadaniem niebanalnym. Inna alternatywa to wykorzystanie gotowej biblioteki napisanej przez kogoś innego (najlepiej eksperta z danej dziedziny). Ja chciałbym zainteresować Czytelników niniejszego tekstu do zwrócenia uwagi na Google SparseHash. Dlaczego akurat ta biblioteka? Po pierwsze, oferuje ona zestaw kontenerów haszujących o różnych charakterystykach wydajnościowych. Po drugie, komponenty dostarczone w ramach SparseHash spełniają wymagania nakładane na kontenery w ramach specyfikacji STL, dzięki czemu możemy swobodnie wykorzystywać razem tą biblioteką tudzież z innymi bibliotekami zgodnymi z STL (np. Boost). Google SparseHash w wersji 1.7 (najnowsza, stabilna wersja biblioteki dostępna w momencie pisania niniejszego artylułu) oferuje nam pięć kontenerów: • • • • •
Jak łatwo się domyśleć, kontenery typu * _ hash _ map oferują funkcjonalność zbliżoną do standardowych kontenerów std::map z biblioteki STL. Z kolei kolekcje typu * _ hash _ set to klasy bliźniacze dla STLowego zbioru (std::set). Przedrostki w nazwach klas określają ich charakterystyki wydajnościowe. Kontenery typu sparse (w języku polskim: rzadkie) zostały zaprojektowane w taki sposób aby uczynić je bardzo wydajnymi pod kątem zużycia pamięci. Z kolei komponenty typu dense (w języku polskim: gęste) są stworzone z myślą o wysokiej wydajności w kontekście czasu trwania ich operacji. Ostatni z wymienionych kontenerów (sparsetable), to tablica, która gwarantuje bezpośredni dostęp (w nomenklaturze nazewniczej biblioteki STL: random access) do elementów, a także operacje wstawiania i usuwania elementów, a wszystko to w stałym czasie! W kolejnych podpunktach niniejszego artykułu omówimy po kolei wyżej wymienione komponenty.
Pierwsze spojrzenie
sparse _ hash _ set ,
Zanim przejdziemy do omówienia szczegółów związanych z biblioteką Google SparseHash, na rozgrzewkę rozważymy prosty przykład jej wykorzystania. Konfiguracja Google SparseHash jest bardzo prosta (patrz: ramka Szybki Start). Spójrzmy na Listing 1, gdzie przedstawiony jest prosty przypadek użycia biblioteki (uwaga: wspomniany przykład należy skompilować przy pomocy narzędzia Microsoft Visual C++; więcej informacji na temat tego ograniczenia znajdziesz w dalszej części artykułu). Czytelnikom, którzy znają podstawy biblioteki STL przedstawiony wyżej przykład powinien wydać się bardzo swojski. W zasadzie, poza definicją typu kontenera, cała reszta kodu wygląda tak, jak byśmy używali standardowego kontenera std::map. Proponuję drogi Czytelniku, abyś spróbował skompilować ten przykład zanim będziesz kontynuował dalsze czytanie... Udało się? Jeśli tak, to jedziemy dalej!
dense _ hash _ set ,
SparseHash: kontenery asocjacyjne
sparse _ hash _ map, dense _ hash _ map, sparsetable .
W nomenklaturze biblioteki STL kontener asocja-
Szybki start
Google SparseHash jest w całości zaimplementowana w plikach nagłówkowych i nie wymagane jest jej budowanie przed użyciem, tudzież dołączanie do projektu, w którym chcielibyśmy jej użyć, dodatkowych bibliotek w postaci binarnej. Jedyne co należy zrobić to poinstruować kompilator gdzie ma szukać nagłówków. W tym celu należy wskazać kompilatorowi katalog SPARSEHASH_HOME/src (zakładamy, że SPARSEHASH_HOME to ścieżka wskazująca na domowy katalog biblioteki). W przypadku gdy pracujemy pod systemem operacyjnym z rodziny Microsoft Windows, dodatkowo musimy wskazać podkatalog SPARSEHASH_HOME/src/windows. Po wykonaniu wspomnianych kroków biblioteka jest gotowa do użycia. Zanim jednak zaczniemy pracę, warto jeszcze poświęcić kilka chwil i uruchomić zestaw testów dołączonych do biblioteki. Instrukcje opisujące jak to zrealizować te zadanie znajdują się w plikach SPARSEHASH_HOME/README oraz SPARSEHASH_HOME/README.windows. Po tym jak wszystkie testy zostaną pomyślnie ukończone możemy z czystym sumieniem zabrać się do pracy!
www.sdjournal.org
7
BIBLIOTEKA MIESIĄCA
cyjny to kolekcja, która przechowuje pary powiązanych ze sobą elementów: kluczy (ang. keys) i wartości (ang. values). Głównym celem tworzenia i używania kontenerów asocjacyjnych jest możliwość szybkiego wyszukiwania wartości na podstawie zadanych kluczy. Podręcznikowy przykład wykorzystania kontenera asocjacyjnego to budowanie histogramu występowania wyrazów w zadanym tekście. Zadanie to można rozwiązać bardzo prosto korzystając ze standardowego kontenera std::map z biblioteki STL. Rozwiązanie takie pokazano na Listingu 2. Kontener asocjacyjny std::map, jak wszystkie kolekcje rodem z biblioteki STL, zaimplementowany Listing 1. Prosty przypadek użycia biblioteki SparseHash #include <cstring>
#include <iostream> #include <hash_map> #include <google/sparse_hash_map> using namespace google;
Listing 2. Zliczanie wystąpień wyrazów w tekście przy pomocy kontenera std::map
using namespace std;
using namespace stdext; // for hash_compare int main()
{
jest w postaci szablonu klasy. Szablon ten przyjmuje dwa parametry: typ klucza oraz typ wartości (tak naprawdę wspomnianych parametrów jest nieco więcej, ale część z nich jest domyślna i nie będziemy sobie teraz zawracali nimi głowy). W naszym przykładzie kluczami są napisy (stąd typ std::string jako pierwszy argument szablonu), zaś wartościami – liczby całkowite (int), które pełnią rolę liczników wystąpień poszczególnych słów. W przypadku gdybyśmy chcieli zbudować histogram występowania wyrazów dla większego tekstu, kluczową staje się kwestia wydajności operacji wyszukiwania wartości na podstawie klucza. W praktyce kontenery asocjacyjne powinny gwarantować, że taka operacja ma maksymalną złożoność algorytmiczną klasy O(log n) (to znaczy, że przy występowaniu n elementów w kontenerze, czas potrzebny do wyszukania zadanego elementu będzie proporcjonalny do wartości log n). Z tego względu, kontenery asocjacyjne opierają się zazwyczaj na wyspecjalizowanych strukturach danych, które taką właśnie zło-
#include <iostream> #include <map>
sparse_hash_map<const char*, int, hash_compare<const char*> > months;
#include <string> #include <boost/foreach.hpp>
months["january"]
= 31;
#define foreach BOOST_FOREACH
months["march"]
= 31;
using namespace std;
= 31;
int main()
months["february"] months["april"]
= 28; = 30;
months["may"]
months["june"]
= 30;
months["july"]
= 31;
months["august"]
{
months["september"] = 30; months["october"]
= 31;
months["december"]
= 31;
months["november"]
cout << "may
while (cin >> word)
= 30;
{ }
-> " << months["may"] << endl;
cout << "june
endl;
entry;
-> " << months["april"] << endl;
cout << "november
foreach (const histogram_entry& entry, histogram)
-> " << months["june"] << endl; -> " << months["november"] <<
{
endl;
}
8
return 0;
histogram[word]++;
typedef map<string, int>::value_type histogram_
cout << "september -> " << months["september"] << cout << "april
string word;
map<string, int> histogram;
= 31;
}
}
cout << entry.first << " -> " << entry.second << endl;
8/2010
Tablice haszujące w języku C++
żoność operacji wyszukiwania gwarantują. Kolekcja std::map jest zazwyczaj implementowana jako zrównoważone drzewo wyszukiwania. Jak to w życiu bywa: kontener kontenerowi nierówny. Zrównoważone drzewo wyszukiwania jest bardzo uniwersalną strukturą danych. W praktyce oznacza to, że możemy taką strukturę danych w locie modyfikować (np. dodawać lub usuwać z niej elementy), wiedząc że operacje wyszukiwania nadal będą działać z rozsądną szybkością. Jednakże, czasami występują sytuacje, kiedy z góry znamy scenariusz przetwarzania danych i wiemy, że taka uniwersalność nie będzie nam potrzebna. Rozważmy takie zadanie: musimy zliczyć liczbę wystąpień z góry określonego, niezmiennego zbioru wyrazów w zadanym tekście. Na przykład możemy liczyć wystąpienia słów kluczowych języka C++. W tej sytuacji możemy na samym początku umieścić zbiór interesujących nas słów w kontenerze asocjacyjnym, zainicjować odpowiadające im licznik wartością zero i rozpocząć przeszukiwanie tekstu. Jeśli w tym przypadku skorzystamy z szablonu klasy std::map, to będziemy płacić za jej uniwersalność spadkiem wydajności. Mimo wszystko spójrzmy jak takie zadanie można zrealizować przy pomocy kontenera std::map (Listing 3). W przedstawionym przykładzie na początku wczytuję wszystkie słowa kluczowe języka C++ z pliku keywords.txt (żeby zaoszczędzić sobie pisania skopiowałem je ze strony http://cs.smu.ca/~porter/csc/ ref/cpp_keywords.html) i umieszczam je w kontenerze std::map. Następnie wczytuję po kolei słowa z pliku test.txt (w ramach testu umieściłem w tym pliku zawartość Listingu 3). Warto jednak zauważyć, że nie używam już konstrukcji histogram[word]++. W zamian za to przy pomocy metody std::map: :find() wyszukuję zadany wyraz. Z konstrukcją histogram[word]++ problem jest taki, iż odgrywa ona rolę lukru syntaktycznego, zaś ten jak, jak mawiał Alan Perlis, powoduje raka średnika... W naszym przypadku użycie tej konstrukcji powodowałoby niepotrzebne wstawianie nowych elementów do kontenera (taki jest efekt uboczny korzystania z niej). Efekt działania programu jest następujący (zakładając, że umieścisz w plikach wejściowych to samo co ja):
Listing 3. Zliczanie wystąpień słów kluczowych języka C++ przy pomocy kontenera std::map #include <cassert> #include <fstream>
#include <iostream> #include <map>
#include <string>
#include <boost/foreach.hpp> #define foreach BOOST_FOREACH using namespace std; int main()
{
assert(!!fis);
string keyword;
map<string, int > histogram; while (fis >> keyword)
{ }
www.sdjournal.org
histogram[keyword] = 0;
fstream fis2("test.txt"); assert(!!fis2); string word;
while (fis2 >> word)
{
map<string, int >::iterator found = histogram.find(word);
if (found != histogram.end())
{
}
}
found->second++;
typedef map<string, int >::value_type histogram_
entry;
foreach (const histogram_entry& entry, histogram) {
if -> 2 int -> 3 namespace -> 1 typedef -> 1 using -> 1 while -> 2. Zauważ, że na Listingu 3 umieściłem spacje po słowie kluczowym int w definicjach typu map<string, int >.
fstream fis("keywords.txt");
if (entry.second > 0)
{
}
}
}
cout << entry.first << " -> " << entry.second << endl;
9
BIBLIOTEKA MIESIĄCA
Zrobiłem to dlatego, że strumień rozdziela wyrazy tylko na podstawie białych znaków. W tym przypadku aż się prosi, aby skorzystać z nieco bardziej efektywnego kontenera. Wprowadźmy więc drobne modyfikacje w naszym przykładowym programie z Listingu 3 (patrz: Listing 4). Oprócz kliku zmian związanych z dołożeniem odpowiednich nagłówków i deklaracji użycia przestrzeni nazw (o czym bardziej szczegółowo za moment), w samym kodzie zmieniło się bardzo niewiele. Podmieniłem w zasadzie tylko typ kontenera asocjacyjnego. Dodałem przy okazji odpowiednią definicję typu: typedef sparse_hash_map<string, int, hash_ histogram_type;
compare<string> >
tak aby na przyszłość łatwiej było wprowadzać takie modyfikacje. Chwilka niepewności w trakcie kompilacji programu i... udało się! Magia STL'a zadziałała! Po uruchomieniu pogram uraczył mnie następującymi danymi na konsoli: if -> 2 using -> 1 while -> 2 typedef -> 1 namespace -> 1 int -> 3. Jeśli spojrzymy na wartości liczników poszczególnych dla słów kluczowych to zauważymy, iż uzyskaliśmy dokładnie taki sam wynik jak w przypadku działania programu z Listingu 3. Tam jednak poszczególne pary
Listing 4. Zliczanie wystąpień słów kluczowych języka C++ przy pomocy kontenera google::sparse_hash_map #include <cassert>
}
#include <iostream>
fstream fis2("test.txt");
#include <hash_map> #include <map>
assert(!!fis2);
#include <string>
string word;
#include <boost/foreach.hpp>
while (fis2 >> word)
#include <google/sparse_hash_map>
{
#define foreach BOOST_FOREACH
if (found != histogram.end())
using namespace std;
{
using namespace stdext; // for hash_compare int main()
{
histogram_type::iterator found = histogram.find(word);
using namespace google;
}
}
found->second++;
fstream fis("keywords.txt");
typedef histogram_type::value_type histogram_
string keyword;
foreach (const histogram_entry& entry,
typedef sparse_hash_map<string, int, hash_
{
assert(!!fis);
entry;
compare<string> >
histogram_type;
while (fis >> keyword)
{
histogram)
if (entry.second > 0)
{
histogram_type histogram;
10
histogram[keyword] = 0;
#include <fstream>
}
}
}
cout << entry.first << " -> " << entry.second << endl;
8/2010
Tablice haszujące w języku C++
klucz/wartość były... posortowane! Wynika to z właściwości struktury danych na której opera się kontener std::map (dzięki przechowywaniu elementów w takim porządku jest on w stanie zapewnić szybki dostęp do nich). Z kolei google::sparse _ hash _ map używa nieco odmiennej strategii (o żadnym sortowaniu nie ma mowy!). Jeśli interesujesz się jaka jest zasada działania kontenerów haszujących, zajrzyj do ramki Tablice haszujące: co tam jest pod maską? Wiemy już, że bardzo łatwo jest podmienić szablon std::map na google::sparse_hash_map. Jednakże czy w tym miejscu historia się kończy? Zdecydowanie nie! Rozważmy więc z jakimi różnicami mamy do czynienia. Po pierwsze – nagłówki. Czy zwróciłeś drogi czytelniku uwagę, że na Listingach 1 i 4 umieściłem nagłówek <hash_map>? Wygląda on jak nagłówek standardowy, jednakże pamięć Ciebie nie myli – takiego pliku nagłówkowego standard C++98 nie przewiduje... O co więc chodzi? Sprawa jest prosta: nagłówek <hash_map> zawiera niestandardową implementację zgodnej z STL'em tablicy haszującej (stworzona przez firmę Dinkumware, która dostarcza implementację STL dla pakietu Microsoft Visual C++). Kontener ten (jak i inne niestandardowe rozszerzenia dostarczone przez Dinkumware) umieszczone są w przestrzeni nazw stdext. Autor biblioteki Google SparseHash postanowił zaimplementować swoją bibliotekę tak, aby była ona jak najbardziej kompatybilna z istniejącymi na rynku rozwiązaniami. Dlatego też google::sparse_hash_map wykorzystuje tę samą infrastrukturę haszowania, co natywne rozwiązania dostarczone dla programistów w ramach ich pakietów narzędziowych. Konkretnie mowa tutaj o szablonie hash_compare, który definiuje politykę tworzenia i porównywania skrótów. Co ciekawe, na platformach wywodzących się z rodziny Unix (tj. na których dostępny jest kompilator GCC) przedstawione tutaj przykłady by się nie skompilowały. Dlaczego? Otóż, GCC używa innej implementacji niestandardowego kontenera haszujacego, wywodzącej się z oryginalnej wersji biblioteki STL rodem z firmy SGI. Na Listingu 5 przedstawiłem zmodyfikowany przykład z Listingu 1, współpracujący z tą implementacją. Widać tutaj dobitnie, że implementacja kompatybilna z SGI ma zupełnie inne parametry szablonowe. W rzeczywistości, różnice pomiędzy tymi implementacjami idą o wiele dalej (ale to w sumie dobry temat na oddzielny artykuł traktujących o szczegółach implementacji kontenerów haszujących). Jak to!? – zapytają zapewne niektórzy Czytelnicy. A co z przenośnością kodu? Cóż – na tym etapie standaryzacji (a w zasadzie jej braku) chyba pozostaje się z tą niedogodnością pogodzić. Najprawdopdobniej nowy standard języka C++ coś poprawi
www.sdjournal.org
w tym zakresie, ale na niego musimy jeszcze trochę poczekać. Jeśli chodzi o funkcjonalność google::sparse_hash_ map, to spełnia on wymagania określone w ramach konceptu Pair Associative Container, stanowiącego część dokumentacji biblioteki STL. Te same wymagania spełnia kontener std::map, więc na poziomie inListing 5. Prosty przypadek użycia biblioteki SparseHash, zmody�kowany pod kątem kompatybilności z kompilatorem GCC #include <iostream>
#include <google/sparse_hash_map> using namespace google; using namespace std; using namespace ext; struct eqstr
{
bool operator()(const char* s1, const char* s2) const
{
};
return (s1 == s2) || (s1 && s2 && strcmp(s1, s2)
}
== 0);
int main()
{
sparse_hash_map<const char*, int, hash<const char*>, eqstr> months;
months["january"] = 31;
months["february"] = 28; months["march"] = 31; months["april"] = 30; months["may"] = 31;
months["june"] = 30; months["july"] = 31;
months["august"] = 31;
months["september"] = 30; months["october"] = 31;
months["november"] = 30; months["december"] = 31; cout << "september -> " << months["september"] << cout << "april cout << "june
cout << "november }
endl;
-> " << months["april"] <<
endl;
-> " << months["june"] << endl; -> " << months["november"] <<
endl;
11
BIBLIOTEKA MIESIĄCA
terfejsów komponenty te (przynajmniej w teorii) można bez problemu wymieniać. Oczywiście, nadal pozostają pewne drobne kwestie wynikające ze szczegółów implementacyjnych. Szczególną uwagę należy zwrócić na następujące kwestie: •
•
•
w przypadku gdy chcemy korzystać z metody erase() należy zaraz po konstrukcji obiektu google::sparse _ hash _ map wywołać metodę set _ deleted _ key(), w sytuacji kiedy element kontenera jest usuwany, przydzielona mu pamięć nie jest z miejsca zwalniana (to podejście pozwala iterować po kontenerze i wywoływać metodę erase() nie powodując unieważnienia wskazujących na niego iteratorów), ustawiając minimalny poziom obciążenia (metoda min _ load _ factor()) na na wartość 0.0 można uzyskać gwarancję, że google::sparse _ hash _ map nigdy nie zmniejszy swojej objętości.
Wszystkie przedstawione wyżej rozważania tyczą się zarówno kontenera google::sparse _ hash _ map jak i google::dense _ hash _ map. Różnice pomiędzy tymi kontenerami wiążą się jedynie z ich charakterystykami wydajnościowymi. Na jakich więc zasadach dokonywać wyboru pomiędzy jednym a drugim? Oto garść porad: •
•
•
12
Jeśli zależy Ci na minimalizacji zużycia pamięci, wybierz google::sparse _ hash _ map, . Powinieneś się jednak liczyć z faktem, iż wykonywane na niej operacji mogą być od trzech do siedmiu razy wolniejsze w porównaniu do jej bliźniaczej implementacji; szczególną uwagę należy zwrócić na operacje wstawiania – tutaj spadek wydajności jest największy. Za to narzut związany ze zużyciem pamięci w przypadku google::sparse _ hash _ map wynosi tylko cztery bity per przechowywany element. Jeśli zależy Ci na ekstremalnej wydajności, kontener google::dense _ hash _ map jest zapewne tym czego szukasz. Używając go pamiętaj, że wprowadza on około 78-procentowy narzut związany z wykorzystaniem pamięci (jeśli elementy przechowywane w kontenerze zajmują n bajtów, to narzut wprowadzony przez google::dense _ hash _ map wynosi n*0.78). W świetle powyższych faktów można stwierdzić, że ten wariant kontenera jest idealny w przypadku gdy przechowujemy w nimi niewiele danych i wykonujemy na nim bardzo dużo operacji wyszukiwania (byłby dobrym kandydatem do zastąpienia google::sparse _ hash _ map w przykładzie z Listingu 4). Pamiętaj, że zarówno google::sparse _ hash _ map jak i google::sparse _ hash _ map to kontenery wy-
specjalizowane (można by powiedzieć: rozwiązania ekstremalne dedykowane do specjalnych zastosowań). Jako bardziej uniwersalną alternatywę możesz rozważyć niestandardowe kontenery haszujące dołączone jako rozszerzenia w większości nowoczesnych kompilatorów. Implementacje tych kontenerów są zazwyczaj bardziej zrówListing 6. Prosty przypadek użycia kontenera google:: dense _ hash _ set
#include <hash_set> #include <iostream> #include <google/dense_hash_set> using namespace google; using namespace std;
using namespace stdext; // for hash_compare typedef dense_hash_set<const char*, hash_compare<const char*> >
DenseHashSetCPtr;
void lookup(const DenseHashSetCPtr& set, const char*
{
element)
cout << element << " "
<< ((set.find(element) != set.end()) ? "found"
: "not found")
}
<< endl;
int main()
{
DenseHashSetCPtr fruits; fruits.set_empty_key(""); fruits.insert("kiwi"); fruits.insert("plum");
fruits.insert("apple"); fruits.insert("mango");
fruits.insert("apricot"); fruits.insert("banana"); lookup(fruits, "mango"); lookup(fruits, "apple");
lookup(fruits, "durian");
}
return 0;
8/2010
Tablice haszujące w języku C++
Listing 6. Przykład użycia kontenera z Google SparseHash z wybranymi komponentami bibliotek STL i Boost #include <algorithm> #include <hash_map> #include <iostream> #include <string>
#include <google/sparse_hash_map> #include <boost/shared_ptr.hpp> using namespace boost;
using namespace google; using namespace std;
using namespace stdext; // for hash_compare struct Employee
{
Employee(string firstName, string lastName, int age) :
m_firstName(firstName), m_lastName(lastName), {
m_age(age)
}
string m_firstName; string m_lastName; };
int m_age;
typedef int Id;
typedef shared_ptr<Employee> EmployeePtr;
typedef sparse_hash_map<Id, EmployeePtr, hash_compare<Id> >
EmployeeMap;
std::ostream& operator<<(std::ostream& os, EmployeeMap::value_type emplyeeEntry) {
return os << '['
<< emplyeeEntry.second->m_firstName << ' '
<< emplyeeEntry.second->m_lastName
<< ", age=" << emplyeeEntry.second->m_age << ']';
} int main()
{
EmployeeMap employees;
employees[100] = EmployeePtr(new Employee("Jan", "Kowalski", 30)); employees[101] = EmployeePtr(new Employee("Adam", "Nowak", 31));
employees[102] = EmployeePtr(new Employee("Stefan", "Kowalski", 29)); copy(employees.begin(), employees.end(),
std::ostream_iterator<EmployeeMap::value_type>(cout, "\n"));
}
return 0;
www.sdjournal.org
13
BIBLIOTEKA MIESIĄCA
noważone zarówno pod kątem zużycia pamięci i efektywności.
SparseHash: zbiory
Zbiór w nomenklaturze biblioteki STL to kontener zawierający zestaw unikalnych elementów i oferujący operacje pozwalające w szybki sposób przekonać się czy dany element występuje w określonym zbiorze, czy nie. Podobnie jak std::map, standardowy kontener zbiór z biblioteki STL (std::set) zaimplementowany jest jako zrównoważone drzewo. Zalety i wady takiego rozwiązania są podobne jak w przypadku std::map (opisanych w poprzednim punkcie niniejszego artykułu). Biblioteka Google SparseHash oferuje dwa zamienniki dla kontenera std::set: google::sparse_hash_set oraz google::dense_hash_set. Na Listingu 6 pokazany jest prosty przykład użycia google::dense_hash_ set (kompatybilny z kompilatorami z rodziny Microsoft Visual C++). To na co warto zwrócić uwagę, to konieczność wywołania metody set_empty_key() na zbiorze, zaraz po jego konstrukcji. Zaniechanie tej czynności spowoduje wystąpienie asercji przy próbie wywołania metody insert(). Generalnie cała dyskusja odnośnie wydajności, narzutów i wskazówek kiedy używać (bądź nie używać) prezentowanych kontenerów, przedstawiona w poprzednim punkcie tego artykułu (w kontekście ko-
lekcji asocjacyjnych), pozostaje w mocy w odniesieniu do zbiorów dostępnych w ramach Google SparseHash. W związku z tym nie będę jej powtarzał w tym miejscu aby uniknąć redundancji.
Dodatkowe smaczki
Kontenery STL mają to do siebie, że posiadają jak to się potocznie mówi tzw. podwójne dno. Innymi słowy, zaprojektowano je w ten sposób aby były jak najprostsze w użyciu dla przeciętnego (mało wymagającego użytkownika). Z drugiej strony, użytkownicy bardziej zaawansowani (lub Ci, którzy z jakichś przyczyn muszą dopasować kontenery do swoich potrzeb) mają możliwość ich dostrojenia. Kluczem jest w tym przypadku zrozumienie znaczenia wszystkich parametrów, które przyjmują szablony klas implementujące kontenery (część z nich ma wartości domyślne, aby niepotrzebnie nie zaprzątać głowy przeciętnych użytkowników biblioteki). Kontenery haszujące można dostrajać na dwa sposoby: modyfikując funkcje odpowiedzialne za tworzenie skrótów (tzw. funkcje haszujące) oraz podłączając swój własny alokator pamięci. W pierwszym przypadku mowa jest o trzecim parametrze występujących w szablonach klasy z biblioteki Google SparseHash. Parametr ten jest zależny od tego z jakim kompilatorem pracujemy (w przypadku MSVC wymagany jest obiekt funkcyjny zgodny z interfejsem hash_comparator zaś w przypadku kompila-
Tablice haszujące: co tam jest pod maską?
W informatyce tablica haszująca (zwana również tablicą mieszającą) to jedna z odmian tablicy asocjacyjnej, tj. struktury danych służącej do przechowywania informacji, w taki sposób aby możliwy był do nich szybki dostęp. Odwołania do przechowywanych elementów dokonywane są na podstawie klucza, który identyfikuje elementy umieszczone w tablicy. Kluczem może być na przykład PESEL pracownika, a wyszukiwaną informacją jego dane adresowe. Strategie implementacji kontenerów asocjacyjnych są różne. W przypadku stosowania tablicy haszującej stosuje się tzw. funkcję haszujacą (zwaną też funkcją mieszającą), która dla danego klucza wyznacza indeks w tablicy. Innymi słowy przekształca ona klucz w liczbę z zadanego zakresu. Funkcje mieszające są zwykle nieskomplikowane, tak aby czas potrzebny na ich wykonywanie nie stanowił zbyt dużego narzutu. W najprostszym przypadku wartość funkcji mieszającej, obliczona dla danego klucza, wyznacza dokładnie indeks poszukiwanego elementu. Jeżeli miejsce wskazywane przez obliczony indeks jest puste, to poszukiwanej informacji nie ma w tablicy. W ten sposób wyszukiwanie elementu ma złożoność czasową O(1). Oznacza to, że dostęp do elementów umieszczonych w takiej tablicy można (w optymistycznym przypadku) uzyskać w czasie stałym! W przypadku tablic mieszajacych pojawia się jednak dość poważny problem w postaci tzw. kolizji. Problem ten manifestuje się w sytuacji gdy funkcję mieszająca wygeneruje te same wartości dla dwóch różny kluczy. Problem ten można obejść na kilka sposobów, zazwyczaj kosztem wydajności operacji wykonywanych na tablicy.
W Sieci • • • •
14
http://code.google.com/p/google-sparsehash/ – strona domowa biblioteki Google SparseHash. http://google-sparsehash.googlecode.com/svn/trunk/doc/index.html – dokumentacja techniczna biblioteki Google SparseHash. http://google-sparsehash.googlecode.com/svn/trunk/doc/implementation.html – opis implementacji kontenerów wchodzących w skład biblioteki Google SparseHash. http://google-sparsehash.googlecode.com/svn/trunk/doc/performance.html – analiza wydajności kontenerów wchodzących w skład biblioteki Google SparseHash.
8/2010
Tablice haszujące w języku C++
torów używających wersji SDL zgodnych z SGI, należy przekazać obiekt funkcyjny zgodny z interfejsem hash oraz, dodatkowo, obiekt funkcyjny odpowiedzialny za porównywanie elementów). Różnice te wynikają między innym z tego, że w implementacji kontenerów haszujących autorstwa firmy Dinkumware obiekty porównywane są na zasadzie równoważności (ang. equivalence), czyli za pomocą operatora <, zaś w implementacji rodem z SGI porównuje się obiekty na zasadzie równości (ang. equality). Wyjaśnienie różnicy pomiędzy tymi pojęciami wykracza poza tematykę niniejszego artykułu. Więcej informacji na ten temat możesz szukać w dokumentacji dostarczonej do Twojego kompilatora. Od wersji 1.7 Google SparseHash pozwala podłączać do kontenerów alokatory pamięci definiowane przez użytkowników (we wcześniejszych wersjach biblioteki szablonowy parametr określający alokator był po prostu ignorowany). Domyślny alokator dołączony razem z biblioteką opera się na funkcjach malloc() i free() ze standardowej biblioteki języka C. Jeszcze jednym kluczem do optymalnego wykorzystania biblioteki jest dokładne zrozumienie zasad jej działania. Na szczęście, firma Google jest w tym zakresie bardzo liberalna i pozwala swoim pracownikom publikować informacje na temat szczegółów implementacji tworzonych przez nich rozwiązań. Link do strony WWW na której opisane są detale implementacji Google SparseHash znajduje się w ramce W sieci.
Miła niespodzianka
Autor biblioteki Google SparseHash przygotował do jej użytkowników miłą niespodziankę w postaci wbudowanych funkcji pozwalających serializować i deserializować oferowane przez nią kontenery na dysk. Serializacja/deserializacja odbywa się w dwóch etapach: najpierw zapisywane są metadane (metoda: write_metadata()) a następnie, dane właściwe (metoda: write_nopointer_data()). Takie podejście działa co prawda tylko z danymi prostymi (typy podstawowe języka C, oraz struktury złożone z tych typów). W przypadku gdy w mamy do czynienia ze wskaźnikami czy obiektami języka C++, pracy jest nieco więcej, ale zadanie nadal jest wykonalne (szczegółowe
Licencja
Biblioteka Google Protocol Buffers jest udostępniana na nowej licencji BSD (ang. New BSD License). Licencja ta przewiduje możliwość wykorzystywania biblioteki bez uiszczania żadnych opłat, tak w otwartych jak i w komercyjnych projektach. Szczegółowe informacje odnośnie tej licencji znajdziesz tutaj: http://www.opensource.org/licenses/bsd-license.php.
www.sdjournal.org
informacje jak to zrobić można znaleźć w dokumentacji biblioteki). W sumie – mała rzecz, a cieszy...
Współpraca między narodami...
...to niewątpliwie kwestia bardzo ważna. Nas jednak bardziej interesuje współpraca pomiędzy bibliotekami. W tym zakresie Google SparseHash sprawuje się znakomicie. Prawdę powiedziawszy – w tym leży spora część jej siły. Zarówno komponenty z STL jak i z Boost nie są jej straszne. W poprzednich przykładach widać było gołym okiem, że omawiane kontenery bezproblemowo współpracują z makrem BOOST_FOREACH. Na Listingu 7 pokazałem jeszcze jeden przykład, który udowadnia iż Google SparseHash potrafi współpracować z inteligentnymi wskaźnikami z Boost oraz z algorytmami z STL.
Podsumowanie
Tak oto dobrnęliśmy do końca artykułu omawiającego bibliotekę Google SparseHash, oferującą zoptymalizowane pod względem zużycia pamięci i czasu implementacje kontenerów haszujących, zgodnych ze standardami STL. Marka Google stojąca za biblioteką mówi sama za siebie. Biblioteka SparseHash, intensywnie wykorzystywana w infrastrukturze Giganta z Mountain View, to świetnie spełniający swoje zadanie kawałek kodu i mogę z czystym sumieniem polecić go każdemu programiście języka C++, który szuka wydajnych kontenerów haszujących. Mam nadzieję, że ten artykuł zachęci Ciebie drogi czytelniku do poczynienia eksperymentów z nowymi typami kolekcji. Czas po temu najwyższy – nowy standard języka C++ nadchodzi, a wraz z nimi – nowe możliwości. Jednakże nawet gdy nowe kontenery asocjacyjne (unordered_map i unordered_set) zagoszczą już pod nasze strzechy, wyspecjalizowane kolekcje z biblioteki Google SparseHash najprawdopodobniej nadal będą znajdować niejedno zastosowanie.
RAFAŁ KOCISZ Pracuje na stanowisku Kierownika ds. Badań i Rozwoju w �rmie Gamelion, wchodzącej w skład Grupy BLStream. Rafał specjalizuje się w technologiach związanych z produkcją oprogramowania na platformy mobilne, ze szczególnym naciskiem na tworzenie gier. Grupa BLStream powstała by efektywniej wykorzystywać potencjał dwóch, szybko rozwijających się producentów oprogramowania – BLStream i Gamelion. Firmy wchodzące w skład grupy specjalizują się w wytwarzaniu oprogramowania dla klientów korporacyjnych, w rozwiązaniach mobilnych oraz produkcji i testowaniu gier. Kontakt z autorem: rafal.kocisz@game-lion.com
15
KLUB TECHNICZNY
Technologie Progress OpenEdge. Część 9 OpenEdge SQL stworzony przez Progress Software Corporation jest implementacją powszechnie znanych standardów, włączając SQL-92, SQL-99 i SQL-2003. Jest częścią otwartego, elastycznego interfejsu i pełni niezwykle ważną rolę w procesie rozwoju nowoczesnych aplikacji biznesowych OpenEdge. Dowiesz się:
Powinieneś wiedzieć:
• O architekturze OpenEdge SQL; • O podstawach implementacji języka OpenEdge SQL.
• Ogólne zagadnienia związane z systemami relacyjnych baz danych.
•
•
•
16
Sinik SQL, który instaluje się jako integralna część systemu baz danych OpenEdge. Silnik ten wspiera architekturę SOA poprzez niezawodną obsługę typów danych, możliwość zmian schematu bazy online oraz optymalizację zapytań. Został on zaprojektowany do osiągania maksymalnej skalowalności i wydajności. Driver JDBC dla OpenEdge – autorstwa DataDirect Tetchnologies. Realizuje dostęp do systemu relacyjnych baz danych OpenEdge dla aplikacji opartych na technologii Java. Jest instalowany jako część produktu OpenEdge SQL Client Access. Driver ODBC dla OpenEdge – również autorstwa DataDirect Tetchnologies. Realizuje dostęp do systemu relacyjnych baz danych OpenEdge dla aplikacji obsługujących interfejs ODBC. Jest instalowany jako część produktu OpenEdge SQL Client Access.
Architektura
Interfejs SQL został zaimplementowany w OpenEdge do pracy w architekturze klient-serwer, składającej się z silnika SQL oraz aplikacji klienckiej, która łączy się z bazą poprzez dwa dostępne interfejsy: JDBC API, ODBC API. Rysunek 1 pokazuje przykładową architekturę klientserwer, w której klienci ABL i OpenEdge SQL przyłąĀȀ̀ЀԀȀऀࠀ܀
ĀȀ̀ЀȀ̀ԀĀࠀ܀
ఀԀഀᤀကĀ᠀ ကᄀഀԀ
ሀᄀጀఀ᐀ᔀ ကᘀఀԀ̀ഀᄀ
N
ieraz w cyklu poświęconym technologiom OpenEdge podkreślane były bardzo ważne cechy tworzonych aplikacji biznesowych: ich otwartość, elastyczność i dostępność. Zapoznamy się teraz ze standardowym, otwartym interfejsem SQL, który zapewnia szybki dostęp do relacyjnych baz OpenEdge. Bazy te wraz z interfejsem dają możliwość osiągnięcia wydajnej integracji z aplikacjami/narzędziami do raportowania, rozwoju czy przetwarzania transakcyjnego. OpenEdge SQL składa się następujących komponentów:
ఀԀഀᜀကĀ᠀
ကᄀഀԀ
ကᄀഀԀ
ఀԀഀༀĀऀ ĀȀ̀ЀȀ̀Ԁࠀऀ
ĀȀ̀ЀԀȀༀĀऀ
Rysunek 1. Architektura klient-serwer ABL i SQL
8/2010
Technologie Progress OpenEdge
czają się do serwera bazy danych. Procesy serwera SQL wykonują operacje bazodanowe poprzez pamięć dzieloną, która jest wspólna dla wszystkich procesów przyłączonych do bazy. Klienci SQL łączą się z bazą danych poprzez procesy serwera SQL. Zdalni klienci ABL łączą się do bazy za pośrednictwem procesów serwerów ABL. Dla bazy danych można wystartować jeden broker uruchamiający procesy serwerów obsługujących klientów ABL i SQL lub też dwa brokery dedykowane dla określonego typu klientów (ABL lub SQL).
ഀༀကᄀࠀሀࠀጀ ᐀܀ᔀᘀ
܀Ȁ̀ȀᔀЀༀĀကᘀЀȀༀᜀĀ᠀
JDBC
Java Database Connectivity (JDBC) jest interfejsem aplikacji Java (API) pozwalającym instrukcjom SQL wykonywać operację na bazie danych. JDBC API składa się z klas napisanych w Javie. Driver JDBC dla baz OpenEdge konwertuje wywołania JDBC API na wywołania specyficzne dla bazy danych. OpenEdge korzysta z drivera JDBC Typ 4. Na Rysunku 2 przedstawiono architekturę aplikacji Java z wykorzystaniem tego drivera. Aplikacja Java zawiera wywołania JDBC API, które muszą być wykonane przy pomocy metod DriverManag er.getConnection lub DataSource.getConnection. Metoda getConnection realizuje połączenie z odpowiednim driverem JDBC. Klasa DriverManager lub DataSource odpowiada za zarządzanie tym połączeniem.
܀Āࠀऀ᐀ᤀᨀ
ԀĀࠀ܀
ĀȀ̀Ѐ
ĀȀ̀ЀԀ ऀࠀ܀ఀ
ĀȀ̀Ѐ̀Ԁऀࠀ܀̀
Rysunek 3. Schemat przyłączenia klienta do bazy poprzez driver ODBC
ఀഀༀఀကᄀ̀ༀሀऀЀऀ̀ጀ ༀఀऀ᐀ऀᔀĀᘀᜀ̀ༀ᠀ᤀࠀ̀ᜀ᐀
Silnik OpenEdge SQL obsługuje specyfikację Java Transaction API (JTA) architektury J2EE, co umożliwia realizację rozproszonych transakcji SQL przez bazę OpenEdge. Baza pełni w tej architekturze rolę „menedżera zasobów”, który w celu koordynacji zatwierdzania i wycofywania rozproszonych transakcji opiera się na zewnętrznych menedżerach transakcji. Aby korzystać z drivera JDBC, trzeba najpierw zdefiniować zmienne środowiskowe, a w szczególności CLASSPATH, która musi być ustawiona na każdej maszynie klienckiej. Wskazuje ona na lokalizację klas drivera OpenEdge JDBC. Definicja może wyglądać następująco:
ᬀᔀ̀Ԁఀᜀ ᰀԀȀᴀԀȀ
ᔀ᐀Āऀ̀ༀᨀЀကЀ̀
ᰀ̀ᔀఀ̀Ḁ ԀఀഀༀԀ ᰀἀ
Rysunek 2. Schemat przyłączenie klienta Java do bazy poprzez driver JDBC
www.sdjournal.org
ऀ̀ЀఀЀഀༀကЀᄀကሀጀ᐀
ĀȀ̀ЀԀȀ܀Āࠀऀ ԀఀഀༀԀကᄀሀ
܀ጀЀጀ᐀ᔀ̀ᘀጀᜀ̀᠀ఀ
ఀऀЀ̀
ĀကᘀЀༀᜀĀ᠀
CLASSPATH=$DLC/java/openedge.jar: $CLASSPATH
ODBC
Driver ODBC (Open Database Connectivity) dla silnika OpenEdge SQL realizuje dostęp do systemu baz
17
KLUB TECHNICZNY
cje SQL na składnię zrozumiałą dla źródła danych i zwraca dane do aplikacji. • Źródło danych – system baz danych, system operacyjny na jakim pracuje oraz oprogramowanie sieciowe niezbędne do realizacji dostępu do źródła. Konfiguracja ODBC na kliencie Windows nie powinna nastręczać wielu trudności. Należy wybrać w Panelu Sterowania Narzędzia Administracyjne, a następnie Źródła Danych (ODBC). Przykładowa konfiguracja ODBC dla bazy danych OpenEdge została pokazana na Rysunku 4.
Język SQL
Niniejszy artykuł nie jest nauką języka SQL, ale warto wspomnieć o jego trzech podzbiorach.
Rysunek 4. De�niowanie źródła danych ODBC
danych OpenEdge dla klienta ODBC. Driver tłumaczy wywołania ODBC na wywołania, które źródło danych potrafi przetworzyć i zwrócić dane do aplikacji. W skład architektury ODBC (patrz Rysunek 3) wchodzą następujące komponenty: • •
•
Aplikacja ODBC – dowolny program wywołujący funkcje ODBC i uruchamiający na ich podstawie instrukcje SQL. Menedżer drivera ODBC – warstwa zarządzająca komunikacją pomiędzy aplikacją a driverem ODBC. Przejmuje wywołania funkcji z aplikacji i przed ich przekazaniem do odpowiedniego drivera wykonuje kontrolę błędów. Umożliwia przyłączanie źródła danych podczas runtime'u. Driver ODBC – warstwa oprogramowania (najczęściej DLL) przetwarzająca wywołania funkcji ODBC dla specyficznego źródła danych. Driver łączy się ze źródłem, tłumaczy standardowe instruk-
• SQL DCL (Data Control Language) odpowiada za część związaną z bezpieczeństwem bazy danych. Obejmuje instrukcje GRANT, REVOKE, COMMIT i ROLLBACK. Dwie pierwsze dotyczą uprawnień użytkownika do oglądania i modyfikacji informacji w bazie danych. Tworząc system zabezpieczeń, można posłużyć się podejściem zarówno OpenEdge SQL, jak i ABL. Między tymi językami istnieje dość znacząca różnica. OpenEdge SQL jest systemem zamkniętym, gdzie zawsze wymagana jest identyfikacja użytkownika do przeprowadzania operacji na bazie. Administrator (SQL DBA) przydziela ponadto poziom uprawnień. ABL jest systemem otwartym. Oznacza to, że użytkownik nie ma żadnych ograniczeń dostępu do danych w nowej bazie. Należy zdefiniować administratora bazy ABL i uprawnienia użytkowników, a także zakazać dostępu do bazy użytkownikom nie posiadającym id i hasła (tzw. Blank User). Można więc podsumować oba podejścia, że w SQL „wszystko, co nie jest dozwolone, jest zakazane”, a w ABL „wszystko, co nie jest zakazane, jest dozwolone”.
Rysunek 5. Przyłączenie się do narzędzia SQL Explorer
18
8/2010
Technologie Progress OpenEdge
Rysunek 6. SQL Editor
SQL DDL (Data Definition Language) dotyczy definiowania obiektów bazy danych i manipulacji nimi. Wyróżnia się tutaj komendy CREATE, ALTER i DROP do dodawania, modyfikacji i usuwania obiektów bazy lub całych baz danych.. SQL DML (Data Manipulation Language) związany jest z wykonywaniem operacji na samych danych i jest najistotniejszy z punktu widzenia logiki biznesowej. Najważniejsze instrukcje SQL DML to: SELECT, INSERT, UPDATE, DELETE.
Narzędzia SQL
Deweloper ma w środowisku OpenEdge dwa gotowe narzędzia do wykonywania instrukcji języka OpenEdge SQL: SQL Explorer w środowisku znakowym oraz SQL Editor w graficznym środowisku OpenEdge Architect. SQL Explorer jest narzędziem napisanym w języku Java. Umożliwia połączenie się z bazą OpenEdge i wykonywanie na niej komendy języka SQL. Abu uruchomić to narzędzie, trzeba mieć skonfigurowane środowisko OpenEdge. Wystarczy w tym celu (w dowolnym systemie operacyjnym) wywołać skrypt proenv, ustawiający odpowiednie zmienne środowiskowe. Następnie należy uruchomić polecenie, którego pełna składnia jest następująca:
sqlexp -db database-name -S port | service-name -H host password
-user userid -password
Niektóre parametry są opcjonalne. Przykładowa komenda może wyglądać dla lokalnej bazy jak na Rysunku 5. W graficznym środowisku OpenEdge Architect można posłużyć się narzędziem SQL Editor, będącym widokiem perspektywy Database Navigator (Rysunek 6). Zainteresowanych pełną implementacją języka OpenEdge SQL odsyłam do dokumentacji na stronach Progress Communities. W następnym odcinku opowiem o Web Serwisach.
PIOTR TUCHOLSKI Autor jest od 12 lat związany z technologiami Progress Software. Wieloletni konsultant i Kierownik Działu Szkoleń Progress Software sp. z o.o., specjalizujący się w technologii OpenEdge. Kontakt z autorem: piotr.tt@gmail.com
W sieci • • •
http://communities.progress.com/pcom/community/psdn/openedge - Progress Software Developers Network, część ukierunkowana na zagadnienia techniczne związane z OpenEdge®; http://web.progress.com – strona Progress Software Corporation; http://www.progress.com/pl - strona Progress Software sp z o.o.
www.sdjournal.org
19
PROGRAMOWANIE PYTHON
Kurs Pythona. Cz. II – Struktury danych, funkcje i moduły W odcinku wprowadzającym zainstalowaliśmy Pythona i trochę pobawiliśmy się różnymi jego cechami. Po nabraniu swobody w wykorzystaniu linii poleceń możemy zabrać się za bardziej metodyczny przegląd tego, co oferuje nam język spod znaku węża. Dowiesz się:
Powinieneś wiedzieć:
• Jak zainstalować i uruchomić interpreter Python 2.6 • Jak poruszać się po interaktywnej sesji w interpreterze
• Czym różni się kultura języka Python od Javy i innych statycznie typowanych języków • Jak używać wbudowanych w Pythona list, słowników, krotek i zbiorów
T
a część kursu zawiera bardzo wiele nowego materiału dla osób, które nie miały wcześniej styczności z językiem. Przedstawiony tu zakres jest omawiany w praktycznie każdej książce poświęconej językowi. W tym kursie jednak za punkt honoru przyjęliśmy sobie promowanie dobrych zwyczajów nad suchą prezentację faktów. Język to budulec, narzędzie służące do opisu zachowania systemu komputerowego. Od powstania pierwszego języka programowania trwa nieustanna walka o to, żeby produkowany kod źródłowy prowadził do oczekiwanego zachowania komputera. Mówimy, "żeby był poprawny", "bez błędów". Kompilowane języki programowania takie jak C czy Java w swojej kulturze kultywują sprawdzanie na programiście na każdym kroku, czy jest pewien swoich poczynań. Składnia wielu fundamentalnych wyrażeń jest celowo bardzo rozwlekła i nadmierna, żeby kontrolowała programistę przed pomyłkami, chwilowymi zaćmieniami umysłu. W szczególności Java jest językiem, w którego kulturze kultywuje się ścisłą kontrolę nad tym, co programista powinien móc zrobić, co powinien widzieć w danym kontekście, do czego powinien mieć w danym momencie dostęp. O ile takie zachowanie jest bardzo pomocne, w szczególności na początku, dla programistów o niewielkim doświadczeniu, po jakimś czasie zaczyna ciążyć zaawansowanym programistom jak kula u nogi.
20
Python idzie natomiast zupełnie pod prąd, traktując swoich programistów od samego początku jak ludzi inteligentnych.
Wszyscy tutaj jesteśmy dorośli
Po kilku latach intensywnego kodowania w Javie zacząłem mieć wrażenie, że zarówno sam język, jak i jego kompilator i maszyna wirtualna traktują mnie jak małe dziecko. Na każdym kroku musiałem wprost i jasno przedstawiać swoje zamiary i założenia, język nie dawał mi tutaj żadnej dowolności. Nie pozostawiając pola do domysłów, minimalizował liczbę usterek. Lub tak mu się wydawało. Kompilator krytykował wiele sprytnych manewrów, oceniając je za podejrzane lub potencjalnie niebezpieczne. W końcu sama maszyna wirtualna ukrywała w czasie uruchamiania wiele kwestii w obawie, że użyłbym ich w sposób nieodpowiedzialny. Python wygląda w porównaniu jak wejście w dorosłość. Nadal zachowania w oczywisty sposób szkodliwe lub nieprzemyślane powodują krytykę interpretera, jednak zostawia on bardzo wiele dowolności w sposobie wykorzystania języka przez programistę. Python zakłada, że wiesz, co robisz. Środowisko programistów Pythona od pierwszych dni istnienia miało świadomość swobody, jaką on daje, i wiążącej się z nią odpowiedzialności. Przez lata intensywnego wykorzystania języka środowisko to
8/2010
Kurs Pythona. Cz II – Struktury danych, funkcje i moduły
wykształciło pewną kulturę, która opisuje rekomendowane rozwiązania różnych typowych kwestii. Ta kultura daje wszystkim bazę do rozwijania oprogramowania, które w sposób domyślny zachowuje się w sposób, którego inni programiści w środowisku oczekują, a którego kod źródłowy jest łatwy do czytania i zrozumienia przez innych programistów. Jak to w dorosłym życiu, zachowania zgodnego z normami kulturalnymi oczekuje się również po początkujących, czyli także po Tobie. Duży nacisk w tym kursie, a w szczególności w tej części, poświęcimy na zapoznanie Cię z zasadami pythonowego savoir-vivre. Jak zobaczysz, zasady te nie powstały bez przyczyny, a w istocie stoją za nimi mocne argumenty. Zacznijmy od najbardziej znanej konwencji w kulturze Pythona...
kiedy elementy będą innych typów, program zwróci oczekiwany wynik, np. dla listy [1, 2.0, ["lista", "jako", "element"]]: Element: 1
Element: 2.0
Element: ['lista', 'jako', 'element']
True
Gdzie tkwi tajemnica? To proste! W Pythonie wszystkie typy danych są obiektami. Wartość łańcuchową (%s) dla dowolnego typu interpreter uzyskuje przez wywołanie specjalnej metody _ _ str _ _ () na danym obiekcie (więcej o specjalnych metodach w Ramce 1 obok). Autorzy Pythona zauważyli więc, że wystarczy wyposażyć możliwie dużo typów danych w taką specjalną metodkę i dla większości kodu źródłowego obiekty tych typów będą wyglądały zupełnie jak prawdziwe łańcuchy znaków. I nie ma znaczenia, że tak naprawdę to nie są łańcuchy. Nie jest to dla nas ważny szczegół, ważne, że działa. I to poprawnie! W środowisku zakwitła więc jedna z najważniejszych zasad kultury programowania w Pythonie: "jeżeli coś wygląda jak kaczka, chodzi jak kaczka i kwacze jak kaczka, to musi to być kaczka!". Oznacza to mniej więcej, że Twój kod powinien przyjmować dowolne typy danych, które spełniają jakiś kontrakt (np. posiadają określoną metodę lub atrybut). Nie ma sensu wieczne sprawdzanie, czy dana zmienna jest odpowiedniego typu. Zakładamy, że jest i że spełnia nasz kontrakt. Taki styl kodowania nazywany jest duck typing. Co on nam daje?
True
•
Jeżeli kwacze jak kaczka...
Jak wspomnieliśmy w pierwszej części naszego kursu, Python jest językiem silnie typowanym, ale o dynamicznym systemie typów. Oznacza to w praktyce, że zmienne (w tym argumenty funkcji) mogą przyjmować argumenty dowolnych typów, ale typ jest rzeczą twardą, tzn. łańcuch znaków nigdy bez jawnej konwersji nie zachowa się jak liczba. Najłatwiej można to zrozumieć, wpisując w linii poleceń: >>> 10 == "10" False
>>> 10 == int("10") >>> str(10) == "10"
Jak widać, Python rozróżnia liczbę 10 od łańcucha znaków reprezentującego "10". Za pomocą jawnej konwersji możemy jednak w razie potrzeby przechodzić między typami. Dynamiczne, ale silne typowanie to podstawowa cecha języka. Umożliwia z jednej strony uzyskanie bardzo zwięzłego kodu, który nie tylko szybciej się pisze, ale też szybciej czyta: first_name = "Winona" birth_year = 1971
Środowisko programistów Pythona wykorzystuje fakt, że każda zmienna może potencjalnie przechowywać wartość dowolnego typu. Przykładowo, mamy kod, który wypisuje na ekranie elementy listy: for elem in a_list:
print "Element: %s" % elem
Kod oczywiście działa dla elementów będących łańcuchami znaków. Jak się jednak okazuje, również
www.sdjournal.org
•
Po pierwsze: możliwość późniejszego rozszerzenia programu w taki sposób, że pod daną zmienną (lub jako argument do danej funkcji) zostanie podana wartość innego typu, który jednak wygląda zupełnie jak oczekiwany typ! Szalenie przydatna możliwość np. w testowaniu jednostkowym kodu. I to bez żadnych zmian w oryginalnym kodzie! Po drugie: ryzyko, że w jakiś sposób trafi do nas niekompatybilny typ. Czy jednak jest się w tym przypadku o co martwić? Możemy niby sprawdzić, czy dana zmienna zawiera wartość danego typu, czy też może dany typ posiada oczekiwaną metodę lub atrybut. Co jednak możemy zrobić, jeżeli okaże się, że nie? Domyślnym zachowaniem Pythona jest rzucenie wyjątku. I nie wymaga to żadnej pracy z naszej strony. Jeżeli chcielibyśmy wobec tego rzucić wyjątek w takim przypadku, nie ma sensu kodować rozwiązania, które naśladuje to, co i tak dzieje się domyślnie.
Poproszę indeks
Biorąc pod uwagę duck typing, skupianie się na pojedynczych strukturach danych nie jest tak bardzo cie-
21
PROGRAMOWANIE PYTHON
kawe jak dokładne omówienie sposobu ich wykorzystania. W końcu zgodnie z naturą języka, wiele metod bezpośrednio przenosi się również na inne typy danych i działa w zupełnie przewidywalny sposób. Zacznijmy od dostępu do danych przez indeks. Struktury danych typu listy, krotki (ang. tuple, patrz Ramka 2), słowniki, łańcuchy znaków i inne umożliwiają dostęp do danych, które przechowują przez bardzo sprytny mechanizm indeksowania. Spójrzmy na przykład:
Jak widać, lista daje dostęp do danych po indeksie (licząc - jak w każdym rozsądnym języku - od zera [1]). Jeżeli wybierzemy niepoprawny indeks, lista rzuca wyjątek IndexError. Więcej o wyjątkach w dalszej części artykułu. Podawany indeks może być ujemny, wówczas liczymy "od końca". To nie jedyny sprytny manewr dostępny przy okazji indeksowania. Python umożliwia również wycinanie (ang. slicing) fragmentów z większych struktur:
>>> movies = ['Lucas', 'Beetlejuice', 'Dracula']
>>> sisters = ['Meg', 'Jo', 'Beth', 'Amy']
'Lucas'
['Meg', 'Jo', 'Beth']
>>> movies[0] >>> movies[1] 'Beetlejuice' >>> movies[2] 'Dracula'
>>> movies[3]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range >>> movies[-1] 'Dracula'
>>> movies[-2] 'Beetlejuice'
>>> movies[-3] 'Lucas'
>>> movies[-4]
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
IndexError: list index out of range
>>> sisters[0:3]
>>> sisters[0:-1]
['Meg', 'Jo', 'Beth'] >>> sisters[:3]
['Meg', 'Jo', 'Beth'] >>> sisters[:-1]
['Meg', 'Jo', 'Beth'] >>> sisters[1:4]
['Jo', 'Beth', 'Amy'] >>> sisters[1:]
['Jo', 'Beth', 'Amy'] >>> sisters[0:4:2] ['Meg', 'Beth']
>>> sisters[1:4:2] ['Jo', 'Amy']
>>> sisters[0::2] ['Meg', 'Beth']
>>> sisters[::2] ['Meg', 'Beth']
Ramka 1: Metody specjalne
Od pierwszego dnia w Pythonie istnieje grupa wbudowanych funkcji, które spełniają określoną funkcjonalność dla podanych argumentów. I to niezależnie od typu tych argumentów. Zwane z angielska builtin functions lub po prostu builtins udostępniają m.in. możliwość sprawdzenia długości danej sekwencji (w tym: łańcucha znaków), porównania dwóch obiektów czy skonwertowania dowolnego typu na łańcuch znaków. Pełna lista builtins wraz z dokumentacją znajduje się pod adresem http: //docs.python.org/library/functions.html. Kiedy tylko jest to możliwe, wykorzystanie builtins jest preferowane nad inne metody programowania. Stąd bardzo często spotkać można kod np.: if len(s) < 10: ...
Obiekty w Pythonie implementują zachowanie poszczególnych builtinów w metodach specjalnych, które łatwo można poznać po dwóch podkreślnikach przed ich nazwą i po ich nazwie. Przykładowo: >>> dir("") ['__add__', ... , '__len__', ...]
Kiedy użytkownik wykona len(s) to interpreter w rzeczywistości wykonuje odpowiednią metodę specjalną s.__len__(). Jest to o tyle fajne, że definiując własne typy danych również możemy definiować metody specjalne, przez co umożliwiamy wykorzystanie builtinów na naszych typach. Jest to bardzo często wykorzystywana cecha języka, ponieważ ujednolicenie interfejsu nowych typów danych do już istniejących bardzo ułatwia zapamiętanie, jak z nich korzystać. Tak wyprodukowane API jest spójne, przewidywalne i intuicyjne. W Pythonie sprawdzanie długości, rozmiaru lub innych tego typu cech zawsze odbywa się przez len(). Jak jest w Javie? Mamy metody length(), atrybuty length, metody size(), count(), getSize(), itd. itp. Możliwe do zapamiętania? Z pewnością. Ale czy wygodne i intuicyjne?
22
8/2010
Kurs Pythona. Cz II – Struktury danych, funkcje i moduły
Jak widać, jako "indeks" można podać zakres, składnia zakresów jest zresztą bardzo intuicyjna, a jednocześnie elastyczna. Można podawać zakresy, używając indeksu rozpoczynającego (włącznie), zamykającego (wyłącznie) lub obu. Indeks zamykający może być podawany jako ujemna wartość liczona elementami od końca. Można też podać opcjonalnie krok, z jakim ma być dokonane wycinanie. Efektem wycinania jest zupełnie nowa lista przechowująca elementy z oryginalnej listy. Oznacza to, że możemy bezstratnie tworzyć wiele skrawków z pojedynczej listy i nie marnujemy w ten sposób pamięci, bo nowe listy przechowują referencje do tych samych elementów co oryginalna. Jest to często wykorzystywana właściwość, np. podczas ładnego wypisywania elementów: >>> def print_mermaids(): ...
for mermaid in mermaids[:-1]:
...
print "%s." % mermaids[-1]
...
print "%s," % mermaid,
>>> print_mermaids() Cher, Ryder, Ricci.
Zdefiniowaliśmy na szybko funkcję wypisującą nam aktorki grające w "Syrenach". Właściwy kod to tylko 3 linijki, a w elegancki sposób udało nam się uzyskać oddzielanie poszczególnych syrenek przecinkami, z kropką na końcu. To bardzo przydatny idiom, prędzej czy później w jakiś sposób będziesz go potrzebować. Ale trochę się zapędziliśmy, o pętlach i funkcjach dopiero za chwilkę! Jeżeli dziwi Cię zachowanie wyrażenia print, zapraszam do Ramki 3. Wracając do indeksowania, zgodnie z naturą Pythona przewidujemy, że w dokładnie ten sam sposób można korzystać z danych w łańcuchach znaków i krotkach. Faktycznie, nie ma żadnej różnicy:
Tymczasem spójrzmy jeszcze pokrótce na "indeksowanie" w słownikach. W tym wypadku sprawa wygląda zgoła inaczej, ponieważ słowniki w swej naturze przechowują zbiory unikalnych kluczy, pod którymi kryją się jakieś wartości. Dostęp do tych wartości przez klucz przypomina indeksowanie w listach, ale z oczywistych względów nie umożliwia zabawy w wycinanki. Spójrzmy na krótki przykład: >>> movie_releases = {'Edward Scissorhands': 1990,
'Dracula': 1992, 'Star Trek': 2009}
>>> movie_releases['Star Trek'] 2009
>>> movie_releases['Dracula'] 1992
>>> movie_releases['A Scanner Darkly'] Traceback (most recent call last):
File "<stdin>", line 1, in <module> KeyError: 'A Scanner Darkly'
Jak widać, można się do przechowywanych obiektów odwoływać poprzez klucze, w przypadku użycia nieistniejącego klucza rzucany jest wyjątek KeyError. Nie ma możliwości odwołania się do "pierwszego" klucza albo "ostatniego" klucza, ponieważ słowniki przechowują z zasady nieuporządkowane zbiory danych. Fakt ten jest wykorzystywany przez Pythona do optymalizacji struktury słownika w pamięci tak, żeby dostęp do dowolnego klucza był tak samo szybki. Może się więc zdarzyć, że wartości są przechowywane w innej kolejności niż je definiowaliśmy. Spójrzmy przykładowo na nasze movie _ releases z przykładu wyżej: >>> movie_releases
{'Dracula': 1992, 'Edward Scissorhands': 1990, 'Star Trek': 2009}
'W'
Jak widać, na moim komputerze interpreter zamienił miejscami Nożycorękiego z Drakulą. W Twoim przypadku wynik może być inny, spróbuj.
'R'
Zgadnij kotku, co mam w środku
>>> celebrity = "Winona Ryder" >>> celebrity[0] >>> celebrity[7] >>> celebrity[-1] 'r'
>>> celebrity[:6] 'Winona'
>>> celebrity[7:] 'Ryder'
Nie jest to jednak najbardziej użyteczny sposób manipulacji łańcuchami znaków, o manipulacji tekstem w ogólności, wyrażeniach regularnych i ich mocy więcej w przyszłym numerze Software Developer's Journal.
www.sdjournal.org
Co prawda interfejs dostępu do kluczy jest w przypadku słowników minimalistyczny, struktura danych wynagradza nam to jednak bogatym API do operowania na kluczach i wartościach. Zacznijmy od metody sprawdzenia, czy dany klucz istnieje w słowniku: >>> alien4 = {'Sigourney Weaver': 'Ellen Ripley', ... 'Winona Ryder': 'Annalee Call'} >>> 'Winona Ryder' in alien4 True
>>> 'Johnny Depp' in alien4 False
23
PROGRAMOWANIE PYTHON
Składnia ELEM in DICT jest bardzo przejrzysta i można ją wykorzystać wszędzie, gdzie oczekujemy wartości boolean, m.in. w instrukcjach warunkowych, warunkach wyjścia z pętli, itd. itp. Są też inne metody na sprawdzenie obecności klucza w słowniku, ale istnieją one już tylko z powodów historycznych i ich użycie nie jest zalecane. Do słowników można oczywiście dokładać kolejne wartości, robimy to zasadniczo na dwa sposoby: •
dopisując nowy klucz bezpośrednio:
>>> alien4['Dan Hedaya'] = 'General Perez' >>> alien4
{'Winona Ryder': 'Annalee Call',
'Sigourney Weaver': 'Ellen Ripley', 'Dan Hedaya': 'General Perez'}
•
rozszerzając słownik za pomocą innego słownika:
>>> secondary_characters = {'Gary Dourdan': 'Christie', ... 'Ron Perlman': 'Johner',
... 'Dominique Pinon': 'Vriess'}
>>> alien4.update(secondary_characters) >>> alien4
{'Dominique Pinon': 'Vriess', 'Winona Ryder': 'Annalee Call',
'Sigourney Weaver': 'Ellen Ripley', 'Dan Hedaya': 'General Perez',
'Ron Perlman': 'Johner', 'Gary Dourdan': 'Christie'}
Obie metody są dobre, ta druga jest zalecana w przypadku zmian w wielu kluczach jednocześnie. A co w przypadku, kiedy jakiś klucz chcemy usunąć ze słownika? Wykorzystujemy wyrażenie del: >>> 'Dominique Pinon' in alien4 True
>>> del alien4['Dominique Pinon'] >>> 'Dominique Pinon' in alien4 False
>>> del alien4['Dominique Pinon']
Traceback (most recent call last):
File "<stdin>", line 1, in <module> KeyError: 'Dominique Pinon'
Jak widać, w przypadku gdy klucza nie ma w słowniku, podczas próby usunięcia elementu rzucany jest wyjątek KeyError. Patrząc na metodę sprawdzania, czy dany element należy do słownika, albo na możliwość usuwania pojedynczego klucza, wydaje się, że użyteczne byłoby coś podobnego dla list, krotek i zbiorów. Jak się okazuje, w Pythonie tak właśnie jest:
24
>>> numbers = [4, 8, 15, 16, 23, 42] >>> numbers[0] 4
>>> numbers[3] 16
>>> 16 in numbers True
>>> del numbers[3] >>> 16 in numbers False
>>> numbers
[4, 8, 15, 23, 42]
Na marginesie, sprawdzenie pierwszego indeksu, pod którym kryje się dana wartość w liście, można wykonać metodką index(), np.: >>> numbers.index(42) 4
>>> numbers[4] 42
>>> numbers.index(44)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ValueError: list.index(x): x not in list
Oczywiście, jak opisuje Ramka 2, krotki są niezmienialne, więc nie możemy usuwać z nich elementów, natomiast do zbiorów nie da się odwołać po indeksie. Stąd też w zbiorach dodawanie i usuwanie elementów trzeba rozwiązać inaczej: >>> flatware = set(('fork', 'knife', 'spoon')) >>> flatware.remove('fork')
>>> flatware.remove('spoon') >>> flatware.add('spork') >>> flatware
set(['spork', 'knife'])
>>> 'chopsticks' in flatware False
>>> flatware.remove('chopsticks')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
KeyError: 'chopsticks'
Funkcje i moduły
W naszych dotychczasowych przykładach przemyciliśmy już kilka funkcji tam, gdzie inaczej nie dało się zaprezentować konkretnej konstrukcji językowej. Przyjrzyjmy się jednak bliżej tym bestiom, bo w Pythonie są one wybitnie potężne, choć na pierwszy rzut oka tego nie widać. Definiowanie funkcji jest proste jak budowa cepa, przykładowo:
8/2010
Kurs Pythona. Cz II – Struktury danych, funkcje i moduły
Ramka 2: Listy, krotki i zbiory
W Pythonie istnieje bardzo niewiele wbudowanych struktur danych, spełniają one jednak niezależne funkcje. Do przechowywania uszeregowanych danych dowolnych typów możemy wykorzystać listy oraz krotki (ang. tuples). Różnica między nimi jest taka, że listy są obiektami modyfikowalnymi (tzn. można do istniejącej listy dokładać nowe wartości, zmieniać je na inne oraz usuwać). Krotki są niezmienne, w celu uzyskania innej postaci krotki należy stworzyć nową: >>> numbers = [1, 2, 3, 4] >>> numbers.append(5) >>> numbers [1, 2, 3, 4, 5] >>> numbers_tuple = (6, 7, 8) >>> numbers_tuple.append(9) Traceback (most recent call last): File "<stdin>", line 1, in <module> AttributeError: 'tuple' object has no attribute 'append' >>> del numbers[0] >>> numbers [2, 3, 4, 5] >>> del numbers_tuple[0] Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: 'tuple' object doesn't support item deletion >>> numbers.extend(numbers_tuple) >>> numbers [2, 3, 4, 5, 6, 7, 8]
Listy są ze swojej natury wolniejsze oraz zajmują więcej pamięci, stąd dla typowych sytuacji, gdzie wymagane jest zebranie wielu wartości bez potrzeby ich późniejszej modyfikacji, zaleca się wykorzystanie krotek. Można zresztą konwertować między tymi typami, np. kiedy chcemy udostępnić jako wynik funkcji niezmienialną postać jakiejś wcześniej budowanej listy (lub odwrotnie): >>> letters = ('a', 'b', 'c') >>> letters ('a', 'b', 'c') >>> letters = list(letters) >>> letters.append('d') >>> letters ['a', 'b', 'c', 'd'] Zarówno listy, jak i krotki udostępniają możliwość sprawdzania, czy dany obiekt do nich należy: >>> 'b' in letters True >>> 'b' in numbers_tuple False
Ze względu na budowę tych struktur takie wyszukanie ma liniową złożoność obliczeniową, więc dla licznych struktur lub częstych wykonań takich operacji jest to niewydajna operacja. Z tego względu Python udostępnia też dedykowany typ set jako nieuporządkowany zbiór wartości. W dodatku na tego typu obiektach można wykonywać też typowe logiczne operacje: vowels = set(['a', 'e', 'i']) >>> hard_consonants = set(['k', 't', 'p']) >>> soft_consonants = set(['b', 'g', 'w']) >>> consonants = hard_consonants | soft_consonants >>> consonants set(['p', 'b', 'g', 't', 'w', 'k']) >>> 'p' in consonants True >>> 'k' in vowels False >>> consonants & hard_consonants set(['p', 'k', 't']) >>> consonants - hard_consonants set(['b', 'w', 'g']) >>> vowels | consonants set(['a', 'b', 'e', 'g', 'i', 'k', 'p', 't', 'w'])
www.sdjournal.org
25
PROGRAMOWANIE PYTHON
"""Returns the n-th fibonacci number.
to, uruchamiając interpreter w katalogu, gdzie znajduje się plik f.py:
>>> fib(1), fib(2), fib(3), fib(4)
$ ls
>>> fib(30)
$ python
def fib(n):
(1, 1, 2, 3) 832040 """
if n in (0, 1): return n
return fib(n-1) + fib(n-2)
Przykład jest bardzo prosty, jednak już tu widać kilka istotnych budulców prawdziwych funkcji: nagłówek z nazwą i przyjmowanymi argumentami, łańcuch dokumentujący (ang. docstring) wraz z przykładem użycia, który można uruchomić jako test jednostkowy funkcji (ang. doctest). Sam kod jest bardzo prosty i przedstawia nieszczególnie wydajny sposób rekursywnego obliczania liczb ciągu Fibonacciego. Jeżeli chcesz pobawić się funkcjami w trakcie czytania, zapisz powyższy kod w pliku , dodając na końcu: if __name__ == '__main__': #1
import sys if len(sys.argv) != 2:
raise SystemExit, "Wrong number of arguments"
num = int(sys.argv[1]) print fib(num)
Powyższy kod rozwiązuje nam w najprostszy poprawny sposób kwestię obsługi linii poleceń. Żeby zrozumieć, co powyższe kilka linijek robi, cofnijmy się na moment. Każdy plik w Pythonie jest nazywany modułem i może być importowany przez inne moduły w celu wykorzystania ich funkcjonalności. Importów można dokonywać również w linii poleceń interpretera. Podczas pierwszego importu w ramach danego wykonania programu, kod modułu jest w całości parsowany i wykonywany. Zupełnie tak samo parsowany i wykonywany, jak gdybyśmy wykorzystywali nasz skrypt z linii poleceń: $ python f.py
Wrong number of arguments $ python f.py 20
26
f.py
Python 2.6.5 (r265:79063, Mar 26 2010, 16:07:38) >>> import f
>>> f.__name__ 'f'
>>> help(f.fib)
Help on function fib in module f: fib(n)
Returns the n-th fibonacci number. >>> fib(1), fib(2), fib(3), fib(4) (1, 1, 2, 3) >>> fib(30) 832040
>>> f.fib(20) 6765
Jak widać, można nasz moduł zaimportować i wykorzystać funkcję w innym programie. Modularyzacja jest podstawowym sposobem podziału kodu w Pythonie na logiczne komponenty. W przykładzie udało nam się też sprawdzić nazwę modułu oraz dokumentację funkcji fib(). Jak widać, przykłady użycia w dokumentacji są w takim przypadku szalenie przydatne, bo można je bezpośrednio wykorzystać w interaktywnej sesji z interpreterem. Jak wspomniałem wyżej, przykłady te nazywa się doctestami, ponieważ możemy je uruchomić, żeby sprawdzić, że funkcja działa poprawnie. Robimy to z konsoli, np. będąc w katalogu z plikiem f.py: $ python -m doctest -v f.py Trying:
fib(1), fib(2), fib(3), fib(4)
Expecting:
(1, 1, 2, 3)
ok
Trying: fib(30)
6765
Expecting:
Linijka oznaczona komentarzem #1 istnieje wobec tego po to, żeby odróżnić wykonanie modułu jako samodzielnego programu z linii poleceń od importu modułu przez inny moduł. W tym pierwszym przypadku Python przypisuje modułowi specjalną nazwę _ _ main _ _ . Jeżeli moduł jest importowany, pod zmienną _ _ name _ _ znajdzie się po prostu nazwa modułu (w naszym przypadku f). Sprawdźmy
ok
832040
1 items had no tests: f
1 items passed all tests: 2 tests in f.fib
2 tests in 2 items.
2 passed and 0 failed. Test passed.
8/2010
Kurs Pythona. Cz II – Struktury danych, funkcje i moduły
O testowaniu jednostkowym opowiemy sobie dokładniej w przyszłych odcinkach kursu [2], ale przyznaj, że motywująca jest świadomość, że nawet dokumentacja techniczna w kodzie może być w Pythonie formą testów jednostkowych. Przy tak wygodnych narzędziach nie wypada nie testować swojego kodu. Zdefiniujmy sobie jeszcze jedną funkcję, żeby porozmawiać o innych ciekawych właściwościach tych konstrukcji w Pythonie: def extreme(seq, function=max):
"""Returns the extreme value of a sequence.
The value is computed using the provided function (max by default).
terycznymi cechami przy okazji definiowania własnych klas.
Wyjątki od reguły
Już parokrotnie zarówno w tym odcinku, jak i w poprzednim, natrafialiśmy na różnego rodzaju wyjątki, które są rzucane w anormalnych sytuacjach, np. kiedy dany indeks w liście nie istnieje lub próbowano wykorzystać zły typ danych w jakimś miejscu. W świecie Pythona każda sytuacja, którą programista określiłby jako nietypową i wymagającą dodatkowej uwagi w kodzie, jest obwarowana wyjątkami. Najczęstsze rodzaje rzucanych wyjątków: •
>>> extreme((1,2,3)) 3
>>> extreme([])
•
Traceback (most recent call last): ...
ValueError: max() arg is an empty sequence
•
>>> extreme((1,2,3), min) 1
>>> extreme(seq=(1,3,5,7), function=sum)
•
"""
•
16
return function(seq)
Podobnie jak w przypadku poprzedniej funkcji, dokumentacja zajmuje więcej miejsca niż właściwy kod. Prezentujemy tu jednak za jednym zamachem kilka istotnych kwestii: • •
•
w Pythonie funkcje mogą przyjmować argumenty opcjonalne. Specyfikuje się je, podając do argumentu w nagłówku funkcji jego wartość domyślną. dla Pythona funkcje są po prostu odrębnym typem obiektów i jako takie mogą być swobodnie przekazywane jako zmienne lub argumenty do innych funkcji (jak w powyższym przykładzie). Funkcje takie wykonujemy naturalnie tak, jakbyśmy wykorzystywali funkcje zdefiniowane przez siebie lub zaimportowane z innych modułów w Pythonie podczas wykonania funkcji można podawać wartości argumentów wraz z nazwami argumentów, do których mają być przypisane. Jest to szczególnie przydatne, jeżeli opcjonalnych argumentów jest wiele i chcemy określić jasno, który z nich chcemy nadpisać. Dodatkowo, takie wykonania jeszcze łatwiej się czyta, ponieważ wiadomo, jaką rolę spełnia dana wartość w wykonaniu funkcji.
Zabaw z funkcjami można wykonać jeszcze wiele, w przyszłym miesiącu zajmiemy się ich bardziej ezo-
www.sdjournal.org
•
nieudane wyszukanie (błędny indeks dla list i krotek, nieistniejący klucz w słowniku, brak oczekiwanego atrybutu w obiekcie itd.) wejścia-wyjścia (problem z odczytem pliku, zerwanie połączenia sieciowego, brak miejsca na dysku, brak pamięci itd.) arytmetyczne (próba dzielenia przez zero, przekroczenie zakresu liczbowego, błędna operacja zmiennoprzecinkowa itd.) nieoczekiwane wartości (zły typ danych, niepoprawny argument) błędy w kodzie (błąd składniowy, niezadeklarowane zmienne, asercja zawiodła, niepowodzenie importu zewnętrznego modułu, błąd z kodowaniem Unicode) zakończenia przetwarzania (koniec pliku, przerwanie iteracji, koniec generatora itd.)
Ramka 3: Wyrażenie print
Często używanym w naszych przykładach wyrażeniem jest print, o tyle specyficzna bestia, że w Pythonie 2.x nie jest to funkcja, tylko wyrażenie o dedykowanej składni. W najprostszym przypadku wykorzystujemy je tak: >>> print "Hello world!" Hello world! >>> print "Hello", "world!" Hello world! >>> print 2, "years" 2 years
Jak widać, wyrażenie przyjmuje dowolną liczbę argumentów (niekoniecznie łańcuchy znaków), które są na wyjściu oddzielane pojedynczą spacją. Możemy również pozostawić przecinek na końcu, w którym to przypadku print nie wstawia znaku końca linii na końcu tekstu, a jedynie spację. W efekcie poniższy kod wyprodukuje tylko jedną linię tekstu: >>> def goodbye(): ... print "Goodbye", ... print "cruel", ... print "world!" >>> goodbye() Goodbye cruel world!
27
PROGRAMOWANIE PYTHON
Szczególnie ten ostatni rodzaj wyjątków budzi kontrowersje użytkowników Pythona, czy takie sytuacje powinny być obsługiwane przez wyjątki. Tego rodzaju wykorzystanie jest więc mocno ograniczone i generalnie nie zachęca się do tworzenia własnych wyjątków działających w analogicznych sytuacjach. Wyjątki w Pythonie nie są rejestrowane, tzn. funkcje i klasy nie określają, jakie wyjątki mogą rzucić. Ze względu na dynamiczną naturę języka bardzo trudne byłoby ograniczenie z góry szeregu wyjątków, które mogą się pojawić w trakcie wykonania danego kodu. Wyjątek rzucamy, podając jego nazwę i po przecinkach dodatkowe argumenty, np.: >>> raise MemoryError, "I can't find my glasses!" Traceback (most recent call last):
File "<stdin>", line 1, in <module>
MemoryError: I can't find my glasses!
Kod, o którym wiemy, że z jakiegoś względu może rzucić wyjątek, możemy obwarować blokiem try:, except:, finally: np. w takiej sytuacji: >>> try: ...
import some_library
...
print "some_library not installed."
... except ImportError:
... some_library not installed.
Często wyjątki zawierają kontekstowe dane, które możemy wykorzystać przy ich obsłudze. Żeby się do nich dostać, definiujemy w bloku except po przecinku nazwę zmiennej, pod którą chcemy mieć dostęp do obiektu wyjątku:
Patronat
Patronem cyklu artykułów poświęconych tematyce Python jest firma STX Next (www.stxnext.pl). Celem całego cyklu jest popularyzacja języka Python, co jest wpisane w misję firmy. STX Next jest firmą typu software house, specjalizującą się w tworzeniu i wdrażaniu rozwiązań internetowych klasy enterprise w oparciu o technologie Python, Plone, django, Pylons i pokrewne. W kolejnych częściach zapoznamy czytelnika z możliwościami języka i biblioteki standardowej, natomiast w drugiej części cyklu przedstawimy najważniejsze frameworki, takie jak: Plone, django, Pylons, Turbogears, Repoze oraz Google App Engine.
Ostatecznie, istotną konwencją jest stosowanie bloków try:, finally: wszędzie tam, gdzie niezależnie od wyjątków musimy wykonać jakieś porządki po wykonaniu bloku kodu. Przykładami takich sytuacji są najczęściej: • • • •
potrzeba zamknięcia pliku potrzeba zamknięcia połączenia do bazy potrzeba zwolnienia pamięci lub miejsca na dysku potrzeba zamknięcia połączenia sieciowego
W następnym odcinku
W przyszłym miesiącu, opierając się o naszą dzisiejszą grubszą lekcję, odwiedzimy rejony programowania obiektowego i innych przestarzałych paradygmatów [3]. Przy okazji zabaw klasami zajmiemy się manipulacją tekstem i liczbami. Poruszymy też kwestię operowania na plikach w Pythonie.
>>> guitarists = {'Oldfield': 'Mike', 'Santana': >>> try:
'Carlos', 'Rea': 'Chris'}
...
print guitarists['Malmsteen']
...
print "%s is no guitarist." % e
... except KeyError, e: ...
'Malmsteen' is no guitarist.
ŁUKASZ LANGA Programuje w Pythonie od 8 lat, aktualnie współpracuje z STX Next, rozwijając oprogramowanie dla sektora bankowego. Prywatnie miłośnik muzyki fortepianowej, początkujący ojciec i mąż. Adres e-mail autora: lukasz@langa.pl.
Przypisy • • •
28
Świetny esej na ten temat napisał już w 1982 roku E. W. Dijkstra: http://userweb.cs.utexas.edu/users/EWD/transcriptions/ EWD08xx/EWD831.html Tymczasem dla niecierpliwych: doskonała dokumentacja doctestów i tego, co można z nimi zrobić znajduje się tutaj: http:/ /docs.python.org/library/doctest.html Pół żartem, pół serio. Python jest językiem łączącym kilka paradygmatów programowania, od strukturalnego, przez obiektowe po funkcyjne. Zauważ, jak daleko udało nam się zabrnąć bez zdefiniowania ani jednej klasy. Ciekawą dyskusję na temat konsekwencji takiej mieszanki funkcjonalności można znaleźć tu: http://lambda-the-ultimate.org/classic/ message6153.html
8/2010
PROGRAMOWANIE JAVA
Przewodnik po SCJP czyli certyfikat z Javy – część 5 Proces zdobywania certyfikatów, potwierdzających umiejętności z różnych dziedzin wiedzy, stał się jednym z ważniejszych elementów osobistego rozwoju. Proces ten ma miejsce również w branży IT; certyfikaty dla programistów (Java lub .NET), administratorów czy sieciowców (Cisco) można coraz częściej odnaleźć w CV osób starających się o pracę. Dowiesz się:
Powinieneś wiedzieć:
• Jak radzić sobie z nietypowymi konstrukcjami przy dziedziczeniu klas; • Jak unikać problemów z dostępnością klas i ich elementów; • Jak inne ważne zasady programowania obiektowego mogą utrudnić Ci zdanie egzaminu.
• Jak skompilować i uruchomić programy w Javie; • Jakie są podstawy składni języka; • Czym różni się klasa od interfejsu i obiektu.
W
cyklu artykułów poświęconych przygotowaniom do egzaminu 310-065 (Sun Certified Java Programmer for Java SE 6) przekroczyliśmy połowę kursu. W dzisiejszym odcinku powracamy do kanonu – po dłuższej „zabawie” z funkcjonalnością, czyli API (wejście/wyjście, łańcuchy znaków, wielowątkowość), czas wrócić do omawiania niuansów związanych stricte z językiem programowania. Tematem tego artykułu będzie programowanie obiektowe w Javie ze wszystkimi (obowiązującymi na egzaminie) specyficznymi niuansami. Nim zanurzymy się w otchłani Javowego kodu, nie mógłbym zapomnieć o opisaniu krótkiego, acz istotnego (i często spotykanego na egzaminie) zagadnienia – powiązań i zwartości.
Flip i Flap
High cohesion & Low coupling – wysoka zwartość i luźne powiązanie to dwa pojęcia, które wiążą się ściśle z projektowaniem klas w Javie i innych obiektowych językach programowania. Podczas egzaminu spotkasz się z dwoma rodzajami zadań, dotyczącymi tych zagadnień. Przede wszystkim, musisz znać definicje tych pojęć, warto także zapoznać się z kodami spełniającymi te dwie reguły. Nie pozostało mi nic innego, jak przedstawić dwa wyżej wspomniane terminy. Wysoka zwartość obliguje programistę do tworzenia klas o ściśle określonej roli. Jeśli chcesz stosować się
30
do reguły wysokiej zwartości, nie powinieneś tworzyć klas takich, jak poniższa: class Menadzer {
public void drukuj(String dane) {} // itd. public int obliczParametr(int n, int m) {}
public InputStream pobierzStrumienSieciowy(String ip) }
{}
Powyższa klasa przedstawia niską zwartość, ponieważ zawiera funkcjonalność związaną z bardzo różnymi operacjami – drukuje dane, wykonuje obliczenia, realizuje operacje na gniazdach sieciowych. Jest to przykład klasy o zbyt rozmaitej funkcjonalności. Dobrze zaprojektowany system zawierałby osobne klasy (lub interfejsy), określające podane funkcjonalności osobno. Luźne powiązanie między klasami oznacza, że te klasy są ze sobą powiązane najsłabiej, jak to tylko możliwe. Mówiąc językiem bardziej formalnym, klasy luźno powiązane powinny wzajemnie korzystać z jak najmniejszej liczby swoich metod, pól i innych możliwych konstrukcji języka. Dotyczy to zwłaszcza wzajemnych modyfikacji danych istniejących w obrębie obiektów (klas). Zbyt ścisłe powiązanie dwóch klas może doprowadzić do sytuacji, przedstawionej na Listingu 1.
8/2010
Certyfikat SCJP
Rzutowanie, dziedziczenie i polimorfizm
W tradycyjnych aspektach programowania obiektowego w Javie tkwi całkiem spore pole do popisu dla autorów zadań egzaminacyjnych. Na egzaminie niezbędna jest praktyczna znajomość polimorfizmu. Typowe zadania egzaminacyjne, sprawdzające wiedzę na temat polimorfizmu, zawierają ogromną liczbę rzutowań – zarówno tych sensownych, podchwytliwych, jak i całkiem głupich. Na początek – przyzwyczaj się do nietypowych, pogmatwanych hierarchii klas, np. B dziedziczy po A, A dziedziczy po C, a dodatkowo A implementuje interfejs D. Po utworzeniu kilku obiektów (w sposób tradycyjny, np. B b = new B();) musisz wiedzieć, które przypisania (rzutowania) mogą zaistnieć w danym przykładzie, np.:
ten stanowi podklasę klasy C, a co za tym idzie – można przypisać tak zrzutowaną referencję. Na etapie wykonania programu referencja d , odwołująca się w istocie do obiektu typu B, zostanie zrzutowana na swój macierzysty typ, a zatem nie dojdzie do wyjątku klasy ClassCastException . Nie zapominaj, że w tego typu zadaniach wyjątkowo potrafią zamieszać zwłaszcza interfejsy. W powyższym przykładzie w referencji d można umieścić odwołanie do obiektów klas B i A, ale klasy C – już nie!
Elementy statyczne vs. instancje – runda 2.
C c = new C();
O elementach (metodach i polach) statycznych była już mowa w jednej z poprzednich części kursu, niemniej musimy omówić je po raz kolejny (wraz z metodami i polami instancji), tym razem w kontekście dziedziczenia. Jednym z większych problemów, występujących w zadaniach egzaminacyjnych, jest połączenie zagadnienia przeciążania i przesłaniania metod z modyfikatorem static, a także z modyfikatorami widoczności (public, protected, itd.). Rozważmy kod umieszczony na Listingu 2. Która z metod print() zostanie wywołana? Czy metoda statyczna z klasy bazowej ma przewagę nad metodą tradycyjną, jednak zadeklarowaną w klasie bieżącej? Obydwa podejścia pogodzi błąd kompilacji. Nie wolno przesłaniać wzajemnie metod statycznych i instancji. To było proste. Przesłanianie metod instancji było już omawiane, przejdziemy więc do przesłaniania metod statycznych. Załóżmy więc, że zarówno metoda print() z klasy A, jak i B, są statyczne. Co wydarzy się w momencie poniższych wywołań?
C c = (B)d;
A a = new A();
W tym przypadku zarówno kompilacja, jak i wykonanie powyższego kodu powiodą się. Z punktu widzenia kompilatora, referencja d jest rzutowana na typ B. Typ
// lub
C c = new C(); D d = new B(); C c = d;
Powyższy kod (przy założeniu, że obowiązują opisane wcześniej relacje między klasami a interfejsem) nawet się nie skompiluje! Problem stanowi wiersz 3. Mimo że referencja d zawiera obiekt klasy B (który dzięki dziedziczeniu jest też obiektem klasy C), nie może być ona bezpośrednio przypisana do referencji klasy C. Kompilator nie wie, co w momencie wykonania programu będzie znajdować się w referencji. Może więc polegać jedynie na typie referencji. Tymczasem interfejs D nie ma żadnego związku z klasą C. Z tego względu kompilacja nie powiedzie się. Zupełnie inaczej będzie wyglądać sytuacja, jeśli zmienisz kod na poniższy:
D d = new B();
a.print();
B b = new B(); b.print();
Listing 1. Przykład zbyt mocno powiązanych ze sobą klas class DataHandler {
InputStream inp;
public DataHandler() {} // tu otwieranie strumienia
}
public byte[] loadData() {} // tu zwracanie danych
class FalseBusinessLogic {
public String createBusinessData(DataHandler
dh) {
// byte[] data = dh.loadData(); - tak powinno być
byte[] bufor = new byte[1024];
dh.inp.read(bufor, 0, bufor.length);// taki kod się skompiluje, ale nie jest to luźne powiązanie!
}
} // metoda biznesowa
www.sdjournal.org
31
PROGRAMOWANIE JAVA
Pierwszy z dwuwierszy wyświetli wartość 1, a drugi – 2. Nic to – cytując klasyka – przecież w obu przypadkach korzystamy wyraźnie z odpowiednich klas. Co jednak stanie się w kolejnej sytuacji? B b = new B();
((A)b).print();
W tej sytuacji dysponujemy obiektem klasy B (referencja również jest tego typu), jednak tuż przed wywołaniem metody rzutujemy referencję na typ A . To właśnie ta operacja okazuje się być decydującą. Powyższy kod wyświetli wartość 1! Wynika to z prostej reguły – w przypadku przesłaniania metod statycznych wybór metody następuje na etapie kompilacji, a w przypadku metod instancji – na etapie wykonania. Zwróć uwagę, że metody statyczne mogą być bez problemu wywoływane za pomocą obiektów danej klasy, a ściśle rzecz mówiąc – referencji do nich. Nie należy jednak przywiązywać do referencji zbytniej wagi – z punktu widzenia maszyny wirtualnej referencja informuje jedynie o typie, z jakiego należy skorzystać podczas wywołania metody statycznej. Co za tym idzie, kod przedstawiony na Listingu 3 jest poprawny! Mimo że wyrażenie C.getC() zostaje sprowadzone do wartości null, maszyna wirtualna wie, jakiego typu wartość jest zwracana przez metodę getC(). Dzięki temu wiemy, z jaką klasą mamy do czynienia i możemy wywołać jej metodę print().
Wyższy poziom wtajemniczenia – przesłanianie + konstruktory
Do panteonu obiektowych kruczków pojawiających się na egzaminie SCJP należy dołączyć problemy z konstruktorami. Zagadnienia omawiane w niniejszym akapicie nie są zbyt wyszukane – najczęściej wystarczy Listing 2. Metoda statyczna vs. niestatyczna – odsłona pierwsza class A {
public static void print() {
}
}
System.out.println("1");
zachować zimną krew, opanować oczopląs i spokojnie przeanalizować treść załączonych kodów źródłowych. Lepiej jednak dmuchać na zimne, dlatego kilka typowych przykładów omówimy już teraz. Poza typowo zaciemniającymi kod praktykami, problem z konstruktorami wynika głównie z zachowania kompilatora. Przypomnijmy sobie jedną z ważnych reguł dotyczących konstruktorów – jeśli w danej klasie konstruktor nie został zadeklarowany, kompilator automatycznie dołączy definicję konstruktora bezparametrycznego, publicznego i zawierającego wywołanie bezparametrycznego konstruktora z klasy nadrzędnej (to prawda, trochę to skomplikowane). Typowy przykład takiego kodu prezentuję poniżej. Tego typu sytuacje mogą nawet mieć miejsce w praktyce – wystarczy zapomnieć o zadeklarowaniu konstruktora domyślnego (oczywiście przy deklaracji także innych konstruktorów), a następnie zadeklarować podklasę, zapominając o umieszczeniu odpowiedniego wywołania konstruktora za pomocą słowa super: class A {
public A(int x) {}
}
class B extends A { public B(int y) {}
}
Błąd tkwi oczywiście w domyślnym i zarazem niejawnym zachowaniu kompilatora. Reguła mówi: każdy konstruktor musi zawierać (jako pierwszą instrukcję) odwołanie do dowolnego konstruktora klasy nadrzędnej lub innego konstruktora tej samej klasy (np. this(x, y);). W przypadku pominięcia tej instrukcji, kompilator automatycznie wstawi wywołanie super();. Zazwyczaj nie stanowi to problemu, jednak jeśli zadeklarujesz jakiekolwiek konstruktory w klasie nadrzędnej (tak jak w klasie A) i zarazem zapomnisz umieścić Listing 3. Specy�czny przykład działania metod statycznych class C {
public static C getC() {
}
class B extends A{
public static void print() {
public void print() {
}
System.out.println("2");
public static void main(String[] args) {
B b = new B();
}
32
}
return null;
}
}
System.out.println("test");
class D {
public static void main(String[] args) {
b.print();
}
}
C.getC().print();
8/2010
Certyfikat SCJP
wywołania w kodzie konstruktora klasy podrzędnej, powstanie błąd. Powyższy kod można naprawić, zmieniając treść konstruktora klasy B : public B(int y) { super(y); }
Oczywiście konkretne rozwiązanie zależy od tego, co faktycznie chcesz osiągnąć. Na egzaminie możesz się spodziewać jedynie wskazania problemu i podania dowolnego poprawnego rozwiązania. Przy okazji konstruktorów i przesłaniania warto wspomnieć o problemie nieco bardziej ogólnym, niemniej równie często występującym na egzaminie. Chodzi o znane i (nie)lubiane modyfikatory dostępu. Rozpatrzmy poprzednie fragmenty kodu właśnie pod kątem modyfikatorów. Co sądzisz o poniższym kodzie?
Czytelników spotkała się z tymi terminami; może niekoniecznie pod taką nazwą. Relacja IS-A określa pokrewieństwo między klasami zgodnie z regułami dziedziczenia. Obiekt klasy B jest także obiektem klasy A (B IS-A A), jeśli spełniona jest jedna z zależności: class A {}
class B extends A {} // lub
interface A {}
class B implements A {}
Oczywiście zależność między klasami nie musi być bezpośrednia. W poniższym przykładzie klasa C również spełnia relację IS-A w odniesieniu do klasy A :
class A {
class A {}
}
class C extends B {}
A(int x) {}
class B extends A {
B(int y) { super(y); }
}
Jest to kod niewątpliwie poprawny. W porównaniu do poprzedniej wersji zmieniliśmy modyfikatory dostępu z public na domyślny, czyli na modyfikator pakietu (dostęp do elementu jedynie w obrębie pakietu). Nie muszę chyba mówić, co się stanie, jeśli powyższy kod zostanie rozdzielony do dwóch pakietów: package a; // plik A.java class A {
A(int x) {}
}
package b; // plik B.java import a.A;
class B extends A {
B(int y) { super(y); }
}
Podział kodu na dwa pliki, i co ważniejsze, dwa pakiety, spowoduje, że konstruktor klasy A nie jest widoczny w konstruktorze klasy B.
Być czy mieć?
Ostatnie podzagadnienie określone w ramach zagadnienia piątego wymaga znajomości relacji IS-A oraz HAS-A. Jest to jedno z podstawowych zagadnień programowania obiektowego i nie wątpię, że większość
Bibliografia
Sierra K., Bates B. – Sun Certi�ed Programmer for Java 5 Study Guide
www.sdjournal.org
class B extends A {} // lub
interface A {}
interface B extends A {} class C implements B {}
Relacja HAS-A określa związek zawierania. Klasa A pozostaje z klasą B w relacji zawierania, jeśli spełniona jest zależność: class A { B b;
}
Konieczne jest więc przechowywanie referencji do danego obiektu klasy. Typowe zadania, związane zarówno z relacją IS-A, jak i HAS-A, dotyczą określenia relacji pomiędzy klasami (interfejsami) na podstawie kodu lub wskazania poprawności podanych zdań. Znajomość obu konceptów w zupełności wystarcza do poprawnego rozwiązywania tego typu zadań. Na tym niewątpliwie prostym zagadnieniu kończymy piątą część kursu SCJP. W części szóstej jedno z najważniejszych, zarówno na egzaminie, jak i w praktyce programistycznej, zagadnienie (a właściwie zagadnienia) – duet kolekcje & generyki.
KRZYSZTOF RYCHLICKI KICIOR Autor programuje w Javie i .NET. Ostatnio zaintrygowało go połączenie Adobe Flex z J2EE, czyli LiveCycle Data Services. Pisze książki i artykuły na różne tematy związane z programowaniem. Jest posiadaczem certy�katu SCJP i SCWCD; od kwietnia 2007 do kwietnia 2008 legitymował się tytułem Microsoft MVP w kategorii Visual C#. Kontakt z autorem: kitikatpl@gmail.com
33
PROGRAMOWANIE JAVA
Pytania 1. W jednej z klas aplikacji pojawia się następujący kod: class Okno { // reprezentuje kontrolkę graficzną okno public void wyswietl(String sciezka) throws IOException{ FileInputStream fis = new FileInputStream(sciezka); byte[] bufor = new byte[1024]; fis.read(bufor); fis.close(); // dalsze operacje z wykorzystaniem bufora } }
Które z poniższych określeń pasuje do powyższego kodu? a) Wysoka zwartość b) Niska zwartość c) Silne powiązania d) Luźne powiązania 2. Którą z podanych instrukcji można wstawić w miejsce oznaczone komentarzem //1, aby kod skompilował się i program nie zwrócił wyjątku rzutowania? class A { } class B extends A { } interface A1 {} interface A2 extends A1 {} class C extends B implements A1 {} public class Main { public static void main(String[] args) { A a = new A(); A1 a1 = new C(); // 1 } }
a) b) c) d) e) f)
A1 a1 = new B(); A2 a = new A(); A a = new C(); A2 a2 = (A2)a1; A1 a1 = a; B b = (B)a1;
3. Jaki efekt spowoduje wywołanie poniższego kodu? public class Main { public static void main(String[] args) { C c = new C(); } }
class A { public static void A(int x) { System.out.print("Klasa A, "); } } class B extends A { public B() { System.out.print("Konstruktor B, "); } } class C extends B { C() { System.out.print("Konstruktor C, "); } }
a) kod nie skompiluje się b) kod skompiluje się, jeśli zostanie dodany modyfikator public w konstruktorze klasy C c) kod skompiluje się i program wyświetli tekst Konstruktor B, Konstruktor C d) kod skompiluje się i program wyświetli tekst Konstruktor C, Konstruktor B 4. Które z poniższych zdań, dotyczących poniższego kodu, jest prawdziwe? class A { A(int x) {} private A(Integer x) {} } class B extends A { B() { this(3); } B(int z) { super(z); } } public class Main { public static void main(String[] args) { B b = new B(); } }
a) kod nie skompiluje się b) kod skompiluje się wtedy, i tylko wtedy, gdy zostanie usunięte słowo private c) kod skompiluje się wtedy, i tylko wtedy, gdy pliki będą znajdowały się w tym samym pakiecie d) kod skompiluje się w takiej postaci, w jakiej jest 5. Które z poniższych zdań są prawdziwe? a) Komputer zawiera procesor b) Komputer jest urządzeniem c) Komputer zawiera urządzenie d) Komputer jest procesorem
Odpowiedzi: 1. 2. 3. 4. 5.
34
b, c – klasa nie jest zwarta, ponieważ realizuje operacje na danych (operacje wejścia/wyjścia), mimo że jest klasą odpowiedzialną za operacje graficzne. Sam fakt wykorzystywania w tak dużym stopniu klasy odpowiedzialnej za operacje (I/O) niezwiązane z tą klasą świadczy o silnych powiązaniach. f – większość przykładów (poza d i f) nie skompiluje się, zaś przykład d zwróci wyjątek ClassCastException. c – Java nie zabrania tworzenia metod, o tej samej nazwie, co klasa. Z tego względu istnienie metody public static void A() nie zakłóca działania przykładu. Również modyfikatory są poprawne. Kolejność wyświetlanych tekstów jest związana z automatycznym wstawianiem wywołania konstruktora klasy bazowej – super(). d – Kod jest poprawny. Wariant c byłby poprawny, gdyby nie sformułowanie wtedy, i tylko wtedy, gdy – na egzaminie należy uważać na odpowiedzi poprawne merytorycznie, które jednak wykluczają inne, również prawidłowe, odpowiedzi. a, b, c – Wariant a jest oczywisty. Wariant b – klasa Komputer może dziedziczyć po klasie Urzadzenie, tak jak np. klasy Drukarka , Telewizor, etc. Wariant c jest najbardziej interesujący – zasilacz, napęd DVD czy nawet PC speaker możemy potraktować jako proste urządzenia – w takim rozumieniu komputer również zawiera urządzenia.
8/2010
PROGRAMOWANIE JAVA
Java na BlackBerry Podstawy pisania aplikacji Artykuł przedstawia podstawy programowania aplikacji w języku Java pod system BlackBerry. W podstawowym zakresie omówione zostały cztery tematy: ogólne sposoby tworzenia aplikacji, budowanie interfejsu użytkownika, programowanie menu telefonu oraz zagadnienie utrwalania danych. Dowiesz się:
Powinieneś wiedzieć:
• Jakie są sposoby tworzenia aplikacji w języku Java na telefony BlackBerry; • Jak tworzyć komponenty interfejsu użytkownika i rozmieszczać je na ekranie; • Jak dodawać elementy do Menu telefonu; • Jak zapisywać i odczytywać dane.
• Podstawowa znajomość języka Java; • Znajomość Eclipse IDE; • Znajomość standardu J2Me może być pomocna.
B
lackBerry jest rodziną telefonów komórkowych zaliczanych do segmentu tzw. smartphone'ów. Jest to produkt kanadyjskiej firmy Research In Motion produkowany już od 11 lat. Cechą charakterystyczną urządzeń BlackBerry jest łatwa obsługa wiadomości e-mail. Stoi za tym technologia „push-mail”, polegająca na współpracy terminali (telefony komórkowe) z serwerami pocztowymi BlackBerry. Współpraca ta przejawia się w „wypychaniu” emaili z serwera na terminale, co daje możliwość odbioru poczty podobnie jak w przypadku odbioru sms'ów. Funkcjonalność ta stała się znakiem firmowym BlackBerry i w dużym stopniu przyczyniła się do sukcesu rynkowego w USA. W Europie technologia ta jest mniej popularna. W USA natomiast udziały firmy RiM w rynku smartphon'ów wynoszą ponad 50%. Drugie miejsce zajmuje dopiero iPhone.
BlackBerry i Java
BlackBerry ma długą historię współpracy z technologią Java. Od modelu 5810 (2002 rok) większość core’owych aplikacji jest napisanych w tym języku. Producent telefonów stworzył również API, przy pomocy którego każdy programista może tworzyć aplikacje w języku Java. Najnowsza wersja API (ver. 5.0) udostępnia zaawansowany zestaw funkcjonalności. Developerzy mogą programować obsługę m.in. GPS,
36
bluetooth, mają szeroki wybór komponentów do tworzenia interfejsu użytkownika. Platforma BlackBerry oferuje również trwałe źródła danych, w związku z czym możliwy jest zapis danych. To wszystko sprawia, że bardzo proste staje się rozwijanie aplikacji wyposażonych w najbardziej zaawansowane funkcje i odpowiadających najbardziej wyszukanym gustom użytkowników.
Sposoby tworzenia aplikacji
Platforma BlackBerry umożliwia tworzenie aplikacji w Javie na dwa sposoby. Pierwszy, o którym wspomnieliśmy wyżej, polega na korzystaniu z wbudowanego API i tworzeniu aplikacji uruchamianych bezpośrednio na platformie BlackBerry (na systemie operacyjnym). Drugi sposób wiąże się z rozwojem aplikacji webowych. Wykorzystuje się tu możliwości wbudowanej przeglądarki internetowej. Sam proces tworzenia aplikacji webowych przebiega identycznie jak w przypadku tworzenia aplikacji pod zwykłe przeglądarki stron internetowych, czyli wykorzystuje się tu technologie takie jak: html, css itd. Twórcy oprogramowania nie mogą narzekać na zestaw narzędzi developerskich dostarczonych przez firmę RiM. Zarówno proces budowania aplikacji bezpośrednio na platformę BlackBerry, jak i tzw. web development mają swoje wsparcie w odpowiednich na-
8/2010
Java na BlackBerry
rzędziach. Są to wtyczki do popularnych środowisk developerskich (IDE), np. do Eclipse'a, lub też całkiem odrębne środowiska programistyczne stworzone od podstaw przez producenta. Istotnym udogodnieniem jest także liczna gama emulatorów poszczególnych modeli telefonów BlackBerry. Po instalacji Java Development Environment (JDE) w którejkolwiek wersji, instalowane są domyślne emulatory. Użytkownik może jednak ściągnąć i zainstalować emulator praktycznie każdego dostępnego na rynku telefonu BlackBerry. Trzeba jednak przyznać, że emulatory nowszych modeli działają dość wolno, stąd też do developmentu wygodne jest korzystanie ze starszych modeli.
BlackBerry JDE
W artykule zajmiemy się tylko pierwszym sposobem tworzenia aplikacji. Będziemy korzystać z wtyczki do środowiska Eclipse (BlackBerry Java Plug-in for Eclipse v1.1) oraz z Java Development Environment w wersji 4.5 (JDE 4.5). Wskazanie wszystkich elementów potrzebnych do konfiguracji całego środowiska developerskiego BlackBerry znajduje się w ramce „W sieci”.
W przeciwieństwie do np. platformy Android jest możliwe jedynie programowalne definiowanie UI, deklaratywne (ustawianie komponentów UI w np. plikach xml) nie jest obsługiwane. Jest to z pewnością pewna luka w API. Pozycjonowanie elementów UI w odseparowanych plikach jest rozwijającym się trendem w programowaniu interfejsów użytkownika. Tworzenie GUI przebiega więc podobnie jak w przypadku programowania z użyciem biblioteki Swing z podstawowego pakietu Javy (J2SE). Klasa każdego komponentu jest tworzona „ręcznie” przez programistę, zwykle w konstruktorach lub blokach statycznych. Listing 1. Główna klasa przykładowej aplikacji public class Notepad extends UiApplication { public Notepad() {
pushScreen(new UICreator().createMainScreen()
}
public static void main(String[] args) {
Notepad notepad = new Notepad();
Prosta aplikacja
Zakładamy, że czytelnik posiada poprawnie zainstalowane JDE, plugin do Eclipse'a oraz emulator. Zajmiemy się więc utworzeniem prostej aplikacji będącej formą notatnika. Z racji tego, że artykuł obejmuje podstawy programowania na platformie BlackBerry, w ilustrującej go aplikacji zastosowaliśmy dość stare JDE w wersji 4.5. Jednakże zawarte tam API w pełni zaspokaja potrzeby tak prostego programu. W procesie tworzenia aplikacji poruszymy trzy zagadnienia. Pierwszym będzie programowanie prostego interfejsu użytkownika (UI), drugim – zaimplementowanie jednej funkcji w „Menu” telefonu, trzecim – oprogramowanie zapisu danych wprowadzonych przez użytkownika do trwałego źródła. Na platformie BlackBerry każda klasa generująca interfejs użytkownika musi dziedziczyć z klasy UIApplication. Klasa ta zawiera metody powołujące do życia m.in. układ wszystkich elementów UI. Taką metodą jest pushScreen(). W naszej aplikacji główną klasą jest Notepad (Listing 1). Jak widać, punktem startowym aplikacji jest metoda main(), w której tworzony jest obiekt klasy Notepad i tym samym inicjalizowana jest cała aplikacja.
}
}
notepad.enterEventDispatcher();
Listing 2. Metoda createMainScreen() tworząca układ i wygląd głównego ekranu aplikacji public MainScreen createMainScreen() {
initMainScreenAndComponents();
addComponentsToMainLayoutManager(); addLayoutManagerToMainScreen();
}
return mainScreen;
Listing 3. Inicjalizacja VerticalFieldManagera oraz dodawanie do niego komponentów private void addComponentsToMainLayoutManager() {
mainVfm = new VerticalFieldManager(Manager.VERTI CAL_SCROLL);
mainVfm.setMargin(10, 0, 0, 10);
Tworzenie interfejsu użytkownika
BlackBerry API udostępnia szeroki wachlarz komponentów służących do budowania interfejsu użytkownika. Klasy komponentów znajdują się w pakiecie
);
mainVfm.add(notesText);
mainVfm.add(saveButton); }
mainVfm.add(vfm);
net.rim.device.api.ui.
www.sdjournal.org
37
PROGRAMOWANIE JAVA
W naszej przykładowej aplikacji za zarządzanie komponentami UI odpowiada klasa UICreator. Klasa ta posiada pola będące referencjami do poszczególnych komponentów oraz jedną metodę publiczną createMainScreen() służącą do stworzenia głównego ekranu aplikacji. Metoda ta przedstawiona jest na Listingu 2. Z klasy UICreator korzysta główna klasa aplikacji Notepad. Jak każde API dostarczające komponentów do tworzenia UI, platforma BlackBerry zawiera komponenty do rozmieszczania innych komponentów na ekranie. Są to tzw. managery (layout managers) i w naszej aplikacji wykorzystamy dwa ich podstawowe rodzaje. Będzie to HorizontalFieldManager (jak sama nazwa wskazuje, rozmieszcza elementy poziomo) i VerticalFieldManager (rozmieszcza pionowo). Spójrzmy na fragment kodu, gdzie wykorzystane jest użycie tych managerów (Listing 3).
Listing 3 pokazuje inicjalizację głównego managera layoutu zawierającego wszystkie pozostałe komponenty graficznego interfejsu użytkownika. Użyta stała Manager.VERTICAL_SCROLL powoduje pojawienie się paska przewijania w sytuacji, gdy wyświetlane dane nie mieszczą się na ekranie. Ustawiony margines mainVfm.setMargin(10, 0, 0, 10); oznacza przesunięcie całej zawartości ekranu w pionie i poziomie o 10 pikseli, tak aby komponenty UI nie były wyświetlane tuż przy krawędzi ekranu. Lista dostępnych komponentów interfejsu użytkownika jest podobna do tej, którą oferują konkurencyjne platformy (Android OS). Znajdziemy tam: komponenty odpowiedzialne za przyciski (buttony), pola tekstowe, listy rozwijane, tzw. radio buttony, check-boxy i inne. Na Listingu 4 pokazujemy inicjalizację większości komponentów użytych w aplikacji. Z listingu tego widać podobieństwa do biblioteki Swing. Akcje wykonywane po naciśnięciu komponentu ButtonField (przycisk) obsługują listenery. W tym konkretnym przypadku jest to FieldChangeListener. Występujący w prawie każdej bibliotece GUI element Dialog odpowiedzialny jest za wyświetlenie okna dialogowego z odpowiednim komunikatem. Jego rola na platformie BlackBerry jest taka sama. Z Listingu 3 i Listingu 4 wyłania się powoli całokształt prostego interfejsu użytkownika. Głównym konListing 4. Inicjalizacja VerticalFieldManagera oraz dodawanie do niego komponentów private void initMainScreenAndComponents() {
mainScreen = new MainScreen();
mainScreen.setTitle(new LabelField("Notatnik")); notesText = new EditField("Notatka: ", ""); saveButton = new ButtonField("Zapisz"); saveButton.setChangeListener(new
FieldChangeListener() {
public void fieldChanged(Field field, int
context) {
NotesInfo notesInfo = new NotesInfo();
notesInfo.setElement(notesText.getText()); pm.persist(notesInfo); Dialog.inform("Zapisano!"); }
notesText.setText(null);
});
Rysunek 1. Ogólny widok interfejsu użytkownika
38
}
vfm = new VerticalFieldManager();
8/2010
Java na BlackBerry
tenerem jest VerticalFieldManager, który posiada trzy elementy: pole tekstowe z opisem, button do zapisu notatki oraz zagnieżdżony VerticalFieldManager służący do wyświetlenia w formie pionowej listy zaciągniętych ze źródła danych notatek. Efekt tych ustawień widać na Rysunku 1.
Menu telefonu
Programowanie aplikacji na smartphony bardzo częListing 5. Komponent MenuItem tworzący element w menu: „Pobierz Notatki” private MenuItem getItem = new MenuItem("Pobierz
notatki", 110, 11) {
public void run() {
Vector data = pm.load(); boolean dataWasLoaded = !data.isEmpty();
sto wiąże się z koniecznością zaprogramowania tzw. menu telefonu, mającego najczęściej postać listy rozwijanej pokazującej się z boku ekranu. Również i platforma BlackBerry posiada API pozwalające na stworzenie menu. W naszej aplikacji za obsługę menu odpowiada klasa UICreator. Posiada ona pole typu MainItem odpowiadające za stworzenie jednego elementu w menu telefonu. Na Listingu 5 jest przedstawiony kod tej klasy. Możemy tam znaleźć kilka interesujących fragmentów kodu. Kod w metodzie run() wykonuje się po wybraniu pozycji w menu. Zmienna pm jest referencją do PersistentManagera, klasy omówionej dokładnie w dalszej części artykułu, a służącej do zarządzania trwałością obiektów (notatek). Widzimy tam zaciągnięcie ze źródła danych wszystkich dotychczas zapisanych notatek, wykonanie iteracji, w końcu wyświetlenie notatek na ekranie po uprzednim umiejscowieniu ich w
if (dataWasLoaded) {
}
}
displayNotes(data);
private void displayNotes(Vector data) {
Enumeration enumeration = data.elements(); vfm.deleteAll(); index = 1; while (enumeration.hasMoreElements()) {
}
}
putNotesToLayoutManager(enumeration);
notesText.setText(null);
private void putNotesToLayoutManager(Enumeratio
n enumeration) {
NotesInfo nf = (NotesInfo) enumeration.next Element();
hfm = new HorizontalFieldManager(); hfm.setMargin(10, 0, 0, 0);
hfm.add(new LabelField(index++ + "."));
hfm.add(new LabelField(nf.getElement()));
}
};
}
vfm.add(hfm);
www.sdjournal.org
Rysunek 2. Menu telefonu z opcją „Pobierz notatki”
39
PROGRAMOWANIE JAVA
HorizontalFieldManager.
Te zadania oczywiście realizują dwie prywatne funkcje displayNotes(Vector data) i putNotesToLayoutManager(Enumeration enumeration). Na Rysunku 2 jest przedstawiony widok aplikacji z rozwiniętym menu telefonu.
Listing 6. Klasa jedynej encji w aplikacji - NotesInfo public final class NotesInfo implements Persistable { public static final int ID = 0;
public static final int NAME = 1; private Vector elements;
Zapis danych
Jedną z podstawowych funkcji praktycznie każdej aplikacji jest przechowywanie danych, na których aplikacja działa. BlackBerry umożliwia zapis danych w systemie i odtwarzanie ich zarówno po ponownym uruchomieniu aplikacji, jak i po restarcie całego systemu operacyjnego (restart telefonu). Od wersji JDE 5.0 istnieje możliwość zapisu danych w bazie danych SQLite. Ten niewielki silnik bazodanowy znany jest także m.in. z systemu Android i świetnie sprawdza się w zastosowaniach mobilnych. Na platformie BlackBerry, każdy obiekt, który chcemy zapisać, musi implementować interfejs Persistable. W naszym projekcie klasą encji jest NotesInfo przechowująca informację o zapisanym tekście notatki. Na Listingu 6, z racji jego niewielkiej objętości, umieściliśmy kod całej klasy. Oczywiście klasa ta opakowuje kilka funkcjonalności. Na przykład zmienna typu Vector może służyć do przechowywania kilku właściwości danej notatki. W naszym przykładzie korzystamy jednak tylko z zapisu jednej właściwości – samego tekstu notatki. Samą logiką zapisu zajmuje się stworzona przez nas klasa PersistentManager. Zawiera ona odwołanie do dostarczonej przez BlackBerry klasy PersistentObject. Stanowi ona faktyczne opakowanie dostępu do źródła danych i interfejs komunikacji z nim. Sam obiekt uzyskuje się, korzystając z klas API: PersistentObject store = PersistentStore.getP ersistentObject(0xdec6a67096f133cL);
W metodzie getPersistentObject(long key) użyliśmy dedykowanego klucza identyfikującego. Każdy obiekt zapisywany do źródła danych (czyli w naszym przypadku – NotesInfo) musi mieć unikalny klucz jednoznacznie identyfikujący go w źródle danych i pozwalający na wykonywanie na nim akcji zapisu lub pobrania. Bazując na tych założeniach, utworzyliśmy dwie metody w PersistentManager – jedną do zapisu notatki, drugą do jej odczytu. Pokazane jest to na Listingu 7.
public NotesInfo() {
elements = new Vector(1); for (int i = 0; i < elements.capacity(); ++i) {
}
}
elements.addElement(new String(""));
public String getElement() {
}
return (String) elements.elementAt(NAME);
public void setElement(String value) {
}
}
elements.addElement(value);
Listing 7. Metody do zapisu i odczytu notatek ze źródła danych public void persist(NotesInfo notesInfo) {
data.addElement(notesInfo); synchronized (store) {
store.setContents(data);
}
}
store.commit();
public Vector load() {
synchronized (store) {
}
}
data = (Vector) store.getContents();
return data;
W Sieci • • • • • •
40
Eclipse IDE do ściągnięcia: http://www.eclipse.org/downloads/ BlackBerry Plugin do Eclipse'a: http://na.blackberry.com/eng/developers/javaappdev/devtools.jsp BlackBerry JDE – różne wersje: http://na.blackberry.com/eng/developers/javaappdev/javadevenv.jsp Dostępne emulatory: http://na.blackberry.com/eng/developers/resources/simulators.jsp Opis instalacji i konfiguracji środowiska developerskiego: http://na.blackberry.com/developers/resources/A1_Setting_up_necessary_tools_v5.0.pdf Dokumentacja BlackBerry API ver. 4.5 (JavaDoc): http://www.blackberry.com/developers/docs/4.5.0api/index.html
8/2010
������������������������
Wykorzystane są tu metody PersistentObject do ustawienia i komitowania danych (setContents() i commit()) oraz do zaciągnięcia wcześniej zapisanych danych (getContents()). Metody te używamy w odpowiednich akcjach wywoływanych na zdarzenie kliknięcia na ButtonField lub element menu. W pierwszym przypadku kod persystencji danych (pm.persist(data);) znajduje się w metodzie listenera i jest widoczny na Listingu 4. Kod odczytu (Vector data = pm.load();) jest umiejscowiony w metodzie run() klasy MenuItem, co wcześniej już pokazaliśmy na Listingu 5.
���������������� ��������������������
Krótkie podsumowanie
Platforma BlackBerry oferuje programistom wygodny interfejs do tworzenia aplikacji. Można tak powiedzieć już o JDE w wersji 4.5, w której utworzyliśmy prostą aplikację. Pełnię możliwości widać jednak w JDE 5.0. BlackBerry API z pewnością zostanie jeszcze rozszerzona w wersji 6.0 (firma RiM wypuściła bowiem w roku 2010 wersję 6.0 systemu operacyjnego). Sam sposób tworzenia aplikacji bardziej przypomina programowanie w standardzie J2Me. Architektura aplikacji różni się znacznie od np. coraz bardziej popularnej platformy Android OS. Nie ma wsparcia dla deklaratywnego definiowania interfejsu użytkownika. Z drugiej strony, od najwcześniejszych wersji systemu i JDE występowało wsparcie dla bluetooth'a czym nie może pochwalić się Google Android. Oczywiście API umożliwia korzystanie z funkcjonalności charakterystycznych dla telefonów BlackBerry, takich jak push data. Dostępne jest również oprogramowanie usług takich jak GPS, bluetooth, PIM (Personal Information Management), Mobile Media API i inne. Najciekawszym, zwłaszcza dla developerów, zagadnieniem jest możliwość umieszczania swoich aplikacji w internetowym serwisie (sklepie) oraz ich sprzedaż. Podobne rozwiązania posiadają konkurencyjne platformy iPhone i Android. Platformą sprzedaży w przypadku BlackBerry jest BlackBerry App World zawierająca aplikacje darmowe i płatne z przeróżnych kategorii. Wydaje się, że rozbudowane API, prosty sposób budowania aplikacji w połączeniu z możliwością ich zarobkowej sprzedaży jest wystarczającą zachętą do rozpoczęcia pisania własnych programów.
TOMASZ MILCZAREK, Prowadzi własną �rmę JCode. Pracuje jako konsultant Java EE przy projektach głównie z sektora telekomunikacyjnego. Oprócz Javy korporacyjnej w wolnych chwilach rozwija swoje umiejętności z zakresu technologii mobilnych. W szczególnym centrum jego zainteresowań znajdują się Google Android OS i platforma BlackBerry. Kontakt z autorem: tomasz.milczarek@gmail.com
www.sdjournal.org
������������������������������� �������������������������� ��������������������������� ��� ����������������������������������� ����������������������������������������� ������������������������������������� ������������������������������������������� ��������������������������������������� ������������������������������������ ����������������������������������������� ������������������������������������� � ���������������������������������������� �������������������������������������� ����������������������������������������������� ���������������������������������� ����������������������������������������� ����������������������������������� ������������������������������������ ���������������������������������������� ������������������������������������������� ��������������������������������������� ��������������������������������������� �������������������������������������� ������������������������������������������� �������������������������������� ����������� ����������������������������������������� ����������������������������������������� ����������������������������������� � �������������������������������������� ���������������������������������������� ��������������������������������������������� ����������������������������� ������������������������ �������������� ����������������� ��������������������������������������
PROGRAMOWANIE JAVA
Wiosna z drugą twarzą w chmurach – część I Kompletny przykład procesu wytwarzania aplikacji Java Server Faces 2.0 z użyciem Spring Framework i Hibernate wraz z jej końcowym wdrożeniem na chmurę obliczeniową Amazon Elastic Compute Cloud (EC2). Dowiesz się:
Powinieneś wiedzieć:
• Jak zainstalować środowisko programistyczne wraz z serwerem aplikacji; • Jak pobrać i zainstalować wymagane biblioteki; • Jak stworzyć prostą, kompletną i działającą aplikację JSF 2.0 w nowym środowisku.
• Znać podstawy języka Java oraz technologii Java EE; • Znać podstawy JSF 1.2; • Znać podstawy HTML-a.
A
42
rtykuł jest pierwszym z serii artykułów prezentujących kompletny proces wytwarzania aplikacji webowej w technologii Java Server Faces (JSF) w wersji 2.0. Założeniem autora jest pokazanie tego procesu od podstaw wraz ze wszystkimi wymaganymi krokami w taki sposób, aby Czytelnik mógł wykonać je samodzielnie. Czytelnik będzie śledził kolejne etapy powstawania aplikacji – na każdym
etapie autor będzie dorzucał kolejną „cegiełkę”, aby finalnie otrzymać gotową w pełni funkcjonalną aplikację przygotowaną do wdrożenia na serwerze. Główne etapy wytwarzania aplikacji obejmą:
Rysunek 1. Katalog instalacyjny Java JDK
Rysunek 2. Katalog instalacyjny Springsource Tool Suite
1. Konfigurację środowiska programistycznego wraz z niezbędnymi bibliotekami oraz uruchomienie tradycyjnego przykładu „Hello World”. 2. Charakterystyka JSF 2.0 – porównanie do wersji JSF 1.2, najciekawsze zmiany (w tym zintegrowane Facelety, wbudowane wsparcie dla Ajax, zmieniony sposób ładowania zasobów, wsparcie dla GET)
8/2010
Wiosna z drugą twarzą w chmurach – część I
3. Bezpieczeństwo aplikacji webowej – autoryzacja użytkownika oraz zasady bezpieczeństwa dostępu do zasobów w oparciu o Spring Security Framework. 4. Integracja z bazą danych – persystencja przy użyciu narzędzi ORM, wykorzystanie Spring Framework. 5. Wdrożenie na lokalny serwer oraz wdrożenie na chmurę obliczeniową Amazon.com
aktualną wersją jest wersja 2.0 z 2009 roku, chociaż dość popularna jest także wersja 1.2 z roku 2006.
Przykładowa aplikacja
Celem niniejszego artykułu jest pokazanie, jak szybko stworzyć szkielet działającej aplikacji w oparciu o JSF 2.0. Czytelnik zapozna się ze wszystkimi kro-
Od kilku lat obserwujemy nieustanny wzrost popularności aplikacji webowych. Idea, jaka za tym stoi, jest prosta: klient nie musi mieć na komputerze niczego poza przeglądarką internetową. Serwisy internetowe wyewoluowały z prostych informacyjnych stron do bogatych interaktywnych aplikacji uruchamianych w przeglądarce. Powstałe i wciąż powstające technologie widoku oferują graficzne komponenty i zachowanie jeszcze do niedawna zarezerwowane aplikacjom typu desktop. Można powiedzieć, że aplikacje webowe coraz bardziej stają się podobne do swoich odpowiedników typu desktop Rysunek 3. Wybór katalogu instalacji w kontekście możliwości. Jedną z dostępnych technologii wytwarzania aplikacji webowych jest Java Server Faces (JSF). Ma ona z założenia łączyć pisanie aplikacji serwerowych Javy z szybkim wytwarzaniem bogatego interfejsu użytkownika. Innymi słowami: jest czymś na wzór „Swinga dla aplikacji serwera”. Główne cechy JSF to: • • •
zbiór gotowych komponentów interfejsu użytkownika model programowania oparty o zdarzenia możliwość tworzenia własnych dodatkowych komponentów
Technologia JSF jest dostępna od roku 2004, kiedy to powstała wersja 1.0. Obecnie Rysunek 4. Wybór katalogu z zainstalowaną Javą JDK
www.sdjournal.org
43
PROGRAMOWANIE JAVA
kami, jakie są niezbędne, żeby wykonać i uruchomić przykładową aplikację na serwerze. Jako środowisko programistyczne (IDE) wykorzystany zostanie Springsource Tool Suite (STS). Środowisko to zawiera wbudowaną specjalną wersję Tomcata (nazywaną „tc Server”) dla aplikacji w Javie, które wykorzystują Spring Framework. Ponieważ Spring nie będzie na razie wykorzystywany, wbudowany Tomcat będzie służył jako tradycyjny serwer, na którym będzie wdrażana i uruchamiana przykładowa aplikacja.
Instalacja środowiska
Zanim przystąpimy do pracy ze Springsource Tool Suite, potrzebujemy zainstalować Java JDK. Dla za-
Rysunek 5. Postęp instalacji
Rysunek 6. Kon�guracja katalogu roboczego..
44
chowania porządku proponuję na dysku C utworzyć katalog „Development”, a w nim w katalogu Java zainstalować JDK. Całość powinna wyglądać tak (Rysunek 1). Następnie przygotujmy katalogi dla Springsource Tool Suite w katalogu Development. Będą to dwa katalogi: instalacyjny dla samego IDE o nazwie „SpringToolSuite” oraz katalog roboczy (workspace) dla tego środowiska o nazwie „SpringToolSuite_Workspace”. Wygląda to tak (Rysunek 2). Czas na pobranie Springsource Tool Suite. Udajemy się do strony http://www.springsource.com/ products/springsource-tool-suite-download i tam po rejestracji otrzymujemy możliwość ściągnięcia środowiska. Wybieramy instalator dla Windows w wersji 32 bitowej (w momencie pisania tego artykułu plik instalatora miał nazwę springsource-tool-suite2.3.2.RELEASE-e3.5.2-win32-installer.exe). Zapisujemy nasz plik instalatora i uruchamiamy go. Instalator składa się z 8 kroków (pokażę te najbardziej istotne). Krok 1 i 2 to ekran powitalny i wyświetlenie licencji. Krok 3 jest ekranem wyboru katalogu docelowego instalacji środowiska. Wybieramy utworzony uprzednio katalog „SpringToolSuite” z C: \Development: (Rysunek 3). W kroku 4 instalator zapyta nas o komponenty, które chcemy zainstalować. Domyślnie wszystkie są zaznaczone i tak je pozostawiamy. W kroku 5 instalator pyta nas o lokalizację instalacji JDK. Zwróćmy uwagę, że instalator wyświetla informację, mówiącą że potrzebna jest JDK, a nie JRE. Wskazujemy Javę z katalogu C: \Development\Java: (Rysunek 4). Krok 6 to instalacja środowiska wraz z wyświetlanym postępem prac: (Rysunek 5). Krok 7 to tworzenie skrótów w Menu Start, natomiast w kroku 8 możemy zdecydować, czy chcemy uruchomić środowisko po instalacji. Uruchamiamy je i na początku jesteśmy pytani o wybranie domyślnego katalogu roboczego środowiska. Wskazujemy uprzednio utworzony katalog „SpringToolSuite_Workspace” w lokalizacji C:\Development: (Rysunek 6).
8/2010
Wiosna z drugą twarzą w chmurach – część I
Środowisko jest zainstalowane i gotowe do pracy. Zwróćmy uwagę, że w zakładce „Servers” na dole pod nazwą „SpringSource tc Server v6.0” widoczny jest Tomcat. Możemy go uruchomić z poziomu środowiska i sprawdzić, czy faktycznie działa, wpisując po uruchomieniu w przeglądarce adres http: //localhost:8080/. Podczas pierwszego uruchomienia serwera środowisko zapyta nas, czy chcemy uruchomić aplikację o nazwie „Insight”, która monitoruje serwer oraz aplikacje na nim działające. Uruchomienie tej aplikacji nie jest obowiązkowe.
Hello World w JSF 2.0
Mamy zainstalowane i działające środowisko, można przystąpić do tworzenia najprostszej przykładowej
aplikacji JSF 2.0. Powstanie przykładu będzie możliwie najprostszym procesem, aby nie komplikować sprawy, nie używam tutaj Mavena, który jest zintegrowany ze środowiskiem. Cały proces przedstawię w kolejnych krokach.
Krok 1. Projekt w IDE
Tworzymy nowy dynamiczny projekt webowy, który będzie uruchamiany za pomocą Tomcata. Jest on dostępny w sekcji "web" menu File->New->Project: (Rysunek 7). Na kolejnych ekranach przechodzimy dalej, niczego nie zmieniając, aż do wybrania opcji „Finish”. Na końcu powinniśmy otrzymać strukturę podobną do: (Rysunek 8).
Rysunek 7. Nowy projekt aplikacji webowej
www.sdjournal.org
45
PROGRAMOWANIE JAVA
Listing 1. Deskryptor wdrożenia web.xml dla przykładowej aplikacji <?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://
java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
<display-name>JSFSample</display-name> <servlet>
<servlet-name>Faces Servlet</servlet-name>
<servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>Faces Servlet</servlet-name> <url-pattern>/faces/*</url-pattern>
</servlet-mapping> <welcome-file-list>
<welcome-file>faces/index.xhtml</welcome-file>
</welcome-file-list>
</web-app>
Krok 2. Instalacja niezbędnych bibliotek
Będziemy potrzebować bibliotek JSF 2.0 (używamy implementacji MyFaces) oraz JSTL. Biblioteki MyFaces są dostępne na stronie http://myfaces.apache.org/ download.html (wersja MyFaces Core 2.0.0 (zip)) jako pojedynczy plik .zip, natomiast JSTL znajduje się na stronie https://jstl.dev.java.net/ (pobieramy 2 pliki .jar: jstl-api-1.2.jar oraz jstl-impl-1.2.jar). Listing 2. Plik WelcomeUserBean.java
Listing 3. Plik HelloMessageBean.java /** *
*/
package com.jsfsample;
import javax.faces.bean.ManagedBean;
import javax.faces.bean.RequestScoped; /**
* @author Pawel
/**
*
*
*/
*/
package com.jsfsample; import javax.faces.bean.ManagedBean;
@ManagedBean(name="messageBean") @RequestScoped
public class HelloMessageBean {
import javax.faces.bean.RequestScoped;
private String name;
/**
public String getName() {
* @author Pawel *
}
*/
@ManagedBean(name="welcomeBean")
public void setName(String name) {
public class WelcomeUserBean {
}
@RequestScoped
}
46
return name;
this.name = name;
public String sayHello(){
public String goBack(){
}
}
return "message";
}
return "welcome";
8/2010
Wiosna z drugą twarzą w chmurach – część I
Implementacja MyFaces ma tę przewagę nad implementacją referencyjną, że jest pozbawiona kilku uciążliwych bugów oraz posiada szereg ulepszeń i usprawnień. Pliki jar z obydwu archiwów kopiujemy fizycznie do katalogu WEB-INF\lib i odświeżamy widok projektu w środowisku (naciskając F5 na całym projekcie). Biblioteki automatycznie pojawią się w sekcji „Web App Libraries”. Jest to najprostsza metoda dodania bi-
bliotek. Alternatywną metodą jest stworzenie bibliotek użytkownika na podstawie powyższych plików .jar i dodanie ich do projektu. W przypadku korzystania z Mavena zarządzanie zależnościami bibliotek spadłoby na niego.
Rysunek 8. Struktura projektu
Rysunek 9. Struktura kompletnego projektu
Listing 4. Strona welcome.xhtml <?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>JSFSample</title>
</h:head> <h:body>
<h:form>
Hello, what is Your name?<br />
<h:inputText value="#{messageBean.name}" /><h:commandButton value="Next" action="#{welcomeBean.sayHello}" </h:form>
/>
</h:body> </html>
www.sdjournal.org
47
PROGRAMOWANIE JAVA
Listing 5. Strona message.xhtml <?xml version='1.0' encoding='UTF-8' ?>
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1transitional.dtd">
<html xmlns="http://www.w3.org/1999/xhtml"
xmlns:ui="http://java.sun.com/jsf/facelets" xmlns:h="http://java.sun.com/jsf/html">
<h:head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8" /> <title>Welcome :)</title>
</h:head> <h:body>
<h:form>
Hello, nice to meet You <h:outputText value=" #{messageBean.name}"/>. <br />
<h:commandButton value="Back" action="#{messageBean.goBack}" /> </h:form>
</h:body>
</html>
Kącik eksperta
Stając przed wyborem stosu technologii dla nowo-powstającego projektu webowego, stajemy przed jedną z najtrudniejszych decyzji architektonicznych. W świecie Javy wybór przestaje być przywilejem z uwagi na mnogość podejść i rozwiązań. W przypadku wyboru stosu frameworków obowiązuje taka sama zasada jak w przypadku każdego nietrywialnego problemu – wszystko zależy od kontekstu. W niniejszej serii artykułów prezentujemy sprawdzony zestaw technologii, który ma zastosowanie w następującym kontekście: • • •
•
•
Aplikacja webowa wspiera procesy biznesowy – dlatego zakładamy, że większość ekranów będą stanowić formularze. W tym wypadku JSF stanowi bardzo produktywne narzędzie do tworzenia tego typu widoków. Dodatkowo wersja 2 jest pozbawiona większość ograniczeń wersji 1.x oraz posiada natywne wsparcie dla AJAX. Spring Framework integruje dwa frameworki webowe: Spring Web MVC oraz Spring Web Flow, które abstrahują od warstwy prezentacji. Dlatego zawsze mamy możliwość dodania (lub całkowitej wymiany) technologii prezentacji w razie potrzeb (np frameworka lżejszego niż JSF). Spodziewamy się dużej ilości operacji CRUD (Create Read Update Delete) – dlatego stosujemy maper relacyjno-obiektowy (ORM), który drastycznie zwiększa produktywność w aplikacjach tej klasy. Oczywiście zawsze możemy posłużyć się natywnym SQL w razie potrzeby – mamy wsparcie ze strony Spring Framework. W kolejnych częściach zaprezentujemy architekturę Command-query Responsibility Segregation, gdzie wykorzystany będą oba podejścia: ORM i natywny SQL. Spring Framework to sprawdzony zestaw bibliotek, który w odróżnieniu od innych tego typu rozwiązań charakteryzuje się bardzo wysoką jakością (zarówno na poziomie kodu, jak i na poziomie koncepcyjnym), co w krytycznych projektach ma niebagatelne znaczenie. Spring możemy wykorzystać do wielu celów, natomiast w tej serii zaprezentujemy jego główną funkcjonalność – wsparcie dla paradygmatu Inversion of Control. Spring wspiera trzy techniki IoC: Wstrzykiwanie zależności, Zdarzenia i Aspect Oriented Programming. W kolejnych częściach zaprezentujemy praktyczne wykorzystanie technik IoC oraz ich wpływ na powstanie systemu o architekturze otwartej na rozbudowę oraz podatnej na testy. Środowisko chmurowe jest alternatywą dla klasycznego skalowania znanego np. ze środowisk Java EE. Chmury zdobywają również popularność w biznesie ze względu na modę na wirtualne oprogramowanie. W niniejszej serii będzie opierać się na chmurze EC2, która posiada wsparcie ze strony Spring Tools Suite. EC2 oferuje jedynie zasoby sprzętowe – w odróżnieniu np od chmury Google, która oferuje również platformę developerską. Aktualnie jednak platformy developerskie posiadają szereg ograniczeń. Niedawno doszło jednak do porozumienia Google ze SpringSource, co mam nadzieje zaowocuje lepszym wsparciem chmury Google dla pełni wykorzystania Spring Framework (które na dzień dzisiejszy pozostawia wiele do życzenia).
Sławomir Sobótka Trener i konsultant w firmie Bottega – Akademia Java. slawomir.sobotka@bottega.com.pl http://art-of-software.blogspot.com
48
8/2010
Wiosna z drugą twarzą w chmurach – część I
Krok 3. Modyfikacje w pliku web.xml
Wygenerowany plik web.xml nie zawiera niczego poza listą plików domyślnie uruchamianych po wpisaniu adresu aplikacji w przeglądarce. Musimy zmodyfikować ten plik na potrzeby JSF. Nowa zawartość tego pliku przedstawia się następująco: (Listing 1).
Krok 4. Tworzenie klas komponentów (beans)
Najpierw w katalogu „src” tworzymy pakiet „com.jsfsample”, a w nim umieszczamy dwie klasy : WelcomeUserBean.java oraz HelloMessageBean.java widoczne na kolejnych listingach (Listing 2, 3). Za pomocą adnotacji @ManagedBean zdefiniowaliśmy rolę każdej z klas wraz z nazwą, która pozwoli na odwołanie się do obiektu danej klasy z poziomu strony .xhtml. Adnotacja @RequestScoped określa czas życia (zasięg) danego beana – w tym wypadku będzie to request. W klasach znajdują się dwie metody sayHello() oraz goBack(), które będą odpowiedzialne za nawigację pomiędzy stronami, które za chwilę stworzymy.
Krok 5. Proste strony xhtml
Mamy klasy beanów, w których w bardzo prosty sposób przechowujemy dane oraz używamy ich do prostej nawigacji. Teraz stworzymy pliki stron, które będą korzystać z beanów poprzez zapisywanie i wyświetlanie zawartych w nich danych. Metody nawigacyjne zawarte w beanach będą wykorzystane do przechodzenia między stronami. Dla beana WelcomeUserBean.java stworzymy stronę o nazwie „welcome.xhtml” widoczną na listingu poniżej: (Listing 4). Dla beana HelloMessageBean.java strona .xhtml będzie miała postać: (Listing 5). Zwróćmy uwagę na sposób odwoływania się ze stron .xhtml do instancji danego beana i jego składowych (zmiennych, metod). Używając języka wyrażeń (expression language, EL) i w połączeniu z konwencją JavaBeans, możemy uzyskać dostęp do konkretnego beana i jego składników. Za przykład niech posłuży wyświetlenie wpisanego uprzednio imienia na stronie message.xhtml za pomocą znacznika: <h:outputText value=" #{messageBean.name}"/>
Wyrażenie „messageBean” to nic innego jak umowna nazwa naszego beana (klasy) HelloMessageBean.java podana w adnotacji @Mana gedBean(name ="messageBean"), natomiast „name” to zmienna instancyjna (pole) w tej klasie. Zgodnie z konwencją JavaBeans próba pobrania wartości z tego pola zaowocuje wywołaniem metody getName() z beana HelloMessageBean.java
www.sdjournal.org
Uruchomienie
Przykładowa aplikacja jest gotowa. Struktura projektu wraz ze wszystkimi stworzonymi i dodanymi plikami powinna wyglądać tak: (Rysunek 9). Możemy wdrożyć ją na serwer poprzez kliknięcie prawym przyciskiem myszy na całym projekcie i wybranie z menu kontekstowego opcji Run As –> Run on Server. Pojawi się okno, w którym IDE zapyta, na jakim serwerze chcemy uruchomić aplikację. Ponieważ nie posiadamy zdefiniowanych innych serwerów poza „SpringSource tc Server v6.0”, wybieramy ten i klikamy „Next”. Kolejne okno pokazuje wszystkie dostępne aplikacje z możliwością uruchomienia ich na wybranym serwerze. Tutaj niczego nie zmieniamy i klikamy „Finish”. Ta operacja spowoduje uruchomienie serwera i wdrożenie naszej przykładowej aplikacji. Po chwili wystarczy wpisać w przeglądarkę adres http://localhost:8080/JSFSample/, aby zobaczyć działający przykład.
Podsumowanie
Powyższy przykład pokazuje, jak w szybki sposób stworzyć najprostszą możliwą aplikację JSF 2.0 i uruchomić ją na zintegrowanym w tym wypadku serwerze Tomcat. Główny nacisk położyłem tutaj na szybkie wręcz „wyklikanie” działającego projektu z minimalną konfiguracją. Dla uproszczenia omawianego przykładu nie używam tutaj żadnych nowości, które oferuje nam JSF 2.0 w porównaniu do JSF 1.2 (jedyna użyta rzecz to brak pliku faces-config.xml, czyli konfiguracja za pomocą adnotacji i prosta nawigacja bezpośrednio w beanach). Także celowo pominąłem użycie Mavena do zarządzania zależnościami i strukturą projektu, aby nie opisywać dodatkowo specyficznych dla Mavena kroków konfiguracyjnych w tak prostym przykładzie. Przykład ten jako działająca aplikacja może z powodzeniem służyć do dalszych własnych eksperymentów z technologią JSF 2.0.
PAWEŁ NIEŚCIORUK Autor jest programistą Java. W pracy zawodowej bierze udział w analizie, projektowaniu oraz wytwarzaniu webowych aplikacji w środowisku Java EE z użyciem najnowszych frameworków. Współpracuje także z �rmą Bottega. Prywatnie interesuje się całościowym podejściem do wytwarzania aplikacji webowych oraz bazami danych i ich optymalizacją. Prowadzi autorskiego bloga poświęconego Javie i programowaniu, który jest rodzajem notatnika ze wskazówkami dla początkujących programistów. Kontakt z autorem: pawel.niescioruk@gmail.pl, blog: http://technology-for-human.blogspot.com
49
NIEZAWODNE OPROGRAMOWANIE
Testowanie gier na urządzenia mobilne Testowanie gier jest niezwykle złożoną materią, zwłaszcza przy dużych, wysokobudżetowych produkcjach. Gry na urządzenia mobilne zwykle do takich nie należą, co nie oznacza, że proces testowania jest tu mało istotny czy możliwy do pominięcia. Jaka jest specyfika tego procesu, dowiecie się z niniejszego artykułu. Dowiesz się:
Powinieneś wiedzieć:
• • • •
• Co to jest gra komputerowa. • Co to jest urządzanie mobilne.
Skąd się biorą błędy; Kiedy i jak testować grę; Jakie są specyficzne aspekty testowania gier mobilnych; Jakie ograniczenia napotkasz przy testowaniu gier na niektóre konsole.
T
estowanie oprogramowania, a więc także i gier to, jak można przeczytać w jednej z wielu różnych definicji: używanie aplikacji w kontrolowany sposób i ocena rezultatów tego działania. Pojęcie kontrolowany sposób oznacza oczywiście zarówno warunki typowe, normalne, jak i warunki nietypowe. W czasie testowania należy świadomie starać się działać źle, aby stwierdzić, czy coś się dzieje, gdy nie powinno, albo nie dzieje, gdy powinno – działanie jest zorientowane na wykrywanie. Zapewnienie jakości to pojęcie szersze, obejmujące cały proces tworzenia oprogramowania. To zapewnienie, że wszystkie wymagane standardy i procedury są spełnione, oraz że znalezione problemy są odpowiednio naprawione – działanie nastawione bardziej na prewencję.
Dlaczego testowanie jest potrzebne?
Co jest misją testera, jakie są jego główne zadania? Pierwsze i najistotniejsze to oczywiście znajdowanie błędów, zwłaszcza tych najważniejszych. Ponadto tester powinien dokonywać ogólnej oceny testowanego produktu oraz sprawdzać, czy jest on zgodny z koniecznymi do spełnienia wymaganiami i standardami. Zbiór subiektywnych ocen różnych testerów pozwala na polepszenie ogólnej jakości produktu. Dodatkowymi zadaniami testerów są ulepszanie i usprawnianie procesu testowania, aby minimalizować potrzebny czas, koszt i efekty uboczne, czy po-
50
stępowanie zgodnie z wyznaczonymi zadaniami i zakresem pracy. Dla kogo tak naprawdę pracuje zespół testerów? Wydaje się, że głównie dla producenta gry – on odpowiada za końcowy kształt projektu i chce mieć pewność, że proces testowania przebiega sprawnie w każdej fazie, jest dokładny, skuteczny i gwarantuje dostarczenie produktu wysokiej jakości w wymaganym czasie. Można także stwierdzić, że dla programistów – przygotowując precyzyjne raporty i opisy błędów, umożliwia szybsze, łatwiejsze i mniej czasochłonne ich naprawienie. Dodatkowo można tu wymienić także inne aspekty pracy testerów, uzasadniające, że ich praca służy pośrednio także działowi marketingu czy zarządowi. Ale tak naprawdę, zwłaszcza w przypadku branży gier, wokół której się obracamy, zespół testerów służy graczom. Największą korzyścią z ich pracy nad produktem jest właśnie satysfakcja odbiorcy i jego zadowolenie z jakości finalnego produktu.
Skąd się biorą błędy i kiedy testować?
Skąd tak naprawdę biorą się błędy w grach? Najbardziej do ich powstawania przyczynia się złożoność oprogramowania. Złożoność współczesnych aplikacji jest trudna do pojęcia dla osoby bez doświadczenia w branży. Niektóre błędy pojawiają się na skutek braku lub słabej komunikacji, co prowadzi do niepełnego zrozumienia wymagań i nieprawidłowego ich spełnie-
8/2010
Testowanie gier na urządzenia mobilne
nia. Część błędów wynika po prostu z błędów programistycznych, które programiści, jak wszyscy inni ludzie, mogą popełniać. Jedną z ważniejszych przyczyn błędów, zwłaszcza w końcowych fazach projektu, jest zmiana wymagań – osoby stojące na zewnątrz projektu mogą nie zdawać sobie sprawy lub nie rozumieć, jakie skutki może przynieść żądanie określonych zmian. A może to wymagać przeprojektowania, przydzielenia innych osób realizujących, dodatkowej optymalizacji, i wtedy dokonywanie zarówno małych, jak i większych zmian może wpłynąć na wiele przewidywalnych i nieprzewidywalnych zależności, uprawdopodabniając powstanie w którymś miejscu błędu. Z dokonywaniem zmian, zwłaszcza w końcowej fazie projektu, związana jest presja czasu – to zjawisko na pewno nie pomaga pozbywaniu się błędów, lecz wręcz przeciwnie – sprzyja ich powstawaniu. I wreszcie całkowicie zewnętrzna przyczyna błędów, ale przy tworzeniu gier często niestety występująca – narzędzia wspomagające. Nie da się tworzyć gier bez dodatkowych narzędzi, edytorów, bibliotek czy middleware (oprogramowania pośredniczącego). Same te wszystkie dodatki zawierają w sobie błędy (które akurat przy użyciu ich do naszej aplikacji mogą się ujawnić) albo ich nie do końca poprawne użycie może takie błędy wprowadzić. Skoro wiadomo już, jakie są główne przyczyny błędów, to pada pytanie – kiedy testować? Nie ma na to jednoznacznej odpowiedzi. Wszystko zależy od tego, w jaki sposób prowadzony jest projekt, czy wszystkie jego istotne elementy realizowane są równolegle i składane w działającą całość w dalszej fazie, czy projekt rozwija się od małego prototypu, przez obudowywanie go dodatkowymi elementami. W procesie tworzenia gier bardzo często można wyróżnić fazy, których zakończenia (milestones) określane są nazwami first playable (grywalny prototyp, często jedynie podstawowa funkcjonalność i tymczasowy wygląd), wersja alpha (dodana znaczna część funkcjonalności i w dużej mierze finalny wygląd), wersja beta (pełna funkcjonalność i ostateczny wygląd, może zawierać jeszcze błędy), wersja release candidate (czyli kandydat do wypuszczenia na zewnątrz – praktycznie ukończona gra, bez braków i istotnych błędów, może zawierać drobne błędy, które są mało istotne albo nie zostały znalezione) i ostatecznie wersja gold master (po ostatecznych testach akceptacyjnych, gotowa do wydania, nie zawiera znanych i znalezionych błędów czy braków w funkcjonalności). Podany schemat jest oczywiście przykładowy i poszczególne projekty mogą stosować się do jego odmian. W zależności od tego, co w konkretnym projekcie zawiera każda z faz, można wybrać najlepszy czas na rozpoczęcie testów i określić ich zakres oraz intensywność. Zwykle na pierwsze testy związane stricte ze
www.sdjournal.org
znajdowaniem błędów najlepszy czas przychodzi po tym, jak gra osiągnie fazę alpha, czyli zawiera znaczną część końcowej funkcjonalności. Obszar i intensywność tych testów należy oczywiście zintensyfikować po osiągnięciu fazy beta, gdzie funkcjonalność jest już zwykle w pełni ukończona i przychodzi czas na naprawianie błędów, które do gry się zakradły. W przypadku gier jest jeszcze jedna istotna rzecz, która grę odróżnia od oprogramowania użytkowego czy korporacyjnego – tego typu oprogramowanie uważa się za dobre, jeśli wypełnia bezbłędnie funkcje, do jakich zostało stworzone. Do stworzenia dobrej gry to nie wystarczy, gra musi być przede wszystkim grywalna i atrakcyjna (wizualnie, dźwiękowo, funkcjonalnie). Dlatego istotne jest subiektywne zdanie zespołu testerów (chociaż nie tylko) odnośnie tego, jak gra wygląda, czy jest intuicyjna, funkcjonalna, czy poziom trudności i jego progresja są odpowiednio dobrane, czy wreszcie gra ma to coś, co powoduje, że gracz nie może się od niej oderwać i ciągle chce poświęcić jej jeszcze chwilkę.
Jak testować?
Jeśli wiadomo już mniej więcej, po co i od kiedy testować, należy odpowiedzieć sobie na pytanie – jak testować? Jakie metody testowania można wyróżnić, jakich narzędzi użyć, czy można proces w pełni zautomatyzować? W zasadzie prawie wszystkie rodzaje testów (a tych rodzajów i klasyfikacji jest bardzo dużo) można podzielić na testy grupy black box (czarnej skrzynki, czyli po polsku testy funkcjonalne) oraz white box (białej skrzynki, czyli testy strukturalne). Zasadniczą różnicą między nimi jest poziom, na którym działają testerzy. W przypadku testów funkcjonalnych testerów interesuje zachowanie się aplikacji przy zadanych warunkach wejściowych, a więc najprościej mówiąc, sprawdzenie działania aplikacji od strony funkcjonalnej, czyli zgodności jej działania z założeniami i wymaganiami. Tego typu testowanie nie wymaga znajomości kodu aplikacji, a jedynie obeznania z charakterystyką jej działania i stawianymi przed nią wymaganiami. Do tego typu testów można zaliczyć m.in. acceptance testing (testy akceptacyjne – mające na celu ostateczną akceptację produktu i potwierdzenie jego zgodności z założeniami), usability testing (testy użytkowe – które mają za zadanie sprawdzić np. przyjazność interfejsu dla użytkownika), install/uninstall testing (testy instalacji i deinstalacji, sprawdzające prawidłowość działania tych operacji), compatibility testing (testy kompatybilności z różnymi wersjami sprzętu lub systemu), exploratory testing (testy eksploracyjne – swobodne testowanie na zasadzie podążania za wiedzą z dokumentacji, bez planowania czy trzymania się planów testów) czy ad–hoc testing (testy doraźne – testowanie całkowicie bez planu, jak i dokumentacji).
51
NIEZAWODNE OPROGRAMOWANIE
Testy strukturalne patrzą na testy z punktu widzenia wewnętrznej struktury projektu i zajmują się sprawdzaniem poprawności działania wszelkich możliwych ścieżek w strukturze programu. Oczywiście wymaga to odpowiednio wysokich umiejętności programistycznych i dobrej znajomości kodu aplikacji. W przypadku konieczności prowadzenia takich testów, zajmują się tym wyznaczeni programiści lub przygotowani do tego inżynierowie testów. W branży gier, w odróżnieniu od branży oprogramowania korporacyjnego, przeważa zdecydowanie testowanie funkcjonalne, które skupia się na testowaniu produktu w taki sposób, jak końcowy odbiorca będzie go używał. Specjalną odmianą testów, popularną, często spotykaną i przynoszącą wiele korzyści, jest playtesting. Jego głównym celem nie jest znalezienie błędów, a sprawdzenie, jak różni odbiorcy postrzegają grę, jak ją oceniają, czy są w niej elementy frustrujące, co uważają za konieczne do zmiany. Istotnymi elementami są dobór osób, które stanowią grupę docelową (zdecydowanie nie powinny to być osoby związane z projektem i znające go, a ponadto powinna to być grupa zawierająca osoby pasujące do typu odbiorcy docelowego, do którego gra jest kierowana), oraz sposób przeprowadzenia testu i określenia jego wyników (tu pomocne jest współuczestniczenie z testującym w sesji testowej i notowanie na bieżąco jego spostrzeżeń, reakcji, komentarzy, ewentualnie udostępnienie mu szczegółowego formularza z pytaniami tuż po zakończeniu testu). Playtesting może być przeprowadzony także w trybie zewnętrznym, na grupie osób, które zadeklarują chęć przystąpienia do niego i udzielenia odpowiedzi na zadane pytania. Istotna jest faza projektu, w której tego typu testy są prowadzone. Ponieważ rezultaty playtestingu pozwalają na poznanie opinii grupy reprezentującej końcowych odbiorców, ich opinie powinny być uwzględnione w projekcie na tyle, na ile to możliwe. Dlatego testy takie powinny być przeprowadzone odpowiednio wcześnie, aby wystarczyło czasu na konieczne zmiany (które niekiedy mogą być nawet fundamentalne), ale na tyle późno, aby grupa testująca wypowiadała się i oceniała grę w postaci zbliżonej do ostatecznej (mechanika gry, jej logika, elementy graficzne i dźwiękowe powinny być w ostatecznej formie). Dobrze jest przeprowadzić kolejną rundę playtestingu po wprowadzeniu zmian na podstawie zgłaszanych uwag, aby sprawdzić, czy chociaż w pewnym stopniu wprowadzone zmiany przyniosły zakładany skutek. Jak wygląda kwestia automatyzacji testowania gier na platformy mobilne? Nie jest to takie proste. Automatyzacja jest stosowana dosyć powszechnie w przypadku chociażby aplikacji enterprise. Niestety specyfika gier jest taka, że bardzo duże znaczenie ma aspekt wizualny, odczucia towarzyszące grze czy jej ścisłe dopasowanie do konkretnego urządzenia (zarówno pod kątem
52
graficznym, wydajnościowym, jak i zgodności z funkcjonalnością urządzenia). Z tego wynika przewaga (i zwykle konieczność) manualnego testowania funkcjonalnego takich gier. Jednakże nie do końca jest tak, że nic się w zakresie automatyzacji nie dzieje. Warto przywołać tu dwa przykłady. Jeden z nich to system TestQuest. Pozwala on dosyć skutecznie testować aplikacje m.in. na zasadzie wywoływania odpowiednich akcji, które są określone przez skrypt testujący, a następnie porównywanie ekranów wyświetlanych przez urządzenie (czyli telefon) z ekranami wzorcowymi. Każde odstępstwo od wzorca może zostać zaznaczone jako błąd. Ograniczeniami tutaj są po pierwsze konieczność posiadania urządzeń, na których gra ma być testowana, a także istnienie tego rozwiązania jedynie dla aplikacji mobilnych działających w systemie operacyjnym Symbian. Inne ciekawe rozwiązanie to usługa Device Anywhere firmy Mobile Complete. Pozwala ona na zdalny dostęp do ponad tysiąca rzeczywistych, fizycznych telefonów pracujących w kilkudziesięciu sieciach operatorów. Już sam zdalny dostęp daje duże korzyści – nie ma konieczności posiadania wszystkich urządzeń, na których ma być przetestowana gra. Ponadto dzięki funkcji Test Automation można tworzyć i uruchamiać scenariusze testowe podobne do opisanych powyżej, działające na dowolnym telefonie dostępnym w bazie. Z rozwiązania tego można skorzystać, kiedy nie ma się dostępu do rzeczywistego urządzenia, na którym można przeprowadzić testy. A jak postąpić, kiedy nie ma się ani urządzenia, ani zasobów ludzkich do przeprowadzenia testów? Można skorzystać z pomocy jednej z wyspecjalizowanych firm testujących (test houses), zlecając przeprowadzenie testów według dostarczonego planu testów, albo testów precertyfikacyjnych, sprawdzających przygotowanie aplikacji do różnego rodzaju certyfikacji. Inne rozwiązanie to stosunkowo nowa usługa – mob4hire. Jest to serwis pośredniczący pomiędzy firmami i osobami fizycznymi, które posiadają telefony i mają chęć coś przetestować za określone przez siebie wynagrodzenie, a twórcami oprogramowania chcącymi takie testowanie według ustalonego przez siebie planu zlecić.
Kto może testować?
Na pozornie łatwe pytanie kto może zostać testerem gier? nie ma jednoznacznej odpowiedzi. Można oczywiście odpowiedzieć bez zastanowienia, że każdy (no może prawie każdy) – i będzie to poniekąd prawda. Ale na pewno nie każdy może być dobrym testerem. Z pewnością zarówno namiętny gracz, spędzający przed ekranem i ulubionymi grami co najmniej kilka godzin dziennie, jak i osoba, która z zasady nie gra lub gra sporadycznie, mogą w jakimś stopniu przyczynić się do tego, że finalny produkt będzie lepiej wykonany, przyniesie większą radość i mniej momentów frustracji koń-
8/2010
Testowanie gier na urządzenia mobilne
cowemu odbiorcy. Oczywiście wszystko zależy od tego, czego oczekujemy od każdej z tych osób. Na pewno nie można wymagać od gracza z przypadku, aby zajął się sprawdzeniem, czy gra jest możliwa do przejścia na najtrudniejszym poziomie, albo czy współczynniki postaci są prawidłowo dobrane. Takie zadania wykona zdecydowanie lepiej gracz nałogowy. Jeśli jednak nasza gra jest typową małą, lekką i przyjemną produkcją, planujemy skierować ją do szerokiej rzeszy odbiorców, to właśnie opinia przypadkowych graczy, którzy lubią czasami zagrać dla przyjemności w wolnej chwili, może być dla nas równie ważna, jak nie ważniejsza, zwłaszcza przy ocenie intuicyjności wykonania interfejsu użytkownika czy przejrzystości pomocy do gry. Czy można określić, jakimi cechami powinien wyróżniać się dobry tester? Oczywiście tak, różne źródła wymieniają listy cech i umiejętności, których posiadanie może wpłynąć pozytywnie na pracę testera. Bezsprzecznie tester powinien charakteryzować się dokładnością, dociekliwością i systematycznością. Osoba podchodząca do wszystkiego pobieżnie, niedokładna, chaotyczna, nie powinna zabierać się za tę profesję. Kolejne ważne cechy to umiejętność czytania i pisania. Zabrzmiało to oczywiście odrobinę humorystycznie, lecz nie chodzi tu oczywiście o stricte te umiejętności. Tester musi potrafić czytać nie powierzchownie, lecz bardzo dogłębnie, intensywnie, ze skupieniem i przede wszystkim z dbałością o szczegóły. Niejednokrotnie od zrozumienia dokumentacji, specyficznych wymagań czy oczekiwań, może zależeć właściwe podjęcie decyzji. A co z pisaniem? Ta cecha jest równie ważna, jeśli nie ważniejsza. I nie chodzi tu o stosowanie idealnych zasad gramatyki i ortografii, ale najważniejszą kwestią jest zdolność do skutecznego porozumiewania się w formie pisemnej. Kiedy tester znajduje problem, musi umieć opisać jego objawy w taki sposób, aby adresat nie miał najmniejszych wątpliwości. I często nie ma tu znaczenia, czy tester zobowiązany jest do pisania raportów w swoim ojczystym języku – chodzi tu bardziej o staranność i precyzję w wyrażaniu myśli. Jakie jeszcze inne cechy można wyróżnić? Można powiedzieć, że tester powinien mieć umiejętność psucia (testowanej aplikacji), patrzenia z punktu widzenia odbiorcy, dążenia do uzyskania jakości. Po części musi być dyplomatą, aby utrzymywać dobrą i przyjazną współpracę z resztą zespołu (któremu przecież musi wytykać błędy), dobrze jest też, gdy rozumie cały proces tworzenia aplikacji. Można w tym miejscu wpisać jeszcze wiele cech, umiejętności i zaleceń co do postępowania, jak chociażby dobra organizacja pracy, umiejętność zarządzania czasem i ustalania priorytetów, łatwość przyswajania nowej wiedzy i oczywiście prawie nigdy nie wyczerpie się tematu.
www.sdjournal.org
Specyfika testowania gier na platformy mobilne – JME/Symbian/BREW/iPhone
Gry na platformy mobilne od lat stanowią przeważającą część rynku testowania gier. Spowodowane jest to tym, iż takich gier powstaje bardo dużo i zwykle są to projekty małe lub co najwyżej średnie, o stosunkowo krótkim czasie produkcji. Do niedawna przeważającą część gier mobilnych stanowiły produkcje napisane w Java Platform Micro Edition – JME (dawniej J2ME). Zdecydowanie największym wyzwaniem przy testowaniu gier mobilnych napisanych w JME jest konieczność ich prawidłowego działania na ogromnej liczbie urządzeń, charakteryzujących się skrajnie różnymi możliwościami i parametrami technicznymi. Pozornie zadanie wydaje się łatwe – z założenia aplikacja JME powinna być zgodna z wymaganiami zawartymi w Unified Testing Criteria for Java(TM) Technology–based Applications for Mobile Devices. Wymagania te zostały stworzone przez porozumienie Unified Testing Initiative, w skład którego weszli najwięksi producenci telefonów komórkowych. W teorii to tylko kilkadziesiąt przypadków testowych, określających wymagania względem aplikacji, ale sytuacja nieco komplikuje się, jeśli jesteśmy zobowiązani wykonać testy na kilkuset urządzeniach. To jest właśnie największe wyzwanie w testowaniu aplikacji na urządzenia obsługujące JME. Dostosowanie przygotowanej aplikacji do działania na wielu różnych urządzeniach mobilnych z platformą JME (a więc głównie na ogromnej ilości różnych modeli telefonów komórkowych) nazywa się portingiem. Jest to bardzo duże wyzwanie zarówno dla programistów, jak i dla zespołu testerów. Przede wszystkim największą przeszkodą jest mnogość urządzeń. Listy modeli telefonów, które muszą być wspierane przez aplikację, sięgają często liczb rzędu kilkuset (500–600 nie należy do rzadkości). Jeśli uwzględnimy, iż aplikacja działa w kilku językach, uzyskujemy całkiem pokaźną ilość pracy do wykonania. Oczywiście nie do końca jest tak trudno – wiele modeli telefonów jest ze sobą zgodnych (kompatybilnych) i wersja aplikacji przygotowana na jeden model będzie działała na innym modelu o takiej samej rozdzielczości ekranu i podobnych parametrach sprzętowych. Pozwala to na ograniczenie ilości pracy związanej z testowaniem i w końcowym wyniku uzyskanie mniejszej liczby unikalnych wersji aplikacji. Takie działanie to grupowanie. Niestety nie jest to działanie automatyczne i uniwersalne – wiele zależy od konkretnej aplikacji, jej rozmiaru, stopnia zaawansowania czy wykorzystania specyficznych cech urządzenia. Oczywiście można wstępnie określić, które urządzenia są za sobą potencjalnie kompatybilne, ale zgodność tą należy potwierdzić poprzez wykonanie podstawowych testów zgodności danej aplikacji, gdyż jest ona w wysokim stopniu zależna od konkretnego przypadku.
53
NIEZAWODNE OPROGRAMOWANIE
Co jest największą bolączką przy przygotowywaniu wersji aplikacji działającej na różnych urządzeniach? Jednym z ograniczeń jest rozmiar pliku jar akceptowany przez urządzenie (tzw. jar size). W przypadku wielu nowych telefonów ograniczenie to nie ma znaczenia, gdyż nie limitują one samego rozmiaru pliku aplikacji, ale starsze urządzenia mają takie ograniczenie. Próba pobrania pliku o większym rozmiarze niż maksymalnie obsługiwany skończy się zwykle wyświetleniem komunikatu o błędzie. Jest to jeden z ważniejszych testów, który decyduje o możliwości pomyślnego pobrania i zainstalowania aplikacji. Niekiedy dystrybutor oprogramowania dodatkowo nakłada ograniczenie na wielkość pliku, co powoduje konieczność limitowania tej wielkości nawet w przypadku urządzeń nie posiadających takiego limitu. Drugie niezmiernie istotne ograniczenie to ilość pamięci dostępna do wykorzystania przez aplikację, czyli tzw. heap size. To ograniczenie jest zdecydowanie decydujące, gdyż nawet telefony nie ograniczające rozmiaru instalowanego pliku mają ograniczenie ilości pamięci dostępnej dla aplikacji. Przy testowaniu gier wymaga to dokładnego sprawdzenia, gdyż nawet uruchamiająca się aplikacja może w dalszych etapach działania (czyli np. w którymś z dalszych poziomów gry) potrzebować większej ilości pamięci niż udostępniana przez urządzenie. Na jakie inne specyficzne aspekty testowania gier na platformy mobilne trzeba zwrócić uwagę? Mnogość urządzeń i w wielu przypadkach brak standardów, których trzymaliby się ich producenci, powodują, że w przypadku poszczególnych urządzeń trzeba dokładnie sprawdzać zgodność działania w grze przycisków dostępnych dla użytkownika ze specyfikacją aplikacji. Analogiczna sytuacja ma miejsce, jeśli chodzi o zachowanie urządzenia w czasie różnorodnych zdarzeń systemowych, takich jak przychodzące połączenie, przychodząca wiadomość tekstowa, informacja o niskim poziomie naładowania baterii czy też np. zamknięcie przez użytkownika klapki telefonu. Wszystkie tego typu zdarzenia mogą mieć bardzo różny wpływ na działanie aplikacji, jak np. niewłaściwe odrysowanie grafiki, brak wyciszenia dźwięku gry w czasie trwania rozmowy, nie przywrócenie dźwięku po zakończonej rozmowie albo wręcz zawieszenie się lub zamknięcie aplikacji. Dodatkową przeszkodą jest fakt, iż wywołanie takiego zdarzenia systemowego w czasie różnych działań gry może spowodować różny skutek, więc nie można poprzestać na sprawdzeniu jednorazowym. Trzeba także pamiętać, że nie zawsze na każdym telefonie uda się prawidłowo i zgodnie ze specyfikacją aplikacji naprawić wszystkie zauważone odstępstwa i problemy. Niestety niektóre urządzenia nie pozwalają na prawidłową implementację wszystkich wymagań z racji braku wsparcia sprzętowego co do nich (jednym z takich przykładów może być reakcja aplikacji na zamknięcie klapki te-
54
lefonu – oczekiwalibyśmy wyciszenia w tym momencie dźwięku gry, lecz zdarzają się modele telefonów, które nie wysyłają do aplikacji informacji o fakcie zamknięcia klapki, co nie pozwala prawidłowo zaimplementować wymaganego zachowania). Pewną odmianą tej przypadłości jest odmienne zachowanie się tych samych telefonów, ale z różną wersją oprogramowania systemowego – odmienne zachowanie gry na innej wersji oprogramowania jest wykrywane wtedy często dopiero w dalszych fazach, w czasie testów akceptacyjnych. Gry mobilne to nie tylko JME, ale także Symbian. Testowanie gier działających w systemie Symbian jest bardzo podobne do testowania gier JME. Część funkcjonalna oraz wszelkiego rodzaju testy zachowania się aplikacji w zderzeniu ze zdarzeniami systemowymi są analogiczne. Różnicę stanowi tu zbiór wymagań – w tym przypadku jest to dokument Symbian Signed Test Criteria. Dokument ten zawiera obostrzenia głównie w kontekście zachowania się testowanej aplikacji w stosunku do systemu operacyjnego i innych aplikacji. Spełnienie tych wymagań jest obowiązkowe (i sprawdzane przez wyznaczone, niezależne firmy testujące), aby aplikacja mogła zostać podpisana cyfrowo i posiadać status Symbian Signed. Oczywiście mogą istnieć także inne dodatkowe wymagania do spełnienia poza wymaganiami Symbian Signed – na przykład przy tworzeniu gier i aplikacji dla firmy Nokia trzeba zachować (i przetestować) zgodność z dodatkowymi specjalnymi wymaganiami, które mają zapewnić m.in. zbliżoną obsługę i zachowanie różnego rodzaju aplikacji w interface telefonu. Pewną część rynku gier na urządzenia mobilne stanowią gry działające na stworzonej przez firmę Qualcomm platformie BREW, rozpowszechnionej zwłaszcza w Stanach Zjednoczonych. Przy testowaniu należy pamiętać o specyficznych wymaganiach, które w przeważającej części zawarte są w dokumencie True BREW Testing Criteria. Wymagania te są bardzo obszerne i szczegółowe, nie są zbytnio zbieżne z wymaganiami dla aplikacji JME czy Symbian, więc stanowią swego rodzaju wyzwanie dla zespołu testującego. Co istotne, każda dostarczona wersja aplikacji jest wnikliwie testowana przez niezależną firmę akceptacyjną (NSTL – National Software Testing Labs), więc nie może być mowy o przymykaniu oczu na jakiekolwiek drobne odstępstwa od wymagań (o ile oczywiście problem nie wynika stricte z samego urządzenia). Dodatkowo operatorzy telekomunikacyjni (gdyż to oni są praktycznie wyłącznym dystrybutorem aplikacji BREW) określają także swoje własne wymagania, które aplikacja musi spełniać, aby zostać dopuszczona przez nich do sprzedaży. Także ich testerzy przeprowadzają testy akceptacyjne pod kątem tych wymagań, a więc dobre i prawidłowe przetestowanie gry BREW nie jest sprawą łatwą. Podkreślić należy jeszcze, że także inne przeszkody stoją przed testerami. Każda
8/2010
Testowanie gier na urządzenia mobilne
wersja aplikacji (dostosowana do danego modelu telefonu) musi posiadać dołączoną dokumentację w postaci dokumentu ASD (Application Specification Document), który zawiera bardzo szczegółowe informacje na temat aplikacji, łącznie z dokładnym diagramem działania aplikacji (menu flow), szczegółowym opisem działania poszczególnych klawiszy czy specyficznymi ograniczeniami urządzenia, które uniemożliwiają spełnienie niektórych wymagań (device limitations). Nie ułatwia testowania także fakt, że większość telefonów wyposażonych w platformę BREW działa w technologii CDMA, co uniemożliwia wykonanie testów, do których konieczne jest połączenie z siecią telekomunikacyjną. W tym przypadku w grę wchodzą jedynie rozwiązania pozwalające na zdalny dostęp do telefonów albo zlecenie wykonania części testów w miejscu, gdzie odpowiedni dostęp jest zapewniony. Przy opisie testowania gier na platformy mobilne nie sposób nie wspomnieć o rozwijającym się dynamicznie od wielu miesięcy rynku gier na urządzenia przenośne firmy Apple (czyli iPhone i iPod Touch). W tym przypadku testowanie ułatwia fakt, że nie musimy tu zachować zgodności z ogromną ilością różnorodnych urządzeń. Mamy do wyboru jedynie cztery urządzenia i ewentualnie różne wersje oprogramowania (w celu sprawdzenia kompatybilności wstecznej). Otwarta pozostaje oczywiReklama
www.sdjournal.org
ście kwestia śledzenia przyszłych zmian oprogramowania urządzenia i ewentualnego testowania poprawności działania z nimi stworzonej wcześniej aplikacji. Specyfika testowania gier na tę platformę jest nieco inna. Oczywiście testowanie funkcjonalne, mające za zadanie potwierdzić zgodność działania aplikacji z założeniami designera i sprawdzić chociażby jej stabilność, także tu ma zastosowanie. Apple nie wprowadziło restrykcyjnych list wymagań do spełnienia. Twórcy oprogramowania muszą stosować się do zaleceń Apple, związanych głównie z interfejsem użytkownika, co ma pozwolić poszczególnym aplikacjom zachowywać się w systemie w sposób przewidywalny i łatwy do odgadnięcia przez użytkownika. Oprócz testowania aplikacji pod kątem zaleceń Apple ważniejsza jest inna kwestia, która nabiera specjalnego znaczenia w kontekście dużego nasycenia rynku aplikacjami i ogromnej konkurencji z racji niskich barier wejścia na rynek. Gra, która ma odnieść sukces, oprócz tego, że musi być w odpowiedni sposób rozreklamowana i promowana, powinna idealnie trafić w gust odbiorcy. Co za tym idzie, należy położyć bardzo duży nacisk na wydanie produktu bardzo dobrej jakości (czyli bez błędów mogących spowodować frustrację gracza czy utrudniających mu rozgrywkę), w pewien sposób unikalnego, ale jednocześnie lekkiego i przyjaznego w odbiorze. Z tego powodu nie wolno pomijać w tym
NIEZAWODNE OPROGRAMOWANIE
przypadku bardzo dużej roli playtestingu, który chociaż w części pozwoli przewidzieć, jakie opinie może wywołać produkt na rynku i jakie aspekty wymieniane są jako największe wady konieczne do poprawienia.
Specyfika testowania gier na konsole do gier – Sony PSP/Nintendo Wii/Nintendo DSi
W ostatniej części warto napisać kilka słów o specyfice testowania gier tworzonych na niektóre z konsol do gier (chociaż nie wszystkie są mobilne) i o aspektach, na jakie przy takim testowaniu należy zwrócić uwagę. W przypadku konsol do gier dużym utrudnieniem przy testowaniu jest fakt konieczności użycia do testów funkcjonalnych specjalistycznego sprzętu, niedostępnego publicznie, który stanowi zamknięte środowisko testowe. Zarówno Nintendo w przypadku konsol Wii i DSi, jak też Sony przy konsoli PSP wymagają zakupu takowego sprzętu dla testerów, gdyż tylko na nim będą oni w stanie uruchamiać przygotowane przez zespół programistów aplikacje. Testowanie gier na powyższe konsole wymaga także stosowania się do wymagań określonych przez producentów. Jest to bardzo ściśle przestrzegane, aby zapewnić użytkownikowi końcowemu produkt, który będzie działał w sposób prawidłowy, przewidywalny i odpowiednio zachowujący się w środowisku systemowym. Określenie dokładnych wymogów i ścisłe ich przestrzeganie na pewno wymusza powstawanie gier wyższej jakości (oczywiście z technicznego punktu widzenia, niekoniecznie grywalności). Wraz z wymaganiami dostarczane są niekiedy małe narzędzia testujące, które ułatwiają sprawdzenie niektórych elementów w sposób automatyczny. Kolejna rzecz, która związana jest z wymaganiami, to dokumentacja. Przeprowadzenie testów zgodności z wymaganiami (czyli w przypadku Nintendo – procedura znana jako Lotcheck, a dla Sony – TRC Technical Requirement Checklist) niesie za sobą konieczność wypełnienia obszernej dokumentacji, tym obszerniejszej, im aplikacja wykorzystuje większą ilość dodatkowych możliwości (obsługa połączeń on–line, wykorzystanie rozszerzeń sprzętowych, gra wieloosobowa itp). Sprawy nie ułatwia fakt, że wszystkie wymagania, odnoszące się do samej aplikacji, jak i do sposobu jej przygotowania do dystrybucji, zawarte są często w wielu różnych dokumentach posiadających wzajem-
ne odniesienia. Wymaga to od osoby zajmującej się testowaniem i kompletowaniem dokumentacji bardzo dużej staranności i systematyczności. Przy testowaniu zgodności z wymaganiami należy zwrócić uwagę na to, że listy wymagań różnią się w zależności od regionu, do którego kierowana jest do wydania gra (szczególnie w zakresie używanej terminologii i konfiguracji). Nieco inne dokumenty i procedury wymagane są przy dystrybucji na rynki: europejski, amerykański czy azjatycki. Ponadto, oprócz wymagań systemowych, gry na omawiane konsole muszą zostać sklasyfikowane pod względem minimalnego wieku gracza. Wymaga to przeprowadzenia procesu certyfikacyjnego, który wiąże się z określeniem odpowiednich wymagań, które gra musi spełniać, aby być dostępna od odpowiednio niskiego wieku, jak i przygotowania dokumentacji (zarówno pisemnej, jak i audiowizualnej) z tym związanej. Proces ten i procedury także zależą od regionu czy nawet konkretnego kraju, gdzie gra ma być dystrybuowana. Jak więc widać, proces testowania gier konsolowych jest nieco bardziej złożony, a przede wszystkim bardziej sformalizowany, niż gier na platformy mobilne, a także utrudniony z racji konieczności użycia specjalistycznego sprzętu testowego.
Podsumowanie
W artykule przedstawiono niektóre aspekty charakteryzujące testowanie gier na urządzenia mobilne i niektóre konsole do gier. Zasygnalizowano, dlaczego testowanie jest potrzebne, kiedy ten proces rozpocząć, jak go sobie ułatwić, kto może go prowadzić. Obszerność tematu nie pozwala oczywiście przywołać wszystkich ważnych aspektów tego procesu, ale starano się zasygnalizować najważniejsze z nich.
GRZEGORZ TARCZYŃSKI Związany z branżą testowania gier od 8 lat, uczestniczył w dziesiątkach projektów o różnej wielkości. Pracuje na stanowisku Managera działu Quality Assurance w �rmie Gamelion, wchodzącej w skład Grupy BLStream. Koordynuje i nadzoruje pracę zespołu testerów nad wieloma projektami na platformach: JME, BREW, iPhone, Symbian, PC, Nintendo Wii, Nintendo DSi oraz Sony PSP. Kontakt z autorem: grzetar@gmail.com
W Sieci • • • • • • •
56
http://javaveri�ed.com/ – strona domowa programu Java Verified; http://www.symbiansigned.com – strona domowa programu Symbian Signed; http://brew.qualcomm.com – strona domowa platformy BREW firmy Qualcomm; http://developer.apple.com/iphone/program/ – strona iPhone Developer Program; http://www.testerzy.pl/ – strona portalu testerzy.pl, zawierającego wiele użytecznych informacji; http://www.deviceanywhere.com/ – strona usługi dostępu do telefonów Device Anywhere; http://www.mob4hire.com/ – strona portalu mob4hire, łączącego developerów z testerami.
8/2010
Z ŻYCIA ITOLOGA
CMMI – Dlaczego powinno Cię to obchodzić? Wielki Wybuch Pamiętam jak zaczynałem swoją przygodę z rozwiązywaniem sudoku. Na początku było niemiłosiernie trudno, potem stopniowo łapałem „wiatr w żagle”. Podobnie jest z modelem CMMI. Na pierwszy rzut oka wydaje się bardzo skomplikowany. Później, w miarę jak stopniowo go poznajemy, zaczynamy dostrzegać jego wewnętrzne „piękno” i logikę, widzimy że jego praktyki naprawdę mają sens i mogą nam się przydać. Tak było ze mną, i tak – jestem o tym przekonany – będzie również z Wami! Dowiesz się:
Powinieneś wiedzieć:
• Czym jest model CMMI i kto wpadł na ten genialny pomysł, żeby go stworzyć • Jakie ma reprezentacje, a więc, czy zależy nam na zdobyciu Górskiej Odznaki Turystycznej (GOT), czy medale zostawiamy żołnierzom. • Która reprezentację powinna wybrać Twoja firma, jeśli już zdecyduje się go wdrożyć • Będzie wreszcie o tym, dlaczego warto
• Na pewno przyda się świadoma wiedza o problemach, związanych z jakością produktu, „porażkach” menedżerskich, które nam towarzyszą, no i o tym, co „misie lubią najbardziej” – problemach z klientem. Ta wiedza w zupełności wystarczy, żeby zrozumieć, o co w tym wszystkim chodzi
I
stnieje co najmniej kilka teorii, wyjaśniających powstawanie Wszechświata. Początkowo naukowcy twierdzili, że wszechświat jest statyczny – raczej kurczy się, niż rozszerza. Była to teza, z którą jednak wielu kosmologów polemizowało. W końcu, za najbardziej prawdopodobny uznano model ewolucji Wszechświata, który mówi o tym, iż ok. 13,75 mld lat temu miał miejsce Wielki Wybuch (ang. Big Bang), który zapoczątkował ekspansję Wszechświata. To był ten moment, w którym wszystko się zaczęło – i czas, i przestrzeń i grawitacja. W naszych codziennych zmaganiach z rozwojem oprogramowania na czas, zgodnie z zaplanowanym budżetem, a także nieodbiegającego od pierwotnej „Książki Wymagań Klienta”, często dochodzimy do Momentu Krytycznego (Wielkiego Wybuchu). Symptomy bywają różne. Nawarstwiające się problemy, związane z jakością produktu: zbyt dużo błędów, zwłaszcza tych (o zgrozo!), wykrytych przez klienta; jak jest dużo błędów, ktoś je musi naprawiać; funkcjonalności nie działają tak, jak sobie tego zażyczył klient, który na domiar złego nagle zaczyna „wymyślać” i narzekać. Do problemów, związanych z jakością oprogramowania dochodzą „porażki” menedżerskie: estymaty okazały się być wyssane z palca (projekt jest mocno niedo-
58
szacowany); harmonogram prac, który tak szczegółowo uzgadnialiśmy z klientem nagle bierze w łeb; do tego już na starcie widać, że budżet zostanie grubo przekroczony; no i ten klient… czego on właściwie chce? Ciągle dzwoni i zmienia swoje oczekiwania – jak w kalejdoskopie; w końcu nie wiemy już, które wymagania mają być faktycznie realizowane, a o których się tylko rozmawiało. Do tego gdzieś zapodziały się maile potwierdzające konkretne ustalenia z klientem, część rzeczy przecież załatwiało się przez skype. Statystyczny Menedżer zaczyna rwać włosy z rozpaczy. To przecież taki dobry klient, duża korporacja i w ogóle. Na domiar wszystkiego pojawiają się problemy z ludźmi, którzy zostali „zaprzęgnięci” do projektu. Dziwnie, przestajemy mieć nad nimi kontrolę – narzekają, że przez te „wrzutki” od klienta, nie mogą normalnie pracować. Kierownik Projektu z kolei przyciska śrubę. Wygląda na to, że niedoszacowaliśmy projektu. To nic, ludzie przecież mogą siedzieć po godzinach, no i mamy jeszcze weekendy. Tylko ten jeden raz, tylko na czas realizacji właśnie tego trudnego projektu. Kiedy w nas coś pęknie? Kiedy właściwie zdiagnozujemy, że jesteśmy „chorzy”? Miałem klienta, który zwrócił się do mnie z prośbą o pomoc dopiero po kilku latach
8/2010
CMMI – Dlaczego powinno Cię to obchodzić?
od czasu wystąpienia pierwszych „objawów choroby”. Zmiany, które musiał wprowadzić w swojej firmie, głównie w zakresie reguły współpracy z klientem (zarządzanie projektem, zarządzanie wymaganiami, zarządzanie konfiguracją), kosztowały go bardzo dużo. Jego organizacja zapłaciła wysoką cenę. Najważniejsze jednak, że Wielki Wybuch nastąpił.
Zagubieni na wyspie
Opisane powyżej „symptomy choroby” można oczywiście leczyć na różne sposoby. Według mnie, jednym z lepszych jest próba zrozumienia aktualnie istniejących procesów w firmie lub ich ewentualnego braku. Doskonale pamiętam początki swojej kariery zawodowej, kiedy wydawało mi się, że jestem świetnym Kierownikiem Projektu, i nie ma takiego żywiołu, który byłby wstanie zawrócić okręt prowadzonego przeze mnie przedsięwzięcia, z pierwotnie obranego kursu. Nic bardziej mylnego. Bez odpowiedniej mapy i kompasu, nie byłem w stanie dotrzeć „drogą morską z Europy do Indii”. Czułem się jak jeden z 71 rozbitków – bohaterów amerykańskiego serialu „Zagubieni”, którzy bezskutecznie próbowali wydostać się z wyspy, na której się znaleźli w wyniku katastrofy lotniczej feralnego lotu 815. Brakowało mi „mapy i kompasu” – zbioru konkretnych reguł, kroków i działań, tworzących coś, co w obiegowej opinii nazywamy „procesami”. Umiejętne zwrócenie uwagi na świat procesów w organizacji jest doskonałym remedium na pojawiające się problemy.
Znaleźć dobrą mapę
Dobrze zdefiniowane procesy są w stanie pomóc organizacji zapanować nad ludźmi, przypisanymi do poszczególnych projektów (ich doświadczenie, inwestycja w kosztowne szkolenia nie zawsze wystarczą, podobnie jak intensyfikacja pracy). Podobnie jest, gdy chodzi o stosowanie określonych technologii. Dobrze przemyślane procesy – na przykład te, które dotyczą projektowania architektury systemu, czy też rozwijania i integracji jego poszczególnych komponentów – zwiększają skuteczność stosowania określonych technologii. Weźmy na przykład norweskiego trębacza Nilsa Molvaera, kojarzonego z nurtem nu-jazzu, właściwie jednego z jego pionierów. Jakież on potrafi wydobywać dźwięki z tego instrumentu; niektórzy określają jego technikę jako „voicing trumpet”, gdyż wydobywany przez niego dźwięk przypomina momentami głos ludzki. Świetna technika w kontekście dobrze zdefiniowanego otoczenia procesowego. Wystarczy jednak wyobrazić sobie Molvaera jako członka sekcji dętej orkiestry OSP w Kopydłowie (fikcyjna miejscowość), żeby zrozumieć dlaczego stosowanie określonej technologii IT musi być osadzone w odpowiednim kontekście procesowym. Wdrożenie określonych procesów w firmie jest ufundowane wdrożeniem określonych reguł gry (np. goniec po-
www.sdjournal.org
rusza się wyłącznie na ukos, w dowolnym kierunku, o dowolną liczbę niezajętych pól, czy bardziej „z naszego podwórka” – podejmowane prace w projekcie powinny być dobrze oszacowane, zanim sporządzimy harmonogram prac), dzięki którym będzie możliwe wykonywanie określonych aktywności projektowych. Same procesy tworzą jednak tylko treść mapy – są jej obiektami oraz ich wzajemnym rozmieszczeniem. Mapa pokazuje jak do nich dotrzeć. Dlatego, myśląc poważnie o poprawie procesów wytwórczych w organizacji, należy w pierwszej kolejności pomyśleć o dobrej mapie.
Model dla wszystkich
Nasze poszukiwania „dobrej mapy” mogą się sprowadzić do dwóch opcji: albo ją stworzymy sami, jeśli oczywiście posiadamy odpowiednią wiedzę geodezyjną lub kupimy tzw. „gotowca”, który został opracowany przez odpowiednią grupę ekspertów, dobrze znających się na rzeczy. Przykładem takiego „gotowca”, przeskakując już do ogródka informatycznego, jest Capability Maturity Model Integration, w skrócie CMMI. CMMI, jak można łatwo zauważyć, nie jest modelem, który można spotkać na okładce jednego z numerów „Men’s Health”. Nie dla wszystkich odpowiednia budowa mięśni, połączona z solidną dawką solarycznego brązu – są właściwym punktem odniesienia. W inżynierii oprogramowania pojęcie „modelu” ma zupełnie przeciwne znaczenie. Można powiedzieć, iż jest to pewien typ idealny – „miejsce” przez wszystkich uznane za cenne i wartościowe, w którym warto być, a nawet się zadomowić. Jestem przekonany, iż każda organizacja IT, która zapozna się (choćby pobieżnie) z praktykami modelu CMMI, uzna, iż są celem, który warto osiągnąć. CMMI powstał w latach 80., z inicjatywy Departamentu Obrony Stanów Zjednoczonych (ang. Department of Defense, w skrócie DoD), który w tym czasie borykał z problemami wyboru właściwych poddostawców i zlecania im w części lub całości realizowanych prac. Jak można się domyśleć sprawa raczej nie była trywialna: mamy lata osiemdziesiąte i standardy, dotyczące jakości dopiero się rodziły (w latach siedemdziesiątych pojawił się artykuł Rona Royce’a, który po raz pierwszy opisał model kaskadowy). Zdecydowano, iż prace, dotyczące opracowania kryteriów wyboru poddostawców zostaną zlecone grupie amerykańskich programistów, pracujących w Software Engineering Institute (w skrócie SEI), przy Carnegie Mellon Univeristy w Pittsburghu. Instytut jest do dzisiaj sponsorowany przez DoD i do dzisiaj kontynuuje swoją pierwotną działalność w zakresie rozwijania modelu, choć na nieco szerszą skalę. Efektem prac Software Engineering Institute był tzw. Kwestionariusz Oceny Dojrzałości Procesów (ang. Maturity Questionnaire), który składał się z 85 pytań, dotyczących między innymi takich zagadnień jak: planowanie projektu, śledzenie postępu prac, zarządzanie harmonogramem, zarządzanie wymaganiami, zarządzanie
59
Z ŻYCIA ITOLOGA
konfiguracją, zapewnienie jakości tworzonego oprogramowania. Pytania te dotyczyły jednocześnie najważniejszych procesów, które powinny być zaimplementowane i wykorzystywane w każdym projekcie informatycznym. SEI, używając swojego kwestionariusza, przebadał setki firm, dochodząc przy tym do ciekawych wniosków. Jednym z ważniejszych było stwierdzenie, iż proces „dojrzewania” do określonego poziomu jakości i produktywności miał podobny przebieg w każdym z badanych przypadków. Wszystkie wnioski zostały zebrane i opublikowane w formie tzw. Raportu Technicznego (ang. Technical Report), który dał początek pierwszej wersji Capability Maturity Model for Software (CMM-SW). Sam model przeżył wiele transformacji, z których chyba najważniejszą jest ta, że w trakcie prac rozwojowych wyodrębnione zostały tzw. „konstelacje” modelu, dzięki którym możliwe jest jego zastosowanie w różnych obszarach, a więc nie tylko w inżynierii oprogramowania (CMMI-DEV), ale i w organizacjach prowadzących działalność serwisową (CMMI-SVC), a także akwizycyjną (CMMI-ACQ). Ciekawym pomysłem SEI, jest również próba zwrócenia uwagi na praktyki, związane z zarządzaniem personelem (People CMM), którym warto poświęcić odrębną publikację, zwłaszcza w kontekście wszechobecnej dzisiaj „mody” (której przyznaję również sam uległem) na tzw. „zwinne” metod tworzenia oprogramowania (agile).
Jak dojść do schroniska?
Ktokolwiek chce wykorzystać model CMMI do poprawy procesów w swojej firmie, powinien traktować go właśnie w kategoriach mapy, jak to zostało już wcześniej podkreślone. Wydaje mi się, że porównanie go do mapy bardzo trafnie oddaje sposób, w jaki powinno się z niego korzystać. Mapa pozwala nam dotrzeć w określone miejsce. W pewnym sensie wyznacza kierunek. Posiadają na przykład mapę Beskidu Żywieckiego wiem, że do schroniska na Hali Krupowej możemy dojść z Babiej Góry, Bystrej Podhalańskiej, Jordanowa, Juszczyna, Kojszówki, schroniska na Luboniu Wielkim etc. Mapa nie mówi, musisz iść tędy, żeby osiągnąć cel. Z drugiej strony ma jasno zdefiniowane cele w postaci konkretnych schronisk. I taki jest w przypadku modelu CMMI, który składa się określonej liczby celów (schronisk), które możemy osiągnąć za pomocą konkretnych praktyk (dobrych i sprawdzonych tras na mapie). Przez „dobre praktyki” rozumiem tutaj konkretne działania, które wielokrotnie sprawdziły się w prowadzeniu projektów informatycznych na całym świecie. Należy bowiem pamiętać, iż CMMI nie jest wytworem „fantazji intelektualnych” kilku naukowców z Pensylwanii, którzy nie wiedzieli jak zagospodarować pieniądze z grantu, ale jest wynikiem pracy i zaangażowania praktyków z firm informatycznych (i nie tylko) na całym świecie. Dlatego też ten model tak intensywnie się rozwija. Jest to zrozumiałe, biorąc pod uwagę w jakim tempie rozwija się sama inżynieria oprogramowania.
60
Model CMMI ma dość złożoną strukturę, jak dobra wojskowa mapa. Dlatego przedstawię ją tylko w bardzo ogólnym zarysie. Składają się na nią następujące komponenty: •
• • •
Poziomy Dojrzałości (ang. Maturity Levels) w Reprezentacji Stałej oraz Poziomy Wydolności (ang. Capability Levels) w Reprezentacji Ciągłej (o reprezentacjach opowiem za chwilę) Obszary Procesowe (ang. Process Areas) Cele – Ogólne (ang. Generic) i Specyficzne (ang. Specific) Praktyki – Ogólne (ang. Generic) i Specyficzne (ang. Specific)
Na pierwszy rzut oka, przypomina to trochę znane wszystkim „dzielenie dzidy bojowej”. Dlatego też zachęcam Czytelnika, żeby na tym etapie nie zaprzątał sobie tym głowy. Natomiast rzeczą chyba najbardziej istotną przy pierwszym spotkaniu z modelem CMMI jest fakt, iż jest to w zasadzie ogromny w worek z Obszarami Procesowymi (nie mylić z procesami!), które mówią jak dojść do schroniska, możliwie najprostszą drogą.
Zdobywanie odznaki
Tutaj dochodzimy do bardzo ważnego aspektu modelu CMMI – jego reprezentacji. A więc, czy zależy nam na zdobyciu Górskiej Odznaki Turystycznej (GOT), czy medale zostawiamy żołnierzom. Do dzisiaj pamiętam swoją książeczkę GOT, i to jak pieczołowicie zbierałem punkty, żeby zdobyć kolejno: „popularną”, „małą brązową”, „małą srebrną” i wreszcie… „małą złotą” odznakę turystyczną. Specjalnie wybierałem na mapie takie trasy (było to zależne zupełnie ode mnie), żeby uzbierać jak najwięcej punktów. Najgorsze w tym wszystkim było to, że odznaki trzeba było zbierać w odpowiedniej kolejności, tzn. nie mogłem na przykład z „popularnej” przeskoczyć na „małą złotą”. Pierwsza z dwóch reprezentacji modelu CMMI to Reprezentacja Stała (ang. Staged). Używając przytoczonej wcześniej metafory, jest to proces zdobywania odznaki turystycznej. Wszystkie Obszary Procesowe modelu zostały podzielone na określone etapy, zwane „Poziomami Dojrzałości” (ang. Maturity Levels), które muszą być kolejno „zdobywane” (jak odznaki turystyczne), żeby osiągnąć określony stopień zaawansowania procesów wytwórczych w organizacji. Model CMMI wyróżnia pięć Poziomów Dojrzałości, i co ważne, podobnie, jak nie możemy najpierw zdobyć „popularnej”, a potem od razu „małej złotej” odznaki turystycznej, tak i tutaj – przedsiębiorstwa muszą się wznosić na szczyty małymi krokami. Najpierw Poziom 2., potem Poziom 3. etc. Nie ma mowy o przeskoczeniu, na przykład z Poziomu 2. od razu na 5. Przyznaję, że jest to bardzo wygodne podejście i bardzo często zachęcam Klientów, żeby z niego skorzystali, zwłasz-
8/2010
CMMI – Dlaczego powinno Cię to obchodzić?
cza jeżeli organizacje dopiero zaczynają swoją przygodę z poprawą procesów wytwórczych, i nie mają odpowiedniego doświadczenia i wiedzy, żeby to zrobić „bez mapy”. Poza tym, Reprezentacja Stała jest w pewnym stopniu intuicyjna. Czujemy przez skórę, że jeżeli nie mamy uporządkowanych podstawowych procesów, związanych z zarządzaniem projektami (Poziom 2.), nie ma sensu dywagować, jak moglibyśmy statystycznie kontrolować procesy w naszej firmie (Poziom 4.) – choć nie ukrywam, jest to bardzo pasjonujący i wciągający temat. Druga reprezentacja, to Reprezentacja Ciągła (ang. Continuous). Tutaj sprawa wydaje się dość prosta. Nie potrzebne mi są żadne odznaki turystyczne. Chcę po prostu dojść do schroniska, trasą którą uwielbiam, i nie wyobrażam sobie, żebym mógł zrobić to inaczej. Można i tak, ale w tym wypadku trzeba już coś nieco wiedzieć o górach, przynajmniej gdzie chcemy dojść, do jakiego schroniska – resztę załatwi nam mapa (która w obu reprezentacjach jest niezbędna). Reprezentacja Ciągła daje firmie duże możliwości manewru, jeśli chodzi o praktyki związane z poprawą istniejących w niej procesów. To firma sama, jak turysta znający „swoje” góry, decyduje o tym, które z istniejących procesów wymagają poprawy. Tutaj nie mamy do czynienia z konkretnym „przepisem na sukces”. W Reprezentacji Ciągłej pojawiają się tzw. Poziomy Wydolności (ang. Capability Levels), których jest sześć (pojawia się poziom zerowy, liczony jako dodatkowy szósty) i które mają zastosowanie tylko (!) do wybranego procesu, który chcemy doskonalić. Proszę zwrócić uwagę na tę subtelną różnicę pomiędzy reprezentacjami. W pierwszej (Stałej) jest 5 Poziomów Dojrzałości, z których każdy jest zbiorem procesów, których odpowiednie wdrożenie gwarantuje firmie osiągnięcie określonej dojrzałości wytwórczej. W drugiej (Ciągłej) natomiast, nie ma wcześniej zdefiniowanej ścieżki rozwoju (doskonalenia); w związku z czym, sami musimy wiedzieć, co tak naprawdę chcemy poprawić (jaki proces) i w jakim stopniu (Poziomy Wydolności). Tutaj sami wybieramy drogę do schroniska. Może być ona krótsza lub dłuższa; przez las lub z punktami widokowymi. Jak się łatwo domyśleć, jest to bardzo praktyczne rozwiązanie dla tych organizacji, które mają już określoną wiedzę na temat swoich procesów wytwórczych – znają swoje mocne i słane strony oraz wiedzą, co chciałyby w nich zmienić i poprawić.
doświadczenia jako konsultanta, gdy niejednokrotnie „błagałem” klientów o wystąpienie i podzielenie się swoimi sukcesami w zakresie doskonalenia procesów wytwórczych na tej lub innej konferencji informatycznej. W wielu przypadkach uzyskiwałem odpowiedź odmowną, słysząc wymówki w stylu: nie mam czasu, mam wyjazd służbowy, może i mógłbym, ale czy jest się czym chwalić etc. Przyznam szczerze, iż nie bardzo wiem z czego to wynika. Z jednej strony rozumiem Menedżerów, pracujących w dużych korporacjach, gdzie procedury dot. upubliczniania wewnętrznych danych firmy są tak skomplikowane, a uzyskanie stosownego „zezwolenia” wymaga kontaktu z trzema odrębnymi działami w firmie (najczęściej zlokalizowanych w różnych krajach), że nikt nie ma sił i czasu, żeby temu wszystkiemu stawić czoło. Z drugiej strony, istnieje duża grupa firm, która po prostu nie chce się dzielić pozytywnymi przykładami i praktykami, które pomogły im uzdrowić sytuację w ich firmie. Próbując to jakoś zmienić, czuję się trochę jak Tom Hanks, w filmie „Cast Away”, który wraz z niezawodnym Panem Wilsonem (piłką do siatki), próbuje stawić czoło emocjonalnym wyzwaniom izolacji – bezradny. Zastosowanie modelu CMMI nie jest ograniczone do konkretnego sektora. Z powodzeniem mogą go stosować wszystkie organizacje, które mają cokolwiek do czynienia z tworzeniem oprogramowania (np. dedykowane wewnętrzne Działy IT w danej firmie). CMMI można więc wdrażać w organizacjach transportu publicznego, administracji rządowej, sektora finansów i ubezpieczeń, ochrony zdrowia, i wielu innych. Dodatkowo wdrożenie CMMI nie zależy od rozmiaru organizacji. Praktyki modelu można więc implementować zarówno w dużych gigantach korporacyjnych, jak i małych i średnich przedsiębiorstwach (nawet, jeśli zatrudniają mniej niż 25 pracowników). Wykorzystując moje osobiste doświadczenia, chciałbym zwrócić uwagę na zaledwie kilka obszarów, w które po wdrożeniu modelu CMMI zyskają na znaczeniu i zyskają uznanie i podziw w oczach Państwa klientów: •
Dlaczego powinno Cię to obchodzić
Osobiście nie lubię pokazywać „ładnych” statystyk (np. Raporty Techniczne regularnie publikowane przez SEI), które rzekomo mają spełniać rolę ostatecznego dowodu na to, że wdrożenie modelu CMMI faktycznie się opłaca. Dużo bardziej wolę pozytywne przykłady „z własnego podwórka”. No właśnie, i z tym jest mały problem. Generalnie firmy niewiele mówią o swoich sukcesach, związanych z wdrożeniem modelu CMMI. Wiem to, z własnego
www.sdjournal.org
•
Planowanie Projektu – absolutny faworyt na mojej liście. Już po wdrożeniu kilku prostych praktyk modelu, życie prowadzonych projektów zmienia się o 180 stopni. Nagle poczujecie, że zaczynamy stosować odpowiednie techniki szacowania parametrów projektu (jak np. Wideband Delphi, Planning Poker, Punkty Funkcyjne itp.), a Kierownik Projektu potrafi nagle sporządzić realny harmonogram prac, wykorzystując do tego wcześniej stworzonego WBS-a i estymaty Zespołu. Monitoring i Kontrola Projektu – czyli „Uśmiechnięty Klient”. Każdy Kierownik Projektu wie, co to oznacza: dobra reputacja, cenne rekomendacje oraz duże prawdopodobieństwo na kontynuację współpracy. Wdrożenie określonych praktyk modelu sprawi, że
61
Z ŻYCIA ITOLOGA
•
•
•
nasz klient będzie czuł się naprawdę „dopieszczony”, przynajmniej w obszarze wymiany informacji (raporty tygodniowe, miesięczne przeglądy projektów, cykliczne spotkania interesariuszy projektu, demonstracje produktu, wyniki przeprowadzonych testów etc.). Zarządzanie Wymaganiami – przestaniemy narzekać, że nie panujemy nad tym, czego oczekuje od nas klient. Wymagania będą zarządzane, bardzo możliwe że w konkretnym narzędziu (za które niekoniecznie będziemy musieli zapłacić krocie), będziemy mieli dokument opisujący życzenia klienta, a także ich „tłumaczenie” na język wymagań technicznych, no i będziemy wreszcie kontrolować zmiany do wymagań, a także śledzić, które z nich zostało przetestowane, a które „stoi w kolejce” (matryca powiązań wymagań). Zarządzanie konfiguracją – zdarzyło mi się raz doradzać Klientowi, który był poddostawcą dwóch dużych koncernów samochodowych, stanowiących dla siebie wzajemną konkurencję. Już na samym początku trochę zdziwiło mnie, dlaczego klient tak bardzo naciska akurat na „uzdrowienie” procesów z obszaru zarządzania konfiguracją. Okazało, że na skutek bałaganu właśnie w tym obszarze, podczas ostatniego wydania produktu, jeden z koncernów (przez przypadek o zgrozo!) otrzymał kod źródłowy swojej konkurencji. CMMI robi na tym polu bardzo dobrą robotę i pozwala uniknąć takich stytuacji. Pliki są poprawnie wersjonowane (zgodnie z jednym, ogólnie obowiązującym standardem), zmiany w elementach konfiguracji są monitorowane, na kolejnych etapach prac rozwojowych tworzone są odpowiednie „punkty odniesienia (ang. baselines), które umożliwiają „kontrolę” rozwijanego oprogramowanie. Miary i analizy – nagle zaczynami się interesować danymi, które mogą coś znaczyć. Metryki to bardzo bogaty świat, który znakomicie wspiera zarządzanie prowadzonym projektem. Oto, pojawią się dane, których wcześniej w ogóle nie zauważaliśmy, jak na przykład: dokładność estymat, liczba błędów zgłaszanych przez testerów i klienta; rozmiar powstającego kodu, produktywność, liczba przypadków testowych, które się nie wykonały etc. Dane zaczynają być wykorzystywane do lepszego zarządzania projektem. Analizowane (!) są przyczyny wystąpienia określonych problemów, menedżerowie definiują akcje korekcyjne.
Czytelników zainteresowanych bardziej „twardymi” danymi w zakresie efektów wdrożenia modelu CMMI w organizacji, odsyłam do Raportu Technicznego SEI (CMU/SEI-2003-SR-009, do pobrania na stronie: http:/ /www.sei.cmu.edu/), który zawiera liczne przypadki użycia w takich firmach jak: Accenture, Boeing, Lockheed Martin, Northrop Grumman, czy Thales. Dodatkowo od-
62
syłam do kilku przypadków użycia z „naszego podwórka”, przedstawionych w mojej książce: „CMMI – Doskonalenie Procesów w Organizacji” (Wydawnictwo Naukowe PWN 2010) z firm: GE Money Bank, GMC Software Technology oraz Hewlett Packard (Global Delivery Poland Center).
O tym i innych artykułach
Prezentowany artykuł jest pierwszym z cyklu, który – parafrazując tytuł jednej z książek Leszka Kołakowskiego – można by potraktować jako „mini-wykłady o maxi sprawach”. Osoby, które miały jakąkolwiek styczność z modelem CMMI, jego językiem (przypominającym momentami utwory Goeffreya Chaucera) – wie, jak wiele wysiłku potrzeba, żeby się go nauczyć, zrozumieć i właściwie zinterpretować; nie mówiąc już o jego wdrożeniu. Dlatego, zwróciłem uwagę tylko na kilka najważniejszych aspektów tego modelu, które – moim zdaniem – każdy początkujący adept powinien na jego temat wiedzieć. Świadomie, nie wchodziłem w szczegóły związane z architekturą modelu, jak i trudną terminologią SEI, która sprawia problemy nawet najbardziej wytrawnym znawcom. Zainteresowanych poznaniem więcej szczegółów oraz możliwych przypadków zastosowania w praktyce, odsyłam do mojej książki, którą wcześniej przytaczałem. Artykuł kończą pozytywne przykłady z „mojego podwórka”, przemawiające na rzecz, jeśli nie wdrożenia, to przynajmniej krótkiej refleksji nad tym, czy model CMMI powinien nas faktycznie obchodzić. W kolejnych artykułach pojawią się tematy związane z możliwością symbiozy modelu CMMI na gruncie metody SCRUM, która ostatnio jest bardzo popularnym i nowatorskim podejściem do zarządzania pracą w projektach. Dodatkowo będą poruszę te zagadnienia inżynierii oprogramowania, które – jak mawiał jeden z moich szefów – są „pilne i ważne”, a więc nie sposób ich pominąć.
MARIUSZ CHRAPKO Mariusz Chrapko jest wieloletnim praktykiem w zakresie doskonalenia procesów tworzenia oprogramowania w oparciu o model CMMI®. Wspiera �rmy informatyczne na terenie całej Europy, prowadząc coaching zespołów projektowych oraz szkolenia na różnych szczeblach organizacyjnych. Dodatkowo jest praktykiem we wdrażaniu i adaptacji metod Agile Software Development (wcześniej Agile Coach/ Centrum Oprogramowania Motoroli w Krakowie), Programu Metryk Organizacyjnych, a także procesu Peer Review. Jest autorem wielu publikacji z zakresu inżynierii oprogramowania oraz prelegentem na konferencjach krajowych i międzynarodowych. Autor pierwszej w Polsce książki na temat modelu CMMI raz jego praktycznego zastosowania. Od roku 2009 współpracuje z �rmą LOYCON® business solutions, udzielając wsparcia �rmom informatycznym w regionie Europy Centralnej i Wschodniej. Kontakt z Autorem: mariusz.chrapko@loycon.pl
8/2010
WYWIAD
Rozmowa z Mariuszem Chrapko
SDJ: Skąd pomysł na książkę o modelu CMMI? MCH: Pamiętam, jak zaczynałem swoją przygodę z CMMI (wtedy był to jeszcze CMM for Software, CMMI powstał kilka lat później), a więc jakieś siedem lat temu, i próbowałem czegokolwiek dowiedzieć się na temat modelu z naszych polskich opracowań. Efekt był taki, że udawało mi się odnaleźć jakieś, mniej lub bardziej spójne, wystąpienia konferencyjne – raczej mocno teoretyczne, które sprawiały wrażenie trochę „pisanych na kolanie”, ale chyba najbardziej mnie denerwował brak wspólnego słownika. Otóż poszczególne komponenty modelu, praktycznie w każdym wystąpieniu były inaczej tłumaczone. Było to bardzo frustrujące. Co więcej, przez te zgoła siedem lat, do momentu wydania mojej książki niewiele się zmieniło. Do dzisiaj, mając do wyboru oryginał lub jego polskie tłumaczenie, zawsze wybieram to pierwsze. Bałagan pojęciowy jest spotykany nawet w jednym i tym samym wydawnictwie. Przykład: dwa tłumaczenia znanych podręczników do inżynierii oprogramowania. Dodatkowo dochodzi problem samych tłumaczy, którzy w ogromnej większości przypadków są brani „z ulicy” i nie zawsze mają, choćby minimalną, wiedzę na tematy IT. Koniec końców, efekt jest taki, że pracownicy firm informatycznych, czytając taką książkę, w ogóle nie wiedzą, o co chodzi. SDJ: A jak Pan sobie poradził z problemem języka branżowego? MCH: Ja miałem trochę „łatwiej”. Przed napisaniem książki „siedziałem” już przez pewien czas w temacie, wgryzłem się w żargon programistów, wiedziałem, jak się z nimi komunikować. Pamiętam, jak jeszcze pracując w krakowskiej Motoroli, napisałem maila, w którym użyłem słowa „testy jednostkowe”. Natychmiast dostałem zwrotkę: o jakie testy ci chodzi? – chodziło oczywiście
64
o „unit testy” (śmiech) – bo tak się właśnie mówiło. Do tego dochodzi cała masa innych słów: „effort”, „stub”, „baseline” (klasyka gatunku), plus spolszczenia, które rzadko kiedy znajdzie Pan w słowniku (np. „impakt”, „dependencje”). Natomiast, oprócz języka – drugim, bardzo ważnym dla mnie motywatorem do napisania książki o CMMI byli pracownicy firm, którym doradzałem. Przy różnego rodzaju szkoleniach czy coachingu często padało pytanie, czy jest coś po polsku na ten temat. Teraz jest! (śmiech) SDJ: Poza normami CMMI, dotyczącymi jakości procesu wytwarzania oprogramowania, jest jeszcze seria norm ISO/IEC 25000 SQuaRE dotyczących jakości samego produktu programowego (jakości wewnętrznej, zewnętrznej oraz jakości w użyciu). W jaki sposób jakość procesu wytwarzania oprogramowania wpływa na samą jakość produktu programowego? MCH: Istnieje szerokie grono osób, którzy twierdzą, że jakość produktu jest czymś zupełnie niezależnym od jakości procesu tworzenia oprogramowania. No bo przecież, koniec końców, naszych klientów bardziej interesuje to, czy software, który mu dostarczamy, jest niezawodny (ang. realiability), użyteczny (ang. usability), wydajny (ang. efficiency), łatwy w utrzymaniu (ang. maintainability), przenośny (ang. portability), niż to, w jakim sposób on powstaje. Klient płaci za jakość produktu, cała reszta jest tylko „czarną skrzynką”, która z punktu widzenia klienta niewiele znaczy (dla niektórych firm nawet lepiej jest, że do niej nie zagląda). Pomijając jednak, mniej lub bardziej złożone, dyskusje na ten temat, osobiście uważam, że jakość procesu wytwórczego ma duży wpływ na jakość wytwarzanego oprogramowania. Ostatnio czytałem zabawną książkę Anthony’ego Bourdaina „Kill Grill. Restauracja od kuchni”, po której to lekturze dwa razy zastanawiam się zanim wybiorę posiłek w restauracji na niedzielny obiad
8/2010
Rozmowa z Mariuszem Chrapko
(śmiech). W swojej książce bynajmniej nie wybiela branży gastronomicznej, pokazując raczej „mroczne” zakamarki nowojorskiej gastronomii. Moim zdaniem książka, w sposób rewelacyjny ilustruje postawioną przeze mnie wcześniej tezę: jakość oprogramowania zależy od jakości procesu jego wytwarzania. Wracając natomiast do zestawienia modelu CMMI i serii norm ISO/IEC 25000 SQuaRe… Otóż według mnie, model CMMI jest „bardziej pojemny”. Zawiera 22 Obszary Procesowe (nie mylić z procesami!), które kondensują szereg dobrych praktyk inżynierii oprogramowania, dotyczących różnych dziedzin życia organizacji oraz prowadzonych w niej projektów. Stąd można powiedzieć, iż model CMMI, ze względu na swoją kompleksową „siłę rażenia”, wskazuje również na aspekty, związane z jakością produktów, o których mówi przytoczona przez Pana seria norma ISO/IEC 25000 SQuaRE. W tym, i wielu innych przypadkach (np. Six Sigma, Scrum, ITIL, COBIT, ISO 9001) model CMMI wykazuję bardzo dużą skłonność do symbiozy. Dlatego, gdy próbujemy go porównywać z jakimikolwiek innymi modelami czy standardami – wcześniej czy później zawsze dojdziemy do wniosku, że jest on komplementarny z porównywanym podejściem. SDJ: Czyli, jak dobrze rozumiem, normy serii ISO/IEC 2500 SQuaRE, Scrum czy Six Sigma pomagają osiągnąć założone przez niego cele i praktyki, a nie działają na jego niekorzyść? MCH: Dokładnie tak. Kilka lat temu, na europejskiej wersji konferencji SEPG, miał ciekawą prezentację Dave Zubrov, który mówił właśnie na temat tego, jak normy serii ISO/IEC 2500 mogą wspierać model CMMI. Przy odrobinie szczęścia można się do niej „dogrzebać” na oficjalnej stronie www.sei.cmu.edu SDJ: Czy jest możliwe, że mimo pełnego wdrożenia CMMI końcowy produkt programowy nie osiąga zadowalającej jakości? MCH: Oczywiście, że tak. Po pomalowaniu domu może się nagle okazać, że kolor „Radość w rozkwicie”, który miał nadać wnętrzu naszego mieszkania „pogodę ducha i optymizm ukwieconej łąki”, nagle okazał się być nietrafiony – zbyt odważny, i wolelibyśmy coś bardziej spokojnego i eleganckiego, np. „Lekko jak z płatka”, kompozycja delikatnego beżu, alabastru i migdałów. Albo zrobiliśmy malowanie, wszystko gra, mija pięć lat i wypadałoby trochę (!) odświeżyć ściany, i jakoś ciągle nam się nie składa. I tak mija sześć, siedem, osiem lat od pierwszego malowania i… pierwotne piękne kolory naszych ścian pokrywa szary osad, który niczym tornado F4 pochłania kolejne wnętrza naszego mieszkania. Myślę, że te dwa przykłady bardzo dobrze ilustrują, z jakimi konsekwencjami należy się liczyć, podejmując decyzję o wdrożeniu modelu CMMI we własnej organizacji. W pierwszym przypadku, jeżeli nie zaplanujemy w sposób właściwy ścieżki poprawy procesów wytwórczych, jak najbardziej możemy spodziewać się negatywnego wpływu na jakość do-
www.sdjournal.org
starczanych produktów. Może się na przykład okazać, że nasi pracownicy zaczną nagle poruszać się w rzeczywistości, żywcem przypominającej jedną z powieści Franza Kafki – i nagle zabraknie czasu na pisanie dobrego kodu. Drugi przypadek – nazwijmy go „leniucha malarza” polega na wdrożeniu modelu i nie podejmowaniu żadnych dalszych działań, związanych z jego „utrzymaniem” w firmie. I nie chodzi tutaj o to, że brakuje nam zaufania do ludzi i trzeba ich pilnować, żeby „podążali” szlakiem przyjętych standardów. Ostatnio wdrażałem praktyki CMMI, związane z „Zarządzaniem Konfiguracją” w jednej z polskich firm IT, i pamiętam jedno ze spotkań projektowych, podczas którego długo rozmawialiśmy o konieczności wprowadzenia tzw. punktów odniesienia (ang. baselines), które pomogłyby nam utrzymać porządek w kodzie źródłowym, przechowywanym w SVN-ie. Dla „niewtajemniczonych” dodam, że jest to narzędzie do kontroli wersji oprogramowania. Przyjęliśmy więc określoną strategię nadawania etykiet konkretnym wersjom kodu, które nie były aż tak bardzo skomplikowane, ale wymagały (przynajmniej w początkowym okresie stosowania) osoby, która co jakiś czas sprawdzałaby, czy etykiety są dobrze nadawane. Wszyscy jednogłośnie uznaliśmy, że takie „utrzymanie” naszej praktyki jest niezbędne, żeby się w tym wszystkim odnaleźć. A proszę zauważyć, że była to tylko jedna z praktyk modelu CMMI, którą trzeba było „utrzymać”. Jeżeli wdrażamy więc na przykład 2. Poziom Dojrzałości, wówczas jest ich dużo więcej, i bez właściwej „pielęgnacji” bardzo szybko może się okazać, że straciliśmy nie tylko czas i pieniądze, ale i nasze wysiłki w żaden sposób nie miały wpływu na jakość dostarczanych produktów. SDJ: A teraz pytanie związane z Pana doświadczeniem w zakresie wdrażania programów metryk. Czy może Pan podać przykłady najczęściej używanych metryk i uzasadnić dlaczego akurat na nie pada wybór? MCH: Są to na pewno metryki, związane z zarządzaniem błędami w projektach IT (tj. liczba wykrytych błędów wg stopnia ważności, metryka gęstości błędów, liczba błędów wykrytych przez klienta, metryka zaległości błędów, a więc ile błędów mamy jeszcze do rozwiązania). Druga grupa to metryki, dotyczące testów, a więc przede wszystkim metryka tzw. „Krzywej Testów”, która pokazuje: ile przypadków testowych zostało wykonanych względem przyjętego planu; ile spośród nich „nie przeszło”, a ile, z różnych przyczyn, zostało zablokowanych. Wreszcie bardzo przydatne i chyba najczęściej stosowane są metryki dotyczące przebiegu projektu, takie jak: dokładność estymat czy pracochłonność (ang. effort). SDJ: Panuje opinia, że praktyki Agile zdają egzamin tylko wśród doświadczonych zespołów. Jakie doświadczenia ma Pan podczas wdrażania praktyk Agile w firmach posiadających słabo wyszkolonych pracowników IT? MCH: Agile to przede wszystkim praca zespołowa, i jeżeli ktoś „odstaje”, zespół to wcześniej czy później zauważy i w naturalny sposób „wyeliminuje”. Mam tutaj
65
WYWIAD
sporo przykładów, że to faktycznie działa. Dlatego, moim zdaniem, zupełnie nieuzasadnione jest twierdzenie, iż praktyki agile sprawdzają się tylko w zespołach ekspertów. Oczywiście należy tutaj rozróżnić praktyki inżynieryjne (głównie związane z Extreme Programming) od praktyk dotyczących stricte zarządzania projektem (Scrum). W tym pierwszym przypadku, weźmy praktykę TDD, oczywiście trzeba mieć wiedzę i doświadczenie w pisaniu testów jednostkowych, ale tak jest przecież również w projektach, które nie używają „zwinnych” metod tworzenia oprogramowania. Natomiast – myślę, że przytoczona przez Pana opinia bardziej dotyczy pracy zespołowej, która jest „twardym rdzeniem” tych metod. Tutaj, Zespoły muszą się wzajemnie komunikować, udzielać sobie we właściwy sposób informacji zwrotnej, rozwiązywać konflikty, być kreatywnymi i szybko reagować na zmiany, których jest niemało. Oczywiście lepiej jest wdrażać Scrum w Zespołach, które zostały już, na skutek wcześniejszych doświadczeń projektowych, jakoś uformowane. Niemniej jednak Scrum dostarcza zestaw wystarczających narzędzi, które w bardzo szybki i „bezbolesny” sposób umożliwiają powstawanie „zwycięskich” zespołów projektowych, których niejednej firmie pozazdrościłby dżentelmen Danny Ocean, bohater znanej wszystkim produkcji amerykańskiej. SDJ: Czy może podać Pan dane statystyczne, które potwierdzają, że wdrożenie CMMI albo praktyk Agile przyniosło wyraźną poprawę zysków firmy? Zarówno danych, jak i pozytywnych przykładów jest tutaj bardzo dużo. Oczywiście mamy pewien deficyt „na naszym podwórku”, ale mam nadzieję, że to się wkrótce zmieni. Jeśli chodzi o CMMI: redukcja o ok. 30% średnich kosztów naprawy błędów (Boeing); zwiększenie z 50% do 90% dokładności harmonogramu projektu (General Motors); w ciągu 3 lat zwiększenie z 25% do 30% produktywności (Lockheed Martin, Harris, Siemens), zmniejszenie o 50% błędów w oprogramowaniu (Lockheed Martin) itd. Dodatkowo polecam wszystkim Czytelnikom bardzo ciekawy artykuł o sukcesach związanych z wdrożeniem, jeszcze starej wersji, CMM for Software w Motoroli (konkretnie w oddziale Government Electronics Division), dostępny do pobrania na stronach IEEE (Diaz M., Sligo J., „How Software Process Improvement Helped Motorola”, IEEE Software, 14 (5), 1997.). Natomiast w przypadku projektów, prowadzonych metodami agile, niestety danych statystycznych nie ma aż tak dużo. Co wynika z faktu, iż nie wszystkie firmy wdrażające nowe metody dokonują ich właściwej ewaluacji. Ale podam Panu kilka liczb, z wdrożeń, które prowadziłem w jednej z dużych firm telekomunikacyjnych w Polsce. Produktywność w projektach po wdrożeniu, w porównaniu z wcześniejszymi baseline’ami, wzrosła w niektórych przypadkach nawet dwukrotnie. Dodatkowo, na skutek niektórych praktyk XP (TDD, Automatyczna Regresja) gęstość błędów spadła prawie o 50%.
66
SDJ: Jakie wiąże pan plany w związku z firmą LOYCON? MCH: Z Firmą LOYCON nawiązałem współpracę w drugiej połowie 2009 roku, w związku z otwarciem jej nowego działu „LOYCON Process Solutions” (dotychczas jej podstawową działalność stanowił outsourcing rozwoju oprogramowania i testów, z czym nadal sobie świetnie radzi), którego głównym zadaniem jest udzielanie wsparcia firmom informatycznym w zakresie poprawy procesów wytwórczych w oparciu o model CMMI oraz metodę Scrum. Muszę powiedzieć, że wszystko rozwija się naprawdę dobrym kierunku. Naszą niewątpliwą siłą jest Zespół. Dzięki prywatnym kontaktom zatrudniliśmy wyjątkowych ekspertów głównie z zagranicy, posiadających bogate doświadczenie w środowisku projektów międzynarodowych. W związku z tym na jesień przygotowujemy coś naprawdę wyjątkowego, proszę śledzić naszą stronę www.loycon.pl i koniecznie www.tweeter.com/ loycon, gdzie znajdziecie najbardziej porywające tweety o CMMI, Scrumie i tym wszystkim, co mnie i moim kolegom spędza sen z oczu. SDJ: Brzmi świetnie. Ale jak Pan postrzega swoją działalność w Polsce, gdzie konsultant ciągle postrzegany jest jako intruz w firmie? MCH: Niestety ma Pan trochę racji. Trochę sami jesteśmy sobie winni. Oprócz dobrych referencji, nie ma żadnego mechanizmu weryfikacji firm konsultingowych, w zakresie jakości świadczonych usług. I nie łudźmy się, że taki mechanizm kiedykolwiek powstanie. Moim zdaniem problem bierze się stąd, że w Polsce jest sporo ludzi, którzy, mówiąc kolokwialnie, „liźnie trochę tematu”, kupi sobie bawełnianą koszulę, nawoskuje mokasyny, włoży średnio pasujący krawat i gotowe… idzie w świat głosić dobrą nowinę, że „jakość jest za darmo” (nawiązanie do książki Philipa Crosby’ego o tym samym tytule). My chcemy to zmienić. SDJ: Życzę więc wielu sukcesów na tej drodze i bardzo dziękuję za rozmowę!
MARIUSZ CHRAPKO Jest wieloletnim praktykiem w zakresie doskonalenia procesów tworzenia oprogramowania w oparciu o model CMMI®. Wspiera �rmy informatyczne na terenie całej Europy, prowadząc coaching zespołów projektowych oraz szkolenia na różnych szczeblach organizacyjnych. Dodatkowo jest praktykiem we wdrażaniu i adaptacji metod Agile Software Development (wcześniej Agile Coach/ Centrum Oprogramowania Motoroli w Krakowie), Programu Metryk Organizacyjnych, a także procesu Peer Review. Jest autorem wielu publikacji z zakresu inżynierii oprogramowania oraz prelegentem na konferencjach krajowych i międzynarodowych. Autor pierwszej w Polsce książki na temat modelu CMMI raz jego praktycznego zastosowania. Od roku 2009 współpracuje z �rmą LOYCON® business solutions, udzielając wsparcia �rmom informatycznym w regionie Europy Centralnej i Wschodniej.
8/2010
KLUB PRO INFOTEX SP.J Śmietanowski i Wsp.
TTS Company Sp. z o.o.
http://www.infotex.com.pl
http://www.OprogramowanieKomputerowe.pl
Dystrybutor XP Unlimited – Serwer Terminali dla Windows XP i VISTA. Program umożliwia łączenie się z dowolnego klienta Windows, Linux z wykorzystaniem protokołu RDP. Cena wersji Classic dla 5 użytkowników - 165€, dla nieograniczonej liczby - 235€. Ponadto oferujemy opiekę serwisową i aplikacje internetowe na zamówienie.
Sprzedaż i dystrybucja oprogramowania komputerowego. Import programów na zamówienie. Ponad 200 producentów w standardowej ofercie. Chcesz kupić oprogramowanie i nie możesz znaleźć polskiego dostawcy? Skontaktuj się z nami – sprowadzimy nawet pojedyncze licencje.
Softline rozwiązania mobilne Volantis jest uznanym na całym świecie dostawcą rozwiązań mobilnych dla firm udostępniających informacje oraz dane przez Internet (operatorów telefonii komórkowej, stacji tv, czasopism internetowych). Dzięki zasadzie „stwórz raz, uruchamiaj gdziekolwiek” nasze rozwiązania zmniejszają złożoność, koszt i czas dostarczenia na rynek serwisów i aplikacji. Volantis jest członkiem organizacji W3C, a naszymi klientami są światowi potentaci telekomunikacyjni.
Proximetry Poland Sp. z o.o.
Proximetry Poland Sp. z o.o. jest polskim oddziałem amerykańskiej firmy Proximetry Inc. – dostawcy systemów zarządzania sieciami bezprzewodowymi opartymi na technologiach WiFi i WiMAX. Naszą misją jest dostarczenie klientom rozwiązań poprawiających jakość usług (QoS) dostarczanych drogą radiową. Dołącz do najlepszych i zostań członkiem naszej ekipy! http://www.proximetry.com
Opera Software
Opera Software’s vision is to deliver the best Internet experience on any device. We are offering browser for PC/desktops and embedded products that operates across devices, platforms and operating systems. Our browser can deliver a faster, more stable and flexible Internet experience than its competitors.
Wiodący producent systemów mobilnych, dostawca aplikacji użytkowych dla biznesu (Symbian OS, Windows Mobile, J2ME ) zaprasza do współpracy. Zostań naszym partnerem. Dołącz do zespołu. http://www.softline.com.pl
Systemy bankowe, ISOF
HEUTHES istnieje na rynku od 1989 r. Obok systemów informatycznych dla banków, oferuje nowoczesne oprogramowanie do obsługi firm. System ISOF jest udostępniany klientom w trybie SaaS lub licencji. Pracuje na platformie Linux i zawiera m.in. takie moduły jak CRM, DMS, Magazyn, Sprzedaż, Logistyka oraz Rachunkowość. http://www.isof.pl
Architektury systemów IT
http://www.opera.com
Twórca frameworków JUVE i serwera aplikacji AVAX oferuje usługi, doradztwo, rozwiązania do tworzenia nowoczesnych, dużych systemów i rozwiązań informatycznych/internetowych, integrujące architektury ery post-J2EE/.NET, wykorzystujące MDD/MDA dla dziedzin – bankowość, telekomunikacja, handel, e-commerce, ERP/Workflow/CRM, rozwiązania internetowe, portalowe. www.mpsystem.com mpsystem@mpsystem.com
Kei.pl
Future Processing
http://www.kei.pl
http://www.future-processing.pl
Kei.pl działa na rynku usług hostingowych od 2000 roku. Do naszych zadowolonych Klientów z dumą możemy zaliczyć wiele przedsiębiorstw sektora MSP, instytucji oraz osób prywatnych. W ofercie Kei.pl znajdują się pakiety hostingowe, a także usługi dla wymagających Użytkowników – platformy e-Biznes oraz serwery fizyczne.
Playsoft
Playsoft jako lider portowania aplikacji na platformy mobilne wciąż powiększa bazę swoich klientów: EA Mobile, Sega, THQ, Konami. W ramach rozszerzania swojej działalności, poszukujemy doświadczonego programisty, który byłby odpowiedzialny za tworzenie aplikacji na platformy Iphone, Windows Mobile, Android. http:// www.playsoft.fr
Future Processing to dynamiczna firma technologiczna działająca na globalnym rynku oprogramowania. Jesteśmy zespołem wysokiej klasy specjalistów posiadających wiedzę i doświadczenie niezbędne do realizacji ambitnych projektów informatycznych. Jeśli programowanie to Twoja pasja dołącz do nas! (możliwość pracy zdalnej).
WSISiZ w Warszawie INFORMATYKA ZARZĄDZANIE
studia stopnia I i II (stacjonarne i niestacjonarne) specjalności: inżynierskie, magisterskie i licencjackie. Szczegółowe plany studiów, opisy poszczególnych specjalności – zapraszamy na stronę uczelni. http://www.wit.edu.pl