Praca_magisterska

118
Politechnika Warszawska Wydzial Matematyki i Nauk Informacyjnych Praca Dyplomowa Magisterska Informatyka PROPOZYCJA DYNAMICZNYCH RÓL I PROGRAMOWANIA PRZEZ KONTRAKT DLA JĘZYKA JAVA Autor: Aleksander Kosicki Promotor: dr inż. Krzysztof Kaczmarski Warszawa Grudzień 2008

Transcript of Praca_magisterska

Page 1: Praca_magisterska

Politechnika Warszawska

Wydział Matematykii Nauk Informacyjnych

Praca Dyplomowa MagisterskaInformatyka

PROPOZYCJA DYNAMICZNYCH RÓLI

PROGRAMOWANIA PRZEZ KONTRAKTDLA JĘZYKA JAVA

Autor: Aleksander KosickiPromotor: dr inż. Krzysztof Kaczmarski

Warszawa Grudzień 2008

Page 2: Praca_magisterska

......................... .........................podpis promotora podpis autora

Page 3: Praca_magisterska

Spis rzeczy

1 Wstęp 31.1 Cel pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 31.2 Motywacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41.3 Dlaczego Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 6

2 Programowanie przez Kontrakt (PPK) dla języka Java 72.1 O mechanizmach PPK . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 7

2.1.1 PPK w praktyce . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102.1.2 Elementy PPK występujące w Javie . . . . . . . . . . . . . . . . . . . 122.1.3 Istniejące implementacje PPK dla języka Java . . . . . . . . . . . . . . 13

2.2 Automaty skończone . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 142.2.1 Klasa jako automat skończony . . . . . . . . . . . . . . . . . . . . . . 17

2.3 Propozycja PPK dla języka Java . . . . . . . . . . . . . . . . . . . . . . . . . 192.3.1 Automat skończony klasy . . . . . . . . . . . . . . . . . . . . . . . . . 202.3.2 Definicja automatu skończonego . . . . . . . . . . . . . . . . . . . . . 212.3.3 Przejścia automatu skończonego . . . . . . . . . . . . . . . . . . . . . 242.3.4 Sprawdzanie stanu automatu skończonego . . . . . . . . . . . . . . . . 272.3.5 Niezmiennik klasy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 292.3.6 Warunki początkowe i końcowe metod . . . . . . . . . . . . . . . . . . 342.3.7 Warunki kontraktu metody . . . . . . . . . . . . . . . . . . . . . . . . 372.3.8 Wyjątki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 372.3.9 Kontrola kontraktów . . . . . . . . . . . . . . . . . . . . . . . . . . . . 41

2.4 Wykorzystanie w wypadku już istniejących komponentów . . . . . . . . . . . 422.5 Techniczne rozwiązanie problemu . . . . . . . . . . . . . . . . . . . . . . . . . 432.6 Uzasadnienie i krytyka propozycji . . . . . . . . . . . . . . . . . . . . . . . . . 47

2.6.1 Możliwości dalszego rozwoju . . . . . . . . . . . . . . . . . . . . . . . 49

3 Dynamiczne role 513.1 Czym są dynamiczne role? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 51

3.1.1 Obecny stan rzeczy - motywacja . . . . . . . . . . . . . . . . . . . . . 513.1.2 Czym nie są dynamiczne role . . . . . . . . . . . . . . . . . . . . . . . 573.1.3 Możliwości języka Java . . . . . . . . . . . . . . . . . . . . . . . . . . . 59

3.2 Propozycja dynamicznych ról dla języka Java . . . . . . . . . . . . . . . . . . 603.2.1 Przegląd biblioteki . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 633.2.2 Tworzenie aktorów . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 653.2.3 Tworzenie ról . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 66

1

Page 4: Praca_magisterska

3.2.4 Odgrywanie ról . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 673.2.5 Odłączanie ról . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 683.2.6 Synchronizacja ról . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 693.2.7 Ograniczenia dotyczące użycia ról . . . . . . . . . . . . . . . . . . . . 74

3.3 Wykorzystanie w wypadku istniejących już rozwiązań . . . . . . . . . . . . . 753.4 Techniczne rozwiązanie problemu . . . . . . . . . . . . . . . . . . . . . . . . . 763.5 Uzasadnienie i krytyka propozycji . . . . . . . . . . . . . . . . . . . . . . . . . 77

3.5.1 Możliwości dalszego rozwoju . . . . . . . . . . . . . . . . . . . . . . . 79

A Mechanizm PPK - przykład użycia 81

B Dynamiczne role - przykład użycia 89

C Narzędzia programistyczne 103C.1 Translator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103C.2 Zadanie dla systemu Apache Ant . . . . . . . . . . . . . . . . . . . . . . . . . 108

D Opis formalny gramatyki dla PPK 110

Bibliografia 114

2

Page 5: Praca_magisterska

Rozdział 1

Wstęp

1.1 Cel pracy

Celem niniejszej pracy jest propozycja oraz implementacja pewnych dwóch koncepcji z zakresujęzyków programowania.

Pierwsza z propozycji dotyczy mechanizmów Programowania przez Kontrakt1. Autor uza-sadnia tutaj, że klasom jako abstrakcjom języka programowania, można przypisywać pewneautomaty skończone, które są przez owe klasy realizowane na poziomie koncepcyjnym. No-wością jest tutaj propozycja autora polegająca na możliwości umieszczenia jawnej deklaracjiautomatu skończonego w definicji klasy, co pozwala na potraktowanie go jako elementu kon-traktu klasy - z wszystkimi tego pożytecznymi konsekwencjami.

Druga z propozycji dotyczy tzw. dynamicznych ról2. Autor precyzuje tutaj znaczeniepojęcia “dynamiczne role” na użytek swojej pracy. Według autora, termin ten ma z grubszaokreślać dynamiczne zachowanie się obiektów, które pozwala przyjmować im interfejsy orazzwiązane z nimi funkcjonalności w sposób dynamiczny, nieokreślony jeszcze na etapie kom-pilacji. Warto tu podkreślić, że mechanizm ten nie jest zwyczajnie rodzajem dynamicznegotypowania. Mianowicie, w systemach z dynamiczną kontrolą typów, zmiennej używanej jakoargument wywołania funkcji nadal stawiany jest wymóg reprezentowania określonego typulub możliwości zrzutowania do zmiennej owego typu, tyle tylko, iż sprawdzenie danego wa-runku następuje dopiero w czasie wywołania. Nie oznacza to jednak, że pewna zmienna możedostosowywać się ad hoc do potrzeb wywołania funkcji.

Implementacja, czyli część praktyczna pracy, polega z kolei na a) specyfikacji pewnego,zawierającego w sobie obie z powyższych koncepcji, języka oraz b) przygotowaniu narzędzipozwalających na programowanie w nim. Pod pojęciem “narzędzia” kryje się tutaj przedewszystkim translator dla owego języka, wraz z dodatkowymi bibliotekami i programami to-warzyszącymi (np. lokalizator błędów).

Ponieważ druga część pracy ma charakter czysto praktyczny i polega na demonstracjiprzedmiotu przy użyciu konkretnego języka programowania, autor zdecydował się przedsta-wić całość materiału w oparciu o już istniejący język. Językiem tym jest Java. W efekcieprowadzi to do powstania nowego języka programowania, dla wygody i zwięzłości nazwanego

1ang. DBC - Design by Contract, w dalszej części niniejszej pracy autor posługuje się skrótowcem literowympolskiego tłumaczenia terminu — PPK; wyjaśnienie pojęcia PPK można znaleźć w sekcji 2.1.

2wyjaśnienie w sekcji 3.1.

3

Page 6: Praca_magisterska

w niniejszej publikacji JavaBC3. Nie oznacza to oczywiście, że teoretyczne propozycje autorasame w sobie ograniczają się jedynie do tego języka lub mają z nim jakiś szczególny związek.Na zasadzie prostej abstrakcji propozycje te można odnieść do dowolnego języka obiektowego,zatem dotyczą one de facto większości z istniejących języków programowania4. Uwaga ta nieodnosi się do szczegółów technicznych składni oraz semantyki, które to siłą rzeczy pozostająjuż w ścisłym związku z Javą, jako językiem wewnątrz którego zostały osadzone. Autor wszczególności kładzie tu duży nacisk na integrację z Javą, wymagając aby jego rozszerze-nie Javy było jej właściwą nadklasą, tj. aby każdy poprawny program napisany w Javie,zachowywał swoją semantykę oraz poprawność składniową w JavaBC.

W związku z powyższym, na pracę niniejszą można spojrzeć w zasadzie z dwóch różnychperspektyw. Otóż, oceniając ją pod kątem zawartości teoretycznej, na pierwszy plan wy-suwają się propozycje dwóch owych koncepcji, a wykorzystanie Javy mieni się jedynie jakopomocniczy środek demonstracyjny. Z drugiej strony pracę ową można potraktować jakopropozycję rozszerzenia języka Java. W takim ujęciu mamy do czynienia z propozycją PPKoraz dynamicznych ról dla języka Java.

1.2 Motywacja

Na wstępie warto zadać pytanie o to, co dla autora stanowiło motywację poruszenia takich anie innych tematów.

Jeśli o chodzi o mechanizmy związane z Programowanie przez Kontrakt, autor sam jestgorącym zwolennikiem ich stosowania. Na pozór czasochłonne specyfikowanie warunków,nie raz pomogło autorowi w tarapatach, wskazując orientacyjne miejsce wystąpienia błędu.Techniki te zresztą doskonale uzupełniają dwie inne niskopoziomowe metody kontroli jakościi poprawności oprogramowania jakimi są testy jednostkowe oraz ręczna praca programisty zdebuggerem. Stosowane wraz z nimi pozwalają wyeliminować większość powstałych na niskimpoziomie abstrakcji błędów.

Niestety szeroki wachlarz narzędzi związanych z Programowaniem przez Kontrakt nie wy-starczał, o dziwo, we wszystkich zagadnieniach z jakimi autor miał styczność. Szczególnymprzypadkiem były tutaj klasy, których instancje miały cykl życia podzielony na ściśle okre-ślone fazy. W pewnych fazach, czy też stanach, można było zdefiniować pewne konkretnewarunki, jakie powinien spełniać obiekt. Także i przejścia pomiędzy fazami nie mogły od-bywać się dowolnie, lecz w sposób ściśle, na poziomie koncepcyjnym, określony. W prostysposób prowadziło to do umieszczanie w klasie innej klasy. Owa zagnieżdżona klasa byłade facto automatem skończonym, którego jedyną funkcją była kontrola poprawności zacho-wania się klasy gospodarza. W ten sposób narodził się pomysł dodania do mechanizmówProgramowania przez Kontrakt kontroli automatów skończonych klas.

3Nazwa wybrana została czysto arbitralnie, głównie w celu uniknięcia powtarzania rozwlekłych określeń wrodzaju “proponowany przez autora język”.

4W zasadzie ciężko jest podać przykładu języka który nie pozwala na korzystanie z paradygmatu progra-mowania obiektowego. Dużą część języków wspiera ów paradygmat bezpośrednio, poprzez uzupełnienie składnio specjalne reguły służące do obsługi obiektów (Python, C++, Ruby), część natomiast pozwala pośrednio naprogramowanie obiektowe, np. za pomocą struktur i wskaźników do funkcji (C) lub domknięć (ang. closures,np. Scheme). Co ciekawsze, wiele języków programowania, które pierwotnie nie pozwalały na programowanieobiektowe, zostało w toku ewolucji w niniejsze cechy wyposażone (Common Lisp, Fortran 2003).

4

Page 7: Praca_magisterska

Niestety Java, jako preferowany przez autora język programowania ogólnego przeznacze-nia, oferuje jedynie namiastkę mechanizmów Programowania przez Kontrakt. Namiastka tadostępna jest poprzez słowo kluczowego assert, pozwalające programiście na umieszczeniew kodzie warunku, który jego zdaniem powinien być zawsze w danym miejscu spełniony.Jakkolwiek cecha ta nie byłaby przydatna, nie zaspokaja ona wszystkich potrzeb związanychz kontrolą prawidłowości wykonania programu. Wyraz temu dany został między innymi wcieszącej się bardzo dużym poparciem prośbie o rozszerzenie języka Java5, a także w dużejilości bibliotek wzbogacających Javę o cechy PPK. W związku z powyższym, autor postanowiłuzupełnić Javę nie tylko o kwestie związane z kontrolą automatów skończonych, co miałobysłużyć prezentacji jego koncepcji, lecz w ogóle o większość typowych dla PPK cech, takich jakniezmienniki (ang. invariants) czy też warunki wstępne i końcowe metod (ang. preconditions,postonditions). Jest to zdaniem autora o tyle sensowniejsze, iż większość, jeśli nie wszystkie,z istniejących bibliotek oferujących PPK dla Javy opiera się na wykorzystaniu adnotacji6 lubzwyczajnych komentarzach. Autor natomiast zdecydował się osadzić PPK bezpośrednio w wskładni języka.

Druga z koncepcji autora, tj. dynamiczne role, ma charakter nieco bardziej ekspery-mentalny. Jest to pewnego rodzaju próba przezwyciężenia trudności związanych z ogólnieprzyjętym modelem identyfikator-zmienna, gdzie identyfikator w kodzie programu wskazujena zmienną z którą wiąże się pewna konkretna i ograniczona jednocześnie funkcjonalność.Problem polega na tym, że model ten nie uwzględnia sytuacji w której programista chciałbyaby dany identyfikator wskazywał raczej na pewien byt niż na ograniczoną funkcjonalność.Byt który może w czasie wykonania przechodzić różne fazy i być odgrywany przez różneobiekty.

Jeszcze raz warto tu podkreślić, że zagadnienie to jest dużo luźniej związane z systemamikontroli typów, nieźli by się to mogło na pierwszy rzut oka wydawać. Popularne w wielujęzykach programowania rzutowanie zmiennych na typ ogólniejszy (np. na Object, void*,etc.) nie powoduje w żadnym wypadku, iż dana zmienna zyskuje na ogólności, lecz prowadzijedynie do odrzucenia część, niepotrzebnych w danym kontekście, informacji o jej typie.

Co ciekawe, niektóre języki programowania podejmują pewne próby w tym zakresie. Przy-kładem może tu być chociażby tzw. “duck typing”, na który pozwala zyskujący ostatnio napopularności język Python. Taką próbą jest także niniejsza propozycja dynamicznych ról.

Warto zauważyć, że cechą wspólną obu wyżej omówionych propozycji jest ich sformuło-wanie w oparciu o języki obiektowe poprzez wykorzystanie i pogłębienie imperatywnych cechjęzyków tej klasy. Obie propozycje funkcjonują mianowicie w oparciu o pewien mechanizmzmiany stanu — czy to stanu automatu skończonego, czy też stanu obiektu na rzecz któregoodgrywana jest pewna rola. Istotne jest tu także to, że koncepcje te uzupełniają się w pewiensposób nawzajem. Pierwsza z nich służy określaniu wewnętrznych faz cyklu życia danegoobiektu za pomocą automatu skończonego. Druga, której zakres jest szerszy, formalizujenatomiast zewnętrzne zmiany funkcjonalności w cyklu życia danego bytu.

Konieczność specyfikacji języka oraz przygotowania narzędzi programistycznych wynikanatomiast w sposób naturalny ze sformułowanych koncepcji i służy uzasadnieniu (czy wręczdemonstracji) ich praktyczności. Bez części praktycznej praca byłaby, zdaniem autora, zwy-czajnie niepełna.

5ang. Request for Enchantment - RFE, patrz. http://bugs.sun.com/view bug.do?bug id=44493836ang. adnotations; cecha obecna w Javie od wersji 1.5, pozwalająca na uzupełnianie kodu metadanymi.

5

Page 8: Praca_magisterska

1.3 Dlaczego Java?

Autor zdecydował się oprzeć część praktyczną swojej pracy o już istniejący język programo-wania. Uczynił tak dlatego, że koncepcje przez niego wysuwane same w sobie nie stanowiązestawu mogącego posłużyć do konstrukcji gotowego języka programowania. Są to raczejpropozycje osadzone w kontekście znanym z istniejących języków programowania. Tworzenienowego języka programowania, jedynie w celu ich prezentacji, nie miałoby ze zrozumiałychprzyczyn szczególnego sensu i prawdopodobnie przekroczyłoby w ogóle możliwości autora. Wzwiązku z tym wykorzystany został istniejący język. Adaptacja istniejącego języka nastąpiławskutek, co bardzo istotne, stworzenia jego nad-klasy.

Dlaczego jednak wybór autora padł na Javę? Powodów, pomijając ten najbardziej oczy-wisty, tj. fakt że autor po prostu lubi ten język, można wymienić kilka:

konstrukcja języka — Składnia i semantyka Javy pozwalają na implementację zamierzeńautora w formie nadklasy języka. Dzięki temu migracja kodu napisanego w czystejJavie do JavaBC nie wymaga na dobrą sprawę żadnego nakładu pracy i jest praktycznienatychmiastowa. Jest to nota bene najważniejszy z powodów.

popularność — Java oraz jej derywaty (jak na przykład C#) stanowią obecnie najpopular-niejszą klasę języków ogólnego przeznaczenia.

prostota — Przy całkiem sporych możliwościach jakie oferuje Java, język ten wciąż za-chowuje względną prostotę i czytelność. Sprzyja to jasnemu przedstawieniu koncepcjiautora, które w wypadku innych języków takich jak np. Perl, mogłyby niejako “zagi-nąć” w gąszczu istniejących cech języka. Co więcej można zaryzykować stwierdzenie, żeskładnia oparta na C jest w mniejszym lub większym stopniu zrozumiała bodajże dlakażdego programisty.

brak mechanizmów DBC — Praktyczny brak mechanizmów DBC w Javie stanowi dodat-kową zachętę do ich implementacji w oparciu o ten język.

szybkość — Wbrew obiegowym opiniom Java jest językiem względnie szybkim, szczególniejak na język z automatycznym oczyszczaniem pamięci, uruchamiany przy pomocy ma-szyny wirtualnej. Co więcej jej szybkość ustawicznie wzrasta wraz z kolejnymi edycjamimaszyn wirtualnych7.

7por. The Computer Language Benchmarks Game - http://shootout.alioth.debian.org/

6

Page 9: Praca_magisterska

Rozdział 2

Programowanie przez Kontrakt(PPK) dla języka Java

2.1 O mechanizmach PPK

Termin “Design by Contract1”, oznaczający w wolnym tłumaczeniu “Programowanie przezKontrak”, ukuty został przez przez Bertranda Meyera w trakcie prac nad językiem progra-mowania Eiffel. Oznacza on pewną koncepcję z zakresu projektowania oprogramowania, po-legającą na specyfikacji warunków tzw. kontraktu pomiędzy wywoływanym podprogramema programem go wywołującym. Najbardziej typowym przypadkiem kontraktu jest wywoła-nie metody funkcji. Samo słowo “kontrakt” użyte zostało tu w nawiązaniu do jego znacze-nia funkcjonującego w świecie biznesu, gdzie zawierany pomiędzy dwiema stronami kontrakt(∼umowa) składa się w zasadzie z pewnych zobowiązań, które obie strony wobec siebie po-dejmują.

Na czym jednak w praktyce polega pojęcie “kontraktu”? Po pierwsze należy zauważyć,że na dobrą sprawę pewne elementy programowania, w których można dopatrzeć się cechkontraktów, istnieją praktycznie od zarania historii języków programowania. Zaliczyć możnado nich np. a) określenie przez funkcję jej arności oraz b) typów poszczególnych argumen-tów, c) określanie przez funkcję typu wartości zwracanej, d) konieczność dostarczenia przezkod wywołujący odpowiedniej ilości parametrów o e) odpowiednich typach, f) koniecznośćpotraktowania wartości zwracanej przez funkcję odpowiednio do jej typu. Kontraktem zatemmożna nazwać zbiór pewnych ograniczeń dotyczących interakcji pomiędzy poszczególnymielementami programu.

Niech przykładowo dany będzie fragment programu napisanego w języku C:

1 double d iv id e ( double div ident , double d i v i s o r ){2 return d iv iden t / d i v i s o r ;3 } ;4 /∗ . . . ∗/5 double quot i ent = d iv id e ( 2 . 0 , 3 . 0 ) ;6 double quot i ent2 = d iv id e ( 1 . 0 ) ;7 double [ ] q u o t i e n t a r r a y = d iv id e ( 2 . 0 , 3 . 0 ) ;

1znany również pod alternatywnymi określeniami: “Programming by Contract” oraz “Contract Program-ming”.

7

Page 10: Praca_magisterska

Zamieszczony fragment zawiera deklarację funkcji dzielenia — divide— oraz jej wywoła-nie. Definicja funkcji dzielenia (linijka 3.) wymaga aby wywoływać ją z dwoma argumentamirzeczywistymi oraz zapewnia, iż sama zwraca typ rzeczywisty. Linijka 8. zawiera przykładprawidłowego, tj. zgodnego z kontraktem, wywołania funkcji dzielenia. Linijki 6. oraz 7.zawierają z kolei przykłady błędnego wywołania funkcji. W pierwszym przypadku wywoła-nie nie dostarcza wszystkich wymaganych parametrów, w drugim natomiast następuje próbapotraktowania funkcji tak jakby miała zwracać tablicę. Obie sytuacje niezgodne są z wymo-gami elementarnego kontraktu określonego w deklaracji funkcji. Sytuacja ta zostanie wykrytaprzez odpowiednie narzędzie jeszcze na etapie kompilacji, co zapobiegnie stworzeniu wadli-wego programu. Niech jednak wywołanie funkcji będzie jak następuje

1 double d iv id e ( d iv iden t a , d i v i s o r b ){2 return d iv iden t / d i v i s o r ;3 } ;4 /∗ . . . ∗/5 double quot i ent = d iv id e ( 2 . 0 , 0 . 0 ) ;

Linijka 8. zawiera poprawne wywołanie funkcji dzielenia. Większość kompilatorów zaak-ceptuje powyższy fragment programu bez żadnych zastrzeżeń. Program ten jednak ewidentnienie jest programem prawidłowym. Dzielenie przez zero, jakie spróbowałby wykonać, jest nie-dopuszczalne. W tej sytuacji spełnienie podstawowych wymogów wywołania funkcji nie jestwarunkiem dostatecznym prawidłowości wywołania. Okazuje się, że w rzeczywistości kon-trakt powinien zostać zaopatrzony w dodatkowy, sprawdzany w czasie wykonania, warunekdotyczący argumentów wywołania. Warunek ten można nazwać warunkiem wstępnym (ang.precondition). W niniejszym przypadku powinien polegać on na dodatkowym wymaganiu codo nie-zerowości dzielnika. Rozwiązania tego problemu mogą być następujące:

• Można założyć, że programista korzystający z funkcji dzielenia, zdaje sobie sprawę ztego, iż drugi z argumentów wywołania funkcji nie może być zerowy, stąd też będzie on(przy odrobinie dobrej woli) przestrzegał tego zakazu.

• Funkcję dzielenia można także opatrzeć dodatkowym komentarzem pozwalającym nasłowne doprecyzowanie warunków kontraktu. W razie wątpliwości, programista możedzięki temu otworzyć plik z deklaracją danej funkcji i, czytając komentarz jakim jestopatrzona, zapoznać się z dodatkowymi wymogami jej kontraktu.

• Wreszcie, w ciele funkcji dzielenia można umieścić instrukcję upewniającą się, że war-tość dzielnika jest różna od zera. W razie podania nieprawidłowej wartości instrukcjata powinna przerwać wykonywanie programu. Przerwanie może ewentualnie zostać po-przedzone komunikatem o złamaniu kontraktu.

Mimo że wyżej przedstawione rozwiązania należą do powszechnie stosowanych, mają one jed-nak pewne istotne wady. Pierwsze pomysł nie jest w zasadzie w ogóle żadnym rozwiązaniem,lecz jedynie nabożnym życzeniem co poprawności tworzonego oprogramowania. Drugi sposóbw zasadzie także nie wymusza żadnej kontroli wywiązywania się z kontraktu, nie gwarantujenawet, że programista zapozna się z komentarzami2. Trzeci z zaproponowanych sposobów jest

2Co prawda niektóre zaawansowane środowiska programistyczne potrafią automatycznie czytać komentarzedotyczące kontraktów (szczególnie jeśli te napisane są w sposób sformalizowany) i przeprowadzać, przy po-

8

Page 11: Praca_magisterska

tutaj możliwie najlepszy, choć posiada jeden zasadniczy mankament — prowadzi do przemie-szania się właściwej logiki programu z logiką sprawdzania kontraktu.

Najlepszym rozwiązaniem omawianego problemu byłaby tutaj jawna deklaracja kontraktu— tak jak na ma to np. miejsce w niniejszym przykładzie napisanym w języku D3:

1 double d iv id e ( d iv iden t a , d i v i s o r b )2 in {3 assert b != 0 ;4 } body {5 return d iv iden t / d i v i s o r ;6 } ;7 /∗ . . . ∗/8 double quot i ent = d iv id e ( 2 . 0 , 0 . 0 ) ;

Oczywiście możliwość jawnej deklaracji kontraktu dostępna jest jedynie w wypadku nie-których języków programowania. Pierwszym z języków, który pozwalał na tak zintegrowanąze składnią specyfikację kontraktów był, wspomniany na początku pracy, język Eiffel. Językten umożliwia programiście na stosowanie następujących konstrukcji z zakresu Programowa-nia przez Kontrakt:

warunków wstępnych podprogramów (ang. preconditions), wykorzystywanych na ogół dosprawdzenia poprawności argumentów wywołania oraz prawidłowości kontekstu wywo-łania. Przez kontekst wywołania autor rozumie tutaj “okoliczności”, w których na-stąpiło wywołanie. Może to być np. stan klasy której metoda wywołana została jakopodprogram. W języku Eiffel konstrukcja określająca takie warunki była związane zesłowem kluczowym require i występowała na początku podprogramu. Praktyka częstołączy ją także ze słowem in.

warunków końcowych podprogramów (ang. postconditions), wykorzystywanych na ogółdo sprawdzenia poprawności wartości zwracanych przez funkcję. W języku Eiffel kon-strukcja określająca takie warunki była związane ze słowem kluczowym ensure i wy-stępowała na końcu podprogramu. Praktyka często łączy ją także ze słowem out.

niezmienników klas (ang. invariats), oznaczających specjalny warunek, który powinien byćspełniony przy wywoływaniu dowolnej z metod klasy.

Oprócz mechanizmów dostępnych w języku Eiffel praktyka PPK wyróżnia parę dodatkowychelementów kontraktu takich jak specyfikacja

efektów ubocznych podprogramów (ang. side effects), czyli pewnych modyfikacji swojegokontekstu, jakich mogą dopuścić się podprogramy — oprócz swojego podstawowego za-dania jakim jest zwracanie wartości. Jako szczególny rodzaj efektów ubocznych, możnatu wyróżnić modyfikacje pola obiektu na rzecz którego wywołana została metoda orazmodyfikacje referencyjnych wartości przekazanych jako parametr. Owa szczególna klasa

mocy tak uzyskanych informacji, statyczną kontrolę kodu lub nawet instrumentować kod wynikowy programu.Niemniej są to dodatkowe mechanizmy nie związane bezpośredni z językiem.

3Wieloparadygmatowy język ogólnego przeznaczenia stworzony przez Waltera Brighta. Oparty na syntak-sie C++ i, z różnym skutkiem, promowany jako jego następca.

9

Page 12: Praca_magisterska

efektów ubocznych wiąże się ściśle z pojęciem “kontroli stałych” (ang. const correct-ness) która w rzeczywistości jest bardzo zawężonym i jednocześnie dosyć powszechniewystępującym mechanizmem Programowania przez Kontrakt. W kontrolę stałych wy-posażone są m.in popularne języki C oraz C++.

błędów jakie mogą mieć miejsce podczas wywołania podprogramu. Element ten na ogółsłuży do jawnego podania listy wyjątków jakie może generować podprogram.

Dodatkowo, wiele języków programowania pozwala na stosowanie

asercji które służą do specyfikowania warunków poprawności programu. Asercje same wsobie nie stanowią elementów kontraktu, ale umieszczone w odpowiednim miejscu mogąbyć używane jako ich substytut. Przykładowo sprawdzanie warunku wstępnego metodymożna uzyskać poprzez umieszczenie asercji z owym warunkiem na samym początkuciała danej metody.

2.1.1 PPK w praktyce

Ponieważ zasady programowania przez kontrakt nie zwiększają samoistnie funkcjonalnościjęzyka, a prawidłowe programy napisane z ich wykorzystanie działają równie dobrze po usu-nięciu kontraktów, rodzi się pytanie o ich faktyczną przydatność. Czemu wprawny, tworzącybiegle prawidłowe i “eleganckie” rozwiązania programista, ma poświęcać dodatkowy czas nażmudną specyfikację kontraktów, które i tak w jego przypadku są zawsze spełnione?

Odpowiedź na to pytanie jest prosta. Nie ma idealnych programistów, a tym bardziejidealnych programów. Autor nigdy osobiście nie słyszał o dużym przedsięwzięciu informa-tycznym, którego produkt końcowy nie zawierałby błędów - często ujawnionych nie tylkona etapie tworzenia oprogramowania i jego testów, lecz także w środowisku produkcyjnym.Jednym słowem, wystąpienia błędów były, są i zapewne przez długi czas będą nieodłącznymelementem procesów wytwarzania oprogramowania.

W celu wykrycia potencjalnych błędów zwykło przeprowadzać się różnego rodzaju testy,które mają uprzedzić potencjalne wystąpienia błędów w środowisku produkcyjnym i doprowa-dzić do ich usunięcia. Oczywiście cały cykl testów i kontroli jakości oprogramowania możnasprowadzić jedynie do funkcjonalnych testów systemowych, gdzie wystąpienie każdego błęduoznacza konieczność, często bardzo trudnej, próby potwierdzenia jego powtarzalności orazdojścia do jego rzeczywistej przyczyny. Mimo że praktyka taka jest często stosowana, wy-krycie błędu na tym etapie pociąga ze sobą bardzo wysokie koszty jego usunięcia. Z tejwłaśnie przyczyny, jeszcze przed testami systemowymi, przeprowadza się często testy integra-cyjne, służące sprawdzeniu interakcji pomiędzy poszczególnymi modułami systemu. Z koleiposzczególne moduły testuje się za pomocą, jeszcze bardziej podstawowych w zakresie swo-jego przedmiotu, testów jednostkowych (zwanych także testami modułowymi). Jest to pewiendodatkowy nakład pracy, który w dłuższej perspektywie zawsze zwraca się z nawiązką. Po-dobnie prewencyjny charakter mają także, mniej wśród programistów popularne, elementyProgramowania przez Kontrakt.

Jeśli potraktujemy elementy Programowania przez Kontrakt jako jeden z etapów testów,okaże się, iż w hierarchii zajmują one szczebel tuż poniżej testów jednostkowych. MechanizmyPPK, potraktowane jako dodatkowy etap testów, okazują się jeszcze bardziej zautomatyzowa-nym sposobem wykrywania błędów niż testy jednostkowe. Sprawdzanie kontraktów zaczynasię tu już na etapie poprzedzającym testy jednostkowe. Sam przedmiot testów jest wówczas

10

Page 13: Praca_magisterska

Rysunek 2.1: Programowanie przez Kontrakt w kontekście testów.

jeszcze ściślej określony, a lokalizacja błędów jeszcze dokładniejsza. Jednak najważniejszącechą testów przez PPK jest w tym wypadku to, iż praktycznie każde dowolne uruchomienieprogramu lub jego fragmentu, staje się automatycznie testem kontraktów. Wreszcie, testy jed-nostkowe mogą być tworzone w oparciu o wyspecyfikowane wcześniej kontrakty. Współczesnenarzędzia programistyczne umożliwiają nawet automatyczne tworzenie testów jednostkowychna podstawie danych kontraktów. Rysunek 2.1 podsumowuje umieszczenie mechanizmówPPK w kontekście etapów testowania oprogramowania.

W tym świetle mechanizmy PPK przestają jawić się jako swego rodzaju puryzm języ-kowy (“skoro język X umożliwia stosowanie elementów DBC, należy je wykorzystywać”) lecznabierają znaczenia jako jeden z etapów wczesnej kontroli jakości oprogramowania. Jest tonajwiększy, aczkolwiek nie jedyny, pożytek płynący z praktycznego stosowania Programowa-nia przez Kontrakt. Stosowanie PPK prowadzi mianowicie do uzyskania dwóch dodatkowych,pożytecznych efektów ubocznych.

Pierwszym z nich jest wymuszenie na programiście bardziej przemyślanego i skonkretyzo-wanego podejścia do tworzonych komponentów. Przypomina to w pewnym sensie metodolo-gię Rozwoju przez Testy (ang. Test Driven Development) zastosowaną w znacznie mniejszejskali. O ile w wypadku RPT tworzenie komponentu zaczynało się od przygotowania testówjego funkcjonalności, o tyle tworzenie klasy bądź funkcji (∼metody) zaczyna się od napisaniajej sygnatury, której częścią jest kontrakt.

Drugim pomniejszym pożytkiem płynących ze stosowania zasad Programowania przezKontrakt jest zwiększenie czytelności i klarowności kodu. Rzut okiem na osadzony w składnijęzyka kontrakt jest niejednokrotnie lepszym i szybszym sposobem na zapoznanie się z wy-mogami narzucanymi użytkownikowi przez procedurę niż czytanie komentarza jakim zostałaopatrzona. Reasumując, Programowanie przez Kontrakt:

• sprzyja wykrywaniu błędów,

• wpaja dobre nawyki programistyczne,

• zwiększa czytelność kodu.

11

Page 14: Praca_magisterska

2.1.2 Elementy PPK występujące w Javie

Język Java nie posiada co prawda pełnych mechanizmów Programowania przez Kontrakt(por. 2.1), oferuje jednak pewną ich namiastkę:

• Java jest językiem z silną kontrolą typów. W wypadku wywołania metody wymusza toautomatycznie spełnienie pewnych warunków wstępnych dotyczących ilości oraz typówparametru a także warunków końcowych dotyczących typu wartości zwracanej. Przy-kładowo metoda mająca w swej sygnaturze wyspecyfikowaną klasę String gwarantuje wsposób oczywisty iż wartością przezeń zwracaną będzie właśnie String a nie, powiedzmy,obiekt zbioru. Niezastosowanie się do tego prostego wymogu jest wykrywane przez kom-pilator. W zasadzie nie jest to mechanizm właściwy dla PPK, ale raczej coś na kształt“proto-kontraktu”.

• Kolejnym z elementów PPK obecnych w Javie jest specyfikacja błędów (∼wyjątków)mogących wystąpić przy wywołaniu metody. Od dostawcy wymagana jest tu jawnainformacja o możliwych błędach, natomiast od odbiorcy jawna ich obsługa — czy toprzez przechwycenie, czy też przez zignorowanie4. Brak owych informacji powodujezgłoszenie błędu przez kompilator.

• Java posiada także pewne elementy kontroli stałych w postaci modyfikatora final.Słowo to, użyte w roli modyfikatora zmiennej, gwarantuje iż przypisanie nowej wartościdanej zmiennej jest zabronione. Próba wykonania takiej operacji wykrywana jest jeszczena etapie kompilacji. Mechanizm ten jest niestety bardzo ograniczony i w szczególnościnie można go wykorzystać do zapobiegania modyfikacjom obiektu na który wskazujereferencja.

• Wreszcie, Java udostępnia prosty mechanizm asercji. W odróżnieniu od dwóch po-wyższych elementów, poprawność asercji nie jest sprawdzana na etapie kompilacji leczpodczas wykonania programu. Złamanie asercji prowadzi do wygenerowania wyjątku,przy czym domyślnie kontrola asercje nie jest włączona podczas wykonania programu.Najprostszym sposobem jej włączenia jest użycie odpowiedniego polecenia maszynywirtualnej jakim jest przełącznik -ea5.

Ponieważ mechanizm asercji stanowi podstawę funkcjonowania mechanizmów PPK stwo-rzonych przez autora, warto więc, w celu jego dodatkowej ilustracji, posłużyć się prostymprzykładem:

1 public class AssertionsDemo {2 public stat ic void main ( St r ing argv [ ] ) {3 assert ! ( argv . l ength > 0 && ” f a i l ” . equa l s ( argv [ 1 ] ) )4 : ”Program f a i l e d ” ;5 System . out . p r i n t l n ( ” He l lo a s s e r t s ” ) ;6 }7 }4W rzeczywistości wymóg ten dotyczy tylko pewnej podklasy możliwych wyjątków. W wypadku pozosta-

łych wyjątków specyfikacja taka nie jest w ogóle wymagana.5Najpopularniejsza z maszyna wirtualnych Javy — Java Hot Spot VM firmy Sun Microsystem — pozwala

użytkownikowi na nieco bardziej wyśrubowany sposób kontroli asercji. Przykładowo, możliwe jest włącze-nie kontroli asercji tylko dla wybranych pakietów. Więcej informacji na ten temat można uzyskać poprzezwywołanie java -?

12

Page 15: Praca_magisterska

Najbardziej istotny element programu stanowią linijki 3 oraz 4 zawierające instrukcjęasercji. Asercja składa się tutaj z obligatoryjnego warunku !( argv.length > 0 &&"fail".equals( argv[1] ) ) oraz opcjonalnego komunikatu Program failed. Wa-runek asercji powinien zostać złamany w sytuacji gdy pierwszym z parametrów wywo-łania programu będzie słowo fail. Próby różnych wywołań programu przedstawiająsię następująco:

$ java AssertionsDemoHello asserts$ java AssertionsDemo failHello asserts$ java -ea AssertionsDemoHello asserts$ java -ea AssertionsDemo failException in thread "main" java.lang.AssertionError: Program failed

at AssertionsDemo.main(AssertionsDemo.java:3)

W przypadku czwartym, w którym program został uruchomiony w trybie sprawdza-nia asercji, a sam warunek asercji został złamany, nastąpiło odrzucenie wyjątku językaJava — java.lang.AssertionError. Ponieważ wyjątek nie został przechwycony, nastandardowym wyjściu błędu został wyświetlony odpowiedni komunikat. W skład ko-munikatu wszedł m.in umieszczony w kodzie programu ciąg Program failed oraz nazwapliku źródłowego wraz z numerem linii, w której została umieszczona feralna asercja.Programiście widzącemu taką wiadomość nie pozostaje nic innego jak “wziąć pod lupę”wadliwy fragment programu.

2.1.3 Istniejące implementacje PPK dla języka Java

Brak pełnego wsparcia dla Programowania przez Kontrakt w języku Java dawał się we znakipraktycznie od momentu poczęcia tego języka. Najpełniejszy temu wyraz dała sformułowanai poparta przez sporą część środowiska skupionego wokół Javy prośba o uzupełnienie językao mechanizmy programowania kontraktowego[6]. Mimo iż od złożenia prośby minęło już bezmała 8 lat, nie należy oczekiwać, iż kiedykolwiek zostanie ona pozytywnie rozpatrzona. Wy-rażone w niej oczekiwania stoją w sprzeczności z kładącą duży nacisk na prostotę koncepcjąrozwojową języka. Nawiasem mówiąc, autor w zupełności popiera takie podejście.

Naturalnym skutkiem takiego stanu rzeczy było pojawienie się różnego rodzaju bibliotekoferujących elementy programowania kontraktowego dla Javy. Tablice 2.1 oraz 2.2 zawierająkrótki przegląd dwóch popularnych implementacji Programowania przez Kontrakt dla językaJava - Java Modeling Language oraz Contract4J.

Biblioteki te dobrze ukazują dwa główne podejścia do kwestii sposobu deklaracji kontrak-tów. Mianowicie pierwsza z nich wymaga umieszczania deklaracji kontraktów w komenta-rzach, z kolei druga wykorzystuje w tym celu niedawno wprowadzony do Javy mechanizmadnotacji. Co istotne, żadna z bibliotek nie wzbogaca języka o nową składnię. Dobrą ce-chą takiego rozwiązania jest to, iż programy z tak zadeklarowanymi kontraktami mogą byćkompilowane przy użyciu standardowego kompilatora Javy - w takiej sytuacji informacje okontraktach zostaną na etapie kompilacji zignorowane. Elastyczność taka ma niestety swoją

13

Page 16: Praca_magisterska

JML - Java Modeling Language[12]

Integracja z językiem - deklaracje zawarte w komentarzachSyngalizacja błędu - poprzez wyjątkiWyłączenie kontroli - powtórna kompilacja programuMożliwości - szeroki wachlarz konstrukcji: warunki wstępne i końcowe,

efekty uboczne (∼kontrola stałych), niezmienniki, asercjeUżycie - kompilacja za pomocą specjalnego programu dołączonego

do biblioteki; uruchomienie tylko z odpowiednią biblioteką

Przykład/*@ requires a != null &&@ (\forall int i; 0 < i && i < a.length;@ a[i-1] <= a[i]); */

int binarySearch ( int [ ] a , int x ) {/* ... */

}

Tablica 2.1: podsumowanie biblioteki JML

cenę w postaci a) gorszej czytelności kodu źródłowego oraz b) mniejszego prawdopodobień-stwa na potencjalne wsparcie ze strony środowisk programistycznych. Co więcej c) żadna zbibliotek nie oferuje mechanizmu dynamicznego wyłączania kontroli kontraktów.

2.2 Automaty skończone

Ponieważ pojęcie automatu skończonego odgrywa zasadniczą rolę w propozycji PPK autora,w niniejszym punkcie zostanie przytoczona jego formalna definicja.

Niech dana będzie pewna piątka

M = (Q,Σ, δ, q0, F )

gdzie Q i Σ — skończone zbiory, q0 ∈ Q, F ⊂ Q oraz δ : Q× Σ→ Q.Tak określona piątka M może zostać potraktowana jako “automat skończony”7 Elementy

zbioru Q można w tej sytuacji nazwać “stanami”, przy czym q0 jest tutaj pewnym wyróż-nionym stanem, zwanym “stanem początkowym” a F pewnym wyróżnionym zbiorem tzw.“stanów akceptujących”. Zbiór Σ, w zależności od kontekstu, określa się mianem “zbioru

7ang. Finite Automaton. Przedstawiona tu konstrukcja jest w rzeczywistości pewnym rodzajem automatuskończonego, nazywanym w literaturze “deterministycznym automatem skończonym” (z a.g DeterministicFinite Automaton). Ponieważ jednak w pracy niniejszej nie pojawiają się żadne inne, poza deterministycz-nym, rodzaje automatów skończonych, autor na określenie “deterministycznego automatu skończonego” będzieposługiwał się tu po prostu nazwą “automat skończony”, a czasami nawet jeszcze bardziej lakonicznym okre-śleniem “automat”. Przyjęta konwencja wynika z troski o zwięzłość.

14

Page 17: Praca_magisterska

Contract4j[1]

Integracja z językiem - przy pomocy adnotacjiSyngalizacja błędu - poprzez wyjątkiWyłączenie kontroli - powtórna kompilacja programuMożliwości - warunki wstępne i końcowe, niezmiennikiUżycie - za pomocą narzędzi środowiska AspectJ6

Przykład@Contractpublic class MyClass {

@Invar ( ”name != n u l l ” )private St r ing name ;

@Pre( ”n != n u l l ” )public void setName ( St r ing n) { name = n ; }

@Post ( ” $return != n u l l ” )public St r ing getName ( ) { return name ; }

}

Tablica 2.2: podsumowanie biblioteki Contract4j

sygnałów” bądź “alfabetem wejściowym”. Funkcja δ nosi nazwę “funkcją przejścia”8.

W wypadku automatów skończonych, istotną rolę odgrywa pojęcie “stanu końcowego”.Mając dany automat skończony M oraz pewien skończony ciąg — k = k1, k2 ..., kn — ele-mentów ze zbioru Σ automatu, można przyjąć iż stan końcowy Sn dla danego wejścia wyrażasię wzorem Sn = δ( ... δ(δ(q0, k1), k2) ... , kn). Jeśli Sn ∈ F można stwierdzić, iż automat za-akceptował wejście. W przeciwnym wypadku można powiedzieć, iż automat nie zaakceptowałdanego wejścia.

Typową implementacją automatu skończonego, tj. swego rodzaju konstrukcją myślowąakceptującą ten sam zbiór ciągów wejściowych co dany automat, jest mechanizm składającysię z a) taśmy zawierającej słowo wejściowe, b) głowicy służącej do odczytu taśmy oraz c) jed-nostki sterującego . W chwili rozpoczęcia obliczeń jednostka sterująca znajduje się w stanieq0, a głowica umieszczona jest nad pierwszą komórką taśmy. Pojedynczy krok obliczeń polegana zmianie stanu, określanej za pomocą funkcji przejścia (argumentami są tu bieżący stansterowania oraz słowo aktualnie czytane przez głowicę), oraz przesunięcia głowicy o jednąkomórkę w prawo. Obliczenia kontynuowane są do chwili odczytania całego wejścia. Stansterowania w chwili zakończenia obliczeń jest stanem końcowym, przy czym stany sterowaniaimplementacji automatu można i należy utożsamiać w tym przypadku ze stanami należącymido zbioru stanów automatu. Więcej szczegółów na temat konstrukcji tego typu implementacjimożna znaleźć w [10, rozdział 7].

Schemat obliczeń implementacji automatu skończonego o konstrukcji podobnej do po-wyższej przedstawia się często za pomocą diagramu. Diagram taki na ogół jest ukazany w

8Można dla uproszczenia przyjąć, że funkcja przejścia jest określona dla wszystkich argumentów ze swojejdziedziny.

15

Page 18: Praca_magisterska

Rysunek 2.2: Diagramu automatu akceptującego niepuste ciągi binarne, nie zawierające se-kwencji złożonych z tych samych cyfr o długości większej niż 3.

postaci grafu skierowanego, w którym wierzchołkami są poszczególne stany automatu, a kra-wędzie etykietowane są sygnałami wejściowymi. Stany akceptujące wyróżnia się podwójnąobwódką, z kolei na stan początkowy wskazuje strzałka nie będąca krawędzią grafu. Inter-pretacja diagramu jest zgodna z intuicją, co więcej, na jej podstawie można w sposób jedno-znaczny odtworzyć definicję automatu skończonego, który przedstawia. Z tego też powodudiagram automatu skończonego można uznać za jego definicję. W dalszej części pracy autorbędzie utożsamiał pojęcie automatu skończonego z jego implementacją, a w miejscu formal-nych definicji będzie posługiwał się diagramami. Rysunek 2.2 przedstawia diagram automatuskończonego akceptującego niepuste ciągi binarne nie zawierające sekwencji złożonej z tychsamych cyfr o długości większej niż 3. Stany q1− q6 są stanami akceptującymi. Przejście dostanu q7 następuje w efekcie wykrycia niedopouszczalnej sekwencji.

Z diagramu wynika że automat, będąc w stanie q7, nie ma możliwości przejścia do stanuakceptującego. Sytuacja, w której wszystkie krawędzie wychodzące z pewnego ze stanów sąkrawędziami zwrotnymi, a sam stan nie jest stanem akceptującym, zdarza się dosyć często9.W związku z tym, dopuszcza się czasami aby na diagramia automatu skończonego występo-wały stany nie mające zdefiniowanych przejść dla niektórych sygnałów. W takim przypadkuzakłada się niejawną definicję dodatkowego, nieakceptującego stanu, do którego prowadząwszystkie niezdefiniowane przejścia. Każda z krawędzi wychodzących z owego dodatkowegostanu jest kwawędzią zwrotną.

W świetle powyższej uwagi, automat z rysunku 2.2 mógłby zostać równie dobrze przed-stawiony jak na na rysunku 2.3. Dzięki zastosowaniu uproszczonej postaci, diagram zyskałnieco na wielkości.

Więcej ogólnych wiadomości teoretycznych na temat automatów skończonych można zna-leźć w klasycznej pracy [11, rozdział 2]. Zagadnienie to jest także dobrze zarysowany w [10].

9Stan charakteryzujący się tego typu właściwościami, określa się niekiedy mianem stanu “martwego”. Por.[2, rozdział 2].

16

Page 19: Praca_magisterska

Rysunek 2.3: Uproszczony diagram automatu z rysunku 2.2. Diagram nie zawiera jawnejinformacji na temat krawędzi wychodzących ze stanów q3 i q6 z odpowiadającymi etykietami0 i 1.

2.2.1 Klasa jako automat skończony

Automaty skończone mają liczne zastosowania, z których dwa najczęściej spotykane to a) roz-poznawanie języków regularnych oraz b) szeroko rozumiane modelowanie. W pierwszym wy-padku automat używany jest po prostu do uzyskania odpowiedzi “tak” lub “nie”. W drugimnatomiast przypadku oczekuje się na ogół odpowiedzi “tak”, która jest wyrazem poprawnościdziałania bądź użytkowania modelowanego bytu. Uzyskanie odpowiedzi “nie” traktuje się tujako błąd w programie — czy to polegający na błędnej definicji modelowanego bytu, czy teżna niewłaściwym jego użyciu.

W kontekście niniejszej pracy interesujące jest zastosowanie drugie - automat skończonyjako model pewnego mechanizmu.

Aby pewien proces (∼obiekt, mechanizm) mógł zostać zamodelowany w oparciu a automatskończony, wystarczy w zasadzie aby:

1. W każdym momencie swojego cyklu życia znajdował się w pewnym konkretnym, po-chodzącym z ograniczonego zbioru, stanie.

2. Posiadał możliwość zmiany swojego stanu na inny.

3. Bezpośrednim impulsem do zmiany stanu powinno być otrzymanie pewnego sygnału.Zbiór sygnałów, tak jak i stanów, powinien być ograniczony.

4. Miał określony stan początkowy, tj. taki stan w którym znajduje się on w momenciebezpośrednio po rozpoczęciu swojego funkcjonowania.

5. Miał określoną listę stanów akceptujących, tj. takich stanów w których może znaleźćsię on w chwili bezpośrednio przed zakończeniem swojego funkcjonowania.

W niniejszej pracy automaty skończone zostaną wykorzystane do modelowania klas, w

17

Page 20: Praca_magisterska

szczególności sposobu ich działania10. Na uzasadnienie takiego podejścia autor przytoczyprosty przykład ilustrujący zagadnienie. Dobór przykładu jest tu o tyle uzasadniony, iżpochodzi z praktycznego doświadczenia autora i, co więcej, w pewnym stopniu skłonił go dorozważań na poruszane tematy.

Niech oto dane będzie centrum telefoniczna w punkcie obsługi klienta. Typowy scenariusz,jaki ma miejsce w takiej instytucji, może wyglądać następująco:

Klient wykręca na swoim telefonie numer punktu obsługi klienta. Łączy się zcentralką telefoniczną. Wysłuchuje menu głosowego z którego wybiera odpowied-nią opcję, a następnie trafia do kolejki oczekujących na połączenie z operatorem.Wreszcie, po upływie pewnego czasu, jest przekierowywany do jednego z opera-torów. W efekcie dochodzi do rozmowy, która kończona jest wraz z zerwaniempołączenia przez jedną ze stron.

Możliwe są także różnorodne wariacje powyższego scenariusza, np. wielokrotne przełączeniarozmowy pomiędzy różnymi operatorami lub odkładanie przez zirytowanego klienta słuchawkiw czasie oczekiwania na wolnego operatora. Proces ten ma definitywnie charakter automatuskończonego. Można wyróżnić w nim poszczególne stany, w tym stan początkowy i końcowy(tj. akceptujący), przejścia pomiędzy stanami oraz sygnały je wyzwalające. Rysunek 2.4zawiera schemat owego automatu (szczegóły nie są istotne).

Każde połączenie przychodzące od klienta jest monitorowane — w trosce o zapewnieniewysokiej jakości usługi. Monitoring ten prowadzony jest poprzez jeden z modułów centralki,gdzie pojedynczemu połączeniu odpowiada pojedynczy obiekt połączenia. Wymusza to naautorze danej klasy dostosowanie jej do schematu działania centralki, w szczególności do ko-lejnym etapów połączenia. W związku z tym model procesu jest wykorzystany także w klasiemonitorującej. Innymi słowy klasa monitorująca jest opisana także za pomocą przedstawio-nego na rysunku 2.4 automatu skończonego.

Warto tu zauważyć, iż każdy możliwy scenariusz powinien kończyć się w stanie akcep-tującym, jakim jest Hng. Otóż klasa monitorująca połączenia nie służy stwierdzaniu, czydany scenariusz jest prawidłowy, czy też nie. Klasa ta akceptuje tylko i wyłącznie scenariuszeprawidłowe. Sytuacja w której obiekt połączenia skończy swoje działanie w stanie innymniż akceptujący nie jest tutaj dopuszczalna. Zaistnienie nieprawidłowego scenariusza (rozu-mianego jako ciąg sygnałów wejściowych) będzie oznaczać błąd implementacji systemu. Jestto podejście często wykorzystywane w wypadku modelowania za pomocą automatów skoń-czonych. W szczególności ten sposób modelowania został przyjęty przez autora niniejszejpracy.

Reasumując, automat skończony można potraktować jako swego rodzaju schemat działa-nia danej klasy. Oczywiście modelowania klas za pomocą automatów skończonych nie zawszema sens. Do przypadków takich można zaliczyć klasy o bardzo prostej strukturze (np. klasareprezentująca wektor) lub klasy o wybitnie jednostanowym charakterze (np. klasy niemo-dyfikowalne). W takich sytuacjach nic jednak nie stoi na przeszkodzie, aby tego nie robić —modelowanie działania klas nie jest elementem obligatoryjnym lecz pomocniczym.

10na modelu klasy stworzonym w oparciu o automat skończony będą opierały się bezpośrednio pewnemechanizmy Programowania przez Kontrakt, sprawdzające zgodność klasy z modelem

18

Page 21: Praca_magisterska

Rysunek 2.4: Diagram automatu dla klasy monitorującej połączenia z centralką telefoniczną.New - nowe połączenie, Cnn - połączenie zarejestrowane, Ivr - połączenie w menu głosowym,Enq - połączenie oczekujące na operatora, Opr - połączenie z operatorem, Hng - połączeniezerwane (zakończone).

2.3 Propozycja PPK dla języka Java

Na przygotowaną przez autora propozycję mechanizmów Programowania przez Kontrakt dlajęzyka Java składają się następujące elementy:

• warunki wstępne i końcowe metod

• niezmienniki klas

• automaty skończone klas

Dokładne omówienie składni i semantyki poszczególnych elementów odnaleźć można wdalszych rozdziałach. Istota działania warunków wstępnych i końcowych oraz niezmiennikówzostała przedstawiona przy okazji przeglądu mechanizmów PPK w sekcji 2.1. Informacjeszczegółowe na temat gramatyki umieszczono w dodatku D.

Krótkie słowo wyjaśnienia należy się natomiast ostatniemu z mechanizmów, tj. “automa-tom skończonym klas”. Otóż, w myśl zamierzeń autora, programista dostaje możliwość jawnejspecyfikacji automatu użytego do zamodelowania klasy. Automat ów wykorzystywany możebyć następnie podczas kontroli kontraktów. Dla tak zdefiniowanego automatu programistaotrzymuje możliwość zmiany stanu obiektu oraz sprawdzenia stanu aktualnego.

Związek pomiędzy modelowaniem klas za pomocą automatów skończonych a mechani-zmami Programowania przez Kontrakt może z początku wydawać się tu niejasny. Wynika tostąd, że mechanizmy PPK kojarzą się na ogół z wywołaniami pojedynczych podprogramów,np. metod pewnego obiektu, z kolei automat skończony wykorzystywany jest do opisaniaschematu funkcjonowania całej klasy. Jest to jednak różnica pozorna. Jawna deklaracjaautomatu skończonego jest, mianowicie, konstrukcją podobną do niezmiennika klasy. Obiedeklaracje nie mają charaktery imperatywnego, lecz są swego rodzaju wytyczną dotyczącązachowania się klasy11. Rzeczywiste zachowanie się klasy, w razie błędnej jej implementacji,może stać w sprzeczności z warunkami niezmiennika lub schematem automatu skończonego.11Jest to w zasadzie najważniejszy z argumentów przemawiających za zaliczeniem tego dodatkowego me-

chanizmu w poczet tradycyjnych elementów Programowania przez Kontrakt

19

Page 22: Praca_magisterska

Tak jak w wypadku niezmiennika klasa gwarantuje, że jego warunki nie zostaną przyjakimkolwiek wywołaniu jej metod pogwałcone, także i samo można odeń wymusić gwarancjędziałania zgodnego z mechanizmem automatu skończonego. W szczególności operacja zmianystanu obiektu, jaką to otrzymuje programista, automatycznie zostaje obwarowana warunkamisprawdzającymi prawidłowość takiego przejścia. Działanie niezgodne ze schematem automatupowinno prowadzić do takich samych efektów, jak w przypadku złamania kontraktu.

Wreszcie, można w ramach kontraktu metody zażyczyć sobie, aby jej wywołanie pro-wadziło do wejścia obiektu w określony stan, lub było możliwe tylko w określonym stanie.Mechanizm ten nie posiada co prawda bezpośredniego wsparcia ze strony składni, jednakżejest on bardzo prosty do zastosowania.

2.3.1 Automat skończony klasy

Autor, w swojej propozycji PPK dla Javy, pozwala na jawną specyfikacją automatu skończo-nego, a także udostępnia operację zmiany stanu oraz sprawdzenia aktualnego stanu. Podsta-wową składnię i sposób użycia owych konstrukcji przedstawia dobrze listing 2.1.

1 public class AutomatonDemo {2 automaton {3 i n i t i a l S0 : S1 ;4 accepting S1 ;5 }67 public void callMeOnce ( ){8 transient : S1 ;9 }

1011 public boolean wasAlreadyCalled ( ){12 return transient ? S1 ;13 }14 }

Listing 2.1: Przykład definicji oraz użycia prostego automatu skończonego.

Linijki 2–5 zawierają definicję automatu modelującego daną klasę. Linijki 3 oraz 4 za-wierają definicje dwóch stanów. Stan S0 określony został jako stan początkowy, a przejściezeń do stanu S1 określone zostało jako dopuszczalne. Stan S1 oznaczony został jako stankońcowy. Procedura callMeAtLestOnce() zawiera pojedynczą instrukcję zmiany stanu — wtym wypadku na stan S1. W linijce 12. znajduje się natomiast wyrażenie sprawdzenia czyaktualny stan jest równy stanowi danemu jako argument wyrażenia.

Pełny schemat automatu skończonego odpowiadającego omawianej klasie pokazany zostałna rysunku 2.5. Na diagramie przedstawiony został dodatkowy stan E. Jest to w pewnymsensie stan wyimaginowany, dopełniający jedynie diagram automatu skończonego. Zbiór sy-gnałów wejściowych jest tutaj równy liczbie zadeklarowanych stanów, przy czym pojedynczysygnał utożsamiany jest z instrukcją zmiany stanu na dany. Wszystkie dopuszczalne przejściaprowadzą do odpowiadających im stanów, natomiast przejścia niedopuszczalne kończą się wstanie E. W rzeczywistości stan ten nie jest nigdy osiągalny, gdyż wejście weń może być tylkoi wyłącznie wynikiem złamania kontraktu.

20

Page 23: Praca_magisterska

Rysunek 2.5: Diagram automatu skończonego dla listingu 2.1, wyrażenie “X” jest tutaj skró-tem dla “transient : X” i oznacza instrukcję zmiany aktualnego stanu na stan X.

Rysunek 2.6: Uproszczony diagram automatu skończonego dla listingu 2.1 — uzyskany pousunięciu stanu martwego E.

Zastosowanie takiego podejścia skutkuje pewnymi ograniczeniami. Na przykład nie jestmożliwe zdefiniowanie sygnału wejściowego, który z różnych stanów początkowych prowa-dziłby do różnych stanów końcowych12. Jest to swego rodzaju kompromis pomiędzy szcze-gółowością definicji i jej klarownością na jaki poszedł autor. Powyższe uproszczenie skutkujezwięzłymi i czytelnymi definicjami automatów. Jest także w większości przypadków w zupeł-ności wystarczające. Co więcej można pokazać, że każdy dowolny deterministyczny automatskończony można przekształcić do powyższej postaci, tak aby wynikowy automat akceptowałmożliwie niewielki nadzbiór języka (tj. “dopuszczalnych scenariuszy” — w niniejszym kontek-ście) automatu pierwotnego. W razie konieczności schemat działania automatu skończonegomożna doprecyzować przy użyciu pozostałych mechanizmów Programowania przez Kontraktproponowanych przez autora.

Podobnie jak to miało miejsce w przypadku automatu przedstawionego na diagramach 2.2(strona 16) i 2.3 (strona 17), stan E można na diagramie pominąć. To samo dotyczy takżenazw sygnałów. W efekcie diagram z rysunku 2.5 można uprościć do postaci z rysunku 2.6,przedstawiającej po prostu zbiór stanów i dopuszczalnych pomiędzy nimi przejść.

2.3.2 Definicja automatu skończonego

Sekcja definiująca automat zaczyna się słowem “automaton”, po którym następuje, zawartepomiędzy nawiasami klamrowymi, ciało automatu, przy czym symbol leksykalny reprezento-

12Tak naprawdę można się spierać o to, co jest sygnałem wejściowym. Zamiast instrukcji przejścia, jakosygnał wejściowy można równie dobrze potraktować wywołanie metody. Przy takim podejściu, byłoby możliweaby te same sygnały powodowały, w zależności od aktualnego stanu, przejścia do różnych stanów. Autor jednaknie uważa takiej interpretacji zbioru sygnałów wejściowych za zbyt praktyczną.

21

Page 24: Praca_magisterska

wany przez leksem “automaton” nie jest słowem kluczowym13,14. Sama definicja automatumoże wystąpić jedynie wewnątrz dowolnej klasy, jako jej bezpośredni członek. Wyjątek sta-nowią tu enumeracje, w których ciele definicje automatów nie są dopuszczalne. Definicjaautomatu dotyczy klasy, w której została bezpośrednio zdefiniowana. Dla danej klasy możeistnieć co najwyżej jedna definicja automatu.

Ciało automatu musi składać się z jednej lub większej liczby definicji stanów. Koniecznejest określenie stanu początkowego. Zaleca się także wybranie jednego lub większej ilościstanów akceptujących15.

Definicja pojedynczego stanu składa się z opcjonalnych modyfikatorów: initial uży-wanego na oznaczenie stanu początkowego oraz accepting na oznaczenia stanu końcowego(∼akceptującego). Stan początkowy może być jednocześnie stanem końcowym. Należy unikaćpowtarzania tego samego modyfikatora w definicji danego stanu, ewentualne duplikaty zostanąw takiej sytuacji zignorowane. Słowa initial oraz accepting nie są traktowane jako słowakluczowe. Po opcjonalnych modyfikatora należy podać identyfikator będący nazwą stanu. Niejest dopuszczalne aby dwa lub więcej ze stanów w danym automacie posiadały tą samą nazwę.Po nazwie stanu następuje opcjonalna sekcja specyfikująca dopuszczalne przejścia, składa sięona ze znaku dwukropka oraz niepustej listy porozdzielanych przecinkami nazw stanów do-celowych. Dopuszczalna jest specyfikacja przejścia zwrotnego. Dublowanie nazw stanów naliście stanów docelowych nie jest zalecane. Zdublowane stany zostaną w takiej sytuacji zigno-rowane. Niedopuszczalne jest natomiast używanie nazw stanów nie zdefiniowanych w danymautomacie. Definicja stanu kończy się obowiązkowym znakiem średnika - nawet sytuacji gdyjest to jedyna definicja w automacie. Wszystkie zdefiniowane w automacie stany powinnybyć osiągalne ze stanu początkowego. Szczegółowe informacje na temat składni zawarte są wdodatku D.

Niech dla przykładu dany będzie uproszczony diagram automatu skończonego jak na ry-sunku 2.7. Definicja automatu skończonego odpowiadająca diagramowi przedstawiona zostałana listingu 2.2.

13W szczególności oznacza to, iż pula dostępnych na użytek programisty identyfikatorów nie zostaje uszczu-plona o słowo “automaton”, dzięki czemu może być ono nadal używane jako np. nazwa pola interfejsu bądźzmiennej. Fakt ten wynika automatycznie z założenia, iż JavaBC jest nadklasą Javy, niemniej autor uważa zastosowne jego wyraźne tu podkreślenie14Czasami na określenie tak wyróżnionych symboli leksykalnych używa się określenia “słowo pseudoklu-

czowe” (ang. pseudo keyword). Wszystkie słowa wprowadzone przez autora na użytek składni Programowaniaprzez Kontrakt są słowami pseudokluczowymi.15Użycie przez autora słów w rodzaju “konieczne”, “zabronione” bądź “niedopuszczalne”, oznacza iż złama-

nie danej reguły doprowadzi do błędu translacji. Taki sam imperatywny charakter posiadają także stwierdzenianapisane w trybie oznajmującym, w rodzaju “wyrażenie kończy się znakiem średnika”. Wyjątek stanowią tustwierdzenia opatrzone wyrazami mniej kategorycznymi w brzmieniu, tj. np. “zalecane”, “nie należy” bądź“nie powinno”. Zalecenia te, bez względu na tryb (oznajmujący czy też rozkazujący) dotyczą reguł którychnieprzestrzeganie kończy się jedynie ostrzeżeniem podczas translacji.

22

Page 25: Praca_magisterska

Rysunek 2.7: Uproszczony diagram automatu skończonego dla listingu 2.2

public class Automaton {automaton {

i n i t i a l S0 : S1 , S2 , S4 , S5 ;S1 : S2 , S3 , S5 ;S2 : S5 , S6 ;accepting S3 : S3 ;S4 : S5 , S0 ;S5 : S2 , S6 , S7 ;accepting S6 ;accepting S7 : S8 ;S8 : S7 , S8 ;

}}

Listing 2.2: Przykłady definicji automatu skończonego.

W listingu 2.3 zawarto przykłady różnych konstrukcji poprawnych semantycznie. Listing2.4 ukazuje natomiast konstrukcje niepoprawne semantycznie.

public class ComplexAutomatonDemo {automaton {

accepting automaton ; /∗ leksem ”automaton ” , tak samo j a k” i n i t i a l ” oraz ” a c c e p t i n g ” , nie j e s t traktowany jako s lowokluczowe , moze byc zatem wykorzystywany jako i d e n t y f i k a t o r ,w tym przypadku nazwa stanu ∗/accepting second : in i t i a l , second , accepting ;i n i t i a l i n i t i a l :

second , STATE3, automaton ;accepting ; STATE3 : not accepting ;no t acc ep t ing ; /∗ s tan nie musi o k r e s l a czadnego stanu docelowego ∗/

}

private interface Nes t ed In t e r f a c e {private class I n t e r f a c e C l a s s {

automaton { // automat d l a k l a s y z a g n i e z d z o n e j

23

Page 26: Praca_magisterska

i n i t i a l accepting STATE3 : no t acc ep t ing ;/∗ unikatowosc nazw stanow obowiazu je j e d y n i ew o b r e b i e danego automatu ∗/not acc ep t ing ;

}}

}}

Listing 2.3: Przykłady definicji automatu skończonego.

public class ComplexAutomatonDemo {automaton {

accepting STAN1; // b l a d − brak stanu poczatkowego}

private enum ENUM {E1 , E2 ;

automaton { // b l a d − d e f i n i c j a automatu w c i e l e enumeracj ii n i t i a l STAN1;STAN2; // o s t r z e z e n i e − STAN2 n i e o s i a g a l n y

} // o s t r z e z e n i e − brak stanu a k c e p t u j a c e g o

class Nested{automaton {

i n i t i a l STAN2 : STANX; // blad , brak d e f i n i c j i STANXaccepting STAN2; // b l a d − powtorna d e f i n i c j a stanu

}}

}

automaton {// b l a d − automat d l a danej k l a s y z o s t a l j u z zde f in iowanyi n i t i a l accepting accepting STAN1 : STAN2;// o s t r z e z e n i e − zdub lowanie modyf ikatoraSTAN2 : STAN1, STAN2, STAN1;// o s t r z e z e n i e − zdub lowanie stanu docelowego

}}

Listing 2.4: Przykłady błędnych definicji automatu skończonego.

2.3.3 Przejścia automatu skończonego

Każda z instancji danej klasy funkcjonuje jako oddzielna implementacja zdefiniowanego wklasie automatu skończonego. W szczególności każdy obiekt danej klasy posiada swój wła-sny aktualny stan. Wraz z chwilą stworzenia nowej instancji otrzymuje ona automatyczniestan początkowy. Od klasy zawierającej definicję automatu skończonego wymaga się także,

24

Page 27: Praca_magisterska

aby każda z jej instancji znajdowała się bezpośrednio po finalizacji w jednym ze stanów koń-cowych16. W szczególności dopuszczalne jest, aby zmiana stanu na końcowy dokonała siędopiero w finalizatorze klasy. Niespełnienie warunku stanu końcowego traktowane jest jakzłamanie kontraktu klasy.

Podczas cyklu życia danego obiektu, mogą być dokonywane zmiany jego stanu (w wypadkuklas których stan początkowy nie jest stanem akceptującym, tj. końcowym, zmiany takie sąwręcz niezbędne w celu wywiązania się z kontraktu). Zmiany stanu obiektu (∼przejścia)można dokonać tylko i wyłącznie za pomocą specjalnej instrukcji zmiany stanu, przy czyminstrukcja owa może być umieszczona jedynie wewnątrz klasy, której dotyczy. Co więcejmusi mieć ona dostęp do instancji klasy, której stan ma zostać zmieniony — w szczególnościoznacza to, iż instrukcji zmian stanu nie można umieszczać w statycznym kontekście.

Instrukcja zmiany stanu składa się ze słowa kluczowego transient, dwukropka oraz nazwystanu docelowego. Jak każda z instrukcji w języku Java, instrukcja zmiany stanu musi byćzakończona średnikiem. Próba wykonania niedopuszczalnego przejścia przy pomocy instrukcjizmiany stanu jest traktowana jak złamanie kontraktu.

Linijka 8. z listingu 2.1 zawiera przykładową instrukcję zmiany aktualnego stanu obiektuna stan S1. Jeśli przejście takie jest dopuszczalne, obiekt zmieni swój stan na stan docelowy.Jeśli przejście nie jest natomiast dopuszczalne, dojdzie do złamania kontraktu. Przykładowoklasa z listingu 2.1 posiada dwa stany przy czym stan początkowy nie jest stanem akcep-tującym. Zmiana stanu na akceptujący może się w jej wypadku dokonać jedynie wewnątrzmetody callMeOnce. Jak sama nazwa wskazuje, metoda ta powinna zostać, dla danegoobiektu, wywołana raz i tylko raz w czasie jego istnienia. Pojedyncze wywołanie owej metodyspowoduje zmianę stanu na końcowy. Inne scenariusze użycia obiektu doprowadzą do złama-nia kontraktu. Jeśli metoda callMeOnce nie zostanie wywołana ani razu, obiekt w chwili tużpo finalizacji nie będzie mógł znajdować się w stanie końcowym, co automatycznie naruszywarunek jego kontraktu. Dla odmiany każda próba powtórnego wywołania owej metody do-prowadzi także do wykonania niedopuszczalnego przejścia (ze stanu S1 z powrotem w stanS1), co również zaowocuje złamaniem kontraktu.

Czasami ta sama nazwa stanu może występować w dwóch różnych automatach, przyczym w danym kontekście możliwa jest zmiana stanu dowolnego z nich. W razie wystąpieniatakiej niejednoznaczności, wykorzystana zostanie możliwie najbardziej zagnieżdżona z klas.Sytuację ową przedstawia listing 2.5.

1 public class Outer {2 class Inner {3 automaton {4 i n i t i a l S0 : S1 ; accepting S1 ;5 }67 void doSomething ( ){8 transient : S1 ;9 /∗ zmiana stanu z o s t a n i e domyslnie dokonana

10 na r z e c z i n s t a n c j i k l a s y Inner ∗/

16Autor, w związku z istnieniem tego warunku, stanowczo odradza działań polegających na wskrzeszaniufinalizowanych obiektów (wskrzeszenie polega na powtórnym uzyskaniu referencji do finalizowanego obiektu).Nawiasem mówiąc ciężko jest w ogóle podać sensowne usprawiedliwienie dla tego typu nagannych praktyk.

25

Page 28: Praca_magisterska

11 }12 }1314 automaton {15 i n i t i a l S0 : S1 ; accepting S1 ;16 }17 }

Listing 2.5: Problem identycznie nazwanych stanów.

Nazwa stanu docelowego może zostać dodatkowo poprzedzona prostą nazwą klasy w któ-rej zdefiniowany został automat skończony na rzecz którego wykonane ma zostać przejście.Pomiędzy nazwą stanu a nazwą klasy umieszczony musi być znak kropki 17. Pozwala toprogramiście na rozstrzyganie niejednoznaczności co do klasy na rzecz której ma zostać wy-konane przejście. Listing 2.6 zawiera przykład z listingu 2.5 zmodyfikowany tak aby instrukcjazmiany stanu dotyczyła klasy Outer.

1 public class Outer {2 class Inner {3 automaton {4 i n i t i a l S0 : S1 ;5 accepting S1 : S1 ;6 }7 void doSomething ( ){8 transient : Outer . S1 ;9 /∗ zmiana stanu z o s t a n i e dokonana na r z e c z k l a s y

10 (w z a s a d z i e − j e j i n s t a n c j i ) Outer ∗/1112 // o c z y w i s c i e mozliwe sa t a k z e i n s t r u k c j e :13 transient : Inner . S1 ;14 // zmiana stanu z o s t a n i e dokonana na r z e c z k l a s y Inner1516 transient : S1 ;17 // zmiana stanu z o s t a n i e dokonana na r z e c z k l a s y Inner18 }19 }2021 automaton {22 i n i t i a l S0 : S1 ;23 accepting S1 ;24 }25 }

Listing 2.6: Rozwiązanie problemu identycznie nazwanych stanów poprzez wykorzystaniekwalifikowanych nazw stanów.

Listing 2.7 ukazuje przykłady niepoprawnych semantycznie konstrukcji zmiany stanu.Szczególną uwagę należy zwrócić tutaj na błędy zawarte w linijkach 3 oraz 25, które polegają

17Nazwa stanu w tej postaci, tj. w raz z poprzedzającą ją nazwą klasy w której umieszczony jest danyautomat, nazywana jest w dalszej części niniejszej pracy “kwalifikowaną nazwą stanu”.

26

Page 29: Praca_magisterska

na próbie odwołania się do automatu skończonego ze statycznego kontekstu. Odwołania ta-kie są niemożliwe z racji tego, że instancje automatów przypisane są bezpośrednio instancjomklas.

1 public class Outer {2 stat ic {3 transient : S1 ; /∗ b l a d − proba zmiany stanu wewnatrz4 s t a t y c z n e g o k o n t e k s t u . I n s t a n c j e automatow sa p r z y p i s a n e5 instancjom Klasy , n ie samej k l a s i e jako t a k i e j ∗/6 }78 automaton {9 i n i t i a l S0 : S1 ;

10 accepting S1 ;11 }1213 Outer ( ){14 transient : Inner . S1 ; //∗ b l a d − i n s t r u k c j a zmiany15 stanu moze byc uzyta j edyn i e wewnatrz danej klasy ,16 w tym wypadku Outer ∗/17 }1819 stat ic class Inner {20 automaton {21 i n i t i a l S0 : S1 ;22 accepting S1 : S1 ;23 }24 void doWrong ( ){25 transient : Outer . S1 ; /∗ b l a d − proba26 odwolania s i e do stanu ze s t a t y c z n e g o k o n t e k s t u ∗/27 transient : S2 ; /∗ b l a d − nieznany stan S3 ∗/28 }29 }30 }

Listing 2.7: Przykłady błędnych instrukcji zmiany stanu.

2.3.4 Sprawdzanie stanu automatu skończonego

Obok instrukcji zmiany stanu(∼przejścia) funkcjonuje odpowiadające jej wyrażenie spraw-dzenia aktualnego stanu. Wyrażenie to składa się ze słowa transient, znaku pytajnika orazniepustej listy stanów. Mechanizm określania automatów, do których odnoszą się poszcze-gólne stany jest taki sam, jak w wypadku instrukcji zmiany stanu. W wypadku niejedno-znaczności w nazewnictwie, nazwy stanów mogą być opcjonalnie nazwami kwalifikowanymi.Jeśli lista stanów składa się z więcej niż jednego elementu, musi zostać dodatkowo ujęta wnawiasy zwyczajne. W takiej sytuacji zaleca się także, aby wszystkie stany odnosiły się dotego samego automatu. Poszczególne elementy na liście rozdzielane są za pomocą przecinków.

Kontekst w którym można skorzystać z wyrażenia sprawdzenia aktualnego stanu jest takisam jak w wypadku instrukcji zmiany stanu. Wyrażenie sprawdzenia stanu atrybutowane jesttypem boolean. Oznacza to, że może ono zostać wykorzystane wszędzie tam, gdzie oczekuje

27

Page 30: Praca_magisterska

się wartości logicznej, tj. chociażby w warunku pętli while. Wyrażenie zwraca wartość równąlogicznej prawdzie w sytuacji, gdy przynajmniej jeden z wyszczególnionych stanów jest stanemaktualny.

Należy tu jeszcze nadmienić, że wyrażenie sprawdzenia stanu ma charakter czysto pomoc-niczy i nie może tego powodu w żadnych okolicznościach prowadzić do złamania kontraktu.

Linijka 12 z listingu 2.1 zawiera przykładowe wyrażenie sprawdzenia aktualnego stanu.Wyrażenie to zwróci wartość true jedynie, gdy stan skojarzonego z obiektem automatu skoń-czonego jest równy S1. W definicji funkcji wasAlreadyCalled został wykorzystany fakt, żewyrażenie sprawdzenia zmiany stanu zwraca wartości logiczną, co pozwoliło na skorzystaniezeń wewnątrz instrukcji return (funkcja sama zwraca wartość logiczną).

W listingu 2.8 zawarto przykłady różnych, poprawnych semantycznie wyrażeń sprawdzeniaaktualnego stanu. Listing 2.9 ukazuje natomiast niepoprawne semantycznie próby użycia owejinstrukcji.

1 public class Test {2 automaton {3 i n i t i a l S0 : S1 ;4 accepting S1 : S2 ;5 S2 : S3 ;6 accepting S3 ;7 }89 boolean i sAccept ing ( ){

10 return transient ? ( S1 , S2 ) ;11 }1213 boolean computeSomeValue ( ){14 return transient ? ( S1 , Test . S2 ) ? transient ? Test . S2 : transient ? ( S3 ) ;15 // wyrazenie sprawdzenia stanu ma najwyzszy mozliwy p r i o r y t e t16 // wyzszy w s z c z e g o l n o s c i od operatora ? :17 }1819 class Inner {20 automaton {21 i n i t i a l S0 : S1 ;22 accepting S1 : S1 ;23 }2425 void doSomething ( ){26 boolean value = transient ? Test . S1 ;27 value = transient ? ( S0 , S1 ) ;28 }2930 boolean getTrue ( ){31 return transient ? ( Test . S0 , Test . S1 , Test . S2 , Test . S3 ) ;32 }33 }34 }

Listing 2.8: Przykłady prawidłowych wyrażeń sprawdzenia stanu.

28

Page 31: Praca_magisterska

1 public class Test {2 automaton {3 i n i t i a l S0 : S1 ;4 accepting S1 : S2 ;5 S2 : S3 ;6 accepting S3 ;∗7 }89 Test ( ){

10 boolean value = transient ? S4 ; // b l a d − nieznany stan S411 transient ? S0 ; // b l a d − wyrazenie nie j e s t samodzie lna i n s t r u k c j a12 }1314 class Inner {15 automaton {16 i n i t i a l S0 : S1 ;17 accepting S1 : S1 ;18 }1920 void doSomething ( ){21 boolean value = transient ? ( S0 , S1 , S2 , S3 ) ;22 // o s t r z e z e n i e − odnoszenie s i e do roznych automatow wewnatrz23 // i n s t r u k c j i sprawdzenia stanu : s tany S0 oraz S1 zos tana24 // domyslnie p r z y p i s a n e do automatu k l a s y Inner . Z k o l e i s tany25 //S2 i S3 dotycza automatu k l a s y Test26 }2728 boolean getTrue ( ){29 return transient ? Test . S4 ;30 // b l a d − automat k l a s y Test nie d e f i n i u j e stanu S431 }32 }33 }

Listing 2.9: Przykłady nieprawidłowych wyrażeń sprawdzenia stanu.

2.3.5 Niezmiennik klasy

Kolejny z zaproponowanych przez autora elementów PPK stanowią niezmienniki klas. Nie-zmienniki, zgodnie ze swoim standardowym przeznaczeniem, pozwalają na deklarację global-nych warunków kontraktu, które powinny być spełnione zawsze w pewnych określonych punk-tach wykonania programu(∼podczas rozpoczęcia i zakończenia wywołania metody). Autor wswojej implementacji wzbogacił nieco składnię niezmienników, pozwalając na ich dodatkowąintegrację z automatem skończonym klasy — jeśli ta oczywiście taki automat definiuje.

1 public class InvariantDemo {2 automaton {3 i n i t i a l accepting IDLE : BUSY;4 accepting BUSY : IDLE ;5 }

29

Page 32: Praca_magisterska

67 private St r ing s t a t u s = ” i d l e ” ;89 invariant {

10 ∗ : done != null : ”done == n u l l ” ;11 IDLE : ” i d l e ” . equa l s ( s t a t u s ) ;12 BUSY : ”busy” . equa l s ( s t a t u s ) ;13 }1415 void begin ( ){16 transient : BUSY;17 s t a t u s = ”busy” ;18 }1920 void f i n i s h ( ) {21 transient : IDLE ;22 s t a t u s = ” i d l e ” ;23 }2425 protected getStatus ( ){26 return s t a t u s ;27 }28 }

Listing 2.10: Przykład użycia niezmiennika.

Listing 2.10 zawiera przykład użycia niezmiennika. Linijki 9–13 zawierają definicję pa-kietowego niezmiennika klasy, tj. niezmiennika, którego warunki są sprawdzane jedynie wwypadku wywołań metod o dostępie pakietowym lub publicznym. W ciele niezmiennika zo-stały określone trzy warunki (linijki 10–12). Warunek pierwszy obowiązuje bez względu nastan automatu, warunek drugi obowiązuje jedynie w stanie S0, natomiast warunek trzeci jedy-nie w stanie S1. W efekcie warunek pierwszy będzie sprawdzany zawsze podczas wchodzenia iwychodzenia z metod begin oraz finish, warunki drugi i trzeci – w zależności od aktualnegostanu. Podczas wywoływania metody getStatus nie będą sprawdzane jakiekolwiek warunkiniezmiennika.

Sekcja definiująca niezmiennik zaczyna się od modyfikatora dostępu. Dopuszczalne mo-dyfikatory dostępu to a) występujące w wypadku normalnych składowych: modyfikator pu-bliczny — public, modyfikator chroniony — protected, oraz modyfikator prywatny —private, b) modyfikator pusty oznaczający dostęp pakietowy oraz c) zdefiniowany specjal-nie na użytek niezmienników modyfikator pakietowo-chroniony — package protected. Pomodyfikatorze następuje słowo invariant, które, podobnie jak i słowo automaton, nie jestsłowem kluczowym. Po słowie invariant umieszczone jest, ograniczone nawiasami klamro-wymi i składające sie z zera lub większej liczby warunków niezmiennika, ciało niezmiennika.

Definicja niezmiennika może zostać umieszczona w dowolnej klasie, pod warunkiem, żeklasa ta nie jest enumeracją.

Modyfikator dostępu niezmiennika służy oznaczeniu grupy metod danej klasy, podczas wy-konania których może być sprawdzany warunek niezmiennika. Dana klasa może zawierać conajwyżej jeden niezmiennik o danym modyfikatorze dostępu. Tablica 2.3 zawiera podsumowa-nie dotyczące zakresu stosowania różnych typów niezmienników. Przykładowo, niezmiennik

30

Page 33: Praca_magisterska

pakietowy jest stosowany jedynie do metod publicznych oraz pakietowych.

rodzaj niezmiennika rodzaj metody

publiczna chroniona pakietowa prywatnapubliczny • ◦ ◦ ◦chroniony • • ◦ ◦pakietowy • ◦ • ◦pakietowo-chroniony • • • ◦prywatny • • • •

Tablica 2.3: Zakres stosowania niezmienników.

Warunki danego niezmiennika mogą być sprawdzane jedynie podczas wywołania niesta-tycznych metod zdefiniowanych bezpośrednio w danej klasie. Wywołanie metody zdefinio-wanej w dowolnej nadklasie lub podklasie klasy zawierającej niezmiennik nie prowadzi nigdydo sprawdzenia warunków owego niezmiennika. Nie oznacza to oczywiście, że podklasy inadklasy danej klasy nie mogą same definiować własnych niezmienników i poprzez to byćsprawdzanymi pod kątem wywiązywania się z kontraktów.

Pojedynczy warunek niezmiennika składa się z trzech sekcji, z czego tylko dwie pierwszesą obowiązkowe:

• Pierwsza z sekcji służy określeniu zbioru stanów, w których spełniony powinien być danywarunek. W miejscu pierwszej sekcji możliwe jest użycie znaku gwiazdki, która oznaczadowolny stan automatu klasy (w wypadku klasy niedefiniującej automatu jest to, na-wiasem mówiąc, jedyna dopuszczalna możliwość). Alternatywą dla znaku gwiazdki jesttutaj podanie jawnej listy prostych nazw stanów, których dotyczy niezmiennik. Jakoseparator elementów listy, używany jest znak przecinka. Każdy z wymienionych stanówmusi być zdefiniowane w automacie skończonym klasy, której niezmiennik jest bezpo-średnim elementem. Powtarzanie stanów na liście danego warunku nie jest zalecane, wrazie powtórzenia zdublowany stan zostanie zignorowany.

• Druga z sekcji zawiera specyfikację warunku. Wymaga się, aby warunek był wyrażeniemktórego wartością jest typ logiczny lub zmienna o typie logicznym18. W razie testuwarunku, przyjmuje się, zgodnie z intuicją, otrzymanie wartości true za spełnieniewarunku, a false za jego złamanie. Więcej szczegółów na temat typów wyrażeń językaJava można znaleźć w [9, rozdział 15].

• Opcjonalna, trzecia sekcja może zostać użyta do podania wyrażenia, które zostaniewyświetlone w razie złamania kontraktu. Jako że Java pozwala na rzutowania dowolnejwartości do typu java.lang.String, wymaga się tu jedynie aby wyrażenie w trzeciej sekcjiposiadało jakąkolwiek wartość.

18Warto tu zauważyć, że wymaganie co do postaci specyfikacji warunku jest w gruncie rzeczy bardzo ogól-nikowe. Otóż gramatyka języka Java pozwala na rozwinięcie nieterminala “wyrażenie” (ang. expression) wszereg nadzwyczaj różnych konstrukcji — poczynając od najprostszych, w rodzaju stałej logicznej, a kończącna bardzo złożonych, w rodzaju definicji typu anonimowego (por. [9, rozdział 18].). W szczególności pozwalato programiście na definiowanie zagnieżdżonych niezmienników.

31

Page 34: Praca_magisterska

Poszczególne sekcje warunku niezmiennika oddzielone są od siebie znakiem dwukropka. Caływarunek niezmiennika musi zostać zakończony znakiem średnika — nawet jeśli jest to ostatniz warunków niezmiennika.

W wypadku drugiej oraz trzeciej sekcji niezmiennika rodzi się pytanie o dostępność dozmiennych, pól klasy, etc. Reguła jest w tej sytuacji prosta - znaczenie i dostępność identyfika-torów w drugiej i trzeciej sekcji niezmiennika można określić, wyobrażając sobie umieszczenieowych wyrażeń na początku pewnej z metod klasy19.

Problem ten został przedstawiony na listingu 2.11. Klasa IdProblem zawiera definicjęniezmiennika z pojedynczym warunkiem. Warunek ten odwołuje się do identyfikatora value.W wypadku sprawdzenia niezmiennika podczas wywołania metody noParam identyfikatorten będzie wskazywał na pole o danej nazwie. W przypadku jednak wywołania metody,gdzie parametr o identycznej nazwie przysłania swoją nazwą pole klasy, identyfikator będziewskazywał już na ów parametr. W związku z tym problemem, autor radzi poprzedzać każdeodwołanie się do pola danej klasy przedrostkiem this, tak jak ukazano to na zmodyfikowanymprzykładzie z listingu 2.12.

1 public class IdProblem {2 invariant {3 ∗ : va lue > 4 ;4 // u z y c i e i d e n t y f i k a t o r a v a l u e5 // prowadzi do n i e j e d n o z n a c z n o s c i6 }78 int value ;9 void intParam ( int value ){ }

10 void noParam (){ }11 }

Listing 2.11: Problem identyfikatorów w warunkach niezmiennika.

1 public class IdProblem {2 invariant {3 ∗ : this . va lue > 4 ;4 //ok , jednoznaczne odwolanie s i e do po la k l a s y5 }67 int value ;8 void intParam ( int value ){ }9 void noParam (){ }

10 }

Listing 2.12: Problem identyfikatorów w warunkach niezmiennika - sugerowana konwencja.

Sprawdzanie warunku niezmiennika może następić jedynie w dwóch sytuacjach: bezpo-średnio po rozpoczęciu wywołania metody oraz bezpośrednio tuż przed jego zakończeniem.

19Do czego de facto dochodzi w fazie tłumaczenia programu napisanego w JavaBC. Poszczególne warunkiniezmiennika są wtedy po prostu kopiowane do odpowiednich metod.

32

Page 35: Praca_magisterska

Aby dany warunek został sprawdzony, wymagane jest żeby a) dana metoda nie była metodąstatyczną, b) niezmiennik, w którym został zdefiniowany warunek obejmował swoim zakresemdaną metodę oraz c) automat skończony danej klasy znajdował się w jednym z zadeklaro-wanych przez warunek niezmiennika stanów. Jeśli klasa nie posiada automatu skończonego,wystarcza jedynie stwierdzenie zgodności niezmiennika.

W listingu 2.13 zademonstrowano przykłady różnych poprawnych, w sensie składniowymi semantycznym, przypadków użycia niezmiennika. Listing 2.14 ukazuje natomiast szeregprzykładów konstrukcji niepoprawnych.

1 public interface Right Invar i ant s {2 stat ic class Nested extends Other {3 int counter = 0 ;45 automaton {6 accepting in i t i a l S1 : S2 ;7 S2 : S3 , S4 ; S3 : S5 , S1 ;8 S4 ;9 accepting S5 : S5 ;

10 }1112 public invariant {13 ∗ : true ;14 S5 : fa l se ;15 }1617 boolean value = fa l se ;1819 private invariant {20 ∗ : va lue == value ;21 S1 : va lue | | ! va lue : ”2nd cond i t i on broken ! ” ;22 S2 , S4 : va lue & counter < 6 ;23 S1 , S2 , S3 : new Right Invar i ant s ( ){24 package protected invariant {25 ∗ : this . hashCode ( ) != 0 : ”Wrong hash code ” ;26 // w wypadku k l a s y n i e d e f i n i u j a c e j automatu znak27 // g w i a z d k i − ’∗ ’ − j e s t jedyna dopuszcza lna moz l iwosc ia28 }29 public invariant {30 ∗ : t oS t r i ng ( ) . l ength ( ) != 13 : ” Fera l s t r i n g ” ;31 }32 } . t oS t r i ng ( ) != null : ” n u l l s t r i n g r e p r e s e n t a t i o n ” ;33 // s z c z e g o l n i e z l o z o n y warunek niezmiennika34 // − z zagniezdzonym niezmiennikiem35 }3637 I n t e g e r method ( ){38 return 3 ;39 }40 }41 }

Listing 2.13: Przykłady prawidłowego użycia niezmiennika.

33

Page 36: Praca_magisterska

1 public class WrongInvariants{2 automaton {3 i n i t i a l INITIAL : ACCEPTING4 accepting ACCEPTING;5 }67 public invariant {8 INITIAL , INITIAL : true ;9 // o s t r z e z e n i e − zdub lowanie stanu na l i s c i e

1011 ∗ : ” always t rue ” ;12 // b l a d − wyrazenie ” always t r u e ” nie ma w a r t o s c i l o g i c z n e j1314 ACCEPTING : check ( ) ;15 // b l a d − nieskonczona r eku renc ja16 }1718 class NestedOne{19 invariant {20 INITIAL : true ;21 // b l a d − proba odwolania s i e do stanu i n n e j k l a s y2223 ∗ : fa l se : voidMethod ( ) ;24 // b l a d − t r z e c i a s e k c j a niezmiennika musi byc25 // wyrazeniem posiadajacym wartosc26 }27 }2829 void voidMethod ( ){30 }3132 public boolean check ( ){33 return true ;34 }3536 public invariant {37 // b l a d − nizmiennik p u b l i c z n y z o s t a l j u z zde f in iowany38 }39 }

Listing 2.14: Przykłady prawidłowego użycia niezmiennika.

2.3.6 Warunki początkowe i końcowe metod

Oprócz warunków niezmienników, których sprawdzenie może następować w przypadku wy-wołań różnych metod (zarówno podczas rozpoczęcia wykonywania jak i przed zakończeniemwywoływania metody) propozycja autora pozwala także na specyfikację warunków szczegól-nych dla danej metody. W skład warunków szczególnych może wejść pewna liczba tzw.warunków początkowych oraz pewna liczb tzw. warunków końcowych. Warunki początkowemetody sprawdzane są bezpośrednio po wywołaniu metody, warunki końcowe natomiast bez-pośrednio przed zakończeniem wywołania. Złamanie warunku początkowego lub końcowego

34

Page 37: Praca_magisterska

jest naturalnie traktowane jako złamanie kontraktu.

1 public class Test {2 stat ic List<Str ing> t runcate ( Lis t<Str ing> input , int l ength )3 in ( input != null , l ength >= 0 , input . s i z e ( ) >= length )4 out ( return . s i z e ( ) == length ) {5 for ( int i = length − input . s i z e ( ) ; i > 0 ; i−−)6 input . remove ( l ength ) ;7 return input ;8 }9 }

Listing 2.15: Przykłady zastosowania warunków wstępnych i końcowych metody.

Listing 2.15 przedstawia przykład użycia warunków początkowych oraz końcowych me-tody. Linijki 2–4 zawierają nagłówek metody. W treści nagłówka metody zostały m.in.umieszczone specyfikacje warunków wstępnych i końcowych. Specyfikacja warunków wstęp-nych umieszczona jest w linijce 3 i składa się z trzech warunków sprawdzających poprawnośćargumentów wywołania: input != null, length >= 0 oraz input.size() >= length. Ko-lejna linijka zawiera specyfikację warunków końcowych, w skład której wchodzi tylko jedenwarunek — return.size() == length. Warunek ten służy sprawdzeniu poprawność warto-ści zwracanej.

Warunki początkowe oraz końcowe mogą zostać umieszczone w nagłówku dowolnej metody(w tym także statycznej), tuż przed nawiasem klamrowym otwierającym ciało metody.

Opcjonalna sekcja warunków początkowych metody zaczyna się słowem in po którymnastępuje, ujęta w nawiasy, lista poszczególnych warunków. Separatorem elementów na liściejest przecinek. Pusta lista warunków nie jest dopuszczalna. Poszczególne elementy na liście,podobnie jak w wypadku niezmiennika, są wyrażeniami o wartości logicznej. Zasady użyciaidentyfikatorów są analogiczne jak w wypadku warunków niezmiennika.

Opcjonalna sekcja warunków końcowych metody ma składnię niemal identyczną jak listawarunków początkowych, z tą różnicą, że zaczyna się słowem out. Pusta lista warunkówkońcowych nie jest także dopuszczalna. Zasady użycia identyfikatorów w wyrażeniach sąanalogiczne jak w wypadku warunków początkowych, z tą różnicą, iż w wypadku metodzwracających wartość, istnieje dodatkowo możliwość odwołania się do wartości zwracanej —za pomocą słowa kluczowego return. Typ wartości reprezentowanej przez słowo returnjest identyczny z typem zwracanym przez daną metodę. W sytuacji gdy warunki końcowesprawdzane są podczas normalnego zakończenia metody, słowo return posiada wartość równąwartości zwróconej przez metodę. Warunki końcowe sprawdzane są także w sytuacji gdy me-toda kończy się poprzez odrzucenie wyjątku. Słowo return posiada wtedy wartość domyślnątypu zwracanego metody, określoną w specyfikacji języka Java[9, paragraf §4.12.5]. Tablica2.4 zawiera podsumowanie wartości domyślnych dla typu return.

Podobnie jak ma to miejsce w wypadku niezmienników, składnia warunków końcowychpozwala użytkownikowi na tworzenie zagnieżdżonych warunków początkowych. W takiejsytuacji, w celu uniknięcia niejednoznaczności, ustala się, że słowo kluczowe return odnosisię zawsze do najbardziej zagnieżdżonej z metod opatrzonych warunkiem końcowym. Należytakże zauważyć że jak niezmiennik z klasą, tak warunki wstępne i końcowe są ściśle związane z

35

Page 38: Praca_magisterska

typ wartość domyślna

dowolny typ referencyjny nulltyp prosty double 0typ prosty float 0typ prosty long 0typ prosty int 0typ prosty short 0typ prosty byte 0typ prosty char 0typ prosty boolean false

Tablica 2.4: Wartości zwracane przez metodę w wypadku odrzucenia wyjątku.

daną definicją metody20. W razie wywołania dowolnej metody nadpisującej daną (tj. metodyo tej samej sygnaturze co dana, zdefiniowanej w podklasie) lub dowolnej metody przez danąnadpisanej (tj. metody o tej samej sygnaturze co dana, zdefiniowanej w nadklasie), nie miejscasprawdzenie warunków wstępnych oraz końcowych.

Listing 2.16 zawiera przykłady błędnych prób użycia warunków wstępnych i końcowych.

1 public class Test {2 int ge t In t ( int a ) out ( a > 2 ) in ( return != 4 ){3 // b l a d − niew lasc iwa k o l e j n o s c warunkow4 return 2 ;5 }67 Object getName ( ) out ( return . l ength ( ) > 0 ) {8 // b l a d − ” re turn ” j e s t typu Object i jako t a k i n ie9 // pos iada p u b l i c z n e j metody l e n g t h ( )

10 return ” Aleksander ” ;11 }1213 void voidMethod ( ) out ( void . class . equa l s ( return . g e tC la s s ( ) ) ){14 // b l a d − odwolanie s i e do w a r t o s c i zwracanej15 //w przypadku metody nie p o s i a d a j a c e j w a r t o s c i16 }1718 int execute ( ) out ( return != null ){19 // b l a d − w a r t o s c i zwracanej przyp i sany j e s t20 // wartosciowy typ i n t ( a nie r e f e r e n c y j n y I n t e g e r )21 return new I n t e g e r ( 10 ) ;22 }2324 void compute ( ) in ( ”qwerty” ){25 } // b l a d − typ S t r i n g nie j e s t typem logicznym26 }

Listing 2.16: Przykłady nieprawidłowych wyrażeń sprawdzenia stanu.

20W implementacji autora treść warunków jest po prostu kopiowana do ciała metody.

36

Page 39: Praca_magisterska

Szczególnie interesującym przykład wykorzystania warunków początkowych dotyczy me-tod, których wykonanie powinno być dopuszczalne jedynie w pewnych konkretnych stanach,a stan otrzymany w wyniku wykonania metody powinien należeć także do pewnego okre-ślonego zbioru stanów. Kontrakt tego typu jest przykładem ścisłej integracji tradycyjnychmechanizmów PPK z wprowadzonymi przez autora automatami skończonymi. Autor zasta-nawiał się tu wręcz nad wprowadzeniem specjalnej składni przeznaczonej na potrzeby tegotypu kontraktów. Ostatecznie jednak do takiego posunięcia nie doszło — zaważyły względykoncepcyjne, w szczególności tendencja do utrzymywania jak najprostszej składni.

Listing 2.17 zawiera przykład tego typu zastosowania warunków.

1 public class {2 automaton {3 i n i t i a l S0 : S1 , S2 ;4 S1 : S3 ;5 accepting S2 : S3 ;6 S3 : S0 ;7 }89 void compute ( ) in ( transient ? ( S1 , S2 ) ) out ( transient ? ( S0 , S1 , S3 ) ){

10 /∗ . . . ∗/11 }12 }

Listing 2.17: Przykład specyfikacji dopuszczalnych stanów początkowych i końcowych metody.

2.3.7 Warunki kontraktu metody

Na warunki kontraktu metody składają się warunki zaczerpnięte z odpowiednich niezmien-ników oraz określone jawnie warunki początkowe i końcowe metody. Sprawdzenie kontraktunastępuje bezpośrednio po rozpoczęciu wykonywania automatu metody, oraz bezpośrednioprzed jej zakończeniem. Warunki końcowe sprawdzane są bez względu na to, czy wywoła-nie metody zakończyło się poprzez wykonanie zwyczajnej instrukcji powrotu, czy też zostałoprzerwane wskutek odrzucenia wyjątku.

Tablica 2.5 przedstawia kolejność w jakiej następuje sprawdzenie poszczególnych grupwarunków podczas rozpoczęcia wykonywania metody. Tablica 2.6 zawiera podobne podsu-mowanie dla sprawdzenia kończącego wywołanie.

Warunki pochodzące z tego samego niezmiennika sprawdzane są w takiej kolejność, wjakiej zostały w danym niezmienniku zadeklarowane. Ta sama reguła dotyczy warunkówkońcowych oraz początkowych metody.

2.3.8 Wyjątki

Sposób potraktowania obsługi wyjątków (∼brak takowej) w propozycji PPK dla języka Javajest na ta tyle istotny, że autor zdecydował się poświęcić jej cały osobny ustęp.

37

Page 40: Praca_magisterska

sekwencja rodzaj warunków

1 warunki niezmiennika publicznego2 warunki niezmiennika chronionego3 warunki niezmiennika pakietowego4 warunki niezmiennika pakietowo-chronionego5 warunki niezmiennika prywatnego6 jawne warunki wstępne metody

Tablica 2.5: Kolejność wstępnego sprawdzania warunków kontraktu metody.

sekwencja rodzaj warunków

1 jawne warunki końcowe metody2 warunki niezmiennika prywatnego3 warunki niezmiennika pakietowo-chronionego4 warunki niezmiennika pakietowego5 warunki niezmiennika chronionego6 warunki niezmiennika publicznego

Tablica 2.6: Kolejność końcowego sprawdzania warunków kontraktu metody.

Mechanizmy PPK same w sobie nie prowadzą nigdy do generacji wyjątków których ob-sługa jest wymagana na etapie kompilacji, nie powinny także praktycznie nigdy generowaćwyjątków typu java.lang.Error oraz java.lang.RuntimeException21,22. Co się jednakstanie w sytuacji, gdy w specyfikacji warunku użyta zostanie konstrukcja mogącą odrzucaćwyjątek, tak jak ma to miejsce na listingu 2.18? W takim przypadku metody, podczas którychwywołania sprawdzany jest dany warunek, mogą same odrzucać ów wyjątek. Jeśli wyjątekten wymaga dodatkowo obsługi na etapie kompilacji, odpowiednie metody muszą jawnie za-deklarować możliwość jego odrzucenia przy pomocy klauzuli throws w swoich nagłówkach.Koniecznością dopisania w odpowiednich miejscach klauzuli throws obarczony jest w tej sy-tuacji programista. Niezapewnienie takiej obsługi traktowane jest jako błąd. W związkuz powyższym, listing 2.18 nie zawiera kodu poprawnego programu. Prawidłowy kod zostałzamieszczony w listingu 2.19.

1 import java . i o . IOException ;23 public class Condit ionExcept ions {4 private boolean checkContit ionA ( ) throws IOException {5 /∗ . . . ∗/6 }

21Z wyłączeniem wyjątku java.lang.AssertionError, którego odrzucenie następuje w wyniku złamaniakontraktu22Wyjątki w języku Java można podzielić na dwie główne kategorie: a) wyjątki których obsługa wy-

magana jest na etapie kompilacji — poprzez przechwycenie za pomocą klauzuli catch lub przekazanie zapomocą klauzuli throws — oraz b) wyjątki których obsługa nie jest wymagana na etapie kompilacji. Doklasy drugiej zalicza się wyjątki dziedziczące, pośrednio lub bezpośrednio, po klasach java.lang.Error lubjava.lang.RuntimeException. Wszystkie pozostałe wyjątki należą do pierwszej klasy.

38

Page 41: Praca_magisterska

78 private boolean checkConditionB ( ) throws Secur i tyExcept ion {9 /∗ . . . ∗/

10 }1112 invariant {13 ∗ : checkConditionA ( ) ;14 }1516 void protected method1 ( ){17 }1819 void method2 ( ) {20 }2122 void method3 ( )23 in ( checkConditionB ( ) ){24 }25 }

Listing 2.18: Problem odrzucania wyjątków wewnątrz warunków (przykład błędny).

1 import java . i o . IOException ;23 public class Condit ionExcept ions {4 private boolean checkContit ionA ( ) throws IOException {5 /∗ . . . ∗/6 }78 private boolean checkConditionB ( ) throws Secur i tyExcept ion {9 /∗ . . . ∗/

10 }1112 invariant {13 ∗ : checkConditionA ( ) ;14 }1516 void protected method1 ( ){17 }1819 void method2 ( ) throws IOException{20 }2122 void method3 ( ) throws IOException , Secur i tyExcept ion23 in ( checkConditionB ( ) ){24 }25 }

Listing 2.19: Problem odrzucania wyjątków wewnątrz warunków (przykład prawidłowy).

Należy w tym miejscu wyraźnie podkreślić, że stosowanie tego typu konstrukcji jest przez

39

Page 42: Praca_magisterska

autora zdecydowanie odradzane. Z tego też powodu autor nie uznał za konieczne wprowadza-nia jakiejkolwiek składni ułatwiającej obsługę tego rodzaju sytuacji (a mogłaby to być np.możliwość opatrzenia niezmiennika klauzulą throws).

Generalnie rzecz biorąc, podobne praktyki nie powinny mieć miejsca w przypadku ja-kiegokolwiek systemu kontroli kontraktu, w tym nawet podczas użycia zwyczajnych asercji.Mianowicie, specyfikacja warunków kontraktu mogących podczas sprawdzenia odrzucać wy-jątki, może wymuszać na programiście zmiany kodu źródłowego nie związane bezpośrednio zdefinicją i kontrolą kontraktów. W przykładzie z listingu 2.19 konstrukcja warunków wymusiładopisanie do niektórych z metod klauzuli “throws”, co automatycznie owocuje koniecznościąodpowiedniego opakowania wywołań niniejszych metod przez ich użytkownika. W efekcie po-wstaje cały ciąg zmian mających źródło jedynie w specyfikacji pewnego kontraktu dla pewnejklasy, co w oczywisty sposób prowadzi do przemieszania właściwej logiki programu z logikąkontroli kontraktów. Jest to gwałcące naruszenie reguły dotyczącej rozdziału funkcjonalno-ści (ang. Separation of Concerns), która stanowiła dla autora jedną z głównych motywacjiuzasadniających konieczność wprowadzenia wsparcia składniowego dla mechanizmów Progra-mowania przez Kontrakt.

Sugestia autora w tej materii jest następująca — wszelkie wyjątki, które mogą zostaćodrzucone podczas kontroli warunków i których obsługa wymagana jest na etapie kompilacji,powinny być opakowywane do typu java.lang.AssertionError (lub innego typu, któregoobsługa nie jest już wymagana na etapie kompilacji). Przy uwzględnieniu owego zalecenia,przykład z listingu 2.19 może zostać zmodyfikowany do postaci z listingu 2.20.

1 import java . i o . IOException ;23 public class Condit ionExcept ions {4 private boolean checkContit ionA ( ) {5 try {6 /∗ . . . ∗/7 } catch ( IOException e ){8 throw new Asse r t i onErro r ( e ) ;9 }

10 }1112 private boolean checkConditionB ( ) {13 try {14 /∗ . . . ∗/15 } catch ( Secur i tyExcept ion e ){16 throw new Asse r t i onErro r ( e ) ;17 }18 }1920 invariant {21 ∗ : checkConditionA ( ) ;22 }2324 void protected method1 ( ){25 }2627 void method2 ( ) {28 }

40

Page 43: Praca_magisterska

2930 void method3 ( ) in ( checkConditionB ( ) ){31 }32 }

Listing 2.20: Problem odrzucania wyjątków wewnątrz warunków (ogólne rozwiązanie sugero-wane przez autora).

2.3.9 Kontrola kontraktów

W niniejszej propozycji Programowania przez Kontrakt dla języka Java można wyodrębnićkilka różnych rodzajów kontraktów. Na zbiór ten składają się trzy kontrakty z warunkamijawnymi, których treść określana jest bezpośrednio przez programistę:

• warunki niezmiennika

• warunki początkowe metody

• warunki końcowe metody

oraz dwa kontrakty, których warunki nie mogą być przez programistę explicite określane, leczwynikają z definicji automatu skończonego:

• prawidłowość przejścia pomiędzy stanami

• przejście do stanu akceptującego w chwili usuwania obiektu

Co ciekawe, kontrakty te nie dotyczą wszystkich elementów wprowadzonych przez autora dojęzyka Java pod etykietą Programowania przez Kontrakt. Otóż weryfikacji kontraktu niepowoduje wykonanie wyrażenia sprawdzającego aktualny stan automatu skończonego. Wy-rażenie to jest o tyle newralgiczne, iż jako jedyna z zaproponowanych przez autora konstrukcjijęzykowych, nie może zostać bez szkody usunięte z programu — można sobie łatwo wyobra-zić, że wymazanie niezmiennika, czy też konkretnych warunków metody, nie doprowadzi dozmiany funkcjonalności programu. Wynika to w oczywisty sposób z reguły rozdziału funk-cjonalności, która nadaje owym elementom w pewnym sensie autonomicznego charakteru.Tego samego nie da się jednak stwierdzić o wyrażeniu sprawdzenia stanu, które może np.wystąpić jako przełącznik instrukcji warunkowej zawierającej logikę programu. Ponieważ wy-rażenie sprawdzenia nie jest wyrażeniem samodzielnym, lecz funkcjonuje w oparciu o definicjępewnego automatu, fakt ten rozciąga się na całą definicję automatu.

W związku z powyższym, automat skończony danej klasy funkcjonuje, bez względu na toczy kontrola kontraktów jest włączona, czy też nie. Wyłączenie kontroli kontraktów skut-kuje, w wypadku automatu skończonego, jedynie tym, że nie jest sprawdzana dopuszczalnośćprzejść podczas wykonywania instrukcji zmiany stanu (to samo dotyczy drugiego z kontraktówzwiązanych z automatem). Działanie takie jest świadomą i celową decyzją autora, wynikającąw duże mierze z doświadczenia. Propozycję związaną z automatami skończonymi można za-tem potraktować nie jako bezpośredni element Programowani przez Kontrakt, lecz jako pewnąkonstrukcję którą bardzo łatwo na potrzeby Programowania przez Kontrakt zaadoptować.

41

Page 44: Praca_magisterska

Nie narusza to w żadnym wypadku rozdziału funkcjonalności, gdyż sprawdzanie kontraktunastępuje niejako “przy okazji” i nie wymaga od programisty żadnego dodatkowego wkładu23

Jedną z wytycznych przy projektowaniu rozszerzenia PPK dla Javy było dla autora za-pewnienie jak największej spójności z już istniejącymi mechanizmami, w tym z mechanizmemasercji. Autor na rzecz owej spójności postanowił ujednolicić sposób powiadamiania o złama-niu kontraktu, tak aby był on kompatybilny z istniejącym dotychczas w Javie mechanizmemsprawdzania asercji (mechanizm ten opisany był dokładniej w sekcji 2.1.2) - podobnie jak zła-manie warunku asercji, tak i złamanie warunku kontraktu, prowadzi do odrzucenia wyjątkujava.lang.AssertionError .

Włączenie i wyłączenie kontroli kontraktów odbywa się w ten sam sposób, co w wypadkuzwyczajnych asercji, czyli za pomocą ustawienia odpowiedniego parametru maszyny wirtu-alnej — na ogół jest to przełącznik-ea. Dzięki takiemu podejściu obsługa skompilowanegokodu nie wymaga żadnych dodatkowych zabiegów w stosunku do programu napisanego wczystej Javie.

1 public class Test{2 public stat ic void main ( St r ing argv [ ] ) in ( fa l se ){3 System . out . p r i n t l n ( ” He l lo World without cont rac t check ing ” ) ;4 }5 }

Listing 2.21: Program łamiący kontrakt.

Niech dany będzie dla przykładu program jak na listingu 2.24. Każda próba uruchomieniaowego programu z włączonym mechanizmem sprawdzania asercji powinna w oczywisty sposóbprowadzić do złamania kontraktu metody main:

$ # Niech w aktualnym katalogu dana bedzie skompilowana klasa Test$ java TestHello World without contract checking$ java -ea TestException in thread "main" java.lang.AssertionError

at Test.main(Test.java:3)

2.4 Wykorzystanie w wypadku już istniejących komponentów

Dzięki definicji mechanizmów Programowania przez Kontrakt dla języka Java w postaci nad-klasy tegoż języka, każdy prawidłowy program napisany w Javie zachowuje także swoją po-prawność oraz funkcjonalność, jeśli zostanie potraktowany jako program napisany w JavaBC.Z tego też powodu migracja istniejącego projektu z Javy do zaproponowanego przez autora

23Na dobrą sprawę, jedynie oznaczenie stanów akceptujących jest wymogiem ściśle związanym z kontroląkontraktów automatu i może być potraktowane jako naruszenie reguły rozdziału funkcjonalności. Wyjątek tenjest jednak z gatunku tych najdrobniejszych i można go po prostu złożyć na karb funkcjonalności i przejrzystościzaproponowanej składni. Należy zresztą pamiętać, że, Java jako taka, nie jest językiem akademickim, leczczysto przemysłowym, stąd przesadny puryzm nie jest w tym wypadku właściwy.

42

Page 45: Praca_magisterska

rozszerzenia może odbyć się praktycznie natychmiast, nie pociągając za sobą właściwie żad-nych kosztów.

W celu przeprowadzenia migracji wystarczy wzbogacić skrypt użytego narzędzia budo-wania o dodatkowy krok polegający na odpowiednim wywołaniu translatora24,25 Samo uzu-pełnianie klas o definicje kontraktów jest już w zupełności opcjonalne i nie musi być wcaleprzeprowadzone na etapie migracji, lecz może odbywać się sukcesywnie — w miarę potrzeb.

2.5 Techniczne rozwiązanie problemu

Implementacja propozycji Programowania przez Kontrakt została przez autora oparta natranslatorze tłumaczącym kod napisany w JavaBC do kodu napisanego w czystym językuJava26. Informacje na temat sposobu użycia translatora można znaleźć w dodatku C.1. Samtranslator został stworzony w oparciu o bibliotekę ANTLR [16].

Szczegóły techniczne rozwiązania nie są tu w gruncie rzeczy istotne, jednakże warto jestprzyjrzeć się efektom działania translatora. Analiza taka może pozwolić na lepsze zrozumieniesemantyki proponowanych przez autora konstrukcji. Niech dany będzie zatem program jak nalistingu 2.22. Przygotowany przez autora translator wygeneruje, dla wejścia w postaci owegoprogramu, program wynikowy jak na przykładzie 2.23.

1 public class Example {2 int r e s u l t ;3 S t r ing type ;45 automaton {6 i n i t i a l S1 : S2 ;7 S2 : S1 , S3 ;8 accepting S3 : S2 ;9 }

1011 public invariant {12 ∗ : r e s u l t != 0 : ” zero r e s u l t ” ;13 S1 , S2 : r e s u l t > 2 | | type . equa l s ( ”SHORT” ) ;14 }1516 package protected invariant {17 S3 : true ;18 }1920 Example ( ){21 transient : S2 ;

24W niniejszym punkcie mowa jest jedynie o należących do JavaBC mechanizmach PPK. W wypadkudynamicznych ról zachodzi jeszcze konieczność dołączenia odpowiedniej biblioteki z definicjami wymaganychadnotacji i klas.25“narzędzie budowania” - ang. build tool. Najpopularniejszymi narzędziami do budowy programów na-

pisanych w Javie są systemy Apache Ant oraz Maven. Autor wraz z implementacją translatora dla PPKdostarczył zadanie dla systemu Ant. Więcej informacji na ten temat można znaleźć w dodatku C.2.26Translacja do pewnego języka wysokiego poziomu jest jednym z częstszych sposobów prototypowania no-

wych języków. Autor zresztą nie miał w tej kwestii zbyt dużego wyboru. Napisanie faktycznego kompilatoraprzeprowadzającego translację do kodu bajtowego maszyny wirtualnej byłoby przedsięwzięciem przekraczają-cym znacznie możliwości czasowe autora.

43

Page 46: Praca_magisterska

22 }2324 protected int method ( ) out ( return ∗ return >= 0 ){25 return 12 ;26 }2728 public void compute ( )29 in ( transient ? ( S1 , S2 ) )30 out ( transient ? Example . S1 ){31 transient : S1 ;32 }33 }

Listing 2.22: Kod źródłowy pewnego programu.

1 public class Example {2 private stat ic enum AutomatonEnumeration Example {34 S1 ( true , false , 1 ) ,5 S2 ( false , false , 0 , 2 ) ,6 S3 ( false , true , 1 ) ;7 private boolean i s I n i t i a l ;8 private boolean i sAccept ing ;9 private int [ ] dst ;

1011 private AutomatonEnumeration Example ( boolean i s I n i t i a l ,12 boolean i sAccept ing , int . . . dst ) {13 this . i s I n i t i a l = i s I n i t i a l ;14 this . i sAccept ing = i sAccept ing ;15 this . dst = dst ;16 }1718 public boolean i s I n i t i a l ( ) {19 return i s I n i t i a l ;20 }2122 public boolean i sAccept ing ( ) {23 return i sAccept ing ;24 }2526 public boolean t r a n s i t i o n P o s s i b l e (27 AutomatonEnumeration Example s ) {28 return java . u t i l . Arrays . b inarySearch (29 dst , s . o r d i n a l ( ) ) >= 0 ;30 }31 }3233 private AutomatonEnumeration Example automaton Example =34 AutomatonEnumeration Example . S1 ;3536 int r e s u l t ;37 St r ing type ;

44

Page 47: Praca_magisterska

3839 Example ( ) {40 assert ( ( Example . this . automaton Example ==41 Example . AutomatonEnumeration Example . S3 ) )42 ? true : true ;43 try {44 assert Example . this . automaton Example . t r a n s i t i o n P o s s i b l e (45 Example . AutomatonEnumeration Example . S2 ) ;46 Example . this . automaton Example =47 Example . AutomatonEnumeration Example . S2 ;48 } f ina l ly {49 assert ( ( Example . this . automaton Example ==50 Example . AutomatonEnumeration Example . S3 ) )51 ? true : true ;52 }53 }5455 protected int method ( ) {56 assert ( ( Example . this . automaton Example ==57 Example . AutomatonEnumeration Example . S3 ) )58 ? true : true ;59 int o u t r e t u r n = 0 ;60 try {61 return o u t r e t u r n = 12 ;62 } f ina l ly {63 f ina l int o u t r e t u r n 2 = o u t r e t u r n ;64 assert o u t r e t u r n 2 ∗ o u t r e t u r n 2 >= 0 ;65 assert ( ( Example . this . automaton Example ==66 Example . AutomatonEnumeration Example . S3 ) )67 ? true : true ;68 }69 }7071 public void compute ( ) {72 assert r e s u l t != 0 : ” zero r e s u l t ” ;73 assert ( ( Example . this . automaton Example ==74 Example . AutomatonEnumeration Example . S1 )75 | | ( Example . this . automaton Example ==76 Example . AutomatonEnumeration Example . S2 ) )77 ? r e s u l t > 2 | | type . equa l s ( ”SHORT” ) : true ;78 assert ( ( Example . this . automaton Example ==79 Example . AutomatonEnumeration Example . S3 ) )80 ? true : true ;81 assert ( Example . this . automaton Example ==82 Example . AutomatonEnumeration Example . S1 | |83 Example . this . automaton Example ==84 Example . AutomatonEnumeration Example . S2 ) ;85 try {86 assert Example . this . automaton Example . t r a n s i t i o n P o s s i b l e (87 Example . AutomatonEnumeration Example . S1 ) ;88 Example . this . automaton Example =89 Example . AutomatonEnumeration Example . S1 ;90 } f ina l ly {

45

Page 48: Praca_magisterska

91 assert ( Example . this . automaton Example ==92 Example . AutomatonEnumeration Example . S1 ) ;93 assert ( ( Example . this . automaton Example ==94 Example . AutomatonEnumeration Example . S3 ) )95 ? true : true ;96 assert r e s u l t != 0 : ” zero r e s u l t ” ;97 assert ( ( Example . this . automaton Example ==98 Example . AutomatonEnumeration Example . S1 )99 | | ( Example . this . automaton Example ==

100 Example . AutomatonEnumeration Example . S2 ) )101 ? r e s u l t > 2 | | type . equa l s ( ”SHORT” ) : true ;102 }103 }104105 @Override106 protected void f i n a l i z e ( ) throws Throwable {107 try {108 super . f i n a l i z e ( ) ;109 } f ina l ly {110 assert automaton Example . i sAccept ing ( )111 | | System . e r r . p r i n t f ( ” Object ” + this + ” ( ”112 + this . g e tC la s s ( ) . getName ( ) +113 ” ) not in accept ing s t a t e during f i n a l i z a t i o n ” )114 != null ;115 }116 }117 }

Listing 2.23: Kod źródłowy programu z listingu 2.22 po translacji (po uprzednim sformato-waniu i usunięciu komentarzy translatora).

Krótka analiza kodu otrzymanego w wyniku translacji pozwala stwierdzić, iż tłumaczeniejest prawidłowe. W szczególności funkcjonalność klasy oraz jej publiczny interfejs nie uległyzmianie. Jak na dłoni widać także sposób, w jaki autor zapewnił wymaganą integrację wpro-wadzonych przez siebie mechanizmów PPK z istniejącym w Javie mechanizmem asercji —sprawdzenia kontraktów są po prostu bezpośrednio tłumaczone na instrukcje asercji. Defi-nicja automatu skończonego doprowadziła natomiast do deklaracji zagnieżdżonej enumeracjireprezentującej schemat automatu oraz pola zawierającego konkretną instancję automatu.Obie składowe są prywatne.

Osobną kwestią jest sposób dobierania identyfikatorów, którego nie można wywnioskowaćz powyższego przykładu. Otóż, identyfikatory, których translator używa do nazwania nowychskładowych klasy, dobierane są w sposób dynamiczny27. Innymi słowy, nie ma podstaw dlastwierdzenia, że JavaBC nie jest faktyczną nadklasą Javy, co nota bene było dla autora jednymz najważniejszych założeń.

Komentarz należy się tutaj jeszcze sposobowi sprawdzania czy obiekt podczas finalizacjiznajduje się w stanie końcowym. Sposób owego kontraktu różni się nieco od wszystkich po-zostałych przypadków. Wynika to z tej prostej przyczyny, że każdy wyjątek odrzucony wfinalizatorze, jest przez maszynę wirtualną zwyczajnie ignorowany. Skutkuje to m.in tym,

27Wynika to, co prawda, w sposób prosty z założenia, że JavaBC jest nadklasą Javy, autor uznał jednak zastosowne wspomnieć o owym fakcie, dla podkreślenia zgodności implementacji z założeniami.

46

Page 49: Praca_magisterska

że na standardowym wyjściu błędu nie zostaje wyświetlona żadna informacja, tak jak mato miejsce w przypadku wystąpienia nieprzechwyconego wyjątku. W szczególności dotyczyto także wyjątków typu java.lang.AssertionError. Stąd wynika konieczność jawnego wy-świetlenia komunikatu na standardowym wyjściu błędu, co widać wyraźnie w wygenerowanymkodzie.

2.6 Uzasadnienie i krytyka propozycji

Wybór elementów Programowania przez Kontrakt oraz ich składnia wraz z semantyka, jakkol-wiek byłby arbitralny, ma swoje uzasadnienie. Przy ich określaniu autor kierował się przedewszystkim względami praktycznymi oraz zgodnością z istniejącą już składnią i semantyką ję-zyka Java. Stąd też dążenie do maksymalnej zwięzłości oraz czytelności propozycji ponoszonejnawet kosztem pewnej funkcjonalności. Poniżej znajduje się krótkie omówienie poszczegól-nych elementów propozycji wraz z rozważaną przez autora składnią dla niego alternatywną.

Automat skończony: definicja automatu umieszczana jest bezpośrednio w ciele klasy, mimoże istniejąca praktyka składniowa sugerowałaby umieszczenie owej definicji w nagłówkuklasy — poprzez analogię z kontraktami metod, które umieszczone są w ich nagłów-kach. Autor rozważał taką możliwość, jednakże zrezygnował z niej z dwóch powodów.Po pierwsze definicja automatu skończonego nie jest sama w sobie czystym elementemkontraktu klasy, lecz jej integralnym elementem funkcjonalnym, którego duży stopieńformalizmu pozwala na łatwe wywnioskowanie zeń kontraktu w sposób automatyczny28.Umieszczanie zatem definicji automatu w nagłówku byłoby nieco mylące. Po drugieskładnia taka jest zwyczajnie mało czytelna i “nieelegancka”, co widać dobrze na li-stingu 2.24.

Ciało automatu skończonego ujęte zostało w nawiasy klamrowe, chcąc w ten sposóbutrzymać dotychczasową konwencję, polegającą na ujmowaniu w ten typ nawiasów ciałwszystkich innych definicji wewnątrz-klasowych, takich jak inicjalizatory statyczne, kon-struktory bądź metody. Jednak koronnym argument przemawiający za ujęciem definicjiautomatu skończonego w nawiasy klamrowe była analogia pomiędzy definicją klasy aautomatu. Obie definicje same składają się listy pomniejszych definicji (lub deklaracji)porozdzielanych znakami średnika. W przypadku klas są to deklaracje pól i metod,w przypadku automatu skończonego — stanów. W świetle tej argumentacji, składniaalternatywna polegająca na ujęciu ciała automatu w nawiasy zwyczajne, przy jedno-czesnym rozdzieleniu elementów za pomocą przecinków, wydaje się niewłaściwa.

Jako separator pomiędzy nazwą stanu a listą stanów docelowych, użyty został znakdwukropka. Autor rozważał tu jeszcze dwuznak ->, jednakże zrezygnował z niego, abynie wprowadzać do języka dodatkowego symbolu leksykalnego. Na rzecz dwukropkaprzemawia także fakt, że występuje on w składni asercji.

Wreszcie autor w wypadku definicji automatów skończonych zrezygnował z jawnegookreślania funkcji przejścia, pozwalając programiście jedynie na specyfikację dopusz-czalnych przejść29. Motywacja była tu następująca: wybrany sposób definicji automatuskończonego powinien w zdecydowanej większości przypadków być wystarczający, a co

28por. uzasadnienie w sekcji 2.3.9.29patrz sekcja 2.3.1.

47

Page 50: Praca_magisterska

najważniejsze znacznie ogranicza objętość sekcji definiującej automat. Jeśliby funkcjaprzejścia miała być definiowana explicite, definicje automatów stałyby się zwyczajniemało czytelne, co stoi w sprzeczności z jedną z idei Programowania przez Kontrakt.

1 public class Class automaton {2 i n i t i a l S0 : S1 ;3 accepting S1 ;4 } {5 /∗ . . . ∗/6 }

Listing 2.24: Alternatywna składnia definicji automatu skończonego.

Obsługa automatu skończonego: do obsługi automatu skończonego klasy służą instruk-cja zmiany stanu oraz wyrażenie sprawdzenia aktualnego stanu. Składnia obu konstruk-cji jest tutaj bardzo prosta. Autor w obydwu przypadkach wykorzystał istniejące jużsłowo kluczowe transient, które w Javie może występować jedynie w roli identyfika-tora. Alternatywą byłoby tu skorzystanie z innego, istniejącego już słowa kluczowego lubwprowadzenie nowego symbolu leksykalnego nie będącego słowem kluczowym, np. ??.Autor nie skorzystał jednak z takiego podejścia, uznając transient słowo transient zanajbardziej odpowiednie. Należy tu zauważyć, że wprowadzenie zupełnie nowego słowakluczowego nie byłoby możliwe z powodów koncepcyjnych (JavaBC jest nadklasą Javy),a słowa pseudokluczowego z powodów gramatycznych. W instrukcji zmiany stanu wy-stępuje, poprzez analogię do składni automatu skończonego, dwukropek. W wyrażeniusprawdzenia stanu dwukropek jest w naturalny sposób zastąpiony znakiem zapytania.

Składnia złożonej nazwy stanu, tj. nazwy stanu poprzedzonej prostą nazwą klasy, jestnajprostszym możliwym rozwiązaniem problemu niejednoznaczności odwołań do sta-nów. W szczególności alternatywna składnia polegająca na podaniu pełnej nazwy klasyautomatu skończonego (tj. nazwy klasy wraz z przypisanym jej pakietem) byłaby w tejsytuacji nadmiarowa. Nawiasem mówiąc, obrany schemat wpisuje się dobrze w konwen-cje nazewniczą Javy, polegającą na rozdzielaniu nazw złożonych symbolem kropki.

Wyrażenie sprawdzenia aktualnego stanu w wersji rozbudowanej (zawierającej listę sta-nów) musiało być ujęte w ogranicznik z powodów gramatycznych. Autor zdecydowałsię tu użyć nawiasów zwyczajnych w roli ogranicznika oraz przecinka jako separatoraelementów na liście, gdyż wpisują się one dobrze w konwencje budowy wyrażeń w językuJava. Alternatywa w postaci użycia nawiasów klamrowych nie miałaby tu większegosensu. Pojawienie się nawiasów klamrowych w wyrażeniu języka Java jest ściśle zwią-zane z tworzeniem nowego obiektu — tablicy lub klasy anonimowej. Jeśli autor użyłbyich na potrzeby wyrażenia sprawdzenia stanu, mogłoby to być nieco mylące dla osobyszybko przeglądającej kod programu.

Niezmienniki: Argumentacja za umieszczeniem niezmiennika bezpośrednio w ciele klasy, anie w jej nagłówku, jest podobna jak w wypadku definicji automatu skończonego. Tosamo tyczy się ujęcia ciała niezmiennika w nawiasy klamrowe, oraz separacji poszcze-gólnych warunków za pomocą znaku średnika.

Składnia poszczególnych warunków była przez autora wzorowana na wyrażeniu asercjioraz instrukcji zmiany stanu. Z tego powodu, jako separator, użyty został znak dwu-kropka. Konieczność pisania znaku gwiazdki, w razi braku podania konkretnego zbioru

48

Page 51: Praca_magisterska

stanów, wynika z powodów czysto gramatycznych — alternatywa w postaci pomija-nia pierwszej sekcji warunku niezmiennika prowadziłaby niestety do niejednoznacznościpodczas rozbioru gramatycznego i z tego powodu została przez autora wykluczona. Au-tor mógł co prawda oznaczyć pierwszą z sekcji jako opcjonalną, musiałoby się to jednakodbyć kosztem rezygnacji z opcjonalności sekcji trzeciej, określającej komunikat błędu.Takie rozwiązanie stałoby jednak w sprzeczności z istniejącą składnią asercji, gdzie towłaśnie wyrażenie zawierające komunikat jest opcjonalne.

Warunki wstępne i końcowe metody: W odróżnieniu od składni z języka D, warunkiwstępne oraz końcowe metody ujęte zostały w nawiasy zwyczajne, poprzedzone dodat-kowo słowem in bądź out. Użycie nawiasów klamrowych sugerowałoby programiście iżma do czynienia z blokiem instrukcji (w języku D takie rozwiązanie ma de facto miejsce).Autor zdecydował jednak że zamiast listy instrukcji, programiście powinna wystarczyćlista wyrażeń logicznych. Separatorem jest tu, z racji użycia danego rodzaju nawiasów,przecinek.

2.6.1 Możliwości dalszego rozwoju

Autor zdaje sobie sprawę z tego, że niniejsza propozycja mechanizmów Programowania przezKontrakt nie jest w pełni kompletna i idealna. Łyżką dziegciu jest tu poniższa lista, którazawiera szereg sugestii dotyczących dalszego rozwoju koncepcji autora w tym zakresie:

• Możliwość definicji niezmienników statycznych, dotyczących kontroli kontraktów metodstatycznych.

• Możliwość odwoływania się w definicji niezmiennika do stanów różnych automatów skoń-czonych. Wbrew pozorom konstrukcja taka miałaby sporą przydatność — na ogół nie-statyczne klasy zagnieżdżone, a tylko takie mogą odwoływać się do automatów klasotaczających, współpracują w sposób ścisły z klasami otaczającymi.

• Podział warunków końcowych na warunki końcowe dotyczące zakończenia metody wnormalnym trybie oraz warunki końcowe dotyczące zakończenia metody w trybie nad-zwyczajnym, to jest w sytuacji odrzucenia wyjątku. Warunki pierwszego rodzaju mo-głyby mieć dostęp do wartości zwracanej, warunki drugiego rodzaju natomiast do odrzu-conego wyjątku. Listing 2.25 przedstawia przykład możliwej składni dla tego rodzajuwarunków.

1 public class Class {2 int method ( ) in ( true ) out ( return > 2 ) error ( fa l se ) {3 return 5 ;4 }5 }

Listing 2.25: Warunki końcowe w razie odrzucenia wyjątku.

• Zmiana składni wyrażeń początkowych oraz końcowych metod na wzór podobnych kon-strukcji z języka D. Propozycja składni z języka D jest o tyle lepsza, iż daje programiściemożliwość wykonania czynności wstępnych, przed przystąpieniem do sprawdzania wa-runku.

49

Page 52: Praca_magisterska

• Rozwiązanie problemu przysłaniania nazw pól klasy przez nazwy zmiennych w wypadkustosowania niezmienników.

• Możliwość stosowania wyrażeń początkowych i końcowych nie tylko dla ciała funkcjilecz w ogólności dla dowolnego bloku kodu30.

30tj. zbioru instrukcji ujętych w nawiasy klamrowe.

50

Page 53: Praca_magisterska

Rozdział 3

Dynamiczne role

3.1 Czym są dynamiczne role?

Ponieważ używane przez autora określenie “dynamiczne role” nie jest pojęciem jasno spre-cyzowanym, zachodzi potrzeba jego pewnej formalizacji. W związku z tym, na potrzebyniniejszej pracy, można przyjąć następujące definicję owego terminu:

“Dynamiczne role” są pewną koncepcją z pogranicza kontroli typów w językachprogramowania. Pojęcie to oznacza swego rodzaju mechanizm, który pozwalana dynamiczne przypisywanie danemu obiektowi wielu różnego rodzaju typów (izwiązanych z nimi wartości) jednocześnie. Obiekt ów nosi miano aktora, a jegoreprezentacje, w postaci wartości o określonych typach, nazywane są rolami1.

Można powiedzieć, że dynamiczne role są swoistym, dynamicznym polimorfizmem. Kon-cepcja ta stanowi pewną próbę przezwyciężenia trudności związanych z modelowaniem by-tów o nieustalonej, dynamicznie zmieniającej się funkcjonalności, dla których zdefiniowaniepojedynczej klasy nie jest na ogół rozwiązaniem wystarczającym. Oczywiście tworzenie kom-ponentów modelujących tego rodzaju byty jest, przy wykorzystaniu tradycyjnych metod ofe-rowanych przez różnego rodzaju języki programowania, jak najbardziej możliwe, jednakżepociąga ono za sobą stosowanie skomplikowanych, mało elastycznych rozwiązań, które cha-rakteryzują się umiarkowaną czytelnością i jasnością implementującego je kodu.

3.1.1 Obecny stan rzeczy - motywacja

W konwencjonalnych językach programowania określona zmienna wiąże się w danej chwili (tj.podczas wykonania programu) z wartością o konkretnym i określonym typie. W wypadkujęzyków ze statyczną kontrolą typów, typ owej wartości znany jest na ogół już na etapie kom-pilacji. W wypadku natomiast różnego rodzaju języków skryptowych z dynamiczną kontrolątypów, typ wartości reprezentowanej przez daną zmienną może być określony dopiero na eta-pie wykonania. W obu jednak przypadkach typ wartości wskazywanej przez daną zmiennąjest, w chwili wykonania, już ściśle określony. Przy uwzględnieniu jedynie języków obiek-

1Autor przyznaje, że podana definicja jest dosyć ogólnikowa. Dokładniejsze znaczenie terminu zostanieimplicite wyklarowane w ramach treści kolejnych rozdziałów.

51

Page 54: Praca_magisterska

towych2, oznacza to, że dana zmienna wiąże się z obiektem konkretnej klasy, posiadającejkonkretny, ustalony interfejs.

Próba potraktowania zmiennej jako wskazania do typu rodzaju innego niż rzeczywisty,prowadzi na ogół do wygenerowania błędu. Błąd ten może powstać jeszcze na etapie kom-pilacji, może też mieć miejsce dopiero podczas uruchomienia programu, przy czym w naj-gorszym scenariuszu, może się on w ogóle bezpośrednio nie ujawnić, prowadząc jedynie doniewłaściwego działania programu. Sposób ujawniania się błędów wynikłych z kontroli typówzależy tu przede wszystkim od konstrukcji danego języka, w szczególności od mechanizmówbezpieczeństwa typów (ang. type safety).

Listingi 3.1 oraz 3.2 zawierają przykłady dwóch błędnych programów napisanych w ję-zyku Java. W obydwu przypadkach autor próbował użyć zmiennej object o wartości typujava.lang.Object jako ciągu znakowego — java.lang.String (linijka 4). W programieprzedstawionym na listingu 3.1 wykrycie błędu następuje jeszcze na etapie kompilacji, także uruchomienie wadliwego programu nie było w ogóle wadliwe. Nieco odmienna sytuacjama miejsce w drugim z programów, gdzie wygenerowanie błędu następuje dopiero podczasuruchomienia programu, prowadząc do bezpośredniego odrzucenia wyjątku informującego oużyciu nieprawidłowego typu — java.lang.ClassCastException.

1 public class WrongTypes {2 public stat ic void main ( St r ing [ ] a rgs ) {3 Object ob j e c t = new Object ( ) ;4 sayText ( ob j e c t ) ;5 }6 public stat ic void sayText ( S t r ing text ){7 System . out . p r i n t l n ( t ex t ) ;8 }9 }

Listing 3.1: Przykład użycia zmiennej o niewłaściwym typie — błąd kompilacji.

1 public class WrongTypes {2 public stat ic void main ( St r ing [ ] a rgs ) {3 Object ob j e c t = new Object ( ) ;4 sayText ( ( S t r ing ) ob j e c t ) ;5 // proba jawnego zrzutowania typu Object na typ S t r i n g6 }7 public stat ic void sayText ( S t r ing text ){8 System . out . p r i n t l n ( t ex t ) ;9 }

10 }

Listing 3.2: Przykład użycia zmiennej o niewłaściwym typie — błąd w czasie uruchomienia.

Jeśli programista zechce użyć zmiennej w roli wskazania na wartości o określonym typie,nie pozostaje mu nic innego jak zapewnić aby zmienna faktycznie posiadała ów typ, lub innytyp “pokrewnego” rodzaju, którego język pozwala używać w miejscu danego typu.

2Podobnie jak miało to miejsce dla propozycji PPK, autor zdecydował się oprzeć koncepcję dynamicznychról o języki obiektowe, a jej implementację — o język Java. Motywacja dla użycia języków obiektowych byłatutaj podobna, jak w przypadku w propozycji PPK.

52

Page 55: Praca_magisterska

Wadliwy program z listingów 3.1 i 3.2 można zatem, w nawiązaniu do powyższej uwagi,poprawić do postaci jak na listingu 3.3. Początkowe przypisanie zmiennej object wartościo nieodpowiednim typie (linijka 3) zostało tu skorygowane powtórnym przypisaniem (linijka4) wartości o właściwym już typie. Należy zauważyć, że drugie przypisanie spowodowałojednocześnie utratę dostępu do wartości, którą początkowo reprezentowała zmienna. Zmiennew Javie, tak jak i w dowolnym innym języku programowania, mogą w danej chwili wskazywaćna co najwyżej jedną wartość3. Oczywiście nie wyklucza to możliwości pośredniego wiązaniawiększej liczby obiektów, o potencjalnie różnych typach z daną zmienną. Możliwe jest to naprzykład w sytuacji gdy wartością danej zmiennej jest tablica zawierająca szereg kolejnychwartości. Niemniej, zmienna jako taka, może odnosić się tylko do jednej wartości o określonymtypie.

Alternatywne podejście do naprawy programu zaprezentowane jest na listingu 3.4, gdziew celu uzyskania odpowiedniego typu, tj. ciągu tekstowego, wywoływana jest pewna metodaobiektu reprezentowanego przez zmienną (linijka 4). Podejście to różni się od poprzedniegotym, że zamiast przypisywać zmiennej nową wartość o odpowiednim typie, następuje tu swegorodzaju jawna adaptacja posiadanej już wartości, jaką jest wywołanie na jej rzecz metodytoString, zwracającej wartość o żądanym już typie.

1 public class WrongTypes {2 public stat ic void main ( St r ing [ ] a rgs ) {3 Object ob j e c t = new Object ( ) ;4 ob j e c t = ”Ala ma kota ”5 sayText ( ( S t r ing ) ob j e c t ) ;6 }7 public stat ic void sayText ( S t r ing text ){8 System . out . p r i n t l n ( t ex t ) ;9 }

10 }

Listing 3.3: Poprawiona wersja programu z listingów 3.1 i 3.2 — przypisanie zmiennej nowejwartości.

1 public class WrongTypes {2 public stat ic void main ( St r ing [ ] a rgs ) {3 Object ob j e c t = new Object ( ) ;4 sayText ( ob j e c t . t oS t r i ng ( ) ) ;5 }6 public stat ic void sayText ( S t r ing text ){7 System . out . p r i n t l n ( t ex t ) ;8 }9 }

Listing 3.4: Poprawiona wersja programu z listingów 3.1 i 3.2 — adaptacja aktualnej wartościzmiennej.

Reasumując, można zauważyć, że praktycznie w każdym z obiektowych języków progra-mowania obowiązują następujące ograniczenia:

3Brak wskazania na jakąkolwiek wartość możliwy jest na przykład w językach, w których odwołania dozmiennych następują poprzez referencję. W przypadku Javy do określenia braku powiązania zmiennej z jaką-kolwiek wartością służy słowo kluczowe null.

53

Page 56: Praca_magisterska

Rysunek 3.1: Cykl rozwojowy motyla. Przejście ze stadium gąsienicy do stadium imagonosi wymowną nazwę “zupełnego przeobrażenia”; Mimo że dany motyl potrafi w trakcieswojego życia zmienić zupełnie postać, cały czas pozostaje tym samym, w sensie identyfikacji,osobnikiem.

• Z daną zmienną może być związana co najwyżej jeden obiekt (wartość).

• Obiekt ów posiada ściśle określony typ, i co z tym się wiąże, interfejs.

Jest to podejście intuicyjne i wygodne w większość zastosowań. Mając daną zmienną wia-domo, że wiąże się ona, w danej chwili wykonania programu, z pojedynczą wartością o pewnymokreślonym typie. Typ ten z kolei posiada pewien konkretny, określony interfejs.

Czasami jednak zachodzi potrzeba zamodelowania bytu o dynamicznie zmieniającym sięinterfejsie. Może wynikać to z faktu, że dany byt należy do kategorii bytów charakteryzującychsię tym, iż w czasie swojego cyklu życia dochodzi do diametralnych zmian jego formy — tj.interfejsu w wypadku modelu programistycznego — przy czym byt ten, przy całych swoichprzeobrażeniach, zachowuje swoistą spójność, która nakazuje traktować go jako jedność, a niepewien łańcuch, w jakiś sposób ze sobą powiązanych, ale jednak odrębnych, bytów. Dobrymprzykładem ilustrującym sedno owego zagadnienia może być, przedstawiony na rysunku 3.1,cykl rozwojowy motyla.

Inną sytuacją, w której statycznie określony interfejs okazuje się niewystarczający, jestkonieczność reprezentacji pewnego bytu, który w ogóle nie posiada ściśle określonego inter-fejsu. Mowa tu o bycie, który w trakcie swojego cyklu życia posiada możliwość nabywaniafunkcjonalności zupełnie odmiennej (w sensie interfejsu) od już posiadanej, a także posiadamożliwość tracenia dotychczasowej funkcjonalności. Wynikać to może chociażby z potrzebyadaptacji do określonych potrzeb. Warto zauważyć, że przypadek poprzedni jest przypadkiemszczególnym owej sytuacji.

Niech, dla celów konkretnego przykładu, dana będzie hierarchia interfejsów jak z rysunku3.2. Hierarchia ta przedstawia pewien zbiór interfejsów związany z modelowaniem rzeczywi-stego bytu jakim jest osoba fizyczna. Podstawowa funkcjonalność osoby udostępniana jestpoprzez interfejs IPerson. Interfejsy IChild, IAdult oraz ICorpse dotyczą podstawowejfunkcjonalności charakterystycznej dla pewnych etapów życia osoby. Specyficzne funkcjonal-ności osoby modelowane są natomiast za pomocą interfejsów IPupil, IEmployee, ICustomer,oraz ich podinterfejsów. Można powiedzieć, iż funkcjonalności reprezentowane przez poszcze-gólne interfejsy są swego rodzaju “rolami”, które dana osoba może na pewnym etapie swojegożycia odgrywać. Niektóre z ról mają charakter podstawowy i związane są z cyklem życia osoby,inne natomiast mogą występować opcjonalnie i dotyczą głównie różnych ról społecznych, któremogą być przez osobę odgrywane. Należy tu zauważyć, że wszystkie z ról łączy jedna wspólnacecha, jaką jest ich przejściowy charakter - z żadną z ról osoba nie jest związana przez całyokres swojego funkcjonowania. Innymi słowy, odgrywanie ról ma charakter “dynamiczny”.

Abstrakcja (∼komponent) użyta do zamodelowania bytu osoby, nazywana dalej “akto-rem”, powinna zatem móc wykazać się cechami takimi jak a) możliwość czasowego przyjmo-

54

Page 57: Praca_magisterska

Rysunek 3.2: Uproszczony diagram UML pewnej hierarchii interfejsów

wania różnego rodzaju interfejsów, czyli odgrywania różnych ról na różnym etapie swojegocyklu życia — b) w szczególności porzucania posiadanych już interfejsów, c) możliwość przyj-mowania wielu różnego rodzaju interfejsów na raz, to jest odgrywania różnych ról w tej samejchwili, d) zachowanie wewnętrznej spójności pomiędzy poszczególnymi rolami.

Rzeczywista realizacja komponentu osoby może opierać się na szeregu różnych pomysłów:

1. Komponent aktora może zostać oparty na pojedynczej klasie implementującej po prostuwszystkie z interfejsów potencjalnych ról.

2. Komponent aktora może istnieć w fazie koncepcyjnej — w oparciu o szereg klas po-wiązanych ze sobą za pomocą określonego wzorca, np. wzorca projektowego Decorator.Każda z klas w takim scenariuszu odpowiedzialna jest za jedną z ról.

3. Byt osoby nie jest w ogóle modelowany za pomocą dającego się wyodrębnić komponentu,lecz polega po prostu na współdziałaniu pewnej liczby klas odpowiedzialnych za różneaspekty jego funkcjonalności (tj. za odgrywanie ról). Rozwiązanie takie jest, bodajże,najczęściej spotykane w praktyce.

Ukazane tu pomysły są przedstawicielami trzech głównych klas rozwiązań danego problemu,różniących się między sobą poziomem enkapsulacji wewnętrznej logiki komponentu (w przy-padku pierwszego rozwiązania enkapsulacja jest ścisła, w przypadku trzeciego w ogóle nie mao niej w ogóle mowy). W związku z tym wyczerpują one w jakiś sposób zbiór wszystkichrozwiązań problemu na jaki pozwalają języki obiektowe. Niestety każdy z przedstawionychpomysłów zawiera pewne zasadnicze wady:

1. Zastosowanie pierwszego rozwiązanie wiedzie wprost do antywzorca projektowego zna-nego pod nazwą Blob4, którego zastosowanie pociąga za sobą wiele negatywnych kon-

4Czasami używane jest tu nieco bardziej specyficzne określenie “God object”.

55

Page 58: Praca_magisterska

sekwencji. Komponent aktora staje się w efekcie gigantyczną klasą o bardzo szerokimzakresie odpowiedzialności, co stoi w oczywistej sprzeczności z zasadami modularnościw programowaniu obiektowym. Co więcej, część funkcjonalności owej klasy dotyczącarzadko odgrywanych ról, może nie być nigdy w życiu większość obiektów wykorzysty-wana, co z kolei prowadzi do marnotrawstwa zasobów. Wreszcie, dodanie klasy dlanowej roli wiąże się automatycznie z ingerencją w istniejącą i funkcjonującą już klasę,co w efekcie może prowadzić do konieczności rekompilacji i ponownego uruchomieniadziałającego systemu.

2. Drugie podejście, polegające na umiarkowanej enkapsulacji, mimo iż najsensowniejsze zprzedstawionych, zawiera także dosyć poważną wadę w postaci dodatkowych wymogównakładanych na klasy ról. Wadą tą jest konieczność tworzenia klas poszczególnych rólw taki sposób aby mogły współdziałać ze sobą w ramach zastosowanego wzorca. Wprzypadku wzorca Decorator wymusza to na klasach posiadanie pewnego, związanegoz tym interfejsu. Wykorzystanie klas nie spełniających owego kryterium, zostaje z tegopowodu wykluczone.

3. Zasadniczą wadą trzeciego podejścia jest brak enkapsulacji. Ów niedostatek sprawia, iżproces tworzenia oprogramowania staje się bardzo podatny na najróżniejszego rodzajubłędy, a jego artefakty sprawiają duże trudności w utrzymaniu i dalszym rozwoju. Roz-wiązanie to jest także najczęściej stosowane w praktyce. Wynika to z pozornej pro-stoty implementacji, opartej często jedynie na doraźnych przesłankach, a niejednokrot-nie także i z braku wiedzy na temat wzorców programowania, co mogłoby pozwolić nazastosowanie drugiego podejścia.

Powyższa dyskusja pokazuje, iż w zasadzie nie jest możliwe, aby za pomocą tradycyjnychtechnik udostępnionych przez typowy obiektowy język programowania, otrzymać dynamicz-nie zachowujący się komponent, przy jednoczesnym zachowaniu prostoty odwoływania siędoń oraz tworzenia jego ról. W pierwszym wypadku abstrakcja modelująca byt nie posiada wogóle dynamizmu — może ona odgrywać jedynie role z pewnego ściśle określonego zestawu,przy czym role te muszą być odkrywane przez cały cykl życia abstrakcji. W przypadkutrzecim natomiast, nie można się do niej w prosty sposób odwoływać za pomocą pojedyn-czej zmiennej — w jej skład wchodzi wiele, potencjalnie rozproszonych, obiektów. Możliwienajsensowniejsza druga z koncepcji nadal wymusza na autorze szczególne traktowanie klasbędących implementacjami ról, co powoduje mocne ograniczenie mechanizmu dynamicznejadaptacji do określonego typu — podobnie jak w przypadku pierwszym tylko określone klasymogą służyć za role, przy czym przyjmowanie i odrzucanie ról przez aktora może odbywaćsię tu już w sposób dynamiczny.

Optymalne rozwiązanie powinno łączyć pewne cechy wszystkich trzech propozycji. Takwięc komponent aktora powinien być reprezentowany przez pojedynczy obiekt (— tak jak wrozwiązaniu pierwszym), który posiadałby możliwość dynamicznego adaptowania się do do-wolnego typu (— a nie tylko do predefiniowanego zestawu ról, jak w przypadku drugim), orazpowinien opierać się na logice ról zdefiniowanych w zwyczajnych klasach (— jak w przypadkutrzecim). Innymi słowy, użycie aktora powinno, w jak największym stopniu, przypominaćużycie zwyczajnej zmiennej. Problem tkwi w tym, że tak jak zostało to w pierwszej częścisekcji pokazane, zmienne w językach programowania oraz wartości przez nie wskazywane nieposiadają odpowiednich dla takiego zastosowania właściwości.

56

Page 59: Praca_magisterska

Aby zrealizować powyższe zamierzenie, należałoby zmodyfikować semantykę dotyczącąwykorzystania zmiennych. To jest, należałoby pozwolić aby jedna zmienna mogła wskazywaćnaraz na parę różnego typu wartości, lub by pewien obiekt mógł dynamicznie adaptować siędo określonego typu. Rozwiązaniem owego problemu są dynamiczne role, które pozwalająobiektom właśnie na dynamiczne dostosowywanie się do określonego typu.

3.1.2 Czym nie są dynamiczne role

Robocza definicja dynamicznych ról została co prawa sformułowana na samym początkusekcji 3.1, jednak w celu rozwiania pewnych wątpliwości oraz dalszego jej wyklarowania,autor postanowił zestawić ów mechanizm z funkcjonującymi w wielu językach programowaniamechanizmami w pewnym sensie podobnymi. Mowa tutaj o różnego rodzaju “karnacjach”polimorfizmu i rozmaitych sposobach kontroli typów:

Polimorfizm w programowaniu obiektowym pozwala obiektom na dynamiczne wiąza-nie różnego rodzaju funkcjonalności z konkretną metodą swojego interfejsu. Metodymogące uczestniczyć w owym mechanizmie, nazywane są metodami wirtualnymi. Choćnie w każdym języku jest to koniecznością, polimorfizm jest na ogół wykorzystywanyw połączeniu z mechanizmami dziedziczenia oraz rzutowania na klasę bazową: klasabazowa zawiera deklarację metody wirtualnej, a szereg klas dziedziczących z owej klasybazowej zawiera natomiast alternatywne implementacje owej metody. Podczas rzutowa-nia instancji różnych podklas na klasę bazową, otrzymuje się wskazanie na klasy bazowez różnymi implementacjami danej metody. W efekcie pojedyncza metoda obiektu pew-nej klasy może w sposób dynamiczny przybierać różnego rodzaju funkcjonalność.

Mechanizm ten nie umożliwia jednak obiektowi zmiany swojego interfejsu, co nie po-zwala już na dynamiczne odgrywanie ról o różnych interfejsach. Po drugie, polimorfizmnie pozwala także na to aby dany obiekt mógł w danej chwili posiadać więcej niż jednąimplementację danego interfejsu. Ograniczenie to nie pozwala z kolei na to aby obiektmógł odgrywać jednocześnie dwie różne role wiążące odmienną funkcjonalność z tymsamym interfejsem.

Polimorfizm w programowaniu funkcyjnym , znany również pod nazwą polimorfizmuad-hoc, pozwala programiście, z grubsza rzecz biorąc, na nazywanie więcej niż jednejfunkcji za pomocą tego samego identyfikatora. Właściwa dla danego wywołania funkcjawybierana jest na podstawie typów parametrów, na ogół jeszcze na etapie kompila-cji. Z tego powodu polimorfizm ad-hoc, jako taki, ma właściwie niewiele wspólnego zjakimkolwiek rodzajem dynamicznego zachowania.

Polimorfizm parametryczny pozwala na deklarowanie funkcji i klas o charakterze ogól-nym, z którymi nie są związane konkretne typy, lecz raczej zbiór dopuszczalnych ty-pów, na których daną abstrakcję można oprzeć. Użycie funkcji bądź klasy oferującejpolimorfizm parametryczny odbywa się poprzez jej sparametryzowanie, tj. określeniekonkretnych typów, z którymi dana instancja klasy bądź dane wywołanie funkcji mawspółpracować. Innymi słowy mechanizm polimorfizmu parametrycznego jest czymśw rodzaju możliwości definiowania szablonów, co umożliwia użytkownikowi uzyskaniewiększej zwięzłości i spójności programu przy jednoczesnym zachowaniu statycznej kon-troli typów.

57

Page 60: Praca_magisterska

Ponieważ parametryzacja jest dla instancji danej klasy zabiegiem jednorazowym, nieda się jej zbytnio wykorzystać do implementacji jakiegokolwiek rodzaju dynamicznychzachowań, w szczególności do mechanizmu “dynamicznych ról”.

Dynamiczne typowanie (ang. dynamic typing) jest rodzajem kontroli typów odbywającejsię w czasie wykonania i na dobrą sprawę nie ma ono nic wspólnego z dynamicznymirolami. To że typ danej zmiennej określany (czy może raczej “sprawdzany”) jest wsposób dynamiczny, nie oznacza wcale, iż typ wartości zmiennej może być dowolny.Wynika on po prostu z wcześniejszego przebiegu programu. Dynamiczne typowaniepozwala jedynie na to, aby pewna zmienna mogła w czasie wykonania być użytą dowskazywania na wartości o różnych typach.

Słabe typowanie (ang. weak typing) pozwala z grubsza na to, aby w trakcie wykonaniaprogramu dochodziło w razie potrzeby, do pewnych niejawnych konwersji pomiędzy ty-pami zupełnie ze sobą “niespokrewnionymi” — np. pomiędzy liczbą i ciągiem znaków.Pozwala to faktycznie na coś w rodzaju “dynamicznych ról”, tyle że dla typów prymi-tywnych, składających się z pojedynczej wartości o elementarnym charakterze (jak naprzykład liczba zmiennoprzecinkowa bądź wartości logiczna) i nie posiadających prak-tycznie żadnych składowych funkcyjnych.

Duck typing — w wolnym tłumaczeniu “kacze typowanie” — jest interesującym mechani-zmem, który polega, mniej więcej, na opóźnieniu sprawdzenie poprawności i legalnościwywołania metody aż do czasu wykonania5. Kluczowe założenie leżące u podstaw tegomechanizmu można wyłożyć za pomocą nieformalnego, utrzymanego w duchu praktycz-nego empiryzmu stwierdzenia:

Jeśli chodzi jak kaczka i kwacze jak kaczka to dla mnie jest kaczką6.

O mechanizmie kaczego typowania wygodnie jest myśleć jako o luźnym wiązaniu klasyz pewnym interfejsem. W myśl owej zasady, przyjmuje się, że aby dany obiekt mógł byćpotraktowany jako instancja pewnej klasy7, wystarczy aby posiadał on wymagany przezową klasę zestaw właściwości. Innymi słowy, jawne deklarowanie zgodności z pewnyminterfejsem, np. poprzez odziedziczenie z danej klasy bądź deklarację stwierdzającąimplementację owego interfejsu, nie jest w przypadku duck typingu wymagane.

1 class Duck :2 def quack ( s e l f ) :3 print ”Quaaaaaack ! ”4 def f e a t h e r s ( s e l f ) :5 print ”The duck has white and gray f e a t h e r s . ”67 class Person :8 def quack ( s e l f ) :9 print ”The person i m i t a t e s a duck . ”

10 def f e a t h e r s ( s e l f ) :

5W związku z tym “kacze typowanie” jest raczej cechą właściwą dla języków skryptowych, takich jak Ruby,Perl czy Groovy. Ciekawostką jest tutaj fakt, że czwarte wydanie języka C#, głównego konkurenta Javy, matakże oferować ów mechanizm.

6ang.: If it walks like a duck and quacks like a duck, I would call it a duck.7W rozumieniu koncepcyjnym, tj. niekoniecznie jako konkretna konstrukcja językowa.

58

Page 61: Praca_magisterska

11 print ”The person takes a f e a t h e r from the ground and shows i t . ”1213 def i n t h e f o r e s t ( duck ) :14 duck . quack ( )15 duck . f e a t h e r s ( )1617 donald = Duck ( )18 john = Person ( )19 i n t h e f o r e s t ( donald )20 i n t h e f o r e s t ( john )

Listing 3.5: Wykorzystanie mechanizmu duck typing w języku Python — przykładzaczerpnięty z Wikipedii[7].

Napisany w języku Python program z listingu 3.5 stanowi prostą ilustrację mechani-zmu kaczego typowania. Linijki 1–5 zawierają definicję klasy Duck, która jest pewnymmodelem kaczki.

Wykonanie programu zaczyna się od linijki 17. Na początku tworzone są dwa obiektyreprezentujące odpowiednio kaczkę i osobę, które używane są następnie w roli argumen-tów przy wywołaniu procedury in the forest. Uruchomienie programu może wyglądaćnastępująco:

$ python duck_typing.pyQuaaaaaack!The duck has white and gray feathers.The person imitates a duck.The person takes a feather from the ground and shows it.

Obiekt osoby został potraktowany jako obiekt kaczki. Chcąc przetłumaczyć ów pro-gram do języka Java, należałoby dokonać daleko idących zmian, w tym a) uwspólnićinterfejs pomiędzy klasami osoby i kaczki oraz b) zmodyfikować daną procedurę tak abyprzyjmowała argument realizujący ów interfejs .

Mimo że mechanizm kaczego typowania może wydawać się na pierwszy rzut oka kon-cepcją zupełnie odmienną od wcześniej wymienionych, jest on w rzeczywistości szcze-gólnym rodzajem dynamicznego typowania, w który podkreśla się pojęciowy (a niedeklaratywny) charakter interfejsów i ich implementacji. Dzięki mechanizmowi kaczegotypowania faktycznie możliwe jest dynamiczne odgrywanie różnego rodzaju ról. Zakresmożliwych do odegrania ról jest tu jednak wyraźnie ograniczony poprzez samą definicjęklasy — aby obiekt osoby mógł udawać kaczkę, musi po prostu posiadać odpowiedniemetody. Przypomina to, w gruncie rzeczy, nieco poprawioną wersję skrytykowanego wsekcji 3.1.1 sposobu tworzenia aktorów za pomocą pojedynczej klasy.

3.1.3 Możliwości języka Java

Mając na uwadze treść poprzedniej sekcji, należałoby dla kompletu omówić pewne mechani-zmy dostępne w Javie. Niniejsza sekcja zawiera bardzo krótki przegląd możliwości języka Java,w przypadku których można posłużyć się określeniem “dynamiczny”. Język programowaniaJava pozwala mianowicie na:

59

Page 62: Praca_magisterska

• Polimorfizm obiektowy, będący jedną z naczelnych zasad obiektowego paradygmatu pro-gramowania, w oparciu o który zaprojektowana została właśnie Java. Java pozwala nakorzystanie z polimorfizmu obiektowego zarówno za pomocą mechanizmu dziedziczeniaz klasy bazowej jak i poprzez implementację tzw. interfejsów, które są swoistymi klasamiczysto abstrakcyjnymi. Mechanizmy te są w zasadzie niemal identyczne — możliwośćdeklarowania i implementacji interfejsów wynika bezpośrednio z zakazu wielodziedzicze-nia.

Polimorfizm obiektowy ma w Javie charakter niejako podstawowego mechanizmu, przywykorzystaniu którego powinno tworzyć się oprogramowanie. Fakt ten jest doskonalepodkreślony poprzez automatyczne traktowanie wszystkich metod jako metod wirtual-nych, co stanowi nota bene dodatkową zachętę do jego stosowania8.

• Polimorfizm parametryczny, dostępny w Javie dopiero od wersji 1.5, funkcjonuje w niejpod nazwą Typów Ogólnych (ang. Generics). Mechanizm ten stanowi jedną z bar-dziej kontrowersyjnych i gorzej dopracowanych cech języka. Z powodu chęci zachowaniakompatybilności wstecznej, informacje o typach zastosowanych jako parametry, usu-wane są jeszcze na etapie kompilacji — podczas procesu nazywanego wymazywaniemtypów (ang. Type Erasure). W efekcie mechanizm typów ogólnych w Javie trudnonazwać prawdziwym polimorfizmem parametrycznym, choć ewidentnie bliżej jest mudo tej koncepcji niż chociażby systemowi szablonów znanemu z języka C++.

• Dynamiczne rzutowanie typów. Kontrola legalności dynamicznego rzutowania odbywasię dopiero w czasie wykonania.

• Dynamiczną kontrolę poprawności wykonania realizowaną za pomocą asercji. Więcejna ten temat można znaleźć w sekcji 2.1.2.

3.2 Propozycja dynamicznych ról dla języka Java

Przygotowana przez autora propozycja dynamicznych ról dla języka Java została skonstru-owana w postaci biblioteki tegoż języka9. Jest to podejście zupełnie odmienne od zastosowa-nego w przypadku propozycji PPK, gdzie poszczególne mechanizmy sprawdzania poprawnościkontraktu, bądź obsługi automatów skończonych, znajdowały swoje odzwierciedlenie na po-ziomie składni języka. W przypadku dynamicznych ról, autor zdecydował się na użycie jużistniejących elementów języka. Są to podstawowe jednostki konstrukcyjne udostępniane pro-gramiście przez Javę, które okazały się być w zupełności, dla celów autora, wystarczające.Mowa tu o klasach, interfejsach, adnotacjach i enumeracjach języka Java. W związku z uty-lizacją istniejących abstrakcji językowych, użycie dostarczonej implementacji nie różni sięzbytnio od użycia typowej biblioteki.

8Takie potraktowanie kwestii wyboru metod wirtualnych jest, zdaniem autora, bardzo ładnym przykłademprostoty i “elegancji” koncepcyjnej, która towarzyszyła Javie na etapie jej wczesnego rozwoju. Dzięki takiemuposunięci, udało się w dużym stopniu wyeliminować sprawiający wiele trudności początkującym programistomproblem rozróżnienia pomiędzy polimorficznym przeciążaniem metody a zwyczajnym przysłanianiem jej wi-doczności. Rozwiązanie to w bardzo ładny sposób podkreśla i eksponuje obiektowy charakter języka. Jest too tyle istotne, iż niestety wciąż można spotkać się z praktykami polegającymi na wykorzystywaniu językówwybitnie obiektowych, w tym Javy, do zwyczajnego programowania strukturalnego.

9Patrz dyskusja w sekcji 3.5.

60

Page 63: Praca_magisterska

Rysunek 3.3: Obiekt aktora i jego funkcjonalne reprezentacje w postaci stowarzyszonych znim obiektów, będących w istocie odgrywanymi rolami.

W tym miejscu należy wspomnieć o jeszcze jednym, bardzo istotnym aspekcie propozycjidynamicznych ról. O ile mianowicie składnia języka nie uległa zmianie, o tyle jego semantykazostała już nieznacznie zmodyfikowana. Trudno jest co prawda jednoznacznie określić conależy a co nie do semantycznych właściwości języka (nawiasem mówiąc, sama specyfikacjajęzyka Java nie definiuje wyraźnie tej granicy), jednakże w wypadku propozycji autora możnazdecydowanie stwierdzić, iż ingeruje ona w zakres funkcjonalności określony semantyką języka.

Zgodnie z definicją dynamicznych ról przedstawioną w sekcji 3.1, propozycja autora po-zwala na tworzenie obiektów aktorów, które posiadają z kolei możliwość odgrywania w sposóbdynamiczny różnego rodzaju ról.

Cała propozycja dynamicznych ról dla języka Java koncentruje się wokół dostarczonegoprzez bibliotekę interfejsu pl.edu.pw.akosicki.roles.Actor. Interfejs ten reprezentujefunkcjonalność rzeczywistej klasy aktora. W szczególności definiuje on metody pozwalającena adaptowanie się do danej roli, porzucanie danej roli oraz sprawdzanie czy obiekt aktoraaktualnie odgrywa daną rolę. Biblioteka nie zawiera natomiast żadnego interfejsu reprezen-tującego pojedynczą rolę aktora. Funkcję ról, tj. swego rodzaju zewnętrznych przejawówaktywności aktora, pełnią zwyczajne klasy języka Java. Sama adaptacja aktora do pewnejroli polega po prostu na zwróceniu instancji obiektu danej klasy, który, choć z pozoru nie-zależny, działa na rzecz określonego aktora. Schemat ów został podsumowany na rysunku3.3.

Obok mającego centralne znaczenie interfejsu aktora funkcjonuje także szereg pomoc-niczych abstrakcji, służących obsłudze różnorakich aspektów związanych z odgrywaniem ról.Poszczególne elementy propozycji autora zostaną omówione szczegółowo w kolejnych sekcjachniniejszego rozdziału.

Z racji tego, że to co zostało do tej pory w kwestii dynamicznych ról powiedziane możebyć, zdaniem autora, wciąż dosyć “mgliste”, najlepszym wprowadzeniem do omówienia pro-pozycji dynamicznych ról będzie konkretna demonstracja użycia jej biblioteki. Listing 3.6zawiera prosty przykład użycia ról. Funkcję definicji ról pełnią zwyczajne klasy — Ship iCar. Klasy te nie posiadają zbyt szerokich możliwości. Jedyne czego można od nich zażą-

61

Page 64: Praca_magisterska

dać, to przemieszczenie reprezentowanego obiektu (statku bądź samochodu). W roli aktorawykorzystana zostaje natomiast specjalna klasa udostępniona przez bibliotekę.

W linijce 30 następuje utworzenie obiektu aktora. Obiekt ten początkowo nie odgryważadnej roli. Sytuacja ta ulega zmianie w linijce 32, gdzie poprzez wywołanie metody playRoleaktorowi przypisywana jest rola typu Ship. Przy okazji utworzenia roli, aktor zwraca referen-cję na obiekt danej roli. Obiekt ów, co bardzo istotne, nie jest zwyczajnym obiektem swojejklasy, jaki można by uzyskać przy pomocy wywołania operatora new. Jak zostało już nawstępie wspomniane, pełni on niejako funkcję agenta danego aktora (ang. proxy). Wszelkiewywołania metod na rzecz agenta są w rzeczywistości wywołaniami na rzecz aktora, któregoów agent reprezentuje. Referencja na obiekt roli wykorzystana jest w kolejnej linijce, gdzienastępuje wywołanie pewnej właściwej dla obiektu Ship metody.

W linijce 36 następuje przypisanie aktorowi kolejnej roli, tym razem reprezentowanej przezklasę Car. Obiekt owej roli zostaje następnie użyty. Oczywiście, jak i w wypadku poprzedniejroli, tak i w tej sytuacji wywołanie pewnej metody jest w rzeczywistości wywołaniem na rzeczaktora, z którym obiekt danej roli jest związany.

Kluczem do wyjaśnienia zaproponowanego przez autora mechanizmu dynamicznych róljest asercja z linijki 38. Otrzymany wynik nie jest zgodny z semantyką Javy, jest natomiastzgodny ze zdrowym rozsądkiem dotyczącym zachowania się modelowanego bytu. Jeśli amfibia,startując z punktu o współrzędnej 0, przepłynęła w jedną stronę 10 jednostek odległości,następnie przejechała w stronę przeciwną jednostek 15, powinna znajdować się na pozycji -5.Efekt taki jest wynikiem synchronizacji pomiędzy składowymi poszczególnych ról. Usługasynchronizacji jest zapewniana automatycznie przez obiekt aktora.

1 import pl . edu .pw . a k o s i c k i . r o l e s . Actor ;2 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;34 public class RoleExample1 {5 stat ic class Ship {6 private int p o s i t i o n = 0 ;78 public int g e t P o s i t i o n ( ){9 return p o s i t i o n ;

10 }1112 public void f l e e t ( int d i s t anc e ){13 this . p o s i t i o n = d i s t anc e ;14 }15 }1617 stat ic class Car {18 private int p o s i t i o n = 0 ;1920 public int g e t P o s i t i o n ( ){21 return p o s i t i o n ;22 }2324 public void move( int d i s t anc e ){25 this . p o s i t i o n += d i s t anc e ;26 }27 }

62

Page 65: Praca_magisterska

2829 public stat ic void main ( St r ing [ ] a rgs ) {30 Actor amphibian = ActorFactory . c reateActor ( ) ;3132 Ship sh ip = amphibian . playRole ( Ship . class ) ;33 sh ip . f l e e t ( 10 ) ;3435 Car car = amphibian . playRole ( Car . class ) ;36 car . move( −15 ) ;3738 assert sh ip . g e t P o s i t i o n ( ) == −5;39 }40 }

Listing 3.6: Przykład użycia dynamicznych ról.

3.2.1 Przegląd biblioteki

Główną wytyczną towarzyszącą autorowi podczas formułowania propozycji dynamicznych rólbyło zachowanie możliwie największej prostoty oraz możliwości współdziałania ze zdefiniowa-nymi już komponentami (klasami). W wyniku takiego założenia biblioteka dynamicznych róludostępnia bardzo ograniczony zbiór klas i interfejsów, które jednak okazują się być zestawemw zupełności wystarczającym.

Wszystkie udostępniane przez bibliotekę klasy i interfejsy10 umieszczone są bezpośredniow pakiecie pl.edu.pl.akosicki.roles. Przy dalszych odwołaniach do typów udostępnia-nych przez bibliotekę, nazwa pakietu będzie na ogół pomijana. Ta sama skrótowa konwencjazostanie także wykorzystana w wypadku typów pochodzących z pakietu java.lang.

Część biblioteki związana bezpośrednio z obsługą aktorów została ograniczona do raptemjednego podstawowego interfejsu, tj. Actor, oraz typów o charakterze pomocniczym. Inter-fejs Actor służy, zgodnie ze swoją nazwą, do reprezentacji aktora. Referencję do interfejsuaktora można uzyskać za pomocą statycznych metod fabryki ActorFactory. EnumeracjaInitialSynchronization służy do określania rodzaju jednorazowej synchronizacji, któradokonuje się w chwili rozpoczęcia odgrywania danej roli. Na rysunku 3.4 umieszczony zostałstosowny diagram UML.

Ponieważ autor zdecydował się na możliwość używania praktycznie dowolnej klasy w for-mie roli11, zbiór typów związanych z definiowaniem ról jest nader ubogi. Składają się nańtrzy adnotacje — Scope, Name i Ignore — służące do szczegółowego określenia sposobu syn-chronizacji pomiędzy poszczególnymi rolami danego aktora. Stosowania adnotacji tych jestczysto opcjonalne.

Oprócz typów przeznaczonych do bezpośredniego użycia, biblioteka definiuje także nie-wielką hierarchię wyjątków. Hierarchia ta została przedstawiona na rysunku 3.5. Wyjątkimogą być odrzucane przede wszystkim podczas bezpośredniego użycia aktora, niemniej ist-nieje możliwość ich odrzucania także podczas użycia obiektu reprezentującego rolę pewnego10Adnotacje języka Java są szczególnym rodzajem interfejsów, enumeracja natomiast – klas.11Początkowo role miały być oddzielnymi konstrukcjami językowymi, tak jak jest to w wypadku klas bądź

interfejsów. Autor, na pewnym etapie, rozważał także wprowadzenie obowiązkowego dziedziczenia z pewnejklasy bazowej dla klas mających być używanymi w formie ról. Pomysły te zostały, rzecz jasna, odrzucone.

63

Page 66: Praca_magisterska

Rysunek 3.4: Diagram klas dla interfejsu aktora i jego otoczenia. Rzadko używany symbol nakońcu jednego ze związków oznacza klasę zagnieżdżoną. W razie wątpliwości co do składniUMLa, autor odsyła do obszernej pozycji [4].

Rysunek 3.5: Diagram klas dla wyjątków zdefiniowanych w bibliotece.

64

Page 67: Praca_magisterska

aktora. Warto zauważyć, że wszystkie wyjątki dziedziczą po typie java.lang.RuntimeException,dzięki czemu programista zostaje zwolniony z obowiązku bezpośredniej ich obsługi.

3.2.2 Tworzenie aktorów

Biblioteka nie udostępnia bezpośrednio klasy reprezentującej aktora. Tworzenie aktorów moż-liwe jest tylko za pomocą fabryki ActorFactory. Klasa fabryki zawiera dwie statyczne pu-bliczne metody pozwalające na uzyskanie referencji do interfejsu Actor:

Actor createActor() — podstawowa metoda tworząca obiekt aktora. Metoda ta zostałaużyta w programie zaprezentowanym na listingu 3.6.

Actor createThreadSafeActor() — metoda tworząca obiekt aktora dostosowany do użyciaw środowisku wielowątkowym. Wszelkie metody uzyskanej w ten sposób implementacjiaktora są synchronizowane. Co więcej, wszystkie metody obiektów użytych w formie róldanego aktora, stają się także metodami synchronizowanymi. Dzięki temu zarówno samaktor, jak i odgrywane przez niego role, stają się de facto monitorami. Użycie metodytworzącej tego rodzaju aktora może w określonej sytuacji wydatnie umniejszyć nakładpracy, który w przeciwnym razie musiałby zostać poniesiony w związku z koniecznościązapewnienia zewnętrznej synchronizacji.

Ciekawostką jest, że drugi ze sposobów tworzenia aktorów można wykorzystać do celów zu-pełnie nie związanych z mechanizmem dynamicznych ról. Można się nim mianowicie posłużyćdo dynamicznego tworzenia synchronizowanych obiektów pewnej klasy. Przykład ten zostałzilustrowany na listingu 3.7. Uruchomienie programu spowoduje wypisanie na standardowymwyjściu następujących komunikatów:

startedfinishedstartedfinished

1 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;23 public class SynchExample implements Runnable {45 public void run ( ) {6 System . out . p r i n t l n ( ” s t a r t e d ” ) ;7 try {8 Thread . s l e e p ( 10000 ) ;9 } catch ( Inter ruptedExcept ion e ) {

10 }11 System . out . p r i n t l n ( ” f i n i s h e d ” ) ;12 }1314 stat ic <T> T createSynchInstance ( Class<T> c l a z z ){15 return ActorFactory . createThreadSafeActor ( ) . p layRole ( c l a z z ) ;16 }17

65

Page 68: Praca_magisterska

18 public stat ic void main ( St r ing [ ] a rgs ) {19 SynchExample ob j e c t = createSynchInstance ( SynchExample . class ) ;2021 Thread thread = new Thread ( ob j e c t ) ;22 thread . s t a r t ( ) ;23 ob j e c t . run ( ) ;24 }25 }

Listing 3.7: Przykład użycia aktora do dynamicznej synchronizacji.

3.2.3 Tworzenie ról

Istotą funkcjonowania obiektu aktora jest odgrywanie ról. Role odgrywane przez danego ak-tora są udostępniane w postaci instancji zwyczajnych klas. Aby obiekt mógł posłużyć za rolędla danego aktora, musi być on utworzony za pomocą udostępnianej przez aktora metodyT playRole(Class<T>, Object...). Pierwszym argumentem metody jest obiekt reprezen-tujący klasę, której instancja ma posłużyć za rolę dla danego aktora. Kolejne argumentyzostaną przekazana dla konstruktora danej klasy. Nowo utworzony obiekt jest automatyczniewiązany z danym aktorem. Jest to nota bene jedyny sposób na związanie obiektu z aktorem.

Wybór odpowiedniego konstruktora odbywa się na zasadzie zgodnej ze schematem sta-tycznego wyboru konstruktora określonym w specyfikacji języka Java, z tą różnicą iż za typargumentów przyjmowany jest nie deklarowany typ danej zmiennej lecz typ faktycznej warto-ści wskazywanej przez zmienną. Modyfikator dostępu do konkretnego konstruktora jest igno-rowany, co umożliwia korzystanie nawet z konstruktorów zadeklarowanych jako prywatne12.Szczegółowe informacje na temat sposobu wyboru konstruktora można znaleźć w [9, paragrafy§15.9.3 oraz §15.12.2]13.

W wypadku użycia zagnieżdżonej klasy nie-statycznej w formie roli, należy pamiętać ookreśleniu explicite argumentów konstruktora wskazujących na klasy otaczające, które nor-malnie dodawane są przez kompilator w sposób niejawny.

W sytuacji gdy przekazanych argumentów nie da się użyć w wypadku żadnego konstruk-tora lub gdy nie da się wybrać pojedynczego konstruktora dla którego dopasowanie parame-trów jest zdecydowanie najlepsze, następuje odrzucenie wyjątku ConstructorException.

Z danym obiektem aktora może być jednocześnie związanych wiele ról, przy czym aktornie ma możliwości odgrywania więcej niż jednej roli określonego typu na raz. Próba użyciametody playRole w wypadku gdy z danym aktorem jest już związana rola danego typu,skończy się po prostu zwróceniem obiektu istniejącej roli. Argumenty wywołania konstruktorasą w takiej sytuacji ignorowane.

12W opinii autora jest to najsensowniejsze rozwiązanie. Alternatywą byłoby tutaj respektowanie modyfi-katorów dostępu, co nastręczyłoby sporo trudnych do rozstrzygnięcia problemów — np. kwestii tego gdzie ikiedy można uzyskać dostęp do konstruktora o dostępie pakietowym.13Strategię wyboru konstruktora można, w dużym uproszczeniu, podzielić na trzy fazy. W każdej z faz

określa się zbiór potencjalnie pasujących konstruktorów, przy czym w każdej kolejnej fazie zbiór ten jest niemniejszy niż w fazie poprzedniej. W drugiej i trzeciej fazie dopuszczalne jest stosowanie automatycznego opa-kowywania typów prostych. W trzeciej fazie zezwolone jest dodatkowo używanie zmiennej liczby argumentów.W zbiorze wybranych konstruktorów definiowana jest pewna relacja częściowego porządku. Jeśli zbiór ówposiada element największy, zostaje on wybrany jako właściwy konstruktor. W przeciwnym razie rozpoczętazostaje kolejna faza dopasowywania. W ostateczności odpowiedni konstruktor może nie być w ogóle znaleziony.

66

Page 69: Praca_magisterska

Listing 3.8 przedstawia przykład uzyskania instancji obiektu roli dla danego aktora. Wy-wołanie metody playRole w linijce 17 prowadzi do utworzenia nowej instancji obiektuRoleCtors i przypisania jej do aktora actor. Stworzenie obiektu odbywa się tutaj przyużyciu konstruktora RoleCtors(A, java.lang.Object). Kolejne dwa wywołania metodyplayRole nie prowadzą do żadnych, poza zwróceniem referencji, efektów — dany aktor od-grywa już po prostu żądaną rolę.

Mimo że parametry wywołań metody playRole z linijek 20 i 21 są ignorowane, nie sąone w gruncie rzeczy rozsądnie dobrane. Jeśliby dowolne z tych wywołań zostało użyte wcelu utworzenia nowego obiektu roli, a nie uzyskania referencji do obiektu już istniejącego,zostałby odrzucony wyjątek związane z niemożliwością doboru prawidłowego konstruktora.

1 import pl . edu .pw . a k o s i c k i . r o l e s . Actor ;2 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;34 public class RoleCtors {5 stat ic class A{}67 stat ic class B{}89 RoleCtors (A o1 , Object o2 ){}

1011 RoleCtors ( Object o1 , B o2 ){}1213 public stat ic void main ( St r ing [ ] a rgs ) {14 Actor ac to r = ActorFactory . c reateActor ( ) ;1516 RoleCtors r1 , r2 , r3 ;17 r1 = actor . playRole ( RoleCtors . class , new A( ) , new Object ( ) ) ;1819 // wowolanie z p o t e n c j a l n i e wadliwymi parametrami k o n s t r u k t o r a20 r2 = actor . playRole ( RoleCtors . class , new A( ) , new B( ) ) ;21 r3 = actor . playRole ( RoleCtors . class , new Object ( ) , new Object ( ) ) ;2223 assert r1 == r2 ;24 assert r2 == r3 ;25 }26 }

Listing 3.8: Tworzenie nowych i uzyskiwanie istniejących obiektów ról.

3.2.4 Odgrywanie ról

Użycie obiektu roli danego aktora jest jednoznaczne z odegraniem roli. Obiekty ról, utwo-rzone przy pomocy metody playRole aktora, działają na rzecz danego aktora. Najważniejszązaletą takiego rozwiązania jest to, że użytkownik obiektu roli nie musi podejmować żadnychdziałań związanych bezpośrednio z mechanizmem dynamicznych ról. Co więcej, może onnawet działać w nieświadomości tego, że obiekt którego używa, jest w rzeczywistości roląpewnego aktora, tj. swego rodzaju agentem działającym w imieniu innego bytu.

67

Page 70: Praca_magisterska

metoda działanie

T playRole(Class<T>,Object...) tworzy nowy obiekt roli lub zwraca obiekt jużistniejący

boolean detachRole(Class<T>) usuwa istniejący obiekt roliboolean hasRole(Class<T>) stwierdza czy aktor odgrywa daną rolę

Tablica 3.1: Zestawienie metod klasy Actor służących do obsługi ról.

Obiekt stworzony jako rola danego aktora, pozostaje nią aż do czasu swojego usunięcialub, opisanego w sekcji 3.2.5, jawnego odłączenia roli.

3.2.5 Odłączanie ról

Propozycję autora nie sposób byłoby określić mianem “dynamicznych ról”, jeśli obiekty akto-rów nie posiadałyby możliwości dynamicznego pozbywania się odgrywanych ról. Odłączanieroli od danego aktora odbywa się za pomocą metody boolean detachRole(Class<T>). Je-śli z danym aktorem jest stowarzyszona rola o danym typie, wywołanie metody detachRolezwraca wartość true oraz powoduje odłączenie roli. W przeciwnym wypadku, wywołaniemetody nie przynosi żadnego efektu, a zwróconą wartością jest false.

Odłączenie danej roli jest równoważne z utratą przez aktora umiejętności jej odgrywania.Prowadzi to automatycznie do usamodzielnienia się obiektu reprezentującego rolę. Obiekt tenmoże być w dalszym ciągu normalnie użytkowany, przy czym nie działa on już na rzecz swojegodawnego aktora. Z perspektywy użytkownika roli różnica pomiędzy zwyczajnym obiektem,obiektem roli oraz obiektem, który pełnił kiedyś funkcję roli jest niedostrzegalna. Należyzauważyć, iż po odłączeniu roli, dany obiekt nie ma już możliwości powtórnego związania sięz obiektem aktora — wiązanie takie może odbywać się jedynie na etapie konstrukcji.

Dany aktor może nabyć powtórnie umiejętność odgrywania pewnej roli poprzez ponownewywołanie metody playRole. W takiej sytuacji aktualny obiekt danej roli aktora oraz obiektbędący kiedyś obiektem owej roli tegoż aktora są zupełnie różnymi obiektami.

Oprócz metod pozwalających na tworzenie i odłączanie ról, obiekt aktora udostępniatakże pomocniczą metodę boolean hasRole(Class<T>), która pozwala stwierdzić czy ak-tor posiada rolę o określonym typie. Tablica 3.1 zawiera podsumowanie metod dotyczącychobsługi ról.

Program z listingu 3.9 stanowi prostą demonstrację odłączania ról. Diagramy obiektów3.6a, 3.6b i 3.6c z rysunku 3.6 obrazują sytuację, odpowiednio, z linijek 17, 23 i 28.

1 import pl . edu .pw . a k o s i c k i . r o l e s . Actor ;2 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;34 public class ActorDetaching {5 stat ic class A{}6 stat ic class B{}78 public stat ic void main ( St r ing [ ] a rgs ) {

68

Page 71: Praca_magisterska

9 Actor ac to r = ActorFactory . c reateActor ( ) ;1011 A a1 , a2 , a3 ;12 B b ;1314 a1 = actor . playRole ( A. class ) ;15 b = acto r . playRole ( B. class ) ;16 a2 = actor . playRole ( A. class ) ;1718 assert a1 == a2 ;19 assert acto r . hasRole ( A. class ) ;20 assert acto r . hasRole ( B. class ) ;2122 ac to r . detachRole ( A. class ) ;2324 assert ! a c to r . hasRole ( A. class ) ;25 assert acto r . hasRole ( B. class ) ;2627 a3 = actor . playRole ( A. class ) ;2829 assert acto r . hasRole ( A. class ) ;30 assert acto r . hasRole ( B. class ) ;31 assert a2 != a3 ;32 }33 }

Listing 3.9: Odłączanie ról.

(a) Stan z linijki 17 (b) Stan z linijki 23 (c) Stan z linijki 28

Rysunek 3.6: Diagramy obiektów UML dla programu z listingu 3.9.

3.2.6 Synchronizacja ról

Podstawową usługą zapewnianą niejawnie przez obiekt aktora jest synchronizacja obiektówodgrywanych ról. Synchronizacja polega na utrzymywaniu spójnych wartości pomiędzy po-lami poszczególnych obiektów ról, dzięki czemu aktor staje się czymś więcej niż zwyczajnymkontenerem obiektów. Po każdorazowym wykonaniu metody pewnej roli, wartości pól obiek-tów pozostałych ról zostają uaktualnione. W uściśleniu, ma to miejsce bezpośrednio przedzakończeniem wywołania danej metody lub konstruktora roli14.14Jest to ten sam punkt wywołania metody, w którym propozycja PPK wymaga sprawdzenia warunków

końcowych.

69

Page 72: Praca_magisterska

Dwa należące do różnych obiektów pola, zostają ze sobą zsynchronizowane, jeśli spełnionesą następujące warunki:

1. Klasy do których należą oba pola zostały zadeklarowane w tym samym pakiecie.

2. Nazwy obu pól są jednakowe.

3. Oba pola są polami statycznymi lub oba pola nie są polami statycznymi.

4. Żadne z pól nie zostało zadeklarowane w klasie java.lang.Object.

5. Oba z pól są dokładnie tego samego typu.

Należy zauważyć, że warunki te nie wykluczają synchronizacji pomiędzy polami zdefiniowa-nymi w tej samej klasie. Do sytuacji takiej może dojść gdy dany aktor odgrywa dwie role,których klasy posiadają wspólną klasę bazową, różną od java.lang.Object. W sytuacji gdynie zostanie spełnione dowolne z pierwszych czterech kryteriów, nie dochodzi po prostu do syn-chronizacji. W przypadku gdy pierwsze cztery warunki są spełnione, lecz typy poszczególnychpól nie zgadzają się ze sobą, zostanie odrzucony wyjątek typu SynchronizationException.Jest to jedyny scenariusz w którym wyjątek zostaje odrzucony podczas użycia roli.

W przypadku programu przedstawionego na listingu 3.6, kryteria wymagane do przepro-wadzenia synchronizacji spełnione zostały przez pola Ship.position oraz Car.position.Oba pola posiadają mianowicie ten sam typ i nazwę, są polami nie-statycznymi oraz należądo klas zadeklarowanych w tym samym pakiecie (tu w tzw. “pakiecie domyślnym” o pustejnazwie). Sama synchronizacja nastąpiła bezpośrednio przed zakończeniem wywołania metodymove obiektu car.

Warto w tym punkcie zwrócić jeszcze uwagę na fakt, że pole może uczestniczyć w syn-chronizacji w dwojaki sposób: a) jako pole którego wartość przypisywana jest innym polomlub b) jako pole, któremu przypisywana jest wartość innego pola. Reguła dotycząca tej kwe-stii jest prosta — wartości pól obiektu roli, na rzecz którego nastąpiło aktualne wywołaniemetody przypisywane są, przy uwzględnieniu odpowiednich kryteriów synchronizacji, polompozostałych obiektów.

Autor, określając wyżej przedstawiony schemat synchronizacji, kierował się przede wszyst-kim jego użytecznością. Ów sposób może okazać się jednak nieodpowiedni dla pewnych po-trzeb. Może to mieć w szczególności miejsce, gdy dynamiczne role mają zostać zastosowanedo istniejącego już zbioru klas, w wypadku których nie stosowano spójnego nazewnictwapól. Czasami wreszcie, może zachodzić potrzeba zupełnego wykluczenia niektórych pól zmechanizmu synchronizacji. W związku z taką ewentualnością, autor zaopatrzył bibliotekędla dynamicznych ról w trzy pomocnicze adnotacje, których można używać w celu zmianydomyślnego sposobu synchronizacji.

Adnotacja Scope może zostać użyta w nagłówku definicji typu. Służy ona do nadpisa-nia nazwy pakietu używanego podczas synchronizacji ról aktora. Po opatrzeniu danej klasyową adnotacją, mechanizm synchronizacji ról będzie traktował ją tak jakby została zadekla-rowana w pakiecie o nazwie podanej w argumencie adnotacji. Definicja adnotacji Scope jestnastępująca:

package pl . edu .pw . a k o s i c k i . r o l e s ;

70

Page 73: Praca_magisterska

@Retention ( Retent ionPo l i cy .RUNTIME )@Target ( ElementType .TYPE )public @inte r f a c e Scope {

St r ing value ( ) ;}

Przeznaczona do użycia w przypadku pól adnotacja Name ma funkcję analogiczną do ad-notacji Scope. Służy ona zmianie nazwy pola używanej podczas synchronizacji. Definicjaadnotacji jest następująca:

package pl . edu .pw . a k o s i c k i . r o l e s ;

@Retention ( Retent ionPo l i cy .RUNTIME )@Target ( ElementType . FIELD )public @inte r f a c e Name {

St r ing value ( ) ;}

Nieco inne zastosowanie posiada trzecia z dostarczonych adnotacji — Ignore. Adnotacjata jest prostym znacznikiem służącym do wyróżniania pól nie biorących udziału w synchroni-zacji. Pole oznaczone adnotacją Ignore będzie zwyczajnie ignorowane — zarówno w sytuacjigdy na rzecz roli o klasie je zawierającej zostanie wywołana metoda, jak i w sytuacji gdypewna metoda zostanie wywołana na rzecz innej roli z obrębu danego aktora. Definicja ano-tacji Ignore jest następujące:

package pl . edu .pw . a k o s i c k i . r o l e s ;

@Retention ( Retent ionPo l i cy .RUNTIME)@Target ( ElementType . FIELD )public @inte r f a c e Ignore {}

Oczywiście, zgodnie ze standardowym zachowaniem się adnotacji, zmiany wprowadzonedo domyślnego schematu synchronizacji poprzez użycie wyżej przedstawionych adnotacji sąuwzględniane także w wypadku klas potomnych. Należy tu jeszcze wspomnieć, że użycie do-wolnej z adnotacji pozostawia trwały, choć bardzo nieznaczny, ślad w danej klasie — wszystkiez adnotacji opatrzone są meta-adnotacją java.lang.annotation.Retention z parametremRUNTIME, co sprawia, że informacje o nich dostępne są w czasie uruchomienia za pomocąstandardowego mechanizmu refleksji15.

Przykład użycia adnotacji w celu zmiany domyślnego sposobu synchronizacji przedsta-wiony został na listingu 3.10.

1 import pl . edu .pw . a k o s i c k i . r o l e s . Actor ;2 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;3 import pl . edu .pw . a k o s i c k i . r o l e s . Scope ;4 import pl . edu .pw . a k o s i c k i . r o l e s .Name ;

15Inną sprawą jest to, że autor zdecydowanie odradza użycia tegoż mechanizmu w kombinacji z dynamicz-nymi rolami - więcej na ten temat w sekcji 3.2.7.

71

Page 74: Praca_magisterska

5 import pl . edu .pw . a k o s i c k i . r o l e s . Ignore ;67 public class Fie ld sSynchron i za t i on {89 stat ic class A {

10 private int value = 1 ;1112 public void setValue ( int value ) {13 this . va lue = value ;14 }1516 public int getValue ( ) {17 return value ;18 }19 }2021 @Scope ( ” p l . some package ” )22 stat ic class B {23 private int value = 2 ;2425 public void setValue ( int value ) {26 this . va lue = value ;27 }2829 public int getValue ( ) {30 return value ;31 }32 }3334 stat ic class C {35 @Name( ” value ” )36 private int counter = 3 ;3738 @Ignore39 private int value = 4 ;4041 public void setCounter ( int value ) {42 this . counter = value ;43 }4445 public int getCounter ( ) {46 return counter ;47 }4849 public void setValue ( int value ) {50 this . va lue = value ;51 }5253 public int getValue ( ) {54 return value ;55 }56 }57

72

Page 75: Praca_magisterska

58 public stat ic void main ( St r ing [ ] a rgs ) {59 Actor ac to r = ActorFactory . c reateActor ( ) ;6061 A a = actor . playRole ( A. class ) ;62 B b = actor . playRole ( B. class ) ;63 C c = acto r . playRole ( C. class ) ;6465 a . setValue ( 5 ) ;6667 assert a . getValue ( ) == 5 ;68 assert b . getValue ( ) == 2 ;69 assert c . getCounter ( ) == 5 ;70 assert c . getValue ( ) == 4 ;71 }72 }

Listing 3.10: Użycie adnotacji modyfikujących domyślny schemat synchronizacji.

Jak już zostało wspomniane, synchronizacja wartości pól pomiędzy poszczególnymi ro-lami jest przeprowadzana w dwóch przypadkach: tuż przed zakończeniem wywoływania me-tody obiektu roli oraz tuż przed zakończeniem tworzenia samego obiektu roli (tj. po wy-wołaniu jej konstruktora). O ile w pierwszym przypadku przyjęcie, że to zmiany powstałew danym obiekcie powinny zostać skopiowane do pozostałych obiektów jest zupełnie sen-sowne i zrozumiałe, o tyle w przypadku wywołania konstruktora kwestia ta pozostaje dys-kusyjna. Czy obiekt nowoutworzonej roli ma dostosować wartości swoich składowych dowartości zawartych w dotychczasowych rolach, czy też ma mieć miejsce synchronizacja zgołaodwrotna? Ponieważ odpowiedź na tak postawione pytanie może zależeć od kontekstu, wktórym używany jest mechanizm dynamicznych ról, autor postanowił umożliwić użytkowni-kowi ustawienie wybranego sposobu początkowej synchronizacji. W tym celu interfejs aktoraudostępnia dwie metody: InitialSynchronization getInitialiSynchronization() orazvoid setInitialSynchronization( InitialSynchronization type ), służące odpowied-nio do sprawdzenia aktualnego sposobu początkowej synchronizacji dla danego aktora orazdo ustawienia sposobu początkowej synchronizacji dla danego aktora.

Typ Actor.InitialSynchronization jest enumeracją określającą cztery rodzaje począt-kowej synchronizacji:

INHERIT jest domyślnym sposobem synchronizacji. Polega on na tym, że nowoutworzonyobiekt dostosowuje wartości swoich pól do wartości zawartych w rolach już istniejących.

OVERRIDE jest odwrotnością INHERIT. Obiekty ról odgrywanych dotychczas przez danego ak-tora dostosowują wartości swoich pól do wartości pól nowoutworzonej roli.

NONE oznacza brak synchronizacji początkowej.

CONFORM , podobnie jak i NONE, oznacza brak synchronizacji początkowej. Oczekuje się tujednak, że wartości pól w nowoutworzonym obiekcie będą takie same jak wartości odpo-wiadających im pól w obiektach odgrywanych już ról. W razie złamania tego nakazu,następuje odrzucenie wyjątku ConformationException.

73

Page 76: Praca_magisterska

3.2.7 Ograniczenia dotyczące użycia ról

Autor przygotował propozycję dynamicznych ról w taki sposób aby definiowanie klas prze-znaczonych na użycie w formie ról było, w miarę możliwości, obarczone jak najmniejszymiograniczeniami. Ograniczenia te nie zostały jednak zupełnie wyeliminowane. Klasy o pew-nych szczególnych właściwościach nie mogą być w ogóle używane jako role. W wypadku częściklas, użycie takie jest z kolei możliwe, lecz wyraźnie przez autora odradzane.

Jedynym ścisłym wymogiem stawianym przez autora klasom ról, jest to aby nie byłyone klasami finalnymi oraz aby żadna ze zdefiniowanych przezeń metod nie była metodą fi-nalną. Każda dowolna klasa spełniająca to kryterium może zostać obiektem roli pewnegoaktora16. Należy zauważyć, że zgodnie z ową regułą, role mogą być odgrywane także przezklasy abstrakcyjne. Odbywa się to poprzez stworzenie klasy adaptera z zalążkami abstrakcyj-nych metod, które zwracają wartość domyślną typu danej metody. Domyślne wartości typówzostały przedstawiona przy okazji warunków końcowych propozycji PPK w tablicy 2.4 (strona36).

Wpomniane ograniczenie wynika niestety z powodów czysto technicznych i jest wynikiemswego rodzaju kompromisu pomiędzy prostotą propozycji dynamicznych ról a jej możliwo-ściami. Więcej informacji temat technicznych aspektów implementacji biblioteki dynamicz-nych ról można znaleźć w sekcji 3.4.

Mimo że mechanizmu dynamicznych ról można używać w kombinacji z praktycznie dowol-nymi klasami, intencją autora było jego użycie w przypadku komponentów udostępniającychswoje właściwości jedynie za pomocą metod. Przykładem tego rodzaju komponentów są klasyutworzone zgodnie z konwencją JavaBeans. Jedną z zasad konwencji JavaBeans jest stosowa-nie tzw. akcesorów czyli specjalnych metod służących do pobrania bądź ustawienia wartościpola klasy. Same pola powinny być zabezpieczone przed bezpośrednim dostępem z zewnątrzza pomocą odpowiednich modyfikatorów. Konwencja taka skutkuje tym, że chcą doprowadzićdo pewnej zmiany stanu klasy, użytkownik jest zmuszony posłużyć się pewną metodą. Jest tobardzo istotne gdyż, jak to zostało w poprzedniej sekcji zaznaczone, synchronizacja obiektówposzczególnych ról ma miejsce jedynie podczas wywołań metod. Umieszczenie w klasie pól odostępie publicznym może prowadzić do rozspójnienia się wartości pól pomiędzy poszczegól-nymi rolami aktora. Wbrew pozorom restrykcja ta nie jest szczególnie uciążliwa. W dobrzezbudowanym systemie większość klas, o nieco bardziej rozbudowanej funkcjonalności, spełniakonwencję JavaBeans.

W punkcie tym należy jeszcze wspomnieć o kwestii związanej z mechanizmami refleksji.Otóż autor, najoględniej rzecz ujmując, stanowczo odradza ich stosowania. Użycie refleksji wcelach introspekcji może w najlepszym razie doprowadzić użytkownika do odkrycia, że obiektyról nie są tym czym się wydają. W gorszym scenariuszu, np. przy próbie zmiany wartościprywatnego pola, może skończyć się to odrzuceniem wyjątku lub w ogóle doprowadzić dozaburzeń w funkcjonowaniu pewnych wewnętrznych mechanizmów dynamicznych ról.

16W rzeczywistości utworzenie roli o danej klasie może nie powieść się z różnych innych powodów. Rolamoże nie być utworzona np. wtedy kiedy mogłoby stanowić to naruszenie obowiązującej polityki bezpieczeństwa(ang. security policy).

74

Page 77: Praca_magisterska

3.3 Wykorzystanie w wypadku istniejących już rozwiązań

Z racji tego, że implementacja dynamicznych ról dla języka Java jest przez autora dostar-czona w postaci zwyczajnej biblioteki, ich użycie nie nastręcza od strony technicznej żadnychtrudności. W szczególności, nie ma tu potrzeby wykonywania jakiegokolwiek dodatkowegokroku podczas fazy budowania oprogramowania, tak jak to miało miejsce chociażby w wy-padku przedstawionej wcześniej propozycji PPK, gdzie przed właściwym procesem kompilacjinależało dodatkowo uruchomiać translator. W związku z tym, po dołączeniu do projektu od-powiedniej biblioteki, role są natychmiast gotowe do użycia.

Niestety integracja w skali znacznie przekraczającej interakcje z pojedynczymi obiektami,a taka właśnie skala jest właściwym środowiskiem działania dla dynamicznych ról, może byćw przypadku istniejących systemów nieco problematyczna. Fakt ten został nawet podkreślonyw tytule niniejszej sekcji — użyte w analogicznej sekcji w propozycji PPK słowo “komponent”został zastąpione tu słowem “rozwiązanie”. Ponieważ mechanizm dynamicznych ról oferujeużytkownikowi dosyć wyrafinowanego rodzaju abstrakcję, której celem jest niejako uprosz-czenie, czy wręcz zastąpieniu dotychczasowych rozwiązań, integracja taka musi wiązać sięniechybnie z daleko idącymi zmianami w projekcie istniejącego systemu. Z tego też powodumożliwość integracji z istniejącymi rozwiązaniami o dużej skali jest tu dosyć ograniczona.

Reasumując, autor niestety nie widzi prostego sposobu na wykorzystanie dynamicznychról w przypadku istniejących już dużych systemów.

Odmiennie natomiast przedstawia się kwestia utylizacji istniejących już klas, o których dasię powiedzieć, że reprezentują swego rodzaju role, czyli pewne aspekty funkcjonalne więk-szych odeń bytów. Możliwość wykorzystania takiego zbioru klas w formie ról zależy od wieluczynników, w tym m.in. od a) jakości kodu istniejących klas, b) ich logicznego umiejscowieniaw systemie i c) spójności wewnętrznych mechanizmów oraz zastosowanych typów danych. Je-śli istniejące klasy pochodzą z tego samego pakietu, lub z pakietów blisko ze sobą związanych,istnieje spora szansa na to, że da się je wykorzystać w formie ról bez szczególnie kosztownychmodyfikacji — może się okazać, że przy spójnym nazewnictwie pól klas, jedynym niezbędnymzabiegiem będzie użycie tu i ówdzie adnotacji Ignore. Taka sytuacja miałaby prawdopodob-nie miejsce w przypadku zbioru klas przedstawionych na rysunku 3.2. Czasami oczywiścieadaptacja istniejącego zbioru klas na użytek dynamicznych ról może pociągać za sobą koniecz-ność wprowadzenia pewnych zmian w kodzie lub też, z powodu wewnętrznych niespójnościpomiędzy klasami, może nie być w ogóle możliwa17.

Choć nie jest to bezpośrednim przedmiotem niniejszego punktu, warto zwrócić uwagę nato, że oprócz użycia zgodnego ze swoim zasadniczym przeznaczeniem, tj. modelowania bytówo dynamicznie zmieniającej się funkcjonalności, zaproponowana przez autora biblioteka możezostać wykorzystana także na szereg innych, w pewnym sensie obocznych, sposobów, w tymnp.:

• jako narzędzie służące do dynamicznego tworzenia synchronizowanych obiektów pew-nej klasy, której pierwotna definicja nie zawierała informacji o potrzebie synchronizacji

17Na dobrą sprawę praktycznie każdy dowolny zbiór klas może zostać użyty w formie ról pewnego bytu, comożna uzyskać poprzez trywialne wykluczenie wszystkich pól z mechanizmu synchronizacji. Sęk w tym, że wtakiej sytuacji pomiędzy poszczególnymi obiektami ról danego aktora nie ma de facto żadnego powiązania asam aktor używany jest po prostu w formie zwykłego kontenera. Funkcję taką może z powodzeniem pełnićdowolna klasa implementująca interfejs java.util.Collection.

75

Page 78: Praca_magisterska

w postaci modyfikatorów synchronized. Stosowny przykład został przedstawiony nalistingu 3.7.

• jako narzędzie do tworzenia adapterów klas abstrakcyjnych. Zastosowanie to może byćprzydatne przy różnego rodzaju prototypowaniach.

3.4 Techniczne rozwiązanie problemu

Implementacja dynamicznycn ról dla języka Java została przez autora dostarczona w for-mie pojedynczej biblioteki tegoż języka. Biblioteka owa została zrealizowana w oparciu obibliotekę Javassist[5], która dostarcza narzędzi pozwalających na strukturalne modyfikacjeobiektów klas18.

Informacje zawarte w poprzednich podrozdziałach, a także w dokumentacji samej biblio-teki, powinny w zupełności wystarczyć potencjalnemu użytkownikowi do sprawnego się niąposługiwania. Jednakże, tak jak i w przypadku propozycji PPK, warto jest przyjrzeć się niecowewnętrznym mechanizmom funkcjonowania implementacji. Pozwoli to na lepsze zrozumienieistoty biblioteki oraz wyjaśni powód dla niektórych ograniczeń w jej stosowaniu.

Kluczowym mechanizmem w funkcjonowaniu biblioteki jest obsługa obiektów ról, w szcze-gólności ich niejawnego wiązania z odpowiednimi obiektami aktorów. Podczas tworzeniaobiektu roli za pomocą metody playRole obiektu aktora, tworzona i zwracana jest w rze-czywistości instancja bezpośredniej podklasy klasy przekazanej jako argument. Podklasę tąmożna nazwać klasą agenta. Klasa agenta nadpisuje wszystkie metody publiczne klasy bazo-wej, tj. klasy roli. Jest to bezpośrednim powodem dla którego implementacja zabrania odgry-wania ról za pomocą klas finalnych lub zawierających metody finalne. Klasa agenta zawieratakże pewne dodatkowe pola prywatne związane z mechanizmem synchronizacji. Nazwa klasyagenta jest tworzona poprzez konkatenację nazwy klasy bazowej oraz napisu $$ RoleAgent .Pakiet klasy agenta jest, ze zrozumiałych powodów, taki sam jak pakiet klasy bazowej.

W związku z wyżej przedstawionym mechanizmem, autor, jak już zostało to wspomniane wsekcji 3.2.7, sugeruje w miarę możliwości w ogóle unikać korzystania z mechanizmów refleksji,a w razie ich stosowania zachować szczególną ostrożność. Nie zmienia to faktu, że nawetpodczas normalnego użytkowania klas, można spotkać się z pewnymi aspektami wewnętrznejimplementacji biblioteki ról - np. podczas procesu debugowania aplikacji.

W świetle powyższej dyskusji, wyjście jakie otrzyma się po wykonaniu programu z listingu3.11, tj.:

class Role$$__RoleAgent__class Role

nie powinno być już dla użytkownika zaskakujące.

1 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;23 public class Role {4 public stat ic void main ( St r ing [ ] a rgs ) {5 Class<?> agentClass =6 ActorFactory . c reateActor ( ) . p layRole ( Role . class ) . g e tC la s s ( ) ;

18Innymi słowy, biblioteka Javassist stanowi w pewnym sensie dopełnienie mechanizmu refleksji, który samw sobie służy jedynie badaniu struktury klas.

76

Page 79: Praca_magisterska

78 System . out . p r i n t l n ( agentClass ) ;9 System . out . p r i n t l n ( agentClass . g e t S u p e r c l a s s ( ) ) ;

10 }11 }

Listing 3.11: Dynamiczne role a mechanizm refleksji.

W punkcie tym należy przypomnieć, że w Javie istnieją dwa sposoby na uzyskanie obiektumetaklasy. Pierwszym z nich jest posłużenie się specjalnym operatorem .class, który zwracaobiekt metaklasy dla konkretnej klasy (np. Role.class). Drugim sposobem jest użycie za-deklarowanej w klasie java.lang.Object publicznej metody Class<?> getClass(), którazwraca aktualny typ danego obiektu. W związku z tym, w wypadku obiektu reprezentują-cego pewną rolę, metoda getClass() zwróci obiekt metaklasy dynamicznie utworzonej klasyagenta.

Autor zdaje sobie sprawę, że optymalnym rozwiązaniem byłaby tutaj sytuacja w któ-rej metoda getClass() zwracałaby obiekt metaklasy klasy użytej przy wywołaniu metodyplayRole() obiektu aktora. Niestety metoda Class<?> getClass() jest obsługiwana w spo-sób szczególny przez maszynę wirtualną Javy przez co nie ma możliwości jej nadpisania.

3.5 Uzasadnienie i krytyka propozycji

Autor zdaje sobie sprawę, że wybrany sposób realizacji dynamicznych ról dla języka Javamożna uznać za dyskusyjny. Jest to w dużej mierze wynikiem eksperymentalnego charakterutej części pracy. O ile w przypadku propozycji PPK zagadnienie było jasno i klarowniesformułowane, o tyle dynamiczne role stanowią pewną próbę przezwyciężenia problemu zzakresu modelowania bytów o dynamicznym charakterze. Poniżej znajduje się uzasadnienieautora dla poszczególnych elementów propozycji dynamicznych ról:

Osadzenie mechanizmu: Najważniejszą z decyzji dotyczących sposobu implementacji ról,było dostarczenie ich w postaci biblioteki. Autor rozważał tu początkowo wsparcieze strony składni, jednakże pomysł ten został prędko odrzucony. W odróżnieniu odpropozycji PPK, koszt wprowadzenia nowych konstrukcji składniowych dla dynamicz-nych ról przekroczyłby zdecydowanie potencjalne korzyści jakie możnaby w ten sposóbosiągnąć. W przypadku PPK specjalna składnia zapewniała konieczną separację po-między właściwą logiką programu a definicjami warunków kontraktów. W przypadkudynamicznych ról konieczności takiej separacji już nie było.

Definiowanie ról: Zgodnie z ostatnimi tendencjami panującymi w środowisku zrzeszonymwokół platformy Java, komponenty definiujące role mogą być zwyczajnymi klasami, tj.tzw. POJO (ang. Plain Old Java Object). Jest to podejście o tyle wygodne, iż pozwalaono programiście na tworzenie komponentów ról w postaci zwykłych obiektów, któremogą w standardowy sposób współpracować z innymi elementami systemu. Naczelnązasadą, którą kierował się w tej kwestii autor, była stara reguła KISS19, w myśl któ-rej powinno preferować się rozwiązania o jak największej prostocie. Jako alternatywa,

19Akronim ten pochodzi z angielskiego i może być rozwijany na różne sposoby, np. jako “Keep It Short andSimple”.

77

Page 80: Praca_magisterska

rozważane było tutaj a) wprowadzenie zupełnie nowej składni na potrzeby definicji ról,b) wymóg dziedziczenia z określonej klasy oraz c) wymóg implementacji pewnego inter-fejsu. Każda z alternatyw wiązała się z pewnymi dodatkowymi restrykcjami, którychnarzucania autor wolał uniknąć.

Obiekty aktorów: W związku z przyjętym sposobem definiowana ról, wprowadzenie spe-cjalnych obiektów aktorów pełniących funkcję integrującą, było tutaj koniecznością.Komponent aktora udostępniany jest w formie zwyczajnej klasy z powodów analogicz-nych jak w poprzednim punkcie. Rozważaną alternatywą było tutaj wprowadzenie no-wego typu podstawowego do języka Java — actor. Autor odrzucił takie podejście, gdyżw gruncie rzeczy, poza swoistą efektownością użycia, nie prowadziło ono do uzyskaniajakichkolwiek korzyści.

Współdziałanie ról: Mechanizmem zapewniającym integrację pomiędzy rolami jest syn-chronizacja wartości poszczególnych pól obiektów odgrywających role. Jest to podej-ście możliwie najprostsze i w związku z tym zapewniające największą elastyczność.Jakikolwiek inny sposób współdziałania ról, np. poprzez wymianę komunikatów, wią-załby się nieuchronnie z narzuceniem pewnych restrykcji związanych z definiowaniemklas przeznaczonych do odgrywania ról. Stałoby to w sprzeczności z przyjętą wcześniejmożliwością utylizacji jak najszerszej liczby klas.

Warto zauważyć, że przyjęty model funkcjonowania ról przypomina nieco rozwiązaniezastosowane w niektórych implementacjach standardu JPA20. W jednym i drugim przy-padku mamy do czynienia z pewnymi obiektami typu POJO, których wartości są wpewien ukryty sposób synchronizowane. Komponenty encyjne z JPA synchronizowanesą mianowicie z odpowiednimi rekordami w bazie danych, obiekty ról z propozycji au-tora z innymi obiektami ról w obrębie tego samego aktora. Porównanie to pozorniewypada na niekorzyść propozycji autora, w której zabronione jest bezpośrednie od-woływanie się do pól obiektu roli. Zakaz taki nie obowiązuje w JPA, gdzie działanietakie nie przeszkadza w żaden sposób mechanizmowi synchronizacji. Wynika to stąd,iż biblioteka dynamicznych ról nie ma możliwości przechwycenia “zdarzenia” polegają-cego na modyfikacji pola obiektu. Możliwość taką posiada natomiast usługa realizującaJPA. Wiąże się to, z grubsza rzecz biorąc, z tym, że dostarczający usługę JPA kontenerEJB kontroluje jednocześnie obiekt ładowacza klas21 używanego m.in. do wczytywaniaklas klas pewnych obiektów korzystających korzystających z danych komponentów en-cyjnych. Biblioteka dynamicznych ról, w odróżnieniu od kontenera EJB, nie sprawujepieczy na ładowaczami klas używanymi do wczytywania klas korzystających z klas ról.Próba uzyskania takiej kontroli wiązałaby się z szeregiem różnego rodzaju niegodności,tak jak ma to miejsce w przypadku kontenerów EJB.

Odgrywanie ról: Odgrywanie ról poprzez użytkowanie zwyczajnego obiektu jest, w połą-czeniu z równie prostym sposobem ich definicji, niewątpliwie największą zaletą propo-zycji autora. Trudno sobie zresztą wyobrazić rozwiązanie wygodniejsze i bardziej, z tejperspektywy, elastyczne.

20ang. Java Persistence API. Wprowadzenie praktyczne do standardu JPA można znaleźć w [15]. Wniniejszym wywodzie przez JPA rozumie się JPA zastosowane w obrębie kontenera EJB (ang. Enterprise JavaBeans).21ang. Class Loader. Omówienie mechanizmu ładowania klas przez maszynę wirtualną Javy można znaleźć

w artykule [13]

78

Page 81: Praca_magisterska

3.5.1 Możliwości dalszego rozwoju

Propozycja dynamicznych ról dla języka Java nie jest zdecydowanie propozycją idealną. Zracji eksperymentalnego charakteru, autor przy jej formułowaniu kierował się nie tylko prze-słankami czysto racjonalnymi ale i też, trzeba to powiedzieć, dokonywał wyborów o arbitral-nym charakterze. Z tego powodu jest bardzo możliwe, że przy pewnym wysiłku, dałoby sięzaproponować odmienną implementację dynamicznych ról dla języka Java o właściwościachlepszych od propozycji autora. Obecną propozycję, dałoby się także nieznacznie usprawnić.Poniższa lista zawiera parę dróg dalszego jej rozwoju.

• Możliwość odgrywania tzw. “słabych” ról. Umiejętność odgrywania roli tego typu mo-głaby być przez aktora tracona w sytuacji gdy poza jego obrębem nie funkcjonowałabyżadna silna referencja (ang. strong reference) na obiekt owej roli. Oznacza to, że rolebezużyteczne mogłoby być automatycznie usuwane. Działanie takie, analogiczne dooferowanego przez specjalny rodzaj mapy — java.util.WeakHashMap, pomagałoby za-pobiegać ewentualnym wyciekom zasobów. Bliższe informacje na temat różnych klasreferencji oraz zarządzania pamięcią w języku Java można znaleźć w [3, Rozdział 17].

• Zmiana semantyki operatora rzutowania użytego w przypadku dynamicznych ról, takaby możliwe było przepisanie przykładu z listingu 3.6 (strona 62) w postaci zaprezen-towanej na listingu 3.12. Język Java nie oferuje co prawda bezpośredniej możliwościprzeciążania operatorów, jednak żądane zachowanie można z powodzeniem osiągnąć zapomocą procesora adnotacji22, którego działanie polegało by na zwyczajnym zastępowa-niu operacji rzutowania wywołaniami metody playRole. Minusem takiego rozwiązaniabyłaby tu właśnie konieczność stosowania procesora adnotacji, przy czym należy zauwa-żyć, że z perspektywy użytkownika, byłoby to wciąż dużo prostsze i mniej uciążliwe odwywołań translatora w propozycji dla PPK.

• Możliwość uzyskiwania od danego aktora zbioru klas aktualnie odgrywanych przez niegoról. Mogłoby się to odbywać np. za pomocą metody java.util.Collection<Class<?>>getRoles(), lub poprzez implementację interfejsu Iterable<Class<?>>.

• Udostępnienie dodatkowych mechanizmów współpracy z instancjami obiektów odgrywa-jących role, w tym np. możliwości stwierdzania, czy dany obiekt jest w rzeczywistościobiektem roli oraz pobierania obiektu aktora, na rzecz którego może on potencjalniedziałać. Funkcjonalność tego rodzaju mogłaby być dostępna za pomocą dodatkowejklasy narzędziowej lub bezpośrednio za pomocą obiektów ról. W drugim przypadkuobiekty ról implementowałyby po prostu pewien interfejs (np. Role— poprzez analogiędo interfejsu Actor), dzięki czemu sprawdzenie czy pewien obiekt jest w rzeczywistościagentem pewnego aktora mogłoby się odbywać za pomocą operatora instanceof.

• Zmniejszenie czasu synchronizacji poprzez oparcie go na bezpośrednim kopiowaniu war-tości odpowiednich pól. W dostarczonej przez autora bibliotece synchronizacja odbywasię za pomocą powolnego mechanizmu refleksji. Nie jest to co prawda uwaga związanabezpośrednio z funkcjonalnością propozycji, niemniej autor dostrzega bardzo wyraźnąpotrzebę zastosowania się doń.

22ang. annotation processor. Zastosowanie procesorów adnotacji, wbrew ich nazwie, może wykraczać pozazadania związane przetwarzaniem adnotacji.

79

Page 82: Praca_magisterska

1 import pl . edu .pw . a k o s i c k i . r o l e s . Actor ;2 import pl . edu .pw . a k o s i c k i . r o l e s . ActorFactory ;34 public class RoleExample1 {5 stat ic class Ship {6 private int p o s i t i o n = 0 ;78 public int g e t P o s i t i o n ( ){9 return p o s i t i o n ;

10 }1112 public void f l e e t ( int d i s t anc e ){13 this . p o s i t i o n = d i s t anc e ;14 }15 }1617 stat ic class Car {18 private int p o s i t i o n = 0 ;1920 public int g e t P o s i t i o n ( ){21 return p o s i t i o n ;22 }2324 public void move( int d i s t anc e ){25 this . p o s i t i o n += d i s t anc e ;26 }27 }2829 public stat ic void main ( St r ing [ ] a rgs ) {30 Actor amphibian = ActorFactory . c reateActor ( ) ;3132 Ship sh ip = ( Ship ) amphibian ;33 sh ip . f l e e t ( 10 ) ;3435 Car car = ( Car ) amphibian ;36 car . move( −15 ) ;3738 assert sh ip . g e t P o s i t i o n ( ) == −5;39 }40 }

Listing 3.12: Propozycja przeciążenia operatora rzutowania. Mimo iż program ten nie różnisię, z funkcjonalnego punktu widzenia, niczym od programu z listingu 3.6 (strona 62), jegoskładnia znacznie bardziej “przemawia do wyobraźni”.

80

Page 83: Praca_magisterska

Dodatek A

Mechanizm PPK - przykład użycia

W rozdziałach 2 oraz 3 zamieszczonych zostało wiele przykładów użycia dostarczonych przezautora implementacji PPK oraz dynamicznych ról dla języka Java. Na ogół przykłady te miałyna celu jedynie prezentację składni i semantyki omawianych w danym kontekście elementówJavaBC. Nie zawierały one natomiast wykonujących konkretne i praktyczne zadania progra-mów. Co prawda autor wspominał tu i ówdzie o praktycznych zastosowaniach zaproponowa-nych przez siebie implementacji, jednakże nigdzie nie zamieścił nieco bardziej konkretnego iwyczerpującego przykładu użycia. Przykładu, który stanowiłby uzasadnienie praktycznościzaproponowanych przez niego koncepcji i towarzyszących im narzędzi programistycznych.

Brak ten zrekompensowany jest w postaci niniejszego dodatku, który zawiera praktycznyprzykład użycia mechanizmu PPK, oraz dodatku B zawierającego podobny przykład dladynamicznych ról.

Jako przykład użycia zaproponowanych mechanizmów PPK, autor postanowił przyto-czyć jedną z klas zdefiniowanych na potrzeby dostarczonego przez siebie translatora dla PPK— pl.edu.pw.akosicki.parser.utils.InvariantsFactory. Jest to klasa fabryki, służącado tworzenia obiektów niezmienników. Użycie fabryki ma miejsce bezpośrednio w trakcierozbioru gramatycznego, podczas napotkania odpowiednich konstrukcji składniowych. Przy-kładowo, podczas wykrycia w kodzie źródłowym bloku niezmiennika następuje wywołaniemetody beginInvariant, a w chwili natrafienia na komunikat pewnej asercji niezmiennika— addAssertionMessage. Interfejs klasy przedstawia się w sposób następujący:

beginInvariant(...) — metoda wywoływana podczas natrafienia na początek sekcji nie-zmiennika.

beginAssertion(...) — metoda wywoływana podczas natrafienia na warunek niezmien-nika.

addAssertionState(...) — metoda wywoływana podczas natrafienia na listę stanów wa-runku niezmiennika.

81

Page 84: Praca_magisterska

addAssertionAllStates(...) — metoda wywoływana podczas natrafienia na listę stanówwarunku niezmiennika określoną za pomocą znaku “*”.

addAssertionCondition(...) — metoda wywoływana podczas natrafienia na główny ele-ment warunku niezmiennika.

addAssertionMessage(...) — metoda wywoływana podczas natrafienia na komunikat wa-runku.

endAssertion(...) — metoda oznaczająca zakończenie przetwarzania aktualnego warunkuniezmiennika.

endInvariant(...) — metoda oznaczająca zakończenie przetwarzania aktualnego niezmien-nka.

addModifier(...) — metoda oznaczająca natrafienie na modyfikator.

clearModifiers(...) — metoda pomocnicza.

getInvariants(...) — właściwa metoda fabryki.

Interesujące w tym punkcie są oczekiwania klasy fabryki co do kolejności wywołań me-tod oraz związany z tym wewnętrzny mechanizm działania oparty o automat skończony.Klasa fabryki nie dopuszcza na przykład dwóch następujących po sobie wywołań metodyendAssertion lub też wywołania metody beginInvariant nie sparowanego odpowiednimwywołaniem endInvariant. Przykładowo dla kodu:

public class Class {private invariant {

∗ : true ;∗ : true | | fa l se : ”Some message” ;

}

82

Page 85: Praca_magisterska

}

parser wywoła następujące metody fabryki (w podanej kolejności):

addModifier

beginInvariant

beginAssertion

addAssertionAllStates

addAssertionCondition

endAssertion

beginAssertion

addAssertionAllStates

addAssertionCondition

addAssertionMessage

endAssertion

endInvariant

getInvariants

W trakcie prac na klasą, autor postanowił podkreślić jej szczególny mechanizm, posługującsię licznymi asercjami1 oraz definiując specjalną zagnieżdżoną klasę służącą za implementacjęautomatu skończonego. Automat skończony był w głównej mierze odpowiedzialny za kon-trolę poprawności kolejności wywołań metod fabryki, liczne asercje natomiast za kontrolęporawności pewnych wewnętrznych aspektów funkcjonowania klasy fabryki.

Klasa ta została, na potrzeby niniejszego przykładu, poddana refaktoryzacji, co nastąpiłopoprzez przetłumaczenie jej kodu do języka JavaBC. W efekcie spora część kodu pierwornegozostała zastąpiona definicją automatu skończonego oraz prywatnego niezmiennika. Dziękitemu kod programu zyskał na czytelności i zwięzłości — objętość klasy spadła z 359 linijekdo 235 linijek2, czyli o blisko 35%.

Oryginalną wersję klasy można znaleźć w kodzie źródłowym translatora. Szczegóły tech-niczne, w tym liczne pola fabryki oraz zagnieżdżona klasa InvariantFactory nie są, w gruncierzeczy, dla zrozumienia sedna przykładu istotne. Najistotniejsze fragmenty kodu znajdują sięmniej więcej w połowie listingu.

package pl . edu .pw . a k o s i c k i . pa r s e r . u t i l s ;

import java . u t i l . ArrayList ;import java . u t i l . HashMap ;import java . u t i l . HashSet ;

1Mimo że warunki kontraktu dotyczyły metod publicznych, kontrola poprawności została oparta na aser-cjach. Autor nie zdecydował się tu na to aby złamanie kontraktu prowadziło do odrzucenia wyjątkówjava.lang.IllegalStateException bądź java.lang.IllegalArgumentException, gdyż klasa fabryki, jakotaka, nie była i nie miała być częścią żadnego zewnętrznego interfejsu programistycznego.

2Wnikliwy czytelnik może spostrzec, że przedstawiony na listingu A.1 program liczy sobie nieco więcej niż235 linijek. Pojawiająca się niezgodność wynika po prostu z konieczności rozbijania zbyt długich linijek koduna kilka krótszych wierszy.

83

Page 86: Praca_magisterska

import java . u t i l . L i s t ;import java . u t i l .Map;import java . u t i l . Set ;

import pl . edu .pw . a k o s i c k i . pa r s e r . u t i l s . Automaton . AutomatonState ;import pl . edu .pw . a k o s i c k i . pa r s e r . u t i l s . I nva r i an t . I n v a r i a n t A s s e r t i o n ;import pl . edu .pw . a k o s i c k i . pa r s e r . u t i l s . SemanticError . Type ;import pl . edu .pw . a k o s i c k i . u t i l s . Pair ;

public class Invar i ant sFacto ry {

private Map<Pair<ClassName , EAccessModif ier >, Invar iantFactory> f a c t o r i e s= new HashMap<Pair<ClassName , EAccessModif ier >, Invar iantFactory >() ;

private List<SemanticError> e r r o r s = new ArrayList<SemanticError >() ;

private Invar iantFactory currentFactory = null ;

private f ina l List<Pair<Str ing , CodePosit ion>> c u r r e n t A s s e r t i o n S t a t e s= new ArrayList<Pair<Str ing , CodePosit ion >>();

private f ina l List<Pair<EAccessModif ier , CodePosit ion>> c u r r e n t M o d i f i e r s= new ArrayList<Pair<EAccessModif ier , CodePosit ion >>();

private Pair<Str ing , CodePosit ion> c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l = null ;

private Pair<Str ing , CodePosit ion> currentAsser t ionMessage = null ;

private boolean s k i p I n v a r i a n t D e c l a r a t i o n = fa l se ;

private class Invar iantFactory {private ClassName className = null ;

private EAccessModi f ier mod i f i e r = null ;

private Automaton automaton = null ;

private CodePosit ion p o s i t i o n = null ;

private List<Invar i an tAsse r t i on> a s s e r t i o n s= new ArrayList<Invar i an tAsse r t i on >() ;

public void i n i t ( CodePosit ion pos i t i on , ClassName className ,EAccessModi f ier modi f i e r , Automaton automaton ) {

this . p o s i t i o n = p o s i t i o n ;this . className = className ;this . mod i f i e r = mod i f i e r ;this . automaton = automaton ;

}

public void addAssert ion ( Pair<Str ing , CodePosit ion> e x p r e s s i o n L i t e r a l ,Pair<Str ing , CodePosit ion> message , Pair<Str ing ,CodePosit ion > . . . s t a t e s ) {

84

Page 87: Praca_magisterska

// a s s e r t s t a t e s . l e n g t h > 0 ;

I n v a r i a n t A s s e r t i o n a s s e r t i o n = new I n v a r i a n t A s s e r t i o n ( ) ;a s s e r t i o n . e x p r e s s i o n L i t e r a l = e x p r e s s i o n L i t e r a l . g e t F i r s t ( ) ;a s s e r t i o n . e x p r e s s i o n L i t e r a l C o d e P o s i t i o n

= e x p r e s s i o n L i t e r a l . getSecond ( ) ;

a s s e r t i o n . message = message != null ? message . g e t F i r s t ( ) : null ;a s s e r t i o n . messageCodePosit ion = message != null

? message . getSecond ( ) : null ;

Set<Str ing> usedStates = new HashSet<Str ing >() ;for ( Pair<Str ing , CodePosit ion> s t a t e : s t a t e s ) {

assert ! s t a t e . g e t F i r s t ( ) . equa l s ( ”∗” ) ;

i f ( usedStates . conta in s ( s t a t e . g e t F i r s t ( ) ) ) {e r r o r s . add ( new SemanticError (

Type . INV STATE REPEATED, s t a t e . getSecond ( ) ) ) ;continue ;

}usedStates . add ( s t a t e . g e t F i r s t ( ) ) ;AutomatonState automatonState

= automaton . getStateByName ( s t a t e . g e t F i r s t ( ) ) ;i f ( automatonState == null ) {

e r r o r s . add ( new SemanticError (Type .INV UNKNOWN STATE USED, s t a t e . getSecond ( ) ) ) ;

continue ;}a s s e r t i o n . s t a t e s . add ( automatonState ) ;

}a s s e r t i o n s . add ( a s s e r t i o n ) ;

}

public Inva r i an t g e t I n v a r i a n t ( ) {Inva r i an t i n v a r i a n t = new Inva r i an t ( ) ;

for ( I n v a r i a n t A s s e r t i o n a s s e r t i o n : a s s e r t i o n s ){// I n v a r i a n t A s s e r t i o n i s s t a t i c c l a s s so ’ parent ’// has to be a s s i g n e d manuallya s s e r t i o n . i n v a r i a n t = i n v a r i a n t ;

}

i n v a r i a n t . a s s e r t i o n s = a s s e r t i o n s ;i n v a r i a n t . automaton = automaton ;i n v a r i a n t . className = className ;i n v a r i a n t . mod i f i e r = mod i f i e r ;i n v a r i a n t . p o s i t i o n = p o s i t i o n ;

return i n v a r i a n t ;}

}

automaton {

85

Page 88: Praca_magisterska

accepting initial OUTSIDE : INVARIANT;INVARIANT : ASSERT, OUTSIDE;ASSERT : ASSERT ST, ASSERT AST;ASSERT ST : ASSERT EXP, ASSERT ST;ASSERT AST : ASSERT EXP;ASSERT EXP : ASSERT MSG, INVARIANT;ASSERT MSG : INVARIANT;

}

public invariant {OUTSIDE : currentFactory == null && ! s k i p I n v a r i a n t D e c l a r a t i o n ;ASSERT MSG : currentAsser t ionMessage != null ;ASSERT EXP, ASSERT MSG : c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l != null ;OUTSIDE, INVARIANT, ASSERT : c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l == null ;OUTSIDE, INVARIANT, ASSERT : currentAsser t ionMessage == null ;OUTSIDE, INVARIANT, ASSERT : c u r r e n t A s s e r t i o n S t a t e s != null

&& c u r r e n t A s s e r t i o n S t a t e s . s i z e ( ) == 0 ;INVARIANT, ASSERT, ASSERT ST, ASSERT AST, ASSERT EXP, ASSERT MSG

: s k i p I n v a r i a n t D e c l a r a t i o n | | currentFactory != null ;∗ : c u r r e n t M o d i f i e r s != null ;

}

public void addModif ier ( S t r ing m o d i f i e r L i t e r a l , CodePosit ion p o s i t i o n ){i f ( ! transient ? OUTSIDE )

return ;

EAccessModi f ier mod i f i e r= EAccessModi f ier . g e tMod i f i e r ( m o d i f i e r L i t e r a l ) ;

c u r r e n t M o d i f i e r s . add (new Pair<EAccessModif ier , CodePosit ion>(modi f i e r , p o s i t i o n ) ) ;

}

public void c l e a r M o d i f i e r s ( ){i f ( ! transient ? OUTSIDE )

return ;c u r r e n t M o d i f i e r s . c l e a r ( ) ;

}

public void beg in Invar i an t ( CodePosit ion pos i t i on ,ClassName className , Automaton automaton ) {

transient : INVARIANT;

EAccessModi f ier mod i f i e r = null ;for ( Pair<EAccessModif ier , CodePosit ion> pa i r : c u r r e n t M o d i f i e r s ){

i f ( mod i f i e r != null ){e r r o r s . add ( new SemanticError (

Type . INV MODIFIER REPEATED, pa i r . getSecond ( ) ) ) ;continue ;

}i f ( pa i r . g e t F i r s t ( ) == EAccessModi f ier .UNKNOWN ){

e r r o r s . add ( new SemanticError (Type . INV INVAILD MODIFIER , pa i r . getSecond ( ) ) ) ;

continue ;

86

Page 89: Praca_magisterska

}assert mod i f i e r == null ;mod i f i e r = pa i r . g e t F i r s t ( ) ;

}i f ( mod i f i e r == null ){

mod i f i e r = EAccessModi f ier .PACKAGE; // d e f a u l t m o d i f i e r}

Pair<ClassName , EAccessModif ier> invar iantKey= new Pair<ClassName , EAccessModif ier>(className , mod i f i e r ) ;

i f ( f a c t o r i e s . containsKey ( invar iantKey ) ) {e r r o r s . add ( new SemanticError ( Type . INV REDEFINED, p o s i t i o n ) ) ;s k i p I n v a r i a n t D e c l a r a t i o n = true ;return ;

}

Invar iantFactory f a c t o r y = new Invar iantFactory ( ) ;f a c t o r y . i n i t ( po s i t i on , className , modi f i e r , automaton ) ;f a c t o r i e s . put ( invariantKey , f a c t o r y ) ;currentFactory = f a c t o r y ;

}

public void beg inAsse r t i on ( ) {i f ( s k i p I n v a r i a n t D e c l a r a t i o n )

return ;transient : ASSERT;

}

public void addAsser t ionState ( S t r ing s tate , CodePosit ion p o s i t i o n ) {i f ( s k i p I n v a r i a n t D e c l a r a t i o n )

return ;transient : ASSERT ST;

c u r r e n t A s s e r t i o n S t a t e s . add (new Pair<Str ing , CodePosit ion>( s ta te , p o s i t i o n ) ) ;

}

public void addAsse r t i onAl lS ta t e s ( CodePosit ion p o s i t i o n ) {i f ( s k i p I n v a r i a n t D e c l a r a t i o n )

return ;transient : ASSERT AST;

}

public void addAssert ionCondit ion ( S t r ing e x p r e s s i o n L i t e r a l ,CodePosit ion p o s i t i o n ) {

i f ( s k i p I n v a r i a n t D e c l a r a t i o n )return ;

transient : ASSERT EXP;

c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l = new Pair<Str ing , CodePosit ion>(e x p r e s s i o n L i t e r a l , p o s i t i o n ) ;

}

87

Page 90: Praca_magisterska

public void addAssert ionMessage (S t r ing message , CodePosit ion p o s i t i o n ) {

i f ( s k i p I n v a r i a n t D e c l a r a t i o n )return ;

transient : ASSERT MSG;

currentAsser t ionMessage = new Pair<Str ing , CodePosit ion>(message , p o s i t i o n ) ;

}

@SuppressWarnings ( ”unchecked” )public void endAssert ion ( ) {

i f ( s k i p I n v a r i a n t D e c l a r a t i o n )return ;

transient : INVARIANT;

// a s s e r t c u r r e n t A s s e r t i o n S t a t e s . s i z e ( ) > 0 ;currentFactory . addAssert ion (

c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l , currentAssert ionMessage ,c u r r e n t A s s e r t i o n S t a t e s . toArray ( new Pair [ ] {} ) ) ;

currentAsser t ionMessage = null ;c u r r e n t A s s e r t i o n E x p r e s s i o n L i t e r a l = null ;c u r r e n t A s s e r t i o n S t a t e s . c l e a r ( ) ;

}

public void endInvar iant ( ) {transient : OUTSIDE;

s k i p I n v a r i a n t D e c l a r a t i o n = fa l se ;currentFactory = null ;

}

public Pair<List<Invar iant >, L i s t<SemanticError>> g e t I n v a r i a n t s ( ) {List<Invar iant> i n v a r i a n t s = new ArrayList<Invar iant >() ;

for ( Invar iantFactory f a c t o r y : f a c t o r i e s . va lue s ( ) ) {i n v a r i a n t s . add ( f a c t o r y . g e t I n v a r i a n t ( ) ) ;

}

return new Pair<List<Invar iant >, L i s t<SemanticError>>(i nva r i an t s , e r r o r s ) ;

}}

Listing A.1: Efekt refaktoryzacji pochodzącej z translatora dla PPK klasyInvariantsFactory.

88

Page 91: Praca_magisterska

Dodatek B

Dynamiczne role - przykład użycia

Niech dana będzie potrzeba stworzenia systemu działającego w modelu usługodawca — usłu-gobiorca. Ogólne wymagania dotyczące systemu można przedstawić w następujących punk-tach:

• Zadaniem systemu ma być świadczenie usług na rzecz klientów systemu.

• System ma umożliwiać danemu klientowi korzystanie z dowolnych oferowanych przezsiebie usług.

• Dana usługa może wymagać klienta konkretnego, wyspecjalizowanego rodzaju.

• Nowe usługi mogą być dodawane do systemu bez przerywania jego działania.

Przykład niniejszy zawiera szkielet systemu zgodnego z powyższymi wymaganiami. Mimoże jest on dosyć obszerny objętościowo, należy sobie zdawać sprawę, iż w dalszym ciągu sta-nowi on jedynie szkic pewnego konkretnego rozwiązania1. Niestety kontekst zastosowaniadynamicznych ról, w odróżnieniu od mechanizmów PPK, wykracza znacznie poza obręb po-jedynczej klasy, co nie pozostaje bez szwanku dla zwartości poniższego przykładu.

System składa się z części właściwej, pełniącej rolę serwera, oraz dodatkowego programukonsoli pozwalającego na łączenie się zeń i wydawanie określonych poleceń. Dwa najistot-niejsze z dostępnych poleceń pozwalają na tworzenie klientów oraz świadczenie na ich rzeczodpowiednich usług. Klienci systemu są reprezentowani przez aktorów, odgrywających roleusługobiorców zgodnych z konkretnymi usługami. Jeśli w chwili skorzystania z danej usługiklient nie posiada roli właściwego usługobiorcy, rola ta zostanie mu w dynamiczny sposóbprzypisana.

W celach demonstracyjnych autor przygotował dodatkowo parę klas usług i związanych znimi klas usługobiorców (należy tu zauważyć, iż klasy te mają dosyć naiwną implementację— funkcjonalność usług nie jest głównym przedmiotem niniejszego przykładu). Podstawowąusługą oferowaną klientom systemu jest prowadzenie rachunku bankowego. Z usługi tej musiskorzystać każdy klient. Oprócz posiadania konta, klienci dostają możliwość wykupienia ubez-pieczenia turystycznego oraz zaciągnięcia kredytu za pomocą karty kredytowej. Tablica B.1zawiera podsumowanie dotyczące plików zawartych w niniejszym przykładzie użycia. Powią-zania klasy usługobiorców i klas usługodawców zostały dodatkowo przedstawione na diagramiez rysunku B.1.

1Przedstawiony przykład w ogóle nie uwzględnia wielu aspektów technicznych i funkcjonalnych systemu,takich jak np. obsługa błędów, zarządzanie zasobami, zarządzenia pulą usługodawców, tworzenie obiektów rólza pomocą konstruktorów innych niż bezparametrowe czy przekazywanie parametrów wywołania usługi.

89

Page 92: Praca_magisterska

roles examples

clients — pakiet zawierający klasy usługobiorców. Zawartości odpowiednich pli-ków przedstawione zostały na stronach 98–99.

AccountHolder.java — przykładowa klasa usługobiorcy (posiadacz rachunkubankowego).CreditCardHolder.java — przykładowa klasa usługobiorcy (posiadacz kartykredytowej). Klasa dziedziczy po AccountHolder.java.TravelInsuranceClient.java — przykładowa klasa usługobiorcy (ubezpie-czony). Klasa dziedziczy po AccountHolder.java.

services — pakiet zawierający klasy usług. Zawartości odpowiednich plików przed-stawione zostały na stronach 100–102.

ActivateCreditCard.java — przykładowa klasa usługi (aktywacja karty kre-dytowej).CheckAccountBalance.java — przykładowa klasa usługi (sprawdzenie stanurachunku bankowego).CheckCreditCardBalance.java — przykładowa klasa usługi (sprawdzeniestanu karty kredytowej).CompenseTravelAccident.java — przykładowa klasa usługi (wypłacenie re-kompensaty w ramach ubezpieczenia podróżnego).GetCredit.java — przykładowa klasa usługi (zaciągnięcie kredytu).InsureTravel.java — przykładowa klasa usługi (wykupienie ubezpieczeniapodróżnego).RepayCredit.java — przykładowa klasa usługi (spłata zaciągniętego kre-dytu).

Console.java — klasa konsoli, za pomocą której można zarządzać systemem. Za-wartość pliku przedstawiona została na listingu B.1 (strona 93).

IServiceProvider.java — generyczny interfejs usługi. Interfejs ten zawiera po-jedynczą metodę service, której argumentem jest obiekt klasy pewnego klienta.Zawartość pliku przedstawiona została na listingu B.2 (strona 93).

Server.java — główna klasa systemu. Zawartość pliku przedstawiona została nalistingu B.3 (strona 93).

ServicesList.java — klasa pomocnicza, której celem jest dostarczanie aktualnejlisty dostępnych usług. Zawartość pliku przedstawiona została na listingu B.4(strona 96).

services.txt — plik z listą usług. Plik ten powinien początkowo zawierać listę klasumieszczonych w pakiecie roles examples.services — tak jak przedstawiono to nalistingu B.15 (strona 102). Jeśli w trakcie działania systemu administrator zdecydujesię dodać nową usługę, plik powinien zostać wzbogacony o odpowiedni wpis.

Tablica B.1: Pliki wchodzące w skład przykładu użycia dynamicznych ról.

90

Page 93: Praca_magisterska

Rysunek B.1: Diagram klas dla usługobiorców i usługodawców z przykładu użycia dynamicz-nych ról. Stereotyp “role” podkreśla, że dana klasa jest używana w formie roli. Na diagramiezostały przedstawione tylko niezbędne informacje.

Poniżej przytoczony został fragment pewnej sesji programu konsoli:

$ list clients<no clients defined>$ create client AleksanderClient Aleksander created.$ list services* ActivateCreditCard* CheckAccountBalance* CheckCreditCardBalance* CompenseTravelAccident* GetCredit* InsureTravel* RepayCredit$ list clients* Aleksander

91

Page 94: Praca_magisterska

- AccountHolder$ service client Aleksander CheckAccountBalanceService called with result: "Aleksander’s balance is: 0."$ list clients* Aleksander- AccountHolder

$ service client Aleksander InsureTravelService called with result: "Aleksander has been insured"$ list clients* Aleksander- AccountHolder- TravelInsuranceClient

$ service client Aleksander CompenseTravelAccidentService called with result: "Compensation for Aleksander has been payed"$ service client Aleksander CheckAccountBalanceService called with result: "Aleksander’s balance is: 9900."

Podczas sesji tej dodano nowego klienta, któremu następnie wyświadczone zostały pewneusługi. Klient ten początkowo posiadał jedynie rolę AccountHolder, jednakże skorzystanie zusługi ubezpieczenia przypisało mu automatycznie dodatkową rolę TravelInsurenceClient.

W dalszej części dodatku znajdują się listingi plików wchodzących w skład przykładuużycia.

92

Page 95: Praca_magisterska

package ro l e s example ;

import java . i o . ∗ ;import java . net . ∗ ;

public class Console {public stat ic void main ( St r ing [ ] a rgs ) throws Exception {

Socket socke t = new Socket (InetAddress . getLocalHost ( ) , Server .PORT ) ;

BufferedReader sReader = new BufferedReader (new InputStreamReader ( socket . getInputStream ( ) ) ) ;

Pr intWriter sWriter = new PrintWriter ( new OutputStreamWriter (socke t . getOutputStream ( ) ) , true ) ;

BufferedReader in = new BufferedReader (new InputStreamReader ( System . in ) ) ;

System . out . p r i n t ( ”$ ” ) ;for ( S t r ing l i n e = in . readLine ( ) ; ; l i n e = in . readLine ( ) ) {

sWriter . p r i n t l n ( l i n e ) ;i f ( ” e x i t ” . equa l s ( l i n e ) | | ”shutdown” . equa l s ( l i n e ) )

break ;for ( S t r ing sLine = sReader . readLine ( ) ; sL ine . l ength ( ) != 0 ;

sLine = sReader . readLine ( ) )System . out . p r i n t l n ( sLine ) ;

System . out . p r i n t ( ”$ ” ) ;}

}}

Listing B.1: Zawartości pliku Console.java.

package ro l e s example ;

public interface ISe rv i c eProv ide r<T> {public Object s e r v i c e ( T c l i e n t ) ;

}

Listing B.2: Zawartości pliku IServiceProvider.java.

package ro l e s example ;

import java . i o . ∗ ;import java . net . ∗ ;import java . u t i l . ∗ ;import pl . edu .pw . a k o s i c k i . r o l e s . ∗ ;import ro l e s example . c l i e n t s . AccountHolder ;

import stat ic ro l e s example . S e r v i c e s L i s t . ∗ ;

public class Server extends Thread {

93

Page 96: Praca_magisterska

public stat ic f ina l int PORT = 25555 ;

private stat ic List<Actor> a c t o r s= C o l l e c t i o n s . synchron i z edL i s t ( new LinkedList<Actor>() ) ;

private f ina l BufferedReader reader ;private f ina l PrintWriter w r i t e r ;

Server ( Socket socke t ) throws Exception {reader = new BufferedReader (

new InputStreamReader ( socket . getInputStream ( ) ) ) ;w r i t e r = new PrintWriter (

new OutputStreamWriter ( socket . getOutputStream ( ) ) , true ) ;}

@Override public void run ( ) {try {

for ( S t r ing l i n e = reader . readLine ( ) ; l i n e != null ;l i n e = reader . readLine ( ) ) {

i f ( ”shutdown” . equa l s ( l i n e ) ) {w r i t e r . p r i n t l n ( ” Server terminated . ” ) ;System . e x i t ( 0 ) ;

} else i f ( ” e x i t ” . equa l s ( l i n e ) ) {w r i t e r . p r i n t l n ( ” Connection c l o s e d . ” ) ;w r i t e r . c l o s e ( ) ;break ;

} else i f ( ” l i s t c l i e n t s ” . equa l s ( l i n e ) )l i s t C l i e n t s ( ) ;

else i f ( ” l i s t s e r v i c e s ” . equa l s ( l i n e ) )l i s t S e r v i c e s ( ) ;

else {St r ing [ ] l i n e s = l i n e . s p l i t ( ”\\ ” ) ;i f ( l i n e . matches ( ” c r e a t e c l i e n t \\w+” ) )

c r e a t e C l i e n t ( l i n e s [ 2 ] ) ;else i f ( l i n e . matches ( ” d e l e t e c l i e n t \\w+” ) )

d e l e t e C l i e n t ( l i n e s [ 2 ] ) ;else i f ( l i n e . matches ( ” s e r v i c e c l i e n t \\w+ \\w+” ) )

s e r v i c e C l i e n t ( l i n e s [ 2 ] , l i n e s [ 3 ] ) ;else

w r i t e r . p r i n t l n ( ”Unknown command” ) ;}w r i t e r . p r i n t l n ( ) ;

}} catch ( Exception e ) {

e . pr intStackTrace ( ) ;}

}

private void l i s t S e r v i c e s ( ) {S e r v i c e s L i s t s e r v i c e s = getRecentSe rv i c e s ( ) ;i f ( s e r v i c e s . s i z e ( ) == 0 )

w r i t e r . p r i n t l n ( ”<no s e r v i c e s a v a i l a b l e>” ) ;else

94

Page 97: Praca_magisterska

for ( ServiceType s e r v i c e : g e tRecentSe rv i c e s ( ) )w r i t e r . p r i n t l n ( ” ∗ ” + s e r v i c e . serviceName ) ;

}

private void l i s t C l i e n t s ( ) {i f ( a c t o r s . s i z e ( ) == 0 ) {

w r i t e r . p r i n t l n ( ”<no c l i e n t s de f ined>” ) ;return ;

}

S e r v i c e s L i s t s e r v i c e s = S e r v i c e s L i s t . g e tRecentSe rv i c e s ( ) ;for ( Actor ac to r : a c t o r s ) {

w r i t e r . p r i n t l n ( ” ∗ ”+ actor . playRole ( AccountHolder . class ) . getName ( ) ) ;

Set<Class<?>> r o l e s = new HashSet<Class <?>>();for ( ServiceType s e r v i c e : s e r v i c e s )

i f ( ac to r . hasRole ( s e r v i c e . c l i e n t C l a s s ) )i f ( r o l e s . add ( s e r v i c e . c l i e n t C l a s s ) )

w r i t e r . p r i n t l n ( ” − ”+ s e r v i c e . c l i e n t C l a s s . getSimpleName ( ) ) ;

}}

private void c r e a t e C l i e n t ( S t r ing name ) {i f ( f i n d C l i e n t ( name ) != null )

w r i t e r . p r i n t l n ( ” C l i en t a l r eady e x i s t . ” ) ;else {

Actor ac to r = ActorFactory . createThreadSafeActor ( ) ;ac to r . playRole ( AccountHolder . class , name ) ;a c t o r s . add ( ac to r ) ;w r i t e r . p r i n t l n ( ” C l i en t ” + name + ” created . ” ) ;

}}

private void d e l e t e C l i e n t ( S t r ing name ) {Actor ac to r = f i n d C l i e n t ( name ) ;i f ( ac to r == null )

w r i t e r . p r i n t l n ( ” C l i en t does not e x i s t . ” ) ;else {

a c t o r s . remove ( ac to r ) ;w r i t e r . p r i n t l n ( ” C l i en t ” + name + ” removed . ” ) ;

}}

private void s e r v i c e C l i e n t ( S t r ing name , S t r ing serviceName ) {Actor ac to r = f i n d C l i e n t ( name ) ;i f ( ac to r == null ) {

w r i t e r . p r i n t l n ( ” C l i en t does not e x i s t . ” ) ;return ;

}ServiceType s e r v i c e

= S e r v i c e s L i s t . g e tRecentSe rv i c e s ( ) . g e t S e r v i c e ( serviceName ) ;

95

Page 98: Praca_magisterska

i f ( s e r v i c e == null ) {w r i t e r . p r i n t l n ( ” S e r v i c e does not e x i s t . ” ) ;return ;

}i f ( ! ac to r . hasRole ( s e r v i c e . c l i e n t C l a s s ) ) {

acto r . playRole ( s e r v i c e . s e r v i c e C l a s s ) ;}

try {Object r e s u l t = s e r v i c e . s e r v i c e C l a s s . getMethod (

” s e r v i c e ” , s e r v i c e . c l i e n t C l a s s ) . invoke (s e r v i c e . s e r v i c e C l a s s . newInstance ( ) ,ac to r . playRole ( s e r v i c e . c l i e n t C l a s s ) ) ;

w r i t e r . p r i n t l n ( ” S e r v i c e c a l l e d with r e s u l t : \”” + r e s u l t + ”\”” ) ;} catch ( Exception e ) {

e . pr intStackTrace ( ) ;}

}

private stat ic Actor f i n d C l i e n t ( S t r ing name ) {for ( Actor ac to r : a c t o r s )

i f ( ac to r . playRole (AccountHolder . class ) . getName ( ) . equa l s ( name ) )

return acto r ;return null ;

}

public stat ic void main ( St r ing [ ] a rgs ) throws Exception {for ( ServerSocket sSocket = new ServerSocket ( PORT ) ; ; )

new Server ( sSocket . accept ( ) ) . s t a r t ( ) ;}

}

Listing B.3: Zawartości pliku Server.java.

package ro l e s example ;

import java . lang . r e f l e c t . Method ;import java . u t i l . ∗ ;

public class S e r v i c e s L i s t extends LinkedList<S e r v i c e s L i s t . ServiceType>{private S e r v i c e s L i s t ( ) { }

private S e r v i c e s L i s t ( S e r v i c e s L i s t l i s t ){super ( l i s t ) ;

}

public stat ic class ServiceType {public f ina l St r ing serviceName ;public f ina l Class<?> s e r v i c e C l a s s ;public f ina l Class<?> c l i e n t C l a s s ;

96

Page 99: Praca_magisterska

public ServiceType ( Class<?> s e r v i c e C l a s s ) {this . s e r v i c e C l a s s = s e r v i c e C l a s s ;serviceName = s e r v i c e C l a s s . getSimpleName ( ) ;

for ( Method method : s e r v i c e C l a s s . getMethods ( ) )i f ( ” s e r v i c e ” . equa l s ( method . getName ( ) )

&& method . getParameterTypes ( ) . l ength == 1&& ! method . getParameterTypes ( ) [ 0 ] . equa l s (

Object . class ) ){c l i e n t C l a s s = method . getParameterTypes ( ) [ 0 ] ;return ;

}throw new I l l ega lArgumentExcept ion ( ) ;

}}

private stat ic f ina l long se r ia lVers ionUID = 1L ;

private stat ic f ina l long REFRESH TIME = 1000 ;private stat ic f ina l St r ing DATA FILE = ” s e r v i c e s . txt ” ;

private stat ic f ina l S e r v i c e s L i s t s e r v i c e s = new S e r v i c e s L i s t ( ) ;private stat ic long l a s t R e f r e s h e d = System . cur rentT imeMi l l i s ( ) ;

stat ic {updateServ i ce s ( ) ;

}

private stat ic void updateServ i ce s ( ) {try {

s e r v i c e s . c l e a r ( ) ;Scanner scanner = new Scanner (

Console . class . getClassLoader ( ) . getResourceAsStream (DATA FILE ) ) ;

while ( scanner . hasNext ( ) ) {ServiceType s e r v i c e

= new ServiceType ( Class . forName ( scanner . next ( ) ) ) ;s e r v i c e s . add ( s e r v i c e ) ;

}} catch ( Exception e ) {

e . pr intStackTrace ( ) ;}

}

public stat ic synchronized S e r v i c e s L i s t ge tRecentSe rv i c e s ( ) {i f ( System . cur rentT imeMi l l i s ( ) − l a s t R e f r e s h e d > REFRESH TIME ) {

l a s t R e f r e s h e d = System . cur rentT imeMi l l i s ( ) ;updateServ i ce s ( ) ;

}return new S e r v i c e s L i s t ( s e r v i c e s ) ;

}

97

Page 100: Praca_magisterska

public ServiceType g e t S e r v i c e ( S t r ing serviceName ){for ( ServiceType s e r v i c e : this )

i f ( s e r v i c e . serviceName . equa l s ( serviceName ) )return s e r v i c e ;

return null ;}

}

Listing B.4: Zawartości pliku ServiceList.java.

package ro l e s example . c l i e n t s ;

public class AccountHolder {protected St r ing name ;protected int balance = 0 ;

public AccountHolder ( ){}

public AccountHolder ( S t r ing name ) {this . name = name ;

}

public St r ing getName ( ) {return name ;

}

public void setName ( St r ing name ) {this . name = name ;

}

public void se tBa lance ( int balance ) {this . ba lance = balance ;

}

public int getBalance ( ) {return balance ;

}}

Listing B.5: Zawartości pliku AccountHolder.java.

package ro l e s example . c l i e n t s ;

public class CreditCardHolder extends AccountHolder {private stat ic int cardNumberCounter = 0 ;

public f ina l int cardNumber = ++cardNumberCounter ;private boolean ac t i va t ed = fa l se ;private int cred i tCardBalance = 0 ;

98

Page 101: Praca_magisterska

public int getCreditCardBalance ( ) {return cred i tCardBalance ;

}

public void c r e d i t ( int sum ){i f ( ! i sAc t i va t ed ( ) )

return ;c red i tCardBalance −= sum ;

}

public void payCredit ( int sum ){i f ( ! i sAc t i va t ed ( ) )

return ;ba lance −= sum ;credi tCardBalance += 0.8 ∗ sum ;

}

public void a c t i v a t e ( ) {ac t i va t ed = true ;

}

public boolean i sAc t i va t ed ( ) {return ac t i va t ed ;

}}

Listing B.6: Zawartości pliku CreditCardHolder.java.

package ro l e s example . c l i e n t s ;

import java . u t i l . Calendar ;

public class Trave l In suranceCl i ent extends AccountHolder {private Calendar s t a r t = null , end = null ;

public void i n s u r e ( Calendar s ta r t , Calendar end ){this . s t a r t = s t a r t ;this . end = end ;ba lance −= 100 ;

}

public boolean compense ( Calendar date ){i f ( s t a r t == null | | s t a r t . a f t e r ( date )

| | end == null | | end . be f o r e ( date ) )return fa l se ;

ba lance += 10000;return true ;

}}

Listing B.7: Zawartości pliku TravelInsuranceHolder.java.

99

Page 102: Praca_magisterska

package ro l e s example . s e r v i c e s ;

import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . CreditCardHolder ;

public class ActivateCreditCard implements ISe rv i c eProv ide r<CreditCardHolder> {@Override public Object s e r v i c e ( CreditCardHolder c l i e n t ) {

i f ( c l i e n t . i sAc t i va t ed ( ) )return c l i e n t . getName ( )

+ ” ’ s c r e d i t card has a l r eady been ac t i va t ed ” ;c l i e n t . a c t i v a t e ( ) ;return c l i e n t . getName ( ) + ” ’ s c r e d i t card has been ac t i va t ed ” ;

}}

Listing B.8: Zawartości pliku ActivateCreditCard.java.

package ro l e s example . s e r v i c e s ;

import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . AccountHolder ;

public class CheckAccountBalance implements ISe rv i c eProv ide r<AccountHolder> {@Override public Object s e r v i c e ( AccountHolder c l i e n t ) {

return c l i e n t . getName ( )+ ” ’ s ba lance i s : ” + c l i e n t . getBalance ( ) + ” . ” ;

}}

Listing B.9: Zawartości pliku CheckAccountBalance.java.

package ro l e s example . s e r v i c e s ;

import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . CreditCardHolder ;

public class CheckCreditCardBalanceimplements ISe rv i c eProv ide r<CreditCardHolder> {

@Override public Object s e r v i c e ( CreditCardHolder c l i e n t ) {return c l i e n t . getName ( )

+ ” ’ s c r e d i t card balance i s : ” + c l i e n t . getCreditCardBalance ( ) + ” . ” ;}

}

Listing B.10: Zawartości pliku CheckCreditCardBalance.java.

100

Page 103: Praca_magisterska

package ro l e s example . s e r v i c e s ;

import java . u t i l . Calendar ;import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . Trave l In suranceC l i ent ;

public class CompenseTravelAccidentimplements ISe rv i c eProv ide r<Trave l InsuranceCl i ent> {

@Override public Object s e r v i c e ( Trave l In suranceCl i ent c l i e n t ) {i f ( c l i e n t . compense ( Calendar . g e t In s tance ( ) ) )

return ”Compensation f o r ” + c l i e n t . getName ( ) +” has been payed” ;else

return c l i e n t . getName ( ) + ” has not been insured ” ;

}}

Listing B.11: Zawartości pliku CompenseTravelAccident.java.

package ro l e s example . s e r v i c e s ;

import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . CreditCardHolder ;

public class GetCredit implements ISe rv i c eProv ide r<CreditCardHolder> {@Override public Object s e r v i c e ( CreditCardHolder c l i e n t ) {

i f ( ! c l i e n t . i sAc t i va t ed ( ) )return c l i e n t . getName ( ) + ” ’ s c r e d i t card has not been ac t i va t ed ” ;

c l i e n t . c r e d i t ( 100 ) ;return c l i e n t . getName ( ) + ” ’ s c r e d i t card has been charged ” ;

}}

Listing B.12: Zawartości pliku GetCredit.java.

package ro l e s example . s e r v i c e s ;

import java . u t i l . Calendar ;import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . Trave l In suranceC l i ent ;

public class InsureTrave l implements ISe rv i c eProv ide r<Trave l InsuranceCl i ent> {@Override public Object s e r v i c e ( Trave l In suranceCl i ent c l i e n t ) {

Calendar endDate = Calendar . g e t In s tance ( ) ;endDate . add ( Calendar .MONTH, 1 ) ;c l i e n t . i n s u r e ( Calendar . g e t In s tance ( ) , endDate ) ;return c l i e n t . getName ( ) + ”has been insured ” ;

}}

Listing B.13: Zawartości pliku InsureTravel.java.

101

Page 104: Praca_magisterska

package ro l e s example . s e r v i c e s ;

import ro l e s example . I S e r v i c e P r o v i d e r ;import ro l e s example . c l i e n t s . CreditCardHolder ;

public class RepayCredit implements ISe rv i c eProv ide r<CreditCardHolder> {@Override public Object s e r v i c e ( CreditCardHolder c l i e n t ) {

i f ( ! c l i e n t . i sAc t i va t ed ( ) )return c l i e n t . getName ( ) + ” ’ s c r e d i t card has not been ac t i va t ed ” ;

c l i e n t . payCredit ( 100 ) ;return c l i e n t . getName ( ) + ” ’ s c r e d i t has been p a r t i a l l y repa id ” ;

}}

Listing B.14: Zawartości pliku RepayCredit.java.

ro l e s example . s e r v i c e s . Act ivateCreditCardro l e s example . s e r v i c e s . CheckAccountBalancero l e s example . s e r v i c e s . CheckCreditCardBalancero l e s example . s e r v i c e s . CompenseTravelAccidentro l e s example . s e r v i c e s . GetCreditro l e s example . s e r v i c e s . InsureTrave lro l e s example . s e r v i c e s . RepayCredit

Listing B.15: Początkowa zawartości pliku services.txt.

102

Page 105: Praca_magisterska

Dodatek C

Narzędzia programistyczne

W niniejszym dodatku omówione zostały dwa proste narzędzia programistyczne związane zimplementacją propozycji PPK - translator z języka JavaBC do języka Java oraz towarzyszącemu zadanie (ang. task) dla systemy automatycznego budowania oprogramowania ApacheAnt. Autor nie stworzył natomiast żadnych narzędzi wspomagających programowanie wzaproponowanej przez niego implementacji dynamicznych ról dla języka Java z racji tego, żezostała ona dostarczona w postaci zwyczajnej biblioteki.

Zarówno biblioteka dla dynamicznych ról jak i narzędzia związane z propozycją PPKzostały dla uproszczenia dostarczone w formie pojedynczego pliku *.jar — javaBC.jar.Biblioteka owa nie wymaga żadnych zależności w postaci innych bibliotek i jest udostępnianana licencji GNU Lesser General Public License. Oprócz kodu stworzonego bezpośrednio przezautora, w jej skład wchodzą także fragmenty bibliotek ANTLR, Javassist, JUnit i ApacheAnt.

W celu użycia biblioteki wymagana jest maszyna wirtualna obsługująca pliki klas zgodnez formatem Javy 6.01.

C.1 Translator

Translator dla PPK jest prostą aplikacją przyjmującą na wejściu program napisany w językuJavaBC i zwracającą jego funkcjonalny ekwiwalent, zawierający jedynie konstrukcje właściwedla języka Java. Oprócz swojego głównego zadania, translator wykonuje dodatkowo spraw-dzenia wejściowego programu pod kątem poprawności składniowej oraz semantycznej. Spraw-dzenie poprawności składniowej dotyczy zarówno składni specyficznej dla propozycji autorajak i, w pewnym ograniczonym zakresie, składni odziedziczonej po języku Java. Sprawdze-nie poprawności semantycznej dotyczy natomiast tylko i wyłącznie konstrukcji związanychbezpośrednio z PPK2. W efekcie użycie translatora do przetłumaczenia błędnego programumoże być czasami możliwe — w takiej sytuacji błędy zawarte w programie źródłowym prze-noszą się po prostu do programu wynikowego. Nie jest to w gruncie rzeczy problemem ażtak dokuczliwym, gdyż dane błędy zostaną i tak, koniec końców, wykryte na etapie właściwejkompilacji. Nawiasem mówiąc, napisanie translatora sprawdzającego pełną poprawność języ-

1Główny numer wersji (ang. major version number) plików klas zawartych w bibliotece wynosi 50.2Translator może np. wykryć błąd polegający na tym, że dany automat skończony nie posiada stanu

początkowego, ale nie jest już w stanie wykryć użycia wyrażenia o wartości innej niż logiczna w warunkuniezmiennika.

103

Page 106: Praca_magisterska

kową otrzymanych programów musiałoby się siłą rzeczy wiązać ze sprawdzaniem zgodnościwszelkich możliwych konstrukcji ze specyfikacją języka Java, co przekraczałoby zdecydowaniemożliwości autora. Warto tu zauważyć, że nawet zaawansowane narzędzia, w rodzaju dołą-czonego do środowiska Eclipse kompilatora ecj, nie zawsze są wstanie wykryć wszystkie błędyjęzykowe i mogą w związku z tym pozwalać na kompilację wadliwych programów. Dobrymprzykładem może tu być np. próba odwołania się do stałej enumeracji jeszcze przed jej kon-strukcją (np. w konstruktorze zadeklarowanej wcześniej innej stałej tej samej enumeracji).O ile kompilator dostarczony przez firmę Sun jest w stanie wykryć tego rodzaju błąd i niedopuścić do kompilacji wadliwego programu, o tyle już ecj nie znajduje tego rodzaju błędui pozwala w efekcie na kompilację, co pozostawia programistę w mylnym przeświadczeniu otym, że dany program jest faktycznie prawidłowy

Należy jeszcze wspomnieć o tym, że translator, zgodnie z częstą praktyką, dzieli wszyst-kie błędy semantyczne na dwie grupy: błędy właściwe i ostrzeżenia. O klasyfikacji danegobłędu decyduje jego bezpośredni wpływ na pomyślność całego procesu translacji. Ostrzeżeniamianowicie nie powodują przerwania procesu translacji, błędy właściwe — tak. Tablica C.1zawiera listę błędów semantycznych wykrywanych przez translator.

typ1 komunikat translatora i jego wyjaśnienie

— definicje automatów skończonych —• Automaton already defined — powtórna definicja automatu w danej klasie• Automatons within enumerations are not allowed — umieszczenie definicji

automatu bezpośrednio wewnątrz ciała enumeracji• Only one state definition allowed — powtórna definicja danego stanu w obrębie

automatu• No initial state founded — brak stanu początkowego w danym automacie• Initial state already defined — oznaczenie więcej niż jednego stanu w danym

automacie jako początkowy• No state definition founded — brak definicji pewnego stanu w danym automacie◦ Inital modifier already occured — dwukrotne użycie modyfikatora initial w

definicji danego stanu◦ Accepting modifier already occured — dwukrotne użycie modyfikatora

accepting w definicji danego stanu◦ Destination state already occured — dwukrotne użycie danego stanu w liście

stanów docelowych◦ No accepting state founded — brak definicji stanu akceptującego w danym

automacie◦ Unreachable state found — definicja stanu nieosiągalnego w danym automacie◦ No accepting state is reachable — brak możliwości osiągnięcia dowolnego stany

akceptującego w danym automacie

— definicje niezmienników —• Unknown state used — użycie niezdefiniowanego stanu w liście stanów pewnego

warunku• Invariant already defined — powtórna definicja danego rodzaju niezmiennika w

pewnej klasie1typ błędu: • — błąd właściwy, ◦ — ostrzeżenie

104

Page 107: Praca_magisterska

typ1 komunikat translatora i jego wyjaśnienie

• Invalid modifier used — użycie nieprawidłowego modyfikatora w nagłówku defi-nicji niezmiennika

• Only one modifier allowed — użycie dwóch modyfikatorów w nagłówku definicjiniezmiennika

◦ State already occured — dwukrotne użycie danego stanu w liście stanów pewnegowarunku

— pozostałe konstrukcje —• Return keyword not allowed in the current context — odwołanie się do

wartości zwracanej przez metodę w niedopuszczalnym• No automaton could be found transition may refer to — użycie nieznanego

stanu• Unknown state used — użycie stanu niezdefiniowanego w danym automacie◦ Multiple automatons referenced — odwołanie się do różnych automatów skoń-

czonych w obrębie pojedynczej instrukcji sprawdzenia aktualnego stanu◦ State already referenced — dwukrotne odwołanie się do pewnego stanu danego

automatu w obrębie pojedynczej instrukcji sprawdzenia aktualnego stanu1typ błędu: • — błąd właściwy, ◦ — ostrzeżenie

Tablica C.1: Błędy semantyczne wykrywane przez translator.

Klasą zawierającą metodę startową programu translatora, tj. tzw. metodę main, jestpl.edu.pw.akosicki.translator.Tool. Klasa ta została wyszczególniona w manifeściepliku javaBC.jar jako klasa startowa i w związku z tym nie ma konieczności jej jawnegopodawania podczas wywołania translatora. Sam sposób wywołania translatora jest następu-jący:

program_command [options] inputFile outputFile

gdzie:

program command jest instrukcją wywołania programu. Jeśli plik biblioteki (javaBC.jar)znajduje się w aktualnym katalogu, wywołanie programu może przyjąć formę java -jarjavaBC.jar lub java -cp javaBC.jar pl.edu.pw.akosicki.translator.Tool.

Wszystkie elementy następujące po instrukcji wywołania programu są traktowane jakojego argumenty.

options pozwala na skorzystanie z dodatkowych możliwości udostępnianych przez translator:

-e prowadzi do wygenerowania pliku zawierającego odwzorowanie programu otrzyma-nego w wyniku translacji do programu w postaci pierwotnej. Odwzorowanie tozostanie umieszczone w pliku <outputFile>.mapping. Zawartość pliku odwzoro-wania zostanie omówiona w dalszej część niniejszego dodatku.

-i prowadzi do wygenerowania plików zawierających powstałe podczas procesu trans-lacji formy pośrednie programu wynikowego. Poszczególne pliki z formami po-średnimi translacji mają nazwy postaci <outputFile>.intermediate<number>,

105

Page 108: Praca_magisterska

gdzie <number> oznacza numer konkretnej formy pośredniej. Formy pośrednie nu-merowane są kolejnymi, począwszy od jedynki, liczbami naturalnymi. Forma znumerem jeden jest najbardziej zbliżona do programu pierwotnego. W większościprzypadków translator generuje tylko jedną formę pośrednią.Opcja ta w zasadzie jest swego rodzaju ciekawostką i jako taka nie niesie ze sobążadnej przydatnej funkcjonalności. Przez autora była ona używana w celach de-bugowych.

inputFile jest argumentem obligatoryjnym, oznaczającym nazwę pliku z programem źródło-wym w języku JavaBC.

outputFile jest drugim argumentem obligatoryjnym, oznaczającym nazwę pliku w którymma zostać umieszczony program wynikowy.

Wywołanie translatora z nieodpowiednim parametrami prowadzi jedynie do wyświetleniekomunikatu o błędzie i instrukcji prawidłowego użycia. Jeśli argumenty wywołania określonesą prawidłowo, translator spróbuje przeprowadzić translację. Niech dla przykładu w danymkatalogu znajduje się plik biblioteki oraz plik Test.javabc o treści przedstawionej na listinguC.1. Uruchomienie translatora może wyglądać następująco:

$java -jar javaBC.jar -e Test.javabc Test.javaCannot translate Test.javabc - program contains errorsErrors:ERR No initial state founded (at 2:1 x 9)ERR Return keyword not allowed in the current context (at 8:30 x 6)

Translacja dla pliku z listingu C.1 nie przebiegła pomyślnie. Translator wykrył dwa błędysemantyczne, w wyniku czego był zmuszony do awaryjnego przerwania swojego działania.Pierwszym z błędów był brak określenia stanu początkowego w definicji automatu skończo-nego, drugim próba odwołania się do wartości zwracanej przez metodę typu void. Po usu-nięciu usterek poprzez przekształcenie programu do postaci z listingu C.1, można spróbowaćponownie uruchomić translator:

$ java -jar javaBC.jar -e Test.javabc Test.javaFile Test.javabc translated to Test.java (with warnings)Warnings:WRN Unreachable state found (at 5:2 x 2)Source mapping saved to Test.java.mapping

Powtórne uruchomienie translatora wypadło pomyślnie. Nie licząc pojedynczego ostrzeżenia onieosiągalności pewnego ze stanów automatu skończonego, nie znalezione zostały żadne błędy.Ponieważ ostrzeżenia same w sobie nie powodują przerwania pracy translatora, w bieżącymkatalogu pojawiły się dwa nowe pliki: Test.java oraz Test.java.mapping. Pierwszy z plikówzawiera wynikowy program, drugi natomiast jest plikiem odwzorowania pomiędzy pozycjamiz programu wynikowego a pozycjami z programu źródłowego.

106

Page 109: Praca_magisterska

1 class Test{2 automaton {3 S0 : S1 ;4 accepting S1 ;5 S2 ;6 }78 void method ( int arg ) out ( return != 0 ){9 }

10 }

Listing C.1: Zawartość pliku Test.javabc.

1 class Test{2 automaton {3 i n i t i a l S0 : S1 ;4 accepting S1 ;5 S2 ;6 }78 void method ( int arg ) out ( true ){9 }

10 }

Listing C.2: Zawartość pliku Test.javabc po usunięciu usterek.

Informacje zawarte w pliku odwzorowania pozwalają stwierdzić jakie jest prawdziwe źró-dło danego fragmentu kodu z pliku wynikowego. Może być to użyteczne w sytuacji wykryciabłędu dopiero na etapie analizy programu wynikowego (otrzymanego w wyniku translacji).Ponieważ ewentualna poprawka owego błędu musi być ze zrozumiałych przyczyn wprowa-dzona do programu w postaci sprzed translacji, a lokalizacja błędu jest niestety podawana“we współrzędnych” programu otrzymanego w wyniku translacji, użytkownik musi dokonaćswego rodzaju odwzorowania pomiędzy pozycjami w kodzie.

Należy w tym miejscu zauważyć, że dany fragment kodu z programu wynikowego możemieć pochodzenie dwojakiego rodzaju. Może być on mianowicie pewnym, przeniesionymlub skopiowanym fragmentem kodu, który istniał już w programie źródłowym. Może byćon też fragmentem kodu wygenerowanym przez translator. W pierwszym przypadku plikodwzorowania będzie zawierał wskazanie na odpowiednie miejsce w kodzie źródłowym. Wdrugim przypadku plik odwzorowania może odsyłać do pewnej abstrakcji odpowiedzialnejbezpośrednio za wygenerowanie danego fragmentu kodu lub ewentualnie nie wskazywać wogóle na cokolwiek.

Sam plik odwzorowania jest plikiem tekstowym o mocno uproszczonej składni (autorprzedłożył tu czytelność nad zwięzłość). Każda linijka pliku odwzorowania odpowiada bez-pośrednio linijce w pliku z programem wynikowym. Pojedyncza linijka składa z numeru, bę-dącego de facto jej numerem, oraz pewnej liczby odwzorowań dotyczących fragmentów koduzawartego w owej linijce. Przykładowy fragment pliku odwzorowania może przedstawiać sięnastępująco:

107

Page 110: Praca_magisterska

33: 1=>7:134: 1-24=>8:1 25=>8:3735: 1-7=>8:6x636: 1=>9:1 2-14=>8:6x6

Napis 1=>7:1 oznacza, że jedyny symbol znajdujący się w 33 linijce pliku wynikowego zostałw rzeczywistości skopiowany z linijki 7 pliku pierwotnego. Napis 1-24=>8:1 informuje, że blokzaczynający się w kolumnie 1 i kończący w kolumnie 24 w 34 linijce pliku wynikowego zostałskopiowany z początku 8 linijki pliku pierwotnego. Dla odmiany napis 1-7=>8:6x6 stwierdza,że dany blok kodu został wygenerowany na etapie kompilacji, a abstrakcja odpowiedzialna zajego generację zaczyna się w 8 linijce pliku źródłowego, w kolumnie 6 i ma długość 6 znaków.

Autor chciałby tu jeszcze podkreślić, że możliwość generacji przez translator plików od-wzorowań jest mechanizmem czysto pomocniczym i w większości przypadków uciekanie siędoń nie powinno być koniecznego. Schemat translacji zaproponowany przez autora jest natyle prosty, że na ogół nie powinno być żadnych wątpliwości co do tego skąd dany fragmentpliku wynikowego rzeczywiście pochodzi.

C.2 Zadanie dla systemu Apache Ant

W celu ułatwnienia integracji z istniającymi systemami budowy oprogramowania, autor przy-gotował proste zadanie (ang. task) systemu Apache Ant[8]. Zadanie zadefiniowane jest wklasie pl.edu.pw.akosicki.translator.AntTask i służy do uruchamiania translatora nagrupie plików źródłowych. Argumenty wywołania zadania są następujące:

srcdir jest parametrem wskazującym na korzeń struktury katalogów, w której znajdują siępliki mające zostać poddane translacji. Parametr ten jest jedynym parametrem obliga-toryjnym.

destdir jest parametrem wskazującym na korzeń struktury katalogów, w której mają zostaćumieszczone pliki wynikowe. Parametr ten jest opcjonalny, a jego domyślną wartościąjest wartość parametru srcdir.

extension jest rozszerzeniem plików które mają zostać poddane translacji. Parametr tenjest opcjonalny a jego domyślną wartością jest .javabc.

mappings jest przełącznikiem określającym, czy obok plików wynikowych mają zostać umiesz-czone także pliki odwzorowania.

Po uruchomieniu zadania przeglądana jest struktura podkatalogów wskazywanych przezsrcdir. Jeśli dany plik posiada rozszerzenie zgodne z określonym w extension, nastę-puje jego translacja. Pliki wynikowe, ewentualnie również towarzyszące im pliki odwzo-rowań, umieszczane są w strukturze katalogów wskazywanych przez destdir, w taki spo-sób aby ich położenie względem danej struktury katalogów zostało zachowane. Jeśli przy-kładowo parametr srcdir wskazywał na javab src a parametr destdir na src, tłuma-czenia plików znajdujących się w javab src/pl/edu/pw/akosicki zostaną umieszczone wsrc/pl/edu/pw/akosicki. Znaczenie parametrów destdir i src jest analogiczne jak w po-pularnym zadaniu javac.

108

Page 111: Praca_magisterska

Nazwa danego pliku wynikowego tworzona jest poprzez zastąpienie rozszerzenia z plikuźródłowego rozszerzeniem .java. Listing C.3 zawiera przykład użycia dostarczonego przezautora zadania. Wyczerpujący opis systemu Apache Ant można znaleźć w podręczniku[14].

1 <?xml v e r s i on =”1.0” ?>2 <project name=”samp le p ro j e c t ” d e f a u l t=”t r a n s l a t e”>34 <target name=”t r a n s l a t e”>5 <taskdef name=”t r a n s l a t o r ”6 classname=”pl . edu .pw . a k o s i c k i . t r a n s l a t o r . AntTask”7 c l a s s pa t h =”. ./ javaBC . j a r ”/>89 <t r a n s l a t o r s r c d i r=”j a v a b c s r c ” d e s t d i r=”s r c ”

10 extens i on =”. javabc ” mappings=”true”/>11 </target>1213 </project>

Listing C.3: Przykładowy plik z projektem systemu Apache Ant, wykorzystujący przygoto-wane przez autora zadanie tegoż systemu.

109

Page 112: Praca_magisterska

Dodatek D

Opis formalny gramatyki dla PPK

Niniejszy dodatek zawiera opis gramatyki JavaBC wraz z krótkim komentarzem dotyczącymintencji autora oraz niektórych dodatkowych obostrzeń składniowych nie dających wyrazićsię za pomocą gramatyki bezkontekstowej. Ponieważ propozycja dynamicznych ról nie zna-lazła swojego odzwierciedlenia w składni, gramatyka zawiera jedynie produkcje dotyczącemechanizmów Programowania przez Kontrakt.

Gramatyka ta jest pochodną gramatyki Javy opisanej w specyfikacji języka Java [9, roz-dział 18] i jako taka nie będzie, z powodu swojej objętości, przytaczana z całości. W zamianautor zdecydował się na umieszczenie w dodatku jedynie reguł zmodyfikowanych w stosunkudo specyfikacji Javy lub też reguł całkowicie nowych. Reguły bądź ich fragmenty istniejącew specyfikacji Javy zaznaczone zostały szarą, pochyłą czcionką. Z kolei fragmenty gramatykidodane przez autora wyróżnione zostały prostym krojem czcionki oraz czarnym kolorem.

Reguły gramatyczne przedstawione zostały w rozszerzonej notacji Backusa-Naura (ang.Extended Backus-Naur Form).

ClassBodyDeclaration ::= ’static’ , Block| [ ’package’ ] , { Modifier } , MemberDecl| ’automaton’ , AutomatonBody ;

AutomatonBody ::= ’{’ , { AutomatonStateDecl } , ’}’ ;

AutomatonStateDecl ::= { AutomatonStateModifier } , Identifier ,[ ’:’ , AutomatonStateList ] , ’;’ ;

AutomatonStateModifier ::= ’accepting’| ’initial’ ;

AutomatonStateList ::= Identifier , { ’,’ , Identifier } ;

Reguła ClassBodyDeclaration została, w stosunku do pierwowzoru, wzbogacona o sekcjępozwalającą na definicję automatu skończonego. Autor zdecydował się umieścić ową produk-cję w tej samej regule, w której znajduje się produkcja pozwalająca na tworzenie statycznegoinicjalizatora klasy z racji podobieństwa owych deklaracji. W skład obu produkcji wchodzi

110

Page 113: Praca_magisterska

pojedyncze słowo kluczowe oraz umieszczone pomiędzy nawiasami klamrowymi ciało danejdeklaracji. Pewną różnicą, której nie da się wywnioskować z opisu gramatyki, jest tutaj na-tomiast fakt, iż ciąg automaton nie jest wbrew pozorom słowem kluczowym. Opcjonalnesłowo package związane jest natomiast z definicją niezmiennika klasy. Słowo to może wystą-pić jedynie wtedy gdy { Modifier } jest pojedynczym słowem protected a MemberDeclrozwija się w definicję niezmiennika.

Reguły z serii Automaton... dotyczą definicji ciała automatu skończonego. Warty za-uważenia jest tutaj fakt, iż podobnie jak ma to miejsce w wypadku słowa automaton, słowainitial oraz accepting nie są słowami kluczowymi.

MemberDecl ::= GenericMethodOrConstructorDecl| MethodOrFieldDecl| ’void’ , Identifier , VoidMethodDeclaratorRest| Identifier , ConstructorDeclaratorRest| InterfaceDeclaration| ClassDeclaration| InvariantDeclaration ;

InvariantDeclaration ::= ’invariant’ , ’{’ , InvariantBody , ’}’ ;

InvariantBody ::= { InvariantAssertionDecl } ;

InvariantAssertionDecl ::= ( AutomatonStateList | ’*’ ) ,’:’ , Expression , [ ’:’ , Expression ] , ’;’ ;

Reguła MemberDecl została wyposażona o dodatkową produkcję rozwijącą sie w definicjęniezmiennika klasy. Reguły z serii Invariant... dotyczą semej definicji niezmiennika.

MethodBody ::= [ ’in’ , ’(’ , ExpressionList , ’)’ ] ,[ ’out’ , ’(’ , ExpressionList , ’)’ ] , Block ;

Prosta reguła MethodBody została wyposażona w dwie opcjonalne sekcje — in(...) orazout(...), które pozwalają odpowiednio na definicję warunku początkowego oraz końcowegometody. Zarówno in jak i out nie są słowami kluczowymi.

Statement ::= Block| ’assert’ , Expression , [ ’:’ , Expression ]| ’if’ , ParExpression , Statement , [ ’else’ , Statement ]| ’for’ , ’(’ , ForControl , ’)’ , Statement| ’while’ , ParExpression , Statement| ’do’ , Statement , ’while’ , ParExpression , ’;’| ’try’ , Block ( Catches | [ Catches ] , ’finally’ , Block )| ’switch’ , ParExpression , { Switch Block Statement Groups }

111

Page 114: Praca_magisterska

| ’synchronized’ , ParExpression, Block| ’return’ , [ Expression ] , ’;’| ’throw’ , Expression , ’;’| ’break’ , [ Identifier ]| ’continue’ , [ Identifier ]| ’;’| StatementExpression , ’;’| Identifier , ’:’ , Statement| ’transient’ , ’:’ , [ Identifier , ’.’ ] , Identifier , ’;’ ;

Reguła Statement rozwija się w samodzielną instrukcję, taką jak np. przerwanie pętli bądźprzypisanie zmiennej. W ten sam sposób, tj. jako samodzielną instrukcję, potraktował autorpolecenie zmiany stanu w automacie skończonym. Dzięki temu produkcja w ową instrukcjęzostała w naturalny sposób zawarta w niniejszej regule. Instrukcja zmiany stanu zaczyna sięod słowa kluczowego transient, które w Javie może wystąpić jedynie w roli modyfikatorapola klasy (nie występuje zatem bezpośrednio wewnątrz bloków instrukcji). Z tego powodu,oraz z faktu, że słowo “transient” oznacza w języku angielskim “przejściowy”, co w pewiensposób wiąże się z przejściowością stanów automatu skończonego, autor zdecydował się użyćgo w składni danej instrukcji.

Primary ::= ParExpression| NonWildcardTypeArguments ( ExplicitGenericInvocationSuffix

| ’this’ , Arguments )| ’this’ , [ Arguments ]| ’super’ , SuperSuffix| Literal| ’new’ , Creator| Identifier , { ’.’ , Identifier } , [ IdentifierSuffix ]| BasicType , { ’[’ , ’]’ } , ’.’ , ’class’| ’void’ , ’.’ , ’class’| ’return’ , { ’.’ , Identifier } , [ IdentifierSuffix ]| ’transient’ , ’?’ , StateCheckRest ;

StateCheckRest ::= [ Identifier , ’.’ ] , Identifier| ’(’ , [ Identifier , ’.’ ] , Identifier ,{ ’,’ , [ Identifier , ’.’ ] , Identifier } , ’)’ ;

Reguła Primary jest najbardziej elementarną (w sensie priorytetu operatorów) z regułdotyczących wyrażeń. Autor zdecydował, że dwa dodatkowo proponowane przez niego wyra-żenia, jakimi są odwołania do wartości zwracanej metody oraz sprawdzenie stanu automatuskończonego, mają równie elementarny charakter. Jest to szczególnie istotne w wypadkuwyrażenia sprawdzenia stanu, które ma wyższy priorytet od podobnego wyrażenia dotyczą-cego operatora trójargumentowego - oba wyrażenia używają znaku pytajnika. Motywacjadotycząca użycia słowa transient w wyrażeniu sprawdzenia stanu jest podobna do tej wprzypadku instrukcji zmiany stanu.

112

Page 115: Praca_magisterska

Wyrażenie odwołania się do aktualnej wartości zwracanej ze zrozumiałych powodów za-czyna się od istniejącego już słowa kluczowego MemberDecl. Co prawda słowo to używanejest w Javie do określenia instrukcji powrotu z metody, jednak nie prowadzi to w żadnymwypadku do niejednoznaczności, gdyż użycie to ma miejsce w innym kontekście. Podobnasytuacja zachodzi w przypadku słowa transient.

Wyżej przedstawiona modyfikacja gramatyki Javy została dokonana przez autora w spo-sób arbitralny, przy czym autor miał przy jej tworzeniu na uwadze: a) klarowność wynikowejgramatyki b) intuicyjne wynikanie semantyki z danej składni oraz c) ograniczenie do koniecz-nego minimum konieczności sprawdzania poprawności programów wykraczającej poza podanereguły gramatyki bezkontekstowej. Przykładowo definicja automatu skończonego mogła zo-stać równie dobrze umieszczona na poziomie reguły MemberDecl. Wtedy jednak zaistniałabykonieczność upewniania się, iż przed definicją automatu nie zostały użyte żadne modyfikatory,na co teoretycznie pozwalałaby gramatyka. Większość pozagramatycznych reguł poprawnościnie zostały w tym dodatku wymienionych, aby uniknąć powtarzania informacji zawartych wsekcji 2.3.

Na koniec wypada jeszcze wspomnieć, że przytoczona powyżej gramatyka nie została widentycznej formie wykorzystana podczas tworzenia translatora dla PPK. Otóż autor, przypisaniu analizatora gramatycznego dla potrzeb translatora, skorzystał z biblioteki ANTLR,będącej w istocie tzw. kompilatorem kompilatorów[16], oraz z gramatyki Javy przygotowanejspecjalnie na potrzeby owego narzędzia[17]. Wymogło to pewne modyfikacje, w stosunkudo specyfikacji gramatyki, związane z możliwościami użytego narzędzia oraz koniecznościąsprawdzania poprawności semantycznej.

113

Page 116: Praca_magisterska

Bibliografia

[1] Contract4j website. http://www.contract4j.org, 2008.

[2] A.V. Aho, R. Sethi, and Ł. Ullman. Kompilatory: reguły, metody i narzędzia. Wydaw-nictwa Naukowo-Techniczne, Warszawa, 2002.

[3] K. Arnold, J. Gosling, and D. Holmes. Java (TM) Programming Language, The. Addison-Wesley Professional, 2005.

[4] G. Booch, J. Rumbaugh, I. Jacobson, and K. Stencel. UML: przewodnik użytkownika.Wydawnictwa Naukowo-Techniczne, 2001.

[5] S. Chiba. Javassist website. http://www.csg.is.titech.ac.jp/~chiba/javassist/,2008.

[6] Sun Developers Network Community. Java Request for Enhancements, bug no 4449383- Support For ‘Design by Contract’, beyond “a simple assertion facility” . http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4449383, 2008.

[7] Wikipedia Community. Wikipedia, the free encyclopedia. http://en.wikipedia.org,2008.

[8] Apache Software Foundation. Apache Ant build system website. http://ant.apache.org/, 2008.

[9] J. Gosling, B. Joy, G. Steele, and G. Bracha. Java (TM) Language Specification, The(Java (Addison-Wesley)). Addison-Wesley Professional, 2005.

[10] W Homenda. Elementy lingwistyki matematycznej i teorii automatów. Oficyna Wydaw-nicza Politechniki Warszawskiej, Warszawa, 2005.

[11] JE Hopcroft, R. Motwani, and JD Ullman. Introduction to Automata Theory, Languages,and Complexity (3rd edition). Addison-Wesley, 2007.

[12] G.T. Leavens and Y. Cheon. Design by Contract with JML. 2006.

[13] S. Liang and G. Bracha. Dynamic class loading in the Java virtual machine. ACMSIGPLAN Notices, 33(10):36–44, 1998.

[14] S. Loughran and E. Hatcher. Ant in Action. Manning, 2007.

[15] R. Monson-Haefel and B. Burke. Enterprise JavaBeans 3.0, 2007.

114

Page 117: Praca_magisterska

[16] T. Parr. The Definitive ANTLR Reference: Building Domain-specific Languages. Prag-matic Bookshelf, 2007.

[17] T. Parr. Java 1.5 grammar for ANTLR v3. http://www.antlr.org/grammar/1152141644268/Java.g, 2008.

115

Page 118: Praca_magisterska

Warszawa, dnia ................

Oświadczenie

Oświadczam, że pracę magisterską pod tytułem:............................................................................,której promotorem jest ......................... wykonałem samodzielnie, co poświad-czam własnoręcznym podpisem.

.........................