Praca_magisterska

  • Uploaded by: Aleksander
  • 0
  • 0
  • December 2019
  • PDF

This document was uploaded by user and they confirmed that they have the permission to share it. If you are author or own the copyright of this book, please report to us by using this DMCA report form. Report DMCA


Overview

Download & View Praca_magisterska as PDF for free.

More details

  • Words: 49,370
  • Pages: 118
Politechnika Warszawska Wydział 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

......................... podpis promotora

......................... podpis autora

Spis rzeczy 1 Wstęp 1.1 Cel pracy . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.2 Motywacja . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 1.3 Dlaczego Java? . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

3 3 4 6

2 Programowanie przez Kontrakt (PPK) dla języka Java 2.1 O mechanizmach PPK . . . . . . . . . . . . . . . . . . . . 2.1.1 PPK w praktyce . . . . . . . . . . . . . . . . . . . 2.1.2 Elementy PPK występujące w Javie . . . . . . . . 2.1.3 Istniejące implementacje PPK dla języka Java . . . 2.2 Automaty skończone . . . . . . . . . . . . . . . . . . . . . 2.2.1 Klasa jako automat skończony . . . . . . . . . . . 2.3 Propozycja PPK dla języka Java . . . . . . . . . . . . . . 2.3.1 Automat skończony klasy . . . . . . . . . . . . . . 2.3.2 Definicja automatu skończonego . . . . . . . . . . 2.3.3 Przejścia automatu skończonego . . . . . . . . . . 2.3.4 Sprawdzanie stanu automatu skończonego . . . . . 2.3.5 Niezmiennik klasy . . . . . . . . . . . . . . . . . . 2.3.6 Warunki początkowe i końcowe metod . . . . . . . 2.3.7 Warunki kontraktu metody . . . . . . . . . . . . . 2.3.8 Wyjątki . . . . . . . . . . . . . . . . . . . . . . . . 2.3.9 Kontrola kontraktów . . . . . . . . . . . . . . . . . 2.4 Wykorzystanie w wypadku już istniejących komponentów 2.5 Techniczne rozwiązanie problemu . . . . . . . . . . . . . . 2.6 Uzasadnienie i krytyka propozycji . . . . . . . . . . . . . . 2.6.1 Możliwości dalszego rozwoju . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

. . . . . . . . . . . . . . . . . . . .

7 7 10 12 13 14 17 19 20 21 24 27 29 34 37 37 41 42 43 47 49

3 Dynamiczne role 3.1 Czym są dynamiczne role? . . . . . . . . . . . 3.1.1 Obecny stan rzeczy - motywacja . . . 3.1.2 Czym nie są dynamiczne role . . . . . 3.1.3 Możliwości języka Java . . . . . . . . . 3.2 Propozycja dynamicznych ról dla języka Java 3.2.1 Przegląd biblioteki . . . . . . . . . . . 3.2.2 Tworzenie aktorów . . . . . . . . . . . 3.2.3 Tworzenie ról . . . . . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

51 51 51 57 59 60 63 65 66

1

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

3.3 3.4 3.5

3.2.4 Odgrywanie ról . . . . . . . . . . . . . . . . . . 3.2.5 Odłączanie ról . . . . . . . . . . . . . . . . . . 3.2.6 Synchronizacja ról . . . . . . . . . . . . . . . . 3.2.7 Ograniczenia dotyczące użycia ról . . . . . . . Wykorzystanie w wypadku istniejących już rozwiązań Techniczne rozwiązanie problemu . . . . . . . . . . . . Uzasadnienie i krytyka propozycji . . . . . . . . . . . . 3.5.1 Możliwości dalszego rozwoju . . . . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

. . . . . . . .

67 68 69 74 75 76 77 79

A Mechanizm PPK - przykład użycia

81

B Dynamiczne role - przykład użycia

89

C Narzędzia programistyczne 103 C.1 Translator . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 103 C.2 Zadanie dla systemu Apache Ant . . . . . . . . . . . . . . . . . . . . . . . . . 108 D Opis formalny gramatyki dla PPK

110

Bibliografia

114

2

Rozdział 1

Wstęp 1.1

Cel pracy

Celem niniejszej pracy jest propozycja oraz implementacja pewnych dwóch koncepcji z zakresu języków programowania. Pierwsza z propozycji dotyczy mechanizmów Programowania przez Kontrakt1 . Autor uzasadnia tutaj, że klasom jako abstrakcjom języka programowania, można przypisywać pewne automaty skończone, które są przez owe klasy realizowane na poziomie koncepcyjnym. Nowością jest tutaj propozycja autora polegająca na możliwości umieszczenia jawnej deklaracji automatu skończonego w definicji klasy, co pozwala na potraktowanie go jako elementu kontraktu klasy - z wszystkimi tego pożytecznymi konsekwencjami. Druga z propozycji dotyczy tzw. dynamicznych ról2 . Autor precyzuje tutaj znaczenie pojęcia “dynamiczne role” na użytek swojej pracy. Według autora, termin ten ma z grubsza określać dynamiczne zachowanie się obiektów, które pozwala przyjmować im interfejsy oraz związane z nimi funkcjonalności w sposób dynamiczny, nieokreślony jeszcze na etapie kompilacji. Warto tu podkreślić, że mechanizm ten nie jest zwyczajnie rodzajem dynamicznego typowania. Mianowicie, w systemach z dynamiczną kontrolą typów, zmiennej używanej jako argument wywołania funkcji nadal stawiany jest wymóg reprezentowania określonego typu lub możliwości zrzutowania do zmiennej owego typu, tyle tylko, iż sprawdzenie danego warunku następuje dopiero w czasie wywołania. Nie oznacza to jednak, że pewna zmienna może dostosowywać 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ędzi pozwalających na programowanie w nim. Pod pojęciem “narzędzia” kryje się tutaj przede wszystkim translator dla owego języka, wraz z dodatkowymi bibliotekami i programami towarzyszącymi (np. lokalizator błędów). Ponieważ druga część pracy ma charakter czysto praktyczny i polega na demonstracji przedmiotu przy użyciu konkretnego języka programowania, autor zdecydował się przedstawić całość materiału w oparciu o już istniejący język. Językiem tym jest Java. W efekcie prowadzi to do powstania nowego języka programowania, dla wygody i zwięzłości nazwanego 1

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

3

w niniejszej publikacji JavaBC3 . Nie oznacza to oczywiście, że teoretyczne propozycje autora same 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 nie odnosi 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 w szczególności kładzie tu duży nacisk na integrację z Javą, wymagając aby jego rozszerzenie 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óżnych perspektyw. Otóż, oceniając ją pod kątem zawartości teoretycznej, na pierwszy plan wysuwają się propozycje dwóch owych koncepcji, a wykorzystanie Javy mieni się jedynie jako pomocniczy środek demonstracyjny. Z drugiej strony pracę ową można potraktować jako propozycję rozszerzenia języka Java. W takim ujęciu mamy do czynienia z propozycją PPK oraz 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 a nie innych tematów. Jeśli o chodzi o mechanizmy związane z Programowanie przez Kontrakt, autor sam jest gorą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ści i poprawności oprogramowania jakimi są testy jednostkowe oraz ręczna praca programisty z debuggerem. Stosowane wraz z nimi pozwalają wyeliminować większość powstałych na niskim poziomie abstrakcji błędów. Niestety szeroki wachlarz narzędzi związanych z Programowaniem przez Kontrakt nie wystarczał, o dziwo, we wszystkich zagadnieniach z jakimi autor miał styczność. Szczególnym przypadkiem 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 konkretne warunki, jakie powinien spełniać obiekt. Także i przejścia pomiędzy fazami nie mogły odbywać się dowolnie, lecz w sposób ściśle, na poziomie koncepcyjnym, określony. W prosty sposób prowadziło to do umieszczanie w klasie innej klasy. Owa zagnieżdżona klasa była de facto automatem skończonym, którego jedyną funkcją była kontrola poprawności zachowania się klasy gospodarza. W ten sposób narodził się pomysł dodania do mechanizmów Programowania przez Kontrakt kontroli automatów skończonych klas. 3

Nazwa wybrana została czysto arbitralnie, głównie w celu uniknięcia powtarzania rozwlekłych określeń w rodzaju “proponowany przez autora język”. 4 W zasadzie ciężko jest podać przykładu języka który nie pozwala na korzystanie z paradygmatu programowania obiektowego. Dużą część języków wspiera ów paradygmat bezpośrednio, poprzez uzupełnienie składni o specjalne reguły służące do obsługi obiektów (Python, C++, Ruby), część natomiast pozwala pośrednio na programowanie 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 programowanie obiektowe, zostało w toku ewolucji w niniejsze cechy wyposażone (Common Lisp, Fortran 2003).

4

Niestety Java, jako preferowany przez autora język programowania ogólnego przeznaczenia, oferuje jedynie namiastkę mechanizmów Programowania przez Kontrakt. Namiastka ta dostępna jest poprzez słowo kluczowego assert, pozwalające programiście na umieszczenie w 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ązanych z kontrolą prawidłowości wykonania programu. Wyraz temu dany został między innymi w cieszącej się bardzo dużym poparciem prośbie o rozszerzenie języka Java5 , a także w dużej iloś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łoby służyć prezentacji jego koncepcji, lecz w ogóle o większość typowych dla PPK cech, takich jak niezmienniki (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 lub zwyczajnych komentarzach. Autor natomiast zdecydował się osadzić PPK bezpośrednio w w składni języka. Druga z koncepcji autora, tj. dynamiczne role, ma charakter nieco bardziej eksperymentalny. Jest to pewnego rodzaju próba przezwyciężenia trudności związanych z ogólnie przyjętym modelem identyfikator-zmienna, gdzie identyfikator w kodzie programu wskazuje na 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łby aby 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óżne obiekty. Jeszcze raz warto tu podkreślić, że zagadnienie to jest dużo luźniej związane z systemami kontroli typów, nieźli by się to mogło na pierwszy rzut oka wydawać. Popularne w wielu ję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 prowadzi jedynie 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. Przykładem może tu być chociażby tzw. “duck typing”, na który pozwala zyskujący ostatnio na popularnoś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łowanie w oparciu o języki obiektowe poprzez wykorzystanie i pogłębienie imperatywnych cech języków tej klasy. Obie propozycje funkcjonują mianowicie w oparciu o pewien mechanizm zmiany stanu — czy to stanu automatu skończonego, czy też stanu obiektu na rzecz którego odgrywana jest pewna rola. Istotne jest tu także to, że koncepcje te uzupełniają się w pewien sposób nawzajem. Pierwsza z nich służy określaniu wewnętrznych faz cyklu życia danego obiektu za pomocą automatu skończonego. Druga, której zakres jest szerszy, formalizuje natomiast zewnętrzne zmiany funkcjonalności w cyklu życia danego bytu. Konieczność specyfikacji języka oraz przygotowania narzędzi programistycznych wynika natomiast w sposób naturalny ze sformułowanych koncepcji i służy uzasadnieniu (czy wręcz demonstracji) ich praktyczności. Bez części praktycznej praca byłaby, zdaniem autora, zwyczajnie niepełna. 5 6

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

5

1.3

Dlaczego Java?

Autor zdecydował się oprzeć część praktyczną swojej pracy o już istniejący język programowania. 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 raczej propozycje osadzone w kontekście znanym z istniejących języków programowania. Tworzenie nowego języka programowania, jedynie w celu ich prezentacji, nie miałoby ze zrozumiałych przyczyn szczególnego sensu i prawdopodobnie przekroczyłoby w ogóle możliwości autora. W związku z tym wykorzystany został istniejący język. Adaptacja istniejącego języka nastąpiła wskutek, co bardzo istotne, stworzenia jego nad-klasy. Dlaczego jednak wybór autora padł na Javę? Powodów, pomijając ten najbardziej oczywisty, 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 czystej Javie do JavaBC nie wymaga na dobrą sprawę żadnego nakładu pracy i jest praktycznie natychmiastowa. Jest to nota bene najważniejszy z powodów. popularność — Java oraz jej derywaty (jak na przykład C#) stanowią obecnie najpopularniejszą klasę języków ogólnego przeznaczenia. prostota — Przy całkiem sporych możliwościach jakie oferuje Java, język ten wciąż zachowuje względną prostotę i czytelność. Sprzyja to jasnemu przedstawieniu koncepcji autora, które w wypadku innych języków takich jak np. Perl, mogłyby niejako “zaginąć” w gąszczu istniejących cech języka. Co więcej można zaryzykować stwierdzenie, że składnia oparta na C jest w mniejszym lub większym stopniu zrozumiała bodajże dla każdego programisty. brak mechanizmów DBC — Praktyczny brak mechanizmów DBC w Javie stanowi dodatkową zachętę do ich implementacji w oparciu o ten język. szybkość — Wbrew obiegowym opiniom Java jest językiem względnie szybkim, szczególnie jak na język z automatycznym oczyszczaniem pamięci, uruchamiany przy pomocy maszyny wirtualnej. Co więcej jej szybkość ustawicznie wzrasta wraz z kolejnymi edycjami maszyn wirtualnych7 .

7

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

6

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 przez Kontrak”, ukuty został przez przez Bertranda Meyera w trakcie prac nad językiem programowania Eiffel. Oznacza on pewną koncepcję z zakresu projektowania oprogramowania, polegającą na specyfikacji warunków tzw. kontraktu pomiędzy wywoływanym podprogramem a programem go wywołującym. Najbardziej typowym przypadkiem kontraktu jest wywołanie metody funkcji. Samo słowo “kontrakt” użyte zostało tu w nawiązaniu do jego znaczenia 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 podejmują. 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ę cech kontraktów, istnieją praktycznie od zarania historii języków programowania. Zaliczyć można do nich np. a) określenie przez funkcję jej arności oraz b) typów poszczególnych argumentów, c) określanie przez funkcję typu wartości zwracanej, d) konieczność dostarczenia przez kod 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 zatem można nazwać zbiór pewnych ograniczeń dotyczących interakcji pomiędzy poszczególnymi elementami programu. Niech przykładowo dany będzie fragment programu napisanego w języku C: 1 2 3 4 5 6 7

double d i v i d e ( double d i v i d e n t , double d i v i s o r ) { return d i v i d e n t / d i v i s o r ; }; /∗ . . . ∗/ double q u o t i e n t = d i v i d e ( 2 . 0 , 3 . 0 ) ; double q u o t i e n t 2 = d i v i d e ( 1 . 0 ) ; double [ ] q u o t i e n t a r r a y = d i v i d e ( 2 . 0 , 3 . 0 ) ; 1

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

7

Zamieszczony fragment zawiera deklarację funkcji dzielenia — divide — oraz jej wywołanie. Definicja funkcji dzielenia (linijka 3.) wymaga aby wywoływać ją z dwoma argumentami rzeczywistymi oraz zapewnia, iż sama zwraca typ rzeczywisty. Linijka 8. zawiera przykład prawidł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łanie nie dostarcza wszystkich wymaganych parametrów, w drugim natomiast następuje próba potraktowania funkcji tak jakby miała zwracać tablicę. Obie sytuacje niezgodne są z wymogami elementarnego kontraktu określonego w deklaracji funkcji. Sytuacja ta zostanie wykryta przez odpowiednie narzędzie jeszcze na etapie kompilacji, co zapobiegnie stworzeniu wadliwego programu. Niech jednak wywołanie funkcji będzie jak następuje 1 2 3 4 5

double d i v i d e ( d i v i d e n t a , d i v i s o r b ) { return d i v i d e n t / d i v i s o r ; }; /∗ . . . ∗/ double q u o t i e n t = d i v i d e ( 2 . 0 , 0 . 0 ) ;

Linijka 8. zawiera poprawne wywołanie funkcji dzielenia. Większość kompilatorów zaakceptuje powyższy fragment programu bez żadnych zastrzeżeń. Program ten jednak ewidentnie nie jest programem prawidłowym. Dzielenie przez zero, jakie spróbowałby wykonać, jest niedopuszczalne. W tej sytuacji spełnienie podstawowych wymogów wywołania funkcji nie jest warunkiem dostatecznym prawidłowości wywołania. Okazuje się, że w rzeczywistości kontrakt powinien zostać zaopatrzony w dodatkowy, sprawdzany w czasie wykonania, warunek dotyczący argumentów wywołania. Warunek ten można nazwać warunkiem wstępnym (ang. precondition). W niniejszym przypadku powinien polegać on na dodatkowym wymaganiu co do 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ę z tego, 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 na słowne doprecyzowanie warunków kontraktu. W razie wątpliwości, programista może dzięki temu otworzyć plik z deklaracją danej funkcji i, czytając komentarz jakim jest opatrzona, zapoznać się z dodatkowymi wymogami jej kontraktu. • Wreszcie, w ciele funkcji dzielenia można umieścić instrukcję upewniającą się, że wartość dzielnika jest różna od zera. W razie podania nieprawidłowej wartości instrukcja ta powinna przerwać wykonywanie programu. Przerwanie może ewentualnie zostać poprzedzone komunikatem o złamaniu kontraktu. Mimo że wyżej przedstawione rozwiązania należą do powszechnie stosowanych, mają one jednak 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ób w zasadzie także nie wymusza żadnej kontroli wywiązywania się z kontraktu, nie gwarantuje nawet, że programista zapozna się z komentarzami2 . Trzeci z zaproponowanych sposobów jest 2

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

8

tutaj możliwie najlepszy, choć posiada jeden zasadniczy mankament — prowadzi do przemieszania 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 2 3 4 5 6 7 8

double d i v i d e ( d i v i d e n t a , d i v i s o r b ) in { assert b != 0 ; } body { return d i v i d e n t / d i v i s o r ; }; /∗ . . . ∗/ double q u o t i e n t = d i v i d e ( 2 . 0 , 0 . 0 ) ;

Oczywiście możliwość jawnej deklaracji kontraktu dostępna jest jedynie w wypadku niektó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ęzyk ten umożliwia programiście na stosowanie następujących konstrukcji z zakresu Programowania przez Kontrakt: warunków wstępnych podprogramów (ang. preconditions), wykorzystywanych na ogół do sprawdzenia 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 nastąpiło wywołanie. Może to być np. stan klasy której metoda wywołana została jako podprogram. W języku Eiffel konstrukcja określająca takie warunki była związane ze sł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 konstrukcja określająca takie warunki była związane ze słowem kluczowym ensure i wystę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ę dodatkowych elementów kontraktu takich jak specyfikacja efektów ubocznych podprogramów (ang. side effects), czyli pewnych modyfikacji swojego kontekstu, jakich mogą dopuścić się podprogramy — oprócz swojego podstawowego zadania jakim jest zwracanie wartości. Jako szczególny rodzaj efektów ubocznych, można tu wyróżnić modyfikacje pola obiektu na rzecz którego wywołana została metoda oraz modyfikacje 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. 3 Wieloparadygmatowy język ogólnego przeznaczenia stworzony przez Waltera Brighta. Oparty na syntaksie C++ i, z różnym skutkiem, promowany jako jego następca.

9

efektów ubocznych wiąże się ściśle z pojęciem “kontroli stałych” (ang. const correctness) która w rzeczywistości jest bardzo zawężonym i jednocześnie dosyć powszechnie występującym mechanizmem Programowania przez Kontrakt. W kontrolę stałych wyposaż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 w sobie nie stanowią elementów kontraktu, ale umieszczone w odpowiednim miejscu mogą być używane jako ich substytut. Przykładowo sprawdzanie warunku wstępnego metody można uzyskać poprzez umieszczenie asercji z owym warunkiem na samym początku ciała danej metody.

2.1.1

PPK w praktyce

Ponieważ zasady programowania przez kontrakt nie zwiększają samoistnie funkcjonalności języka, a prawidłowe programy napisane z ich wykorzystanie działają równie dobrze po usunięciu kontraktów, rodzi się pytanie o ich faktyczną przydatność. Czemu wprawny, tworzący biegle 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 bardziej idealnych programów. Autor nigdy osobiście nie słyszał o dużym przedsięwzięciu informatycznym, którego produkt końcowy nie zawierałby błędów - często ujawnionych nie tylko na 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łącznym elementem 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 doprowadzić do ich usunięcia. Oczywiście cały cykl testów i kontroli jakości oprogramowania można sprowadzić jedynie do funkcjonalnych testów systemowych, gdzie wystąpienie każdego błędu oznacza konieczność, często bardzo trudnej, próby potwierdzenia jego powtarzalności oraz dojścia do jego rzeczywistej przyczyny. Mimo że praktyka taka jest często stosowana, wykrycie błędu na tym etapie pociąga ze sobą bardzo wysokie koszty jego usunięcia. Z tej właśnie przyczyny, jeszcze przed testami systemowymi, przeprowadza się często testy integracyjne, służące sprawdzeniu interakcji pomiędzy poszczególnymi modułami systemu. Z kolei poszczególne moduły testuje się za pomocą, jeszcze bardziej podstawowych w zakresie swojego przedmiotu, testów jednostkowych (zwanych także testami modułowymi). Jest to pewien dodatkowy nakład pracy, który w dłuższej perspektywie zawsze zwraca się z nawiązką. Podobnie prewencyjny charakter mają także, mniej wśród programistów popularne, elementy Programowania 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. Mechanizmy PPK, potraktowane jako dodatkowy etap testów, okazują się jeszcze bardziej zautomatyzowanym sposobem wykrywania błędów niż testy jednostkowe. Sprawdzanie kontraktów zaczyna się tu już na etapie poprzedzającym testy jednostkowe. Sam przedmiot testów jest wówczas 10

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 uruchomienie programu lub jego fragmentu, staje się automatycznie testem kontraktów. Wreszcie, testy jednostkowe mogą być tworzone w oparciu o wyspecyfikowane wcześniej kontrakty. Współczesne narzędzia programistyczne umożliwiają nawet automatyczne tworzenie testów jednostkowych na podstawie danych kontraktów. Rysunek 2.1 podsumowuje umieszczenie mechanizmów PPK w kontekście etapów testowania oprogramowania. W tym świetle mechanizmy PPK przestają jawić się jako swego rodzaju puryzm językowy (“skoro język X umożliwia stosowanie elementów DBC, należy je wykorzystywać”) lecz nabierają znaczenia jako jeden z etapów wczesnej kontroli jakości oprogramowania. Jest to największy, aczkolwiek nie jedyny, pożytek płynący z praktycznego stosowania Programowania 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 skonkretyzowanego podejścia do tworzonych komponentów. Przypomina to w pewnym sensie metodologię Rozwoju przez Testy (ang. Test Driven Development) zastosowaną w znacznie mniejszej skali. O ile w wypadku RPT tworzenie komponentu zaczynało się od przygotowania testów jego funkcjonalności, o tyle tworzenie klasy bądź funkcji (∼metody) zaczyna się od napisania jej sygnatury, której częścią jest kontrakt. Drugim pomniejszym pożytkiem płynących ze stosowania zasad Programowania przez Kontrakt jest zwiększenie czytelności i klarowności kodu. Rzut okiem na osadzony w składni języka kontrakt jest niejednokrotnie lepszym i szybszym sposobem na zapoznanie się z wymogami narzucanymi użytkownikowi przez procedurę niż czytanie komentarza jakim została opatrzona. Reasumując, Programowanie przez Kontrakt: • sprzyja wykrywaniu błędów, • wpaja dobre nawyki programistyczne, • zwiększa czytelność kodu.

11

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 to automatycznie spełnienie pewnych warunków wstępnych dotyczących ilości oraz typów parametru a także warunków końcowych dotyczących typu wartości zwracanej. Przykładowo metoda mająca w swej sygnaturze wyspecyfikowaną klasę String gwarantuje w sposó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 kompilator. 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 jawna informacja o możliwych błędach, natomiast od odbiorcy jawna ich obsługa — czy to przez przechwycenie, czy też przez zignorowanie4 . Brak owych informacji powoduje zgł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ści danej zmiennej jest zabronione. Próba wykonania takiej operacji wykrywana jest jeszcze na etapie kompilacji. Mechanizm ten jest niestety bardzo ograniczony i w szczególności nie można go wykorzystać do zapobiegania modyfikacjom obiektu na który wskazuje referencja. • Wreszcie, Java udostępnia prosty mechanizm asercji. W odróżnieniu od dwóch powyższych elementów, poprawność asercji nie jest sprawdzana na etapie kompilacji lecz podczas 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 maszyny wirtualnej jakim jest przełącznik -ea5 . Ponieważ mechanizm asercji stanowi podstawę funkcjonowania mechanizmów PPK stworzonych przez autora, warto więc, w celu jego dodatkowej ilustracji, posłużyć się prostym przykładem: 1 2 3 4 5 6 7

public c l a s s AssertionsDemo { public s t a t i c void main ( S t r i n g argv [ ] ) { assert ! ( argv . l e n g t h > 0 && ” f a i l ” . e q u a l s ( argv [ 1 ] ) ) : ” Program f a i l e d ” ; System . out . p r i n t l n ( ” H e l l o a s s e r t s ” ) ; } }

4 W 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. 5 Najpopularniejsza 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łączenie kontroli asercji tylko dla wybranych pakietów. Więcej informacji na ten temat można uzyskać poprzez wywołanie java -?

12

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. Warunek 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 AssertionsDemo Hello asserts $ java AssertionsDemo fail Hello asserts $ java -ea AssertionsDemo Hello asserts $ java -ea AssertionsDemo fail Exception 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 sprawdzania asercji, a sam warunek asercji został złamany, nastąpiło odrzucenie wyjątku języka Java — java.lang.AssertionError. Ponieważ wyjątek nie został przechwycony, na standardowym wyjściu błędu został wyświetlony odpowiedni komunikat. W skład komunikatu wszedł m.in umieszczony w kodzie programu ciąg Program failed oraz nazwa pliku ź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 znaki praktycznie od momentu poczęcia tego języka. Najpełniejszy temu wyraz dała sformułowana i poparta przez sporą część środowiska skupionego wokół Javy prośba o uzupełnienie języka o mechanizmy programowania kontraktowego[6]. Mimo iż od złożenia prośby minęło już bez mała 8 lat, nie należy oczekiwać, iż kiedykolwiek zostanie ona pozytywnie rozpatrzona. Wyraż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 bibliotek oferują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ęzyka Java - Java Modeling Language oraz Contract4J. Biblioteki te dobrze ukazują dwa główne podejścia do kwestii sposobu deklaracji kontraktów. Mianowicie pierwsza z nich wymaga umieszczania deklaracji kontraktów w komentarzach, z kolei druga wykorzystuje w tym celu niedawno wprowadzony do Javy mechanizm adnotacji. Co istotne, żadna z bibliotek nie wzbogaca języka o nową składnię. Dobrą cechą takiego rozwiązania jest to, iż programy z tak zadeklarowanymi kontraktami mogą być kompilowane przy użyciu standardowego kompilatora Javy - w takiej sytuacji informacje o kontraktach zostaną na etapie kompilacji zignorowane. Elastyczność taka ma niestety swoją

13

JML - Java Modeling Language[12] Integracja z językiem Syngalizacja błędu Wyłączenie kontroli Możliwości Użycie

- deklaracje zawarte w komentarzach - poprzez wyjątki - powtórna kompilacja programu - szeroki wachlarz konstrukcji: warunki wstępne i końcowe, efekty uboczne (∼kontrola stałych), niezmienniki, asercje - 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 b i n a r y S e a r c h ( 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 z bibliotek 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 7 ang. Finite Automaton. Przedstawiona tu konstrukcja jest w rzeczywistości pewnym rodzajem automatu skończonego, nazywanym w literaturze “deterministycznym automatem skończonym” (z a.g Deterministic Finite Automaton). Ponieważ jednak w pracy niniejszej nie pojawiają się żadne inne, poza deterministycznym, rodzaje automatów skończonych, autor na określenie “deterministycznego automatu skończonego” będzie posł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

Contract4j[1] Integracja z językiem Syngalizacja błędu Wyłączenie kontroli Możliwości Użycie

-

przy pomocy adnotacji poprzez wyjątki powtórna kompilacja programu warunki wstępne i końcowe, niezmienniki za pomocą narzędzi środowiska AspectJ6

Przykład @Contract public c l a s s MyClass { @Invar ( ”name != n u l l ” ) private S t r i n g name ; @Pre ( ”n != n u l l ” ) public void setName ( S t r i n g n ) { name = n ; } @Post ( ” $ r e t u r n != n u l l ” ) public S t r i n g 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 — elementów ze zbioru Σ automatu, można przyjąć iż stan końcowy Sn dla danego wejścia wyraża się wzorem Sn = δ( ... δ(δ(q0 , k1 ), k2 ) ... , kn ). Jeśli Sn ∈ F można stwierdzić, iż automat zaakceptował 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ący się z a) taśmy zawierającej słowo wejściowe, b) głowicy służącej do odczytu taśmy oraz c) jednostki sterującego . W chwili rozpoczęcia obliczeń jednostka sterująca znajduje się w stanie q0 , a głowica umieszczona jest nad pierwszą komórką taśmy. Pojedynczy krok obliczeń polega na zmianie stanu, określanej za pomocą funkcji przejścia (argumentami są tu bieżący stan sterowania 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. Stan sterowania w chwili zakończenia obliczeń jest stanem końcowym, przy czym stany sterowania implementacji automatu można i należy utożsamiać w tym przypadku ze stanami należącymi do zbioru stanów automatu. Więcej szczegółów na temat konstrukcji tego typu implementacji można znaleźć w [10, rozdział 7]. Schemat obliczeń implementacji automatu skończonego o konstrukcji podobnej do powyższej przedstawia się często za pomocą diagramu. Diagram taki na ogół jest ukazany w 8 Można dla uproszczenia przyjąć, że funkcja przejścia jest określona dla wszystkich argumentów ze swojej dziedziny.

15

Rysunek 2.2: Diagramu automatu akceptującego niepuste ciągi binarne, nie zawierające sekwencji 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 krawę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. Interpretacja diagramu jest zgodna z intuicją, co więcej, na jej podstawie można w sposób jednoznaczny odtworzyć definicję automatu skończonego, który przedstawia. Z tego też powodu diagram automatu skończonego można uznać za jego definicję. W dalszej części pracy autor będzie utożsamiał pojęcie automatu skończonego z jego implementacją, a w miejscu formalnych definicji będzie posługiwał się diagramami. Rysunek 2.2 przedstawia diagram automatu skończonego akceptującego niepuste ciągi binarne nie zawierające sekwencji złożonej z tych samych cyfr o długości większej niż 3. Stany q1 − q6 są stanami akceptującymi. Przejście do stanu 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 stanu akceptują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ępowały stany nie mające zdefiniowanych przejść dla niektórych sygnałów. W takim przypadku zakł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 dodatkowego stanu jest kwawędzią zwrotną. W świetle powyższej uwagi, automat z rysunku 2.2 mógłby zostać równie dobrze przedstawiony 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 znaleźć w klasycznej pracy [11, rozdział 2]. Zagadnienie to jest także dobrze zarysowany w [10]. 9

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

16

Rysunek 2.3: Uproszczony diagram automatu z rysunku 2.2. Diagram nie zawiera jawnej informacji na temat krawędzi wychodzących ze stanów q3 i q6 z odpowiadającymi etykietami 0 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) rozpoznawanie języków regularnych oraz b) szeroko rozumiane modelowanie. W pierwszym wypadku automat używany jest po prostu do uzyskania odpowiedzi “tak” lub “nie”. W drugim natomiast przypadku oczekuje się na ogół odpowiedzi “tak”, która jest wyrazem poprawności działania bądź użytkowania modelowanego bytu. Uzyskanie odpowiedzi “nie” traktuje się tu jako 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ńczony jako model pewnego mechanizmu. Aby pewien proces (∼obiekt, mechanizm) mógł zostać zamodelowany w oparciu a automat skończony, wystarczy w zasadzie aby: 1. W każdym momencie swojego cyklu życia znajdował się w pewnym konkretnym, pochodzą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 momencie bezpoś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

szczególności sposobu ich działania10 . Na uzasadnienie takiego podejścia autor przytoczy prosty 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 do rozważ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ę z centralką telefoniczną. Wysłuchuje menu głosowego z którego wybiera odpowiednią 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 operatorów. W efekcie dochodzi do rozmowy, która kończona jest wraz z zerwaniem połączenia przez jedną ze stron. Możliwe są także różnorodne wariacje powyższego scenariusza, np. wielokrotne przełączenia rozmowy pomiędzy różnymi operatorami lub odkładanie przez zirytowanego klienta słuchawki w czasie oczekiwania na wolnego operatora. Proces ten ma definitywnie charakter automatu skoń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.4 zawiera schemat owego automatu (szczegóły nie są istotne). Każde połączenie przychodzące od klienta jest monitorowane — w trosce o zapewnienie wysokiej 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 na autorze danej klasy dostosowanie jej do schematu działania centralki, w szczególności do kolejnym etapów połączenia. W związku z tym model procesu jest wykorzystany także w klasie monitorującej. Innymi słowy klasa monitorująca jest opisana także za pomocą przedstawionego na rysunku 2.4 automatu skończonego. Warto tu zauważyć, iż każdy możliwy scenariusz powinien kończyć się w stanie akceptującym, jakim jest Hng. Otóż klasa monitorująca połączenia nie służy stwierdzaniu, czy dany scenariusz jest prawidłowy, czy też nie. Klasa ta akceptuje tylko i wyłącznie scenariusze prawidłowe. Sytuacja w której obiekt połączenia skończy swoje działanie w stanie innym niż akceptujący nie jest tutaj dopuszczalna. Zaistnienie nieprawidłowego scenariusza (rozumianego jako ciąg sygnałów wejściowych) będzie oznaczać błąd implementacji systemu. Jest to 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 niniejszej pracy. Reasumując, automat skończony można potraktować jako swego rodzaju schemat działania danej klasy. Oczywiście modelowania klas za pomocą automatów skończonych nie zawsze ma sens. Do przypadków takich można zaliczyć klasy o bardzo prostej strukturze (np. klasa reprezentująca wektor) lub klasy o wybitnie jednostanowym charakterze (np. klasy niemodyfikowalne). W takich sytuacjach nic jednak nie stoi na przeszkodzie, aby tego nie robić — modelowanie działania klas nie jest elementem obligatoryjnym lecz pomocniczym. 10

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

18

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łączenie zerwane (zakończone).

2.3

Propozycja PPK dla języka Java

Na przygotowaną przez autora propozycję mechanizmów Programowania przez Kontrakt dla ję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 w dalszych rozdziałach. Istota działania warunków wstępnych i końcowych oraz niezmienników została przedstawiona przy okazji przeglądu mechanizmów PPK w sekcji 2.1. Informacje szczegółowe na temat gramatyki umieszczono w dodatku D. Krótkie słowo wyjaśnienia należy się natomiast ostatniemu z mechanizmów, tj. “automatom skończonym klas”. Otóż, w myśl zamierzeń autora, programista dostaje możliwość jawnej specyfikacji automatu użytego do zamodelowania klasy. Automat ów wykorzystywany może być następnie podczas kontroli kontraktów. Dla tak zdefiniowanego automatu programista otrzymuje możliwość zmiany stanu obiektu oraz sprawdzenia stanu aktualnego. Związek pomiędzy modelowaniem klas za pomocą automatów skończonych a mechanizmami Programowania przez Kontrakt może z początku wydawać się tu niejasny. Wynika to stą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 opisania schematu funkcjonowania całej klasy. Jest to jednak różnica pozorna. Jawna deklaracja automatu skończonego jest, mianowicie, konstrukcją podobną do niezmiennika klasy. Obie deklaracje 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. 11 Jest to w zasadzie najważniejszy z argumentów przemawiających za zaliczeniem tego dodatkowego mechanizmu w poczet tradycyjnych elementów Programowania przez Kontrakt

19

Tak jak w wypadku niezmiennika klasa gwarantuje, że jego warunki nie zostaną przy jakimkolwiek 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 zmiany stanu obiektu, jaką to otrzymuje programista, automatycznie zostaje obwarowana warunkami sprawdzającymi prawidłowość takiego przejścia. Działanie niezgodne ze schematem automatu powinno 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 prowadził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że jest 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ńczonego, a także udostępnia operację zmiany stanu oraz sprawdzenia aktualnego stanu. Podstawową składnię i sposób użycia owych konstrukcji przedstawia dobrze listing 2.1. 1 2 3 4 5 6 7 8 9 10 11 12 13 14

public c l a s s AutomatonDemo { automaton { i n i t i a l S0 : S1 ; accepting S1 ; } public void callMeOnce ( ) { transient : S1 ; } public boolean w a s A l r e a d y C a l l e d ( ) { return transient ? S1 ; } }

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 zawierają definicje dwóch stanów. Stan S0 określony został jako stan początkowy, a przejście zeń do stanu S1 określone zostało jako dopuszczalne. Stan S1 oznaczony został jako stan końcowy. Procedura callMeAtLestOnce() zawiera pojedynczą instrukcję zmiany stanu — w tym wypadku na stan S1. W linijce 12. znajduje się natomiast wyrażenie sprawdzenia czy aktualny 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 pewnym sensie stan wyimaginowany, dopełniający jedynie diagram automatu skończonego. Zbiór sygnałów wejściowych jest tutaj równy liczbie zadeklarowanych stanów, przy czym pojedynczy sygnał utożsamiany jest z instrukcją zmiany stanu na dany. Wszystkie dopuszczalne przejścia prowadzą do odpowiadających im stanów, natomiast przejścia niedopuszczalne kończą się w stanie E. W rzeczywistości stan ten nie jest nigdy osiągalny, gdyż wejście weń może być tylko i wyłącznie wynikiem złamania kontraktu.

20

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 po usunięciu stanu martwego E. Zastosowanie takiego podejścia skutkuje pewnymi ograniczeniami. Na przykład nie jest możliwe zdefiniowanie sygnału wejściowego, który z różnych stanów początkowych prowadziłby do różnych stanów końcowych12 . Jest to swego rodzaju kompromis pomiędzy szczegółowością definicji i jej klarownością na jaki poszedł autor. Powyższe uproszczenie skutkuje zwię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 automat skoń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ńczonego można doprecyzować przy użyciu pozostałych mechanizmów Programowania przez Kontrakt proponowanych 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że nazw 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, zawarte pomiędzy nawiasami klamrowymi, ciało automatu, przy czym symbol leksykalny reprezento12 Tak naprawdę można się spierać o to, co jest sygnałem wejściowym. Zamiast instrukcji przejścia, jako sygnał wejściowy można równie dobrze potraktować wywołanie metody. Przy takim podejściu, byłoby możliwe aby te same sygnały powodowały, w zależności od aktualnego stanu, przejścia do różnych stanów. Autor jednak nie uważa takiej interpretacji zbioru sygnałów wejściowych za zbyt praktyczną.

21

wany przez leksem “automaton” nie jest słowem kluczowym13,14 . Sama definicja automatu może wystąpić jedynie wewnątrz dowolnej klasy, jako jej bezpośredni członek. Wyjątek stanowią tu enumeracje, w których ciele definicje automatów nie są dopuszczalne. Definicja automatu dotyczy klasy, w której została bezpośrednio zdefiniowana. Dla danej klasy może istnieć co najwyżej jedna definicja automatu. Ciało automatu musi składać się z jednej lub większej liczby definicji stanów. Konieczne jest określenie stanu początkowego. Zaleca się także wybranie jednego lub większej ilości stanów akceptujących15 . Definicja pojedynczego stanu składa się z opcjonalnych modyfikatorów: initial używanego 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łowa kluczowe. Po opcjonalnych modyfikatora należy podać identyfikator będący nazwą stanu. Nie jest 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 docelowych. Dopuszczalna jest specyfikacja przejścia zwrotnego. Dublowanie nazw stanów na liście stanów docelowych nie jest zalecane. Zdublowane stany zostaną w takiej sytuacji zignorowane. Niedopuszczalne jest natomiast używanie nazw stanów nie zdefiniowanych w danym automacie. Definicja stanu kończy się obowiązkowym znakiem średnika - nawet sytuacji gdy jest to jedyna definicja w automacie. Wszystkie zdefiniowane w automacie stany powinny być osiągalne ze stanu początkowego. Szczegółowe informacje na temat składni zawarte są w dodatku D. Niech dla przykładu dany będzie uproszczony diagram automatu skończonego jak na rysunku 2.7. Definicja automatu skończonego odpowiadająca diagramowi przedstawiona została na listingu 2.2.

13

W szczególności oznacza to, iż pula dostępnych na użytek programisty identyfikatorów nie zostaje uszczuplona 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 za stosowne jego wyraźne tu podkreślenie 14 Czasami na określenie tak wyróżnionych symboli leksykalnych używa się określenia “słowo pseudokluczowe” (ang. pseudo keyword). Wszystkie słowa wprowadzone przez autora na użytek składni Programowania przez Kontrakt są słowami pseudokluczowymi. 15 Użycie przez autora słów w rodzaju “konieczne”, “zabronione” bądź “niedopuszczalne”, oznacza iż złamanie danej reguły doprowadzi do błędu translacji. Taki sam imperatywny charakter posiadają także stwierdzenia napisane w trybie oznajmującym, w rodzaju “wyrażenie kończy się znakiem średnika”. Wyjątek stanowią tu stwierdzenia 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órych nieprzestrzeganie kończy się jedynie ostrzeżeniem podczas translacji.

22

Rysunek 2.7: Uproszczony diagram automatu skończonego dla listingu 2.2 public c l a s s 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. Listing 2.4 ukazuje natomiast konstrukcje niepoprawne semantycznie. public c l a s s ComplexAutomatonDemo { automaton { accepting automaton ; /∗ l e k s e m ” automaton ” , t a k samo j a k ” i n i t i a l ” oraz ” accepting ” , nie j e s t traktowany jako slowo k l u c z o w e , moze byc zatem w y k o r z y s t y w a n y j a k o i d e n t y f i k a t o r , w tym p r z y p a d k u nazwa s t a n u ∗/ accepting s e c o n d : i n i t i a l , second , accepting ; initial initial : second , STATE3, automaton ; accepting ; STATE3 : not accepting ; n o t a c c e p t i n g ; /∗ s t a n n i e musi o k r e s l a c zadnego s t a n u d o c e l o w e g o ∗/ } private i n t e r f a c e N e s t e d I n t e r f a c e { private c l a s s 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

i n i t i a l accepting STATE3 : n o t a c c e p t i n g ; /∗ u n i k a t o w o s c nazw stanow o b o w i a z u j e j e d y n i e w o b r e b i e danego automatu ∗/ not accepting ; } } } }

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

public c l a s s ComplexAutomatonDemo { automaton { accepting STAN1 ; // b l a d − b r a k s t a n u p o c z a t k o w e g o } 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 e n u m e r a c j i i 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 − b r a k s t a n u a k c e p t u j a c e g o c l a s s Nested { automaton { i n i t i a l STAN2 : STANX; // b l a d , b r a k d e f i n i c j i STANX accepting STAN2 ; // b l a d − powtorna d e f i n i c j a s t a n u } } } automaton { // b l a d − automat d l a d a n e j k l a s y z o s t a l j u z z d e f i n i o w a n y i n i t i a l accepting accepting STAN1 : STAN2 ; // o s t r z e z e n i e − z d u b l o w a n i e m o d y f i k a t o r a STAN2 : STAN1, STAN2, STAN1 ; // o s t r z e z e n i e − z d u b l o w a n i e s t a n u d o c e l o w e g o } }

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 w klasie automatu skończonego. W szczególności każdy obiekt danej klasy posiada swój własny aktualny stan. Wraz z chwilą stworzenia nowej instancji otrzymuje ona automatycznie stan początkowy. Od klasy zawierającej definicję automatu skończonego wymaga się także,

24

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 jak złamanie kontraktu klasy. Podczas cyklu życia danego obiektu, mogą być dokonywane zmiany jego stanu (w wypadku klas 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 czym instrukcja owa może być umieszczona jedynie wewnątrz klasy, której dotyczy. Co więcej musi mieć ona dostęp do instancji klasy, której stan ma zostać zmieniony — w szczególności oznacza 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 nazwy stanu 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 instrukcji zmiany stanu jest traktowana jak złamanie kontraktu. Linijka 8. z listingu 2.1 zawiera przykładową instrukcję zmiany aktualnego stanu obiektu na 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ładowo klasa z listingu 2.1 posiada dwa stany przy czym stan początkowy nie jest stanem akceptującym. Zmiana stanu na akceptujący może się w jej wypadku dokonać jedynie wewnątrz metody callMeOnce. Jak sama nazwa wskazuje, metoda ta powinna zostać, dla danego obiektu, wywołana raz i tylko raz w czasie jego istnienia. Pojedyncze wywołanie owej metody spowoduje zmianę stanu na końcowy. Inne scenariusze użycia obiektu doprowadzą do złamania 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 naruszy warunek jego kontraktu. Dla odmiany każda próba powtórnego wywołania owej metody doprowadzi także do wykonania niedopuszczalnego przejścia (ze stanu S1 z powrotem w stan S1), co również zaowocuje złamaniem kontraktu. Czasami ta sama nazwa stanu może występować w dwóch różnych automatach, przy czym w danym kontekście możliwa jest zmiana stanu dowolnego z nich. W razie wystąpienia takiej niejednoznaczności, wykorzystana zostanie możliwie najbardziej zagnieżdżona z klas. Sytuację ową przedstawia listing 2.5. 1 2 3 4 5 6 7 8 9 10

public c l a s s Outer { class Inner { automaton { i n i t i a l S0 : S1 ; accepting S1 ; } void doSomething ( ) { transient : S1 ; /∗ zmiana s t a n u z o s t a n i e d o m y s l n i e dokonana na r z e c z i n s t a n c j i k l a s y I n n e r ∗/ 16

Autor, w związku z istnieniem tego warunku, stanowczo odradza działań polegających na wskrzeszaniu finalizowanych 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

11 12 13 14 15 16 17

} } automaton { i n i t i a l S0 : S1 ; accepting S1 ; } }

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 to programiście na rozstrzyganie niejednoznaczności co do klasy na rzecz której ma zostać wykonane przejście. Listing 2.6 zawiera przykład z listingu 2.5 zmodyfikowany tak aby instrukcja zmiany stanu dotyczyła klasy Outer. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

public c l a s s Outer { class Inner { automaton { i n i t i a l S0 : S1 ; accepting S1 : S1 ; } void doSomething ( ) { transient : Outer . S1 ; /∗ zmiana s t a n u z o s t a n i e dokonana na r z e c z k l a s y (w z a s a d z i e − j e j i n s t a n c j i ) Outer ∗/ // 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 : transient : I n n e r . S1 ; // zmiana s t a n u z o s t a n i e dokonana na r z e c z k l a s y I n n e r transient : S1 ; // zmiana s t a n u z o s t a n i e dokonana na r z e c z k l a s y I n n e r } } automaton { i n i t i a l S0 : S1 ; accepting S1 ; } }

Listing 2.6: Rozwiązanie problemu identycznie nazwanych stanów poprzez wykorzystanie kwalifikowanych 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ą 17

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

26

na próbie odwołania się do automatu skończonego ze statycznego kontekstu. Odwołania takie są niemożliwe z racji tego, że instancje automatów przypisane są bezpośrednio instancjom klas. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

public c l a s s Outer { static { transient : S1 ; /∗ b l a d − proba zmiany s t a n u wewnatrz 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 e i n s t a n c j o m Klasy , n i e samej k l a s i e j a k o t a k i e j ∗/ } automaton { i n i t i a l S0 : S1 ; accepting S1 ; } Outer ( ) { transient : I n n e r . S1 ; // ∗ b l a d − i n s t r u k c j a zmiany s t a n u moze byc uzyta j e d y n i e wewnatrz d a n e j k l a s y , w tym wypadku Outer ∗/ } static class Inner { automaton { i n i t i a l S0 : S1 ; accepting S1 : S1 ; } void doWrong ( ) { transient : Outer . S1 ; /∗ b l a d − proba o d w o l a n i a s i e do s t a n u z e s t a t y c z n e g o k o n t e k s t u ∗/ transient : S2 ; /∗ b l a d − n i e z n a n y s t a n S3 ∗/ } } }

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 sprawdzenia aktualnego stanu. Wyrażenie to składa się ze słowa transient, znaku pytajnika oraz niepustej listy stanów. Mechanizm określania automatów, do których odnoszą się poszczególne stany jest taki sam, jak w wypadku instrukcji zmiany stanu. W wypadku niejednoznacznoś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 w nawiasy zwyczajne. W takiej sytuacji zaleca się także, aby wszystkie stany odnosiły się do tego 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 taki sam jak w wypadku instrukcji zmiany stanu. Wyrażenie sprawdzenia stanu atrybutowane jest typem boolean. Oznacza to, że może ono zostać wykorzystane wszędzie tam, gdzie oczekuje 27

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 stanem aktualny. Należy tu jeszcze nadmienić, że wyrażenie sprawdzenia stanu ma charakter czysto pomocniczy 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, że wyrażenie sprawdzenia zmiany stanu zwraca wartości logiczną, co pozwoliło na skorzystanie zeń wewnątrz instrukcji return (funkcja sama zwraca wartość logiczną). W listingu 2.8 zawarto przykłady różnych, poprawnych semantycznie wyrażeń sprawdzenia aktualnego stanu. Listing 2.9 ukazuje natomiast niepoprawne semantycznie próby użycia owej instrukcji. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34

public c l a s s Test { automaton { i n i t i a l S0 : S1 ; accepting S1 : S2 ; S2 : S3 ; accepting S3 ; } boolean i s A c c e p t i n g ( ) { return transient ? ( S1 , S2 ) ; } boolean computeSomeValue ( ) { return transient ? ( S1 , Test . S2 ) ? transient ? Test . S2 : transient ? ( S3 ) ; // w y r a z e n i e s p r a w d z e n i a s t a n u ma n a j w y z s z y mozliwy p r i o r y t e t // w y z s z y w s z c z e g o l n o s c i od o p e r a t o r a ? : } class Inner { automaton { i n i t i a l S0 : S1 ; accepting S1 : S1 ; } void doSomething ( ) { boolean v a l u e = transient ? Test . S1 ; v a l u e = transient ? ( S0 , S1 ) ; } boolean getTrue ( ) { return transient ? ( Test . S0 , Test . S1 , Test . S2 , Test . S3 ) ; } } }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

public c l a s s Test { automaton { i n i t i a l S0 : S1 ; accepting S1 : S2 ; S2 : S3 ; accepting S3 ; ∗ } Test ( ) { boolean v a l u e = transient ? S4 ; // b l a d − n i e z n a n y s t a n S4 transient ? S0 ; // b l a d − w y r a z e n i e n i e j e s t s a m o d z i e l n a i n s t r u k c j a } class Inner { automaton { i n i t i a l S0 : S1 ; accepting S1 : S1 ; } void doSomething ( ) { boolean v a l u e = transient ? ( S0 , S1 , S2 , S3 ) ; // o s t r z e z e n i e − o d n o s z e n i e s i e do r o z n y c h automatow wewnatrz // i n s t r u k c j i s p r a w d z e n i a s t a n u : s t a n y S0 o r a z S1 z o s t a n a // d o m y s l n i e p r z y p i s a n e do automatu k l a s y I n n e r . Z k o l e i s t a n y // S2 i S3 d o t y c z a automatu k l a s y Test } boolean getTrue ( ) { return transient ? Test . S4 ; // b l a d − automat k l a s y Test n i e d e f i n i u j e s t a n u S4 } } }

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. Niezmienniki, zgodnie ze swoim standardowym przeznaczeniem, pozwalają na deklarację globalnych warunków kontraktu, które powinny być spełnione zawsze w pewnych określonych punktach wykonania programu(∼podczas rozpoczęcia i zakończenia wywołania metody). Autor w swojej 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 2 3 4 5

public c l a s s InvariantDemo { automaton { i n i t i a l accepting IDLE : BUSY; accepting BUSY : IDLE ; }

29

6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

private S t r i n g s t a t u s = ” i d l e ” ; invariant { ∗ : done != null : ” done == n u l l ” ; IDLE : ” i d l e ” . e q u a l s ( s t a t u s ) ; BUSY : ” busy ” . e q u a l s ( s t a t u s ) ; } void b e g i n ( ) { transient : BUSY; s t a t u s = ” busy ” ; } void f i n i s h ( ) { transient : IDLE ; status = ” idle ” ; } protected g e t S t a t u s ( ) { return s t a t u s ; } }

Listing 2.10: Przykład użycia niezmiennika. Listing 2.10 zawiera przykład użycia niezmiennika. Linijki 9–13 zawierają definicję pakietowego niezmiennika klasy, tj. niezmiennika, którego warunki są sprawdzane jedynie w wypadku wywołań metod o dostępie pakietowym lub publicznym. W ciele niezmiennika zostały określone trzy warunki (linijki 10–12). Warunek pierwszy obowiązuje bez względu na stan automatu, warunek drugi obowiązuje jedynie w stanie S0, natomiast warunek trzeci jedynie w stanie S1. W efekcie warunek pierwszy będzie sprawdzany zawsze podczas wchodzenia i wychodzenia z metod begin oraz finish, warunki drugi i trzeci – w zależności od aktualnego stanu. Podczas wywoływania metody getStatus nie będą sprawdzane jakiekolwiek warunki niezmiennika. Sekcja definiująca niezmiennik zaczyna się od modyfikatora dostępu. Dopuszczalne modyfikatory dostępu to a) występujące w wypadku normalnych składowych: modyfikator publiczny — public, modyfikator chroniony — protected, oraz modyfikator prywatny — private, b) modyfikator pusty oznaczający dostęp pakietowy oraz c) zdefiniowany specjalnie na użytek niezmienników modyfikator pakietowo-chroniony — package protected. Po modyfikatorze następuje słowo invariant, które, podobnie jak i słowo automaton, nie jest słowem kluczowym. Po słowie invariant umieszczone jest, ograniczone nawiasami klamrowymi 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, że klasa ta nie jest enumeracją. Modyfikator dostępu niezmiennika służy oznaczeniu grupy metod danej klasy, podczas wykonania których może być sprawdzany warunek niezmiennika. Dana klasa może zawierać co najwyżej jeden niezmiennik o danym modyfikatorze dostępu. Tablica 2.3 zawiera podsumowanie dotyczące zakresu stosowania różnych typów niezmienników. Przykładowo, niezmiennik 30

pakietowy jest stosowany jedynie do metod publicznych oraz pakietowych.

rodzaj niezmiennika

rodzaj metody

publiczny chroniony pakietowy pakietowo-chroniony prywatny

publiczna • • • • •

chroniona ◦ • ◦ • •

pakietowa ◦ ◦ • • •

prywatna ◦ ◦ ◦ ◦ •

Tablica 2.3: Zakres stosowania niezmienników.

Warunki danego niezmiennika mogą być sprawdzane jedynie podczas wywołania niestatycznych metod zdefiniowanych bezpośrednio w danej klasie. Wywołanie metody zdefiniowanej w dowolnej nadklasie lub podklasie klasy zawierającej niezmiennik nie prowadzi nigdy do sprawdzenia warunków owego niezmiennika. Nie oznacza to oczywiście, że podklasy i nadklasy 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 pierwsze są obowiązkowe: • Pierwsza z sekcji służy określeniu zbioru stanów, w których spełniony powinien być dany warunek. W miejscu pierwszej sekcji możliwe jest użycie znaku gwiazdki, która oznacza dowolny stan automatu klasy (w wypadku klasy niedefiniującej automatu jest to, nawiasem mówiąc, jedyna dopuszczalna możliwość). Alternatywą dla znaku gwiazdki jest tutaj podanie jawnej listy prostych nazw stanów, których dotyczy niezmiennik. Jako separator elementów listy, używany jest znak przecinka. Każdy z wymienionych stanów musi 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, w razie powtórzenia zdublowany stan zostanie zignorowany. • Druga z sekcji zawiera specyfikację warunku. Wymaga się, aby warunek był wyrażeniem którego wartością jest typ logiczny lub zmienna o typie logicznym18 . W razie testu warunku, przyjmuje się, zgodnie z intuicją, otrzymanie wartości true za spełnienie warunku, a false za jego złamanie. Więcej szczegółów na temat typów wyrażeń języka Java można znaleźć w [9, rozdział 15]. • Opcjonalna, trzecia sekcja może zostać użyta do podania wyrażenia, które zostanie wyświetlone w razie złamania kontraktu. Jako że Java pozwala na rzutowania dowolnej wartości do typu java.lang.String, wymaga się tu jedynie aby wyrażenie w trzeciej sekcji posiadało jakąkolwiek wartość. 18

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

31

Poszczególne sekcje warunku niezmiennika oddzielone są od siebie znakiem dwukropka. Cały warunek niezmiennika musi zostać zakończony znakiem średnika — nawet jeśli jest to ostatni z warunków niezmiennika. W wypadku drugiej oraz trzeciej sekcji niezmiennika rodzi się pytanie o dostępność do zmiennych, pól klasy, etc. Reguła jest w tej sytuacji prosta - znaczenie i dostępność identyfikatorów w drugiej i trzeciej sekcji niezmiennika można określić, wyobrażając sobie umieszczenie owych 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 identyfikator ten 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ędzie wskazywał już na ów parametr. W związku z tym problemem, autor radzi poprzedzać każde odwołanie się do pola danej klasy przedrostkiem this, tak jak ukazano to na zmodyfikowanym przykładzie z listingu 2.12. 1 2 3 4 5 6 7 8 9 10 11

public c l a s s IdProblem { invariant { ∗ : value > 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 e // p r o w a d z i do n i e j e d n o z n a c z n o s c i } int v a l u e ; void intParam ( int v a l u e ) { } void noParam ( ) { } }

Listing 2.11: Problem identyfikatorów w warunkach niezmiennika.

1 2 3 4 5 6 7 8 9 10

public c l a s s IdProblem { invariant { ∗ : this . value > 4 ; // ok , j e d n o z n a c z n e o d w o l a n i e s i e do p o l a k l a s y } int v a l u e ; void intParam ( int v a l u e ) { } void noParam ( ) { } }

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. 19

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

32

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 zakresem daną metodę oraz c) automat skończony danej klasy znajdował się w jednym z zadeklarowanych 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ładniowym i semantycznym, przypadków użycia niezmiennika. Listing 2.14 ukazuje natomiast szereg przykładów konstrukcji niepoprawnych. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41

public i n t e r f a c e R i g h t I n v a r i a n t s { s t a t i c c l a s s Nested extends Other { int c o u n t e r = 0 ; automaton { accepting i n i t i a l S1 : S2 ; S2 : S3 , S4 ; S3 : S5 , S1 ; S4 ; accepting S5 : S5 ; } public invariant { ∗ : true ; S5 : f a l s e ; } boolean v a l u e = f a l s e ; private invariant { ∗ : v a l u e == v a l u e ; S1 : v a l u e | | ! v a l u e : ” 2nd c o n d i t i o n broken ! ” ; S2 , S4 : v a l u e & c o u n t e r < 6 ; S1 , S2 , S3 : new R i g h t I n v a r i a n t s ( ) { package protected invariant { ∗ : t h i s . hashCode ( ) != 0 : ”Wrong hash code ” ; // w wypadku k l a s y n i e d e f i n i u j a c e j automatu znak // g w i a z d k i − ’ ∗ ’ − j e s t j e d y n a d o p u s z c z a l n a m o z l i w o s c i a } public invariant { ∗ : t o S t r i n g ( ) . l e n g t h ( ) != 13 : ” F e r a l s t r i n g ” ; } } . t o S t r i n g ( ) != 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 ” ; // s z c z e g o l n i e z l o z o n y warunek n i e z m i e n n i k a // − z zagniezdzonym n i e z m i e n n i k i e m } I n t e g e r method ( ) { return 3 ; } } }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39

public c l a s s WrongInvariants { automaton { i n i t i a l INITIAL : ACCEPTING accepting ACCEPTING; } public invariant { INITIAL , INITIAL : true ; // o s t r z e z e n i e − z d u b l o w a n i e s t a n u na l i s c i e ∗ : ” always t r u e ” ; // b l a d − w y r a z e n i e ” a l w a y s t r u e ” n i e ma w a r t o s c i l o g i c z n e j ACCEPTING : check ( ) ; // b l a d − n i e s k o n c z o n a r e k u r e n c j a } c l a s s NestedOne { invariant { INITIAL : true ; // b l a d − proba o d w o l a n i a s i e do s t a n u i n n e j k l a s y ∗ : f a l s e : voidMethod ( ) ; // b l a d − t r z e c i a s e k c j a n i e z m i e n n i k a musi byc // wyrazeniem p o s i a d a j a c y m w a r t o s c } } void voidMethod ( ) { } public boolean check ( ) { return true ; } public invariant { // b l a d − n i z m i e n n i k p u b l i c z n y z o s t a l j u z z d e f i n i o w a n y } }

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 wywołań różnych metod (zarówno podczas rozpoczęcia wykonywania jak i przed zakończeniem wywoływania metody) propozycja autora pozwala także na specyfikację warunków szczególnych 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ątkowe metody sprawdzane są bezpośrednio po wywołaniu metody, warunki końcowe natomiast bezpośrednio przed zakończeniem wywołania. Złamanie warunku początkowego lub końcowego 34

jest naturalnie traktowane jako złamanie kontraktu. 1 2 3 4 5 6 7 8 9

public c l a s s Test { s t a t i c L i s t <S t r i n g > t r u n c a t e ( L i s t <S t r i n g > input , int l e n g t h ) in ( i n p u t != null , l e n g t h >= 0 , i n p u t . s i z e ( ) >= l e n g t h ) out ( return . s i z e ( ) == l e n g t h ) { f o r ( int i = l e n g t h − i n p u t . s i z e ( ) ; i > 0 ; i −−) i n p u t . remove ( l e n g t h ) ; return i n p u t ; } }

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 metody. 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ępnych 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. Kolejna linijka zawiera specyfikację warunków końcowych, w skład której wchodzi tylko jeden warunek — 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órym następuje, ujęta w nawiasy, lista poszczególnych warunków. Separatorem elementów na liście jest 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życia identyfikatorów są analogiczne jak w wypadku warunków niezmiennika. Opcjonalna sekcja warunków końcowych metody ma składnię niemal identyczną jak lista warunków początkowych, z tą różnicą, że zaczyna się słowem out. Pusta lista warunków koń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 metod zwracają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 return jest identyczny z typem zwracanym przez daną metodę. W sytuacji gdy warunki końcowe sprawdzane 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 metoda 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]. Tablica 2.4 zawiera podsumowanie wartości domyślnych dla typu return. Podobnie jak ma to miejsce w wypadku niezmienników, składnia warunków końcowych pozwala użytkownikowi na tworzenie zagnieżdżonych warunków początkowych. W takiej sytuacji, w celu uniknięcia niejednoznaczności, ustala się, że słowo kluczowe return odnosi się zawsze do najbardziej zagnieżdżonej z metod opatrzonych warunkiem końcowym. Należy także zauważyć że jak niezmiennik z klasą, tak warunki wstępne i końcowe są ściśle związane z 35

typ

wartość domyślna

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

null 0 0 0 0 0 0 0 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. metody o 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 miejsca sprawdzenie 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

public c l a s s Test { int g e t I n t ( int a ) out ( a > 2 ) in ( return != 4 ) { // b l a d − n i e w l a s c i w a k o l e j n o s c warunkow return 2 ; } Object getName ( ) out ( return . l e n g t h ( ) > 0 ) { // b l a d − ” r e t u r n ” j e s t t y p u O b j e c t i j a k o t a k i n i e // p o s i a d a p u b l i c z n e j metody l e n g t h ( ) return ” A l e k s a n d e r ” ; } void voidMethod ( ) out ( void . c l a s s . e q u a l s ( return . g e t C l a s s ( ) ) ) { // b l a d − o d w o l a n i e s i e do w a r t o s c i z w r a c a n e j //w p r z y p a d k u metody n i e p o s i a d a j a c e j w a r t o s c i } int e x e c u t e ( ) out ( return != null ) { // b l a d − w a r t o s c i z w r a c a n e j p r z y p i s a n y j e s t // w a r t o s c i o w y t y p i n t ( a n i e r e f e r e n c y j n y I n t e g e r ) return new I n t e g e r ( 10 ) ; } void compute ( ) in ( ” qwerty ” ) { } // b l a d − t y p S t r i n g n i e j e s t typem l o g i c z n y m }

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

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

36

Szczególnie interesującym przykład wykorzystania warunków początkowych dotyczy metod, 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 tradycyjnych mechanizmów PPK z wprowadzonymi przez autora automatami skończonymi. Autor zastanawiał się tu wręcz nad wprowadzeniem specjalnej składni przeznaczonej na potrzeby tego typu kontraktów. Ostatecznie jednak do takiego posunięcia nie doszło — zaważyły względy koncepcyjne, w szczególności tendencja do utrzymywania jak najprostszej składni. Listing 2.17 zawiera przykład tego typu zastosowania warunków. 1 2 3 4 5 6 7 8 9 10 11 12

public c l a s s { automaton { i n i t i a l S0 : S1 , S2 ; S1 : S3 ; accepting S2 : S3 ; S3 : S0 ; } void compute ( ) in ( transient ? ( S1 , S2 ) ) out ( transient ? ( S0 , S1 , S3 ) ) { /∗ . . . ∗/ } }

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 niezmienników oraz określone jawnie warunki początkowe i końcowe metody. Sprawdzenie kontraktu następuje bezpośrednio po rozpoczęciu wykonywania automatu metody, oraz bezpośrednio przed jej zakończeniem. Warunki końcowe sprawdzane są bez względu na to, czy wywołanie metody zakończyło się poprzez wykonanie zwyczajnej instrukcji powrotu, czy też zostało przerwane wskutek odrzucenia wyjątku. Tablica 2.5 przedstawia kolejność w jakiej następuje sprawdzenie poszczególnych grup warunków podczas rozpoczęcia wykonywania metody. Tablica 2.6 zawiera podobne podsumowanie dla sprawdzenia kończącego wywołanie. Warunki pochodzące z tego samego niezmiennika sprawdzane są w takiej kolejność, w jakiej zostały w danym niezmienniku zadeklarowane. Ta sama reguła dotyczy warunków koń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 Java jest na ta tyle istotny, że autor zdecydował się poświęcić jej cały osobny ustęp. 37

sekwencja

rodzaj warunków

1 2 3 4 5 6

warunki niezmiennika publicznego warunki niezmiennika chronionego warunki niezmiennika pakietowego warunki niezmiennika pakietowo-chronionego warunki niezmiennika prywatnego jawne warunki wstępne metody

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

rodzaj warunków

1 2 3 4 5 6

jawne warunki końcowe metody warunki niezmiennika prywatnego warunki niezmiennika pakietowo-chronionego warunki niezmiennika pakietowego warunki niezmiennika chronionego 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 obsł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ę jednak stanie 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órych wywołania sprawdzany jest dany warunek, mogą same odrzucać ów wyjątek. Jeśli wyjątek ten wymaga dodatkowo obsługi na etapie kompilacji, odpowiednie metody muszą jawnie zadeklarować możliwość jego odrzucenia przy pomocy klauzuli throws w swoich nagłówkach. Koniecznością dopisania w odpowiednich miejscach klauzuli throws obarczony jest w tej sytuacji programista. Niezapewnienie takiej obsługi traktowane jest jako błąd. W związku z powyższym, listing 2.18 nie zawiera kodu poprawnego programu. Prawidłowy kod został zamieszczony w listingu 2.19. 1 2 3 4 5 6

import j a v a . i o . IOException ; public c l a s s C o n d i t i o n E x c e p t i o n s { private boolean c h e c k C o n t i t i o n A ( ) throws IOException { /∗ . . . ∗/ } 21

Z wyłączeniem wyjątku java.lang.AssertionError, którego odrzucenie następuje w wyniku złamania kontraktu 22 Wyjątki w języku Java można podzielić na dwie główne kategorie: a) wyjątki których obsługa wymagana jest na etapie kompilacji — poprzez przechwycenie za pomocą klauzuli catch lub przekazanie za pomocą klauzuli throws — oraz b) wyjątki których obsługa nie jest wymagana na etapie kompilacji. Do klasy drugiej zalicza się wyjątki dziedziczące, pośrednio lub bezpośrednio, po klasach java.lang.Error lub java.lang.RuntimeException. Wszystkie pozostałe wyjątki należą do pierwszej klasy.

38

7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

private boolean checkConditionB ( ) throws S e c u r i t y E x c e p t i o n { /∗ . . . ∗/ } invariant { ∗ : checkConditionA ( ) ; } void protected method1 ( ) { } void method2 ( ) { } void method3 ( ) in ( checkConditionB ( ) ) { } }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25

import j a v a . i o . IOException ; public c l a s s C o n d i t i o n E x c e p t i o n s { private boolean c h e c k C o n t i t i o n A ( ) throws IOException { /∗ . . . ∗/ } private boolean checkConditionB ( ) throws S e c u r i t y E x c e p t i o n { /∗ . . . ∗/ } invariant { ∗ : checkConditionA ( ) ; } void protected method1 ( ) { } void method2 ( ) throws IOException { } void method3 ( ) throws IOException , S e c u r i t y E x c e p t i o n in ( checkConditionB ( ) ) { } }

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

autora zdecydowanie odradzane. Z tego też powodu autor nie uznał za konieczne wprowadzania 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 jakiegokolwiek systemu kontroli kontraktu, w tym nawet podczas użycia zwyczajnych asercji. Mianowicie, specyfikacja warunków kontraktu mogących podczas sprawdzenia odrzucać wyjątki, może wymuszać na programiście zmiany kodu źródłowego nie związane bezpośrednio z definicją i kontrolą kontraktów. W przykładzie z listingu 2.19 konstrukcja warunków wymusiła dopisanie do niektórych z metod klauzuli “throws”, co automatycznie owocuje koniecznością odpowiedniego opakowania wywołań niniejszych metod przez ich użytkownika. W efekcie powstaje cały ciąg zmian mających źródło jedynie w specyfikacji pewnego kontraktu dla pewnej klasy, 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 motywacji uzasadniających konieczność wprowadzenia wsparcia składniowego dla mechanizmów Programowania 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órego obsł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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28

import j a v a . i o . IOException ; public c l a s s C o n d i t i o n E x c e p t i o n s { private boolean c h e c k C o n t i t i o n A ( ) { try { /∗ . . . ∗/ } catch ( IOException e ) { throw new A s s e r t i o n E r r o r ( e ) ; } } private boolean checkConditionB ( ) { try { /∗ . . . ∗/ } catch ( S e c u r i t y E x c e p t i o n e ) { throw new A s s e r t i o n E r r o r ( e ) ; } } invariant { ∗ : checkConditionA ( ) ; } void protected method1 ( ) { } void method2 ( ) { }

40

29 30 31 32

void method3 ( ) in ( checkConditionB ( ) ) { } }

Listing 2.20: Problem odrzucania wyjątków wewnątrz warunków (ogólne rozwiązanie sugerowane 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 warunkami jawnymi, 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, lecz wynikają 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 do języka Java pod etykietą Programowania przez Kontrakt. Otóż weryfikacji kontraktu nie powoduje wykonanie wyrażenia sprawdzającego aktualny stan automatu skończonego. Wyrażenie to jest o tyle newralgiczne, iż jako jedyna z zaproponowanych przez autora konstrukcji językowych, nie może zostać bez szkody usunięte z programu — można sobie łatwo wyobrazić, że wymazanie niezmiennika, czy też konkretnych warunków metody, nie doprowadzi do zmiany funkcjonalności programu. Wynika to w oczywisty sposób z reguły rozdziału funkcjonalnoś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ż wyraż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 to czy kontrola kontraktów jest włączona, czy też nie. Wyłączenie kontroli kontraktów skutkuje, w wypadku automatu skończonego, jedynie tym, że nie jest sprawdzana dopuszczalność przejść podczas wykonywania instrukcji zmiany stanu (to samo dotyczy drugiego z kontraktów zwią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 zatem potraktować nie jako bezpośredni element Programowani przez Kontrakt, lecz jako pewną konstrukcję którą bardzo łatwo na potrzeby Programowania przez Kontrakt zaadoptować.

41

Nie narusza to w żadnym wypadku rozdziału funkcjonalności, gdyż sprawdzanie kontraktu nastę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 zapewnienie jak największej spójności z już istniejącymi mechanizmami, w tym z mechanizmem asercji. Autor na rzecz owej spójności postanowił ujednolicić sposób powiadamiania o złamaniu kontraktu, tak aby był on kompatybilny z istniejącym dotychczas w Javie mechanizmem sprawdzania asercji (mechanizm ten opisany był dokładniej w sekcji 2.1.2) - podobnie jak złamanie warunku asercji, tak i złamanie warunku kontraktu, prowadzi do odrzucenia wyjątku java.lang.AssertionError . Włączenie i wyłączenie kontroli kontraktów odbywa się w ten sam sposób, co w wypadku zwyczajnych asercji, czyli za pomocą ustawienia odpowiedniego parametru maszyny wirtualnej — na ogół jest to przełącznik-ea. Dzięki takiemu podejściu obsługa skompilowanego kodu nie wymaga żadnych dodatkowych zabiegów w stosunku do programu napisanego w czystej Javie. 1 2 3 4 5

public c l a s s Test { public s t a t i c void main ( S t r i n g argv [ ] ) in ( f a l s e ) { System . out . p r i n t l n ( ” H e l l o World w i t h o u t c o n t r a c t c h e c k i n g ” ) ; } }

Listing 2.21: Program łamiący kontrakt. Niech dany będzie dla przykładu program jak na listingu 2.24. Każda próba uruchomienia owego programu z włączonym mechanizmem sprawdzania asercji powinna w oczywisty sposób prowadzić do złamania kontraktu metody main: $ # Niech w aktualnym katalogu dana bedzie skompilowana klasa Test $ java Test Hello World without contract checking $ java -ea Test Exception 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 nadklasy tegoż języka, każdy prawidłowy program napisany w Javie zachowuje także swoją poprawność 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 23 Na 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 ten jest jednak z gatunku tych najdrobniejszych i można go po prostu złożyć na karb funkcjonalności i przejrzystości zaproponowanej składni. Należy zresztą pamiętać, że, Java jako taka, nie jest językiem akademickim, lecz czysto przemysłowym, stąd przesadny puryzm nie jest w tym wypadku właściwy.

42

rozszerzenia może odbyć się praktycznie natychmiast, nie pociągając za sobą właściwie żadnych kosztów. W celu przeprowadzenia migracji wystarczy wzbogacić skrypt użytego narzędzia budowania o dodatkowy krok polegający na odpowiednim wywołaniu translatora24,25 Samo uzupełnianie klas o definicje kontraktów jest już w zupełności opcjonalne i nie musi być wcale przeprowadzone 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 na translatorze tłumaczącym kod napisany w JavaBC do kodu napisanego w czystym języku Java26 . Informacje na temat sposobu użycia translatora można znaleźć w dodatku C.1. Sam translator został stworzony w oparciu o bibliotekę ANTLR [16]. Szczegóły techniczne rozwiązania nie są tu w gruncie rzeczy istotne, jednakże warto jest przyjrzeć się efektom działania translatora. Analiza taka może pozwolić na lepsze zrozumienie semantyki proponowanych przez autora konstrukcji. Niech dany będzie zatem program jak na listingu 2.22. Przygotowany przez autora translator wygeneruje, dla wejścia w postaci owego programu, program wynikowy jak na przykładzie 2.23. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21

public c l a s s Example { int r e s u l t ; S t r i n g type ; automaton { i n i t i a l S1 : S2 ; S2 : S1 , S3 ; accepting S3 : S2 ; } public invariant { ∗ : r e s u l t != 0 : ” z e r o r e s u l t ” ; S1 , S2 : r e s u l t > 2 | | type . e q u a l s ( ”SHORT” ) ; } package protected invariant { S3 : true ; } Example ( ) { transient : S2 ; 24 W niniejszym punkcie mowa jest jedynie o należących do JavaBC mechanizmach PPK. W wypadku dynamicznych ról zachodzi jeszcze konieczność dołączenia odpowiedniej biblioteki z definicjami wymaganych adnotacji i klas. 25 “narzędzie budowania” - ang. build tool. Najpopularniejszymi narzędziami do budowy programów napisanych w Javie są systemy Apache Ant oraz Maven. Autor wraz z implementacją translatora dla PPK dostarczył zadanie dla systemu Ant. Więcej informacji na ten temat można znaleźć w dodatku C.2. 26 Translacja do pewnego języka wysokiego poziomu jest jednym z częstszych sposobów prototypowania nowych języków. Autor zresztą nie miał w tej kwestii zbyt dużego wyboru. Napisanie faktycznego kompilatora przeprowadzającego translację do kodu bajtowego maszyny wirtualnej byłoby przedsięwzięciem przekraczającym znacznie możliwości czasowe autora.

43

22 23 24 25 26 27 28 29 30 31 32 33

} protected int method ( ) out ( return ∗ return >= 0 ) { return 1 2 ; } public void compute ( ) in ( transient ? ( S1 , S2 ) ) out ( transient ? Example . S1 ) { transient : S1 ; } }

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

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

public c l a s s Example { private s t a t i c enum AutomatonEnumeration Example { S1 ( true , f a l s e , 1 ) , S2 ( f a l s e , f a l s e , 0 , 2 ) , S3 ( f a l s e , true , 1 ) ; private boolean i s I n i t i a l ; private boolean i s A c c e p t i n g ; private int [ ] d s t ; private AutomatonEnumeration Example ( boolean i s I n i t i a l , boolean i s A c c e p t i n g , int . . . d s t ) { this . i s I n i t i a l = i s I n i t i a l ; this . isAccepting = isAccepting ; this . dst = dst ; } public boolean i s I n i t i a l ( ) { return i s I n i t i a l ; } public boolean i s A c c e p t i n g ( ) { return i s A c c e p t i n g ; } public boolean t r a n s i t i o n P o s s i b l e ( AutomatonEnumeration Example s ) { return j a v a . u t i l . Arrays . b i n a r y S e a r c h ( dst , s . o r d i n a l ( ) ) >= 0 ; } } private AutomatonEnumeration Example automaton Example = AutomatonEnumeration Example . S1 ; int r e s u l t ; S t r i n g type ;

44

38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90

Example ( ) { assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; try { assert Example . t h i s . automaton Example . t r a n s i t i o n P o s s i b l e ( Example . AutomatonEnumeration Example . S2 ) ; Example . t h i s . automaton Example = Example . AutomatonEnumeration Example . S2 ; } finally { assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; } } protected int method ( ) { assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; int o u t r e t u r n = 0; try { = 12; return o u t r e t u r n } finally { f i n a l int o u t r e t u r n 2 = out return ; assert o u t r e t u r n 2 ∗ out return2 >= 0 ; assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; } } public void compute ( ) { assert r e s u l t != 0 : ” z e r o r e s u l t ” ; assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S1 ) | | ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S2 ) ) ? r e s u l t > 2 | | type . e q u a l s ( ”SHORT” ) : true ; assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; assert ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S1 | | Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S2 ) ; try { assert Example . t h i s . automaton Example . t r a n s i t i o n P o s s i b l e ( Example . AutomatonEnumeration Example . S1 ) ; Example . t h i s . automaton Example = Example . AutomatonEnumeration Example . S1 ; } finally {

45

91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117

assert ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S1 ) ; assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S3 ) ) ? true : true ; assert r e s u l t != 0 : ” z e r o r e s u l t ” ; assert ( ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S1 ) | | ( Example . t h i s . automaton Example == Example . AutomatonEnumeration Example . S2 ) ) ? r e s u l t > 2 | | type . e q u a l s ( ”SHORT” ) : true ; } } @Override protected void f i n a l i z e ( ) throws Throwable { try { super . f i n a l i z e ( ) ; } finally { assert automaton Example . i s A c c e p t i n g ( ) | | System . e r r . p r i n t f ( ” Object ” + t h i s + ” ( ” + t h i s . g e t C l a s s ( ) . getName ( ) + ” ) not i n a c c e p t i n g s t a t e d u r i n g f i n a l i z a t i o n ” ) != null ; } } }

Listing 2.23: Kod źródłowy programu z listingu 2.22 po translacji (po uprzednim sformatowaniu i usunięciu komentarzy translatora). Krótka analiza kodu otrzymanego w wyniku translacji pozwala stwierdzić, iż tłumaczenie jest prawidłowe. W szczególności funkcjonalność klasy oraz jej publiczny interfejs nie uległy zmianie. Jak na dłoni widać także sposób, w jaki autor zapewnił wymaganą integrację wprowadzonych 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. Definicja automatu skończonego doprowadziła natomiast do deklaracji zagnieżdżonej enumeracji reprezentują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 nowych składowych klasy, dobierane są w sposób dynamiczny27 . Innymi słowy, nie ma podstaw dla stwierdzenia, że JavaBC nie jest faktyczną nadklasą Javy, co nota bene było dla autora jednym z najważniejszych założeń. Komentarz należy się tutaj jeszcze sposobowi sprawdzania czy obiekt podczas finalizacji znajduje się w stanie końcowym. Sposób owego kontraktu różni się nieco od wszystkich pozostałych przypadków. Wynika to z tej prostej przyczyny, że każdy wyjątek odrzucony w finalizatorze, jest przez maszynę wirtualną zwyczajnie ignorowany. Skutkuje to m.in tym, 27

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

46

że na standardowym wyjściu błędu nie zostaje wyświetlona żadna informacja, tak jak ma to miejsce w przypadku wystąpienia nieprzechwyconego wyjątku. W szczególności dotyczy to 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 wygenerowanym kodzie.

2.6

Uzasadnienie i krytyka propozycji

Wybór elementów Programowania przez Kontrakt oraz ich składnia wraz z semantyka, jakkolwiek byłby arbitralny, ma swoje uzasadnienie. Przy ich określaniu autor kierował się przede wszystkim 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 ponoszonej nawet kosztem pewnej funkcjonalności. Poniżej znajduje się krótkie omówienie poszczególnych 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łówku klasy — poprzez analogię z kontraktami metod, które umieszczone są w ich nagłówkach. 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 elementem kontraktu 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 drugie składnia taka jest zwyczajnie mało czytelna i “nieelegancka”, co widać dobrze na listingu 2.24. Ciało automatu skończonego ujęte zostało w nawiasy klamrowe, chcąc w ten sposób utrzymać dotychczasową konwencję, polegającą na ujmowaniu w ten typ nawiasów ciał wszystkich innych definicji wewnątrz-klasowych, takich jak inicjalizatory statyczne, konstruktory bądź metody. Jednak koronnym argument przemawiający za ujęciem definicji automatu skończonego w nawiasy klamrowe była analogia pomiędzy definicją klasy a automatu. 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ładnia alternatywna polegająca na ujęciu ciała automatu w nawiasy zwyczajne, przy jednoczesnym 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ł znak dwukropka. Autor rozważał tu jeszcze dwuznak ->, jednakże zrezygnował z niego, aby nie wprowadzać do języka dodatkowego symbolu leksykalnego. Na rzecz dwukropka przemawia także fakt, że występuje on w składni asercji. Wreszcie autor w wypadku definicji automatów skończonych zrezygnował z jawnego określania funkcji przejścia, pozwalając programiście jedynie na specyfikację dopuszczalnych przejść29 . Motywacja była tu następująca: wybrany sposób definicji automatu skończonego powinien w zdecydowanej większości przypadków być wystarczający, a co 28 29

por. uzasadnienie w sekcji 2.3.9. patrz sekcja 2.3.1.

47

najważniejsze znacznie ogranicza objętość sekcji definiującej automat. Jeśliby funkcja przejścia miała być definiowana explicite, definicje automatów stałyby się zwyczajnie mało czytelne, co stoi w sprzeczności z jedną z idei Programowania przez Kontrakt. 1 2 3 4 5 6

public c l a s s C l a s s automaton { i n i t i a l S0 : S1 ; accepting S1 ; } { /∗ . . . ∗/ }

Listing 2.24: Alternatywna składnia definicji automatu skończonego. Obsługa automatu skończonego: do obsługi automatu skończonego klasy służą instrukcja zmiany stanu oraz wyrażenie sprawdzenia aktualnego stanu. Składnia obu konstrukcji 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 identyfikatora. Alternatywą byłoby tu skorzystanie z innego, istniejącego już słowa kluczowego lub wprowadzenie 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 za najbardziej odpowiednie. Należy tu zauważyć, że wprowadzenie zupełnie nowego słowa kluczowego 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 występuje, poprzez analogię do składni automatu skończonego, dwukropek. W wyrażeniu sprawdzenia 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, jest najprostszym możliwym rozwiązaniem problemu niejednoznaczności odwołań do stanów. W szczególności alternatywna składnia polegająca na podaniu pełnej nazwy klasy automatu skończonego (tj. nazwy klasy wraz z przypisanym jej pakietem) byłaby w tej sytuacji nadmiarowa. Nawiasem mówiąc, obrany schemat wpisuje się dobrze w konwencje nazewniczą Javy, polegającą na rozdzielaniu nazw złożonych symbolem kropki. Wyrażenie sprawdzenia aktualnego stanu w wersji rozbudowanej (zawierającej listę stanó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 separatora elementów na liście, gdyż wpisują się one dobrze w konwencje budowy wyrażeń w języku Java. Alternatywa w postaci użycia nawiasów klamrowych nie miałaby tu większego sensu. 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łby ich na potrzeby wyrażenia sprawdzenia stanu, mogłoby to być nieco mylące dla osoby szybko przeglądającej kod programu. Niezmienniki: Argumentacja za umieszczeniem niezmiennika bezpośrednio w ciele klasy, a nie w jej nagłówku, jest podobna jak w wypadku definicji automatu skończonego. To samo tyczy się ujęcia ciała niezmiennika w nawiasy klamrowe, oraz separacji poszczególnych warunków za pomocą znaku średnika. Składnia poszczególnych warunków była przez autora wzorowana na wyrażeniu asercji oraz instrukcji zmiany stanu. Z tego powodu, jako separator, użyty został znak dwukropka. Konieczność pisania znaku gwiazdki, w razi braku podania konkretnego zbioru 48

stanów, wynika z powodów czysto gramatycznych — alternatywa w postaci pomijania pierwszej sekcji warunku niezmiennika prowadziłaby niestety do niejednoznaczności podczas rozbioru gramatycznego i z tego powodu została przez autora wykluczona. Autor mógł co prawda oznaczyć pierwszą z sekcji jako opcjonalną, musiałoby się to jednak odbyć 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 to wł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, warunki wstępne oraz końcowe metody ujęte zostały w nawiasy zwyczajne, poprzedzone dodatkowo 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 przez Kontrakt nie jest w pełni kompletna i idealna. Łyżką dziegciu jest tu poniższa lista, która zawiera szereg sugestii dotyczących dalszego rozwoju koncepcji autora w tym zakresie: • Możliwość definicji niezmienników statycznych, dotyczących kontroli kontraktów metod statycznych. • 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ół niestatyczne klasy zagnieżdżone, a tylko takie mogą odwoływać się do automatów klas otaczają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 w normalnym trybie oraz warunki końcowe dotyczące zakończenia metody w trybie nadzwyczajnym, to jest w sytuacji odrzucenia wyjątku. Warunki pierwszego rodzaju mogłyby mieć dostęp do wartości zwracanej, warunki drugiego rodzaju natomiast do odrzuconego wyjątku. Listing 2.25 przedstawia przykład możliwej składni dla tego rodzaju warunków. 1 2 3 4 5

public c l a s s C l a s s { int method ( ) in ( true ) out ( return > 2 ) error ( f a l s e ) { return 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 konstrukcji z języka D. Propozycja składni z języka D jest o tyle lepsza, iż daje programiście możliwość wykonania czynności wstępnych, przed przystąpieniem do sprawdzania warunku.

49

• Rozwiązanie problemu przysłaniania nazw pól klasy przez nazwy zmiennych w wypadku stosowania niezmienników. • Możliwość stosowania wyrażeń początkowych i końcowych nie tylko dla ciała funkcji lecz w ogólności dla dowolnego bloku kodu30 .

30

tj. zbioru instrukcji ujętych w nawiasy klamrowe.

50

Rozdział 3

Dynamiczne role 3.1

Czym są dynamiczne role?

Ponieważ używane przez autora określenie “dynamiczne role” nie jest pojęciem jasno sprecyzowanym, zachodzi potrzeba jego pewnej formalizacji. W związku z tym, na potrzeby niniejszej pracy, można przyjąć następujące definicję owego terminu: “Dynamiczne role” są pewną koncepcją z pogranicza kontroli typów w językach programowania. Pojęcie to oznacza swego rodzaju mechanizm, który pozwala na dynamiczne przypisywanie danemu obiektowi wielu różnego rodzaju typów (i związanych z nimi wartości) jednocześnie. Obiekt ów nosi miano aktora, a jego reprezentacje, w postaci wartości o określonych typach, nazywane są rolami1 . Można powiedzieć, że dynamiczne role są swoistym, dynamicznym polimorfizmem. Koncepcja ta stanowi pewną próbę przezwyciężenia trudności związanych z modelowaniem bytów o nieustalonej, dynamicznie zmieniającej się funkcjonalności, dla których zdefiniowanie pojedynczej klasy nie jest na ogół rozwiązaniem wystarczającym. Oczywiście tworzenie komponentów modelujących tego rodzaju byty jest, przy wykorzystaniu tradycyjnych metod oferowanych przez różnego rodzaju języki programowania, jak najbardziej możliwe, jednakże pociąga ono za sobą stosowanie skomplikowanych, mało elastycznych rozwiązań, które charakteryzują 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 wypadku języków ze statyczną kontrolą typów, typ owej wartości znany jest na ogół już na etapie kompilacji. 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 etapie 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 obiek1

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

51

towych2 , oznacza to, że dana zmienna wiąże się z obiektem konkretnej klasy, posiadającej konkretny, 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 kompilacji, może też mieć miejsce dopiero podczas uruchomienia programu, przy czym w najgorszym scenariuszu, może się on w ogóle bezpośrednio nie ujawnić, prowadząc jedynie do niewłaściwego działania programu. Sposób ujawniania się błędów wynikłych z kontroli typów zależy tu przede wszystkim od konstrukcji danego języka, w szczególności od mechanizmów bezpieczeń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 typu java.lang.Object jako ciągu znakowego — java.lang.String (linijka 4). W programie przedstawionym 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 sytuacja ma miejsce w drugim z programów, gdzie wygenerowanie błędu następuje dopiero podczas uruchomienia programu, prowadząc do bezpośredniego odrzucenia wyjątku informującego o użyciu nieprawidłowego typu — java.lang.ClassCastException. 1 2 3 4 5 6 7 8 9

public c l a s s WrongTypes { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Object o b j e c t = new Object ( ) ; sayText ( o b j e c t ) ; } public s t a t i c void sayText ( S t r i n g t e x t ) { System . out . p r i n t l n ( t e x t ) ; } }

Listing 3.1: Przykład użycia zmiennej o niewłaściwym typie — błąd kompilacji. 1 2 3 4 5 6 7 8 9 10

public c l a s s WrongTypes { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Object o b j e c t = new Object ( ) ; sayText ( ( S t r i n g ) o b j e c t ) ; // proba jawnego z r z u t o w a n i a t y p u O b j e c t na t y p S t r i n g } public s t a t i c void sayText ( S t r i n g t e x t ) { System . out . p r i n t l n ( t e x t ) ; } }

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 inny typ “pokrewnego” rodzaju, którego język pozwala używać w miejscu danego typu. 2

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

52

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ści o nieodpowiednim typie (linijka 3) zostało tu skorygowane powtórnym przypisaniem (linijka 4) wartości o właściwym już typie. Należy zauważyć, że drugie przypisanie spowodowało jednocześnie utratę dostępu do wartości, którą początkowo reprezentowała zmienna. Zmienne w 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ązania większej liczby obiektów, o potencjalnie różnych typach z daną zmienną. Możliwe jest to na przykład w sytuacji gdy wartością danej zmiennej jest tablica zawierająca szereg kolejnych wartości. Niemniej, zmienna jako taka, może odnosić się tylko do jednej wartości o określonym typie. Alternatywne podejście do naprawy programu zaprezentowane jest na listingu 3.4, gdzie w celu uzyskania odpowiedniego typu, tj. ciągu tekstowego, wywoływana jest pewna metoda obiektu reprezentowanego przez zmienną (linijka 4). Podejście to różni się od poprzedniego tym, że zamiast przypisywać zmiennej nową wartość o odpowiednim typie, następuje tu swego rodzaju jawna adaptacja posiadanej już wartości, jaką jest wywołanie na jej rzecz metody toString, zwracającej wartość o żądanym już typie. 1 2 3 4 5 6 7 8 9 10

public c l a s s WrongTypes { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Object o b j e c t = new Object ( ) ; o b j e c t = ” Ala ma kota ” sayText ( ( S t r i n g ) o b j e c t ) ; } public s t a t i c void sayText ( S t r i n g t e x t ) { System . out . p r i n t l n ( t e x t ) ; } }

Listing 3.3: Poprawiona wersja programu z listingów 3.1 i 3.2 — przypisanie zmiennej nowej wartości. 1 2 3 4 5 6 7 8 9

public c l a s s WrongTypes { public s t a t i c void main ( S t r i n g [ ] a r g s ) { Object o b j e c t = new Object ( ) ; sayText ( o b j e c t . t o S t r i n g ( ) ) ; } public s t a t i c void sayText ( S t r i n g t e x t ) { System . out . p r i n t l n ( t e x t ) ; } }

Listing 3.4: Poprawiona wersja programu z listingów 3.1 i 3.2 — adaptacja aktualnej wartości zmiennej. Reasumując, można zauważyć, że praktycznie w każdym z obiektowych języków programowania obowiązują następujące ograniczenia: 3 Brak wskazania na jakąkolwiek wartość możliwy jest na przykład w językach, w których odwołania do zmiennych 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

Rysunek 3.1: Cykl rozwojowy motyla. Przejście ze stadium gąsienicy do stadium imago nosi wymowną nazwę “zupełnego przeobrażenia”; Mimo że dany motyl potrafi w trakcie swojego ż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ą wiadomo, że wiąże się ona, w danej chwili wykonania programu, z pojedynczą wartością o pewnym okreś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ących się 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 swoich przeobrażeniach, zachowuje swoistą spójność, która nakazuje traktować go jako jedność, a nie pewien łańcuch, w jakiś sposób ze sobą powiązanych, ale jednak odrębnych, bytów. Dobrym przykł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, jest konieczność reprezentacji pewnego bytu, który w ogóle nie posiada ściśle określonego interfejsu. Mowa tu o bycie, który w trakcie swojego cyklu życia posiada możliwość nabywania funkcjonalności zupełnie odmiennej (w sensie interfejsu) od już posiadanej, a także posiada możliwość tracenia dotychczasowej funkcjonalności. Wynikać to może chociażby z potrzeby adaptacji do określonych potrzeb. Warto zauważyć, że przypadek poprzedni jest przypadkiem szczególnym owej sytuacji. Niech, dla celów konkretnego przykładu, dana będzie hierarchia interfejsów jak z rysunku 3.2. Hierarchia ta przedstawia pewien zbiór interfejsów związany z modelowaniem rzeczywistego bytu jakim jest osoba fizyczna. Podstawowa funkcjonalność osoby udostępniana jest poprzez interfejs IPerson. Interfejsy IChild, IAdult oraz ICorpse dotyczą podstawowej funkcjonalności charakterystycznej dla pewnych etapów życia osoby. Specyficzne funkcjonalności osoby modelowane są natomiast za pomocą interfejsów IPupil, IEmployee, ICustomer, oraz ich podinterfejsów. Można powiedzieć, iż funkcjonalności reprezentowane przez poszczegó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óre mogą być przez osobę odgrywane. Należy tu zauważyć, że wszystkie z ról łączy jedna wspólna cecha, jaką jest ich przejściowy charakter - z żadną z ról osoba nie jest związana przez cały okres swojego funkcjonowania. Innymi słowy, odgrywanie ról ma charakter “dynamiczny”. Abstrakcja (∼komponent) użyta do zamodelowania bytu osoby, nazywana dalej “aktorem”, powinna zatem móc wykazać się cechami takimi jak a) możliwość czasowego przyjmo54

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 swojego cyklu życia — b) w szczególności porzucania posiadanych już interfejsów, c) możliwość przyjmowania wielu różnego rodzaju interfejsów na raz, to jest odgrywania różnych ról w tej samej chwili, 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 prostu wszystkie z interfejsów potencjalnych ról. 2. Komponent aktora może istnieć w fazie koncepcyjnej — w oparciu o szereg klas powią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óżne aspekty 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 przypadku pierwszego rozwiązania enkapsulacja jest ścisła, w przypadku trzeciego w ogóle nie ma o niej w ogóle mowy). W związku z tym wyczerpują one w jakiś sposób zbiór wszystkich rozwiązań problemu na jaki pozwalają języki obiektowe. Niestety każdy z przedstawionych pomysłów zawiera pewne zasadnicze wady: 1. Zastosowanie pierwszego rozwiązanie wiedzie wprost do antywzorca projektowego znanego pod nazwą Blob4 , którego zastosowanie pociąga za sobą wiele negatywnych kon4

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

55

sekwencji. Komponent aktora staje się w efekcie gigantyczną klasą o bardzo szerokim zakresie odpowiedzialności, co stoi w oczywistej sprzeczności z zasadami modularności w programowaniu obiektowym. Co więcej, część funkcjonalności owej klasy dotycząca rzadko odgrywanych ról, może nie być nigdy w życiu większość obiektów wykorzystywana, co z kolei prowadzi do marnotrawstwa zasobów. Wreszcie, dodanie klasy dla nowej 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 uruchomienia działającego systemu. 2. Drugie podejście, polegające na umiarkowanej enkapsulacji, mimo iż najsensowniejsze z przedstawionych, zawiera także dosyć poważną wadę w postaci dodatkowych wymogów nakładanych na klasy ról. Wadą tą jest konieczność tworzenia klas poszczególnych ról w taki sposób aby mogły współdziałać ze sobą w ramach zastosowanego wzorca. W przypadku wzorca Decorator wymusza to na klasach posiadanie pewnego, związanego z tym interfejsu. Wykorzystanie klas nie spełniających owego kryterium, zostaje z tego powodu wykluczone. 3. Zasadniczą wadą trzeciego podejścia jest brak enkapsulacji. Ów niedostatek sprawia, iż proces tworzenia oprogramowania staje się bardzo podatny na najróżniejszego rodzaju błędy, a jego artefakty sprawiają duże trudności w utrzymaniu i dalszym rozwoju. Rozwiązanie to jest także najczęściej stosowane w praktyce. Wynika to z pozornej prostoty implementacji, opartej często jedynie na doraźnych przesłankach, a niejednokrotnie także i z braku wiedzy na temat wzorców programowania, co mogłoby pozwolić na zastosowanie drugiego podejścia. Powyższa dyskusja pokazuje, iż w zasadzie nie jest możliwe, aby za pomocą tradycyjnych technik udostępnionych przez typowy obiektowy język programowania, otrzymać dynamicznie 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 w ogó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 przypadku trzecim natomiast, nie można się do niej w prosty sposób odwoływać za pomocą pojedynczej zmiennej — w jej skład wchodzi wiele, potencjalnie rozproszonych, obiektów. Możliwie najsensowniejsza druga z koncepcji nadal wymusza na autorze szczególne traktowanie klas będących implementacjami ról, co powoduje mocne ograniczenie mechanizmu dynamicznej adaptacji do określonego typu — podobnie jak w przypadku pierwszym tylko określone klasy mogą 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. Tak więc komponent aktora powinien być reprezentowany przez pojedynczy obiekt (— tak jak w rozwiązaniu pierwszym), który posiadałby możliwość dynamicznego adaptowania się do dowolnego typu (— a nie tylko do predefiniowanego zestawu ról, jak w przypadku drugim), oraz powinien opierać się na logice ról zdefiniowanych w zwyczajnych klasach (— jak w przypadku trzecim). 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ęści sekcji pokazane, zmienne w językach programowania oraz wartości przez nie wskazywane nie posiadają odpowiednich dla takiego zastosowania właściwości.

56

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ątku sekcji 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 programowania mechanizmami 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ązanie różnego rodzaju funkcjonalności z konkretną metodą swojego interfejsu. Metody mogące uczestniczyć w owym mechanizmie, nazywane są metodami wirtualnymi. Choć nie w każdym języku jest to koniecznością, polimorfizm jest na ogół wykorzystywany w połączeniu z mechanizmami dziedziczenia oraz rzutowania na klasę bazową: klasa bazowa zawiera deklarację metody wirtualnej, a szereg klas dziedziczących z owej klasy bazowej zawiera natomiast alternatywne implementacje owej metody. Podczas rzutowania instancji różnych podklas na klasę bazową, otrzymuje się wskazanie na klasy bazowe z różnymi implementacjami danej metody. W efekcie pojedyncza metoda obiektu pewnej klasy może w sposób dynamiczny przybierać różnego rodzaju funkcjonalność. Mechanizm ten nie umożliwia jednak obiektowi zmiany swojego interfejsu, co nie pozwala już na dynamiczne odgrywanie ról o różnych interfejsach. Po drugie, polimorfizm nie 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 obiekt mógł odgrywać jednocześnie dwie różne role wiążące odmienną funkcjonalność z tym samym interfejsem. Polimorfizm w programowaniu funkcyjnym , znany również pod nazwą polimorfizmu ad-hoc, pozwala programiście, z grubsza rzecz biorąc, na nazywanie więcej niż jednej funkcji za pomocą tego samego identyfikatora. Właściwa dla danego wywołania funkcja wybierana jest na podstawie typów parametrów, na ogół jeszcze na etapie kompilacji. Z tego powodu polimorfizm ad-hoc, jako taki, ma właściwie niewiele wspólnego z jakimkolwiek rodzajem dynamicznego zachowania. Polimorfizm parametryczny pozwala na deklarowanie funkcji i klas o charakterze ogólnym, z którymi nie są związane konkretne typy, lecz raczej zbiór dopuszczalnych typów, na których daną abstrakcję można oprzeć. Użycie funkcji bądź klasy oferującej polimorfizm parametryczny odbywa się poprzez jej sparametryzowanie, tj. określenie konkretnych typów, z którymi dana instancja klasy bądź dane wywołanie funkcji ma współpracować. Innymi słowy mechanizm polimorfizmu parametrycznego jest czymś w rodzaju możliwości definiowania szablonów, co umożliwia użytkownikowi uzyskanie większej zwięzłości i spójności programu przy jednoczesnym zachowaniu statycznej kontroli typów.

57

Ponieważ parametryzacja jest dla instancji danej klasy zabiegiem jednorazowym, nie da się jej zbytnio wykorzystać do implementacji jakiegokolwiek rodzaju dynamicznych zachowań, w szczególności do mechanizmu “dynamicznych ról”. Dynamiczne typowanie (ang. dynamic typing) jest rodzajem kontroli typów odbywającej się w czasie wykonania i na dobrą sprawę nie ma ono nic wspólnego z dynamicznymi rolami. To że typ danej zmiennej określany (czy może raczej “sprawdzany”) jest w sposób dynamiczny, nie oznacza wcale, iż typ wartości zmiennej może być dowolny. Wynika on po prostu z wcześniejszego przebiegu programu. Dynamiczne typowanie pozwala jedynie na to, aby pewna zmienna mogła w czasie wykonania być użytą do wskazywania na wartości o różnych typach. Słabe typowanie (ang. weak typing) pozwala z grubsza na to, aby w trakcie wykonania programu dochodziło w razie potrzeby, do pewnych niejawnych konwersji pomiędzy typami 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 prymitywnych, składających się z pojedynczej wartości o elementarnym charakterze (jak na przykład liczba zmiennoprzecinkowa bądź wartości logiczna) i nie posiadających praktycznie żadnych składowych funkcyjnych. Duck typing — w wolnym tłumaczeniu “kacze typowanie” — jest interesującym mechanizmem, który polega, mniej więcej, na opóźnieniu sprawdzenie poprawności i legalności wywołania metody aż do czasu wykonania5 . Kluczowe założenie leżące u podstaw tego mechanizmu można wyłożyć za pomocą nieformalnego, utrzymanego w duchu praktycznego 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 klasy z 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 przez ową klasę zestaw właściwości. Innymi słowy, jawne deklarowanie zgodności z pewnym interfejsem, np. poprzez odziedziczenie z danej klasy bądź deklarację stwierdzającą implementację owego interfejsu, nie jest w przypadku duck typingu wymagane. 1 2 3 4 5 6 7 8 9 10

c l a s s Duck : def quack ( s e l f ) : print ” Quaaaaaack ! ” def f e a t h e r s ( s e l f ) : print ”The duck has w h i t e and gray f e a t h e r s . ” c l a s s Person : def quack ( s e l f ) : print ”The p e r s o n i m i t a t e s a duck . ” def f e a t h e r s ( s e l f ) :

5

W 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, ma także oferować ów mechanizm. 6 ang.: If it walks like a duck and quacks like a duck, I would call it a duck. 7 W rozumieniu koncepcyjnym, tj. niekoniecznie jako konkretna konstrukcja językowa.

58

print ”The p e r s o n t a k e s a f e a t h e r from t h e ground and shows i t . ”

11 12 13 14 15 16 17 18 19 20

def i n t h e f o r e s t ( duck ) : duck . quack ( ) duck . f e a t h e r s ( ) donald john = in the in the

= Duck ( ) Person ( ) f o r e s t ( donald ) f o r e s t ( john )

Listing 3.5: Wykorzystanie mechanizmu duck typing w języku Python — przykład zaczerpnięty z Wikipedii[7]. Napisany w języku Python program z listingu 3.5 stanowi prostą ilustrację mechanizmu kaczego typowania. Linijki 1–5 zawierają definicję klasy Duck, która jest pewnym modelem kaczki. Wykonanie programu zaczyna się od linijki 17. Na początku tworzone są dwa obiekty reprezentujące odpowiednio kaczkę i osobę, które używane są następnie w roli argumentów przy wywołaniu procedury in the forest. Uruchomienie programu może wyglądać następująco: $ python duck_typing.py Quaaaaaack! 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 program 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 aby przyjmowała argument realizujący ów interfejs . Mimo że mechanizm kaczego typowania może wydawać się na pierwszy rzut oka koncepcją zupełnie odmienną od wcześniej wymienionych, jest on w rzeczywistości szczególnym rodzajem dynamicznego typowania, w który podkreśla się pojęciowy (a nie deklaratywny) charakter interfejsów i ich implementacji. Dzięki mechanizmowi kaczego typowania faktycznie możliwe jest dynamiczne odgrywanie różnego rodzaju ról. Zakres moż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ć odpowiednie metody. Przypomina to, w gruncie rzeczy, nieco poprawioną wersję skrytykowanego w sekcji 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 mechanizmy 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 programowania Java pozwala mianowicie na: 59

• Polimorfizm obiektowy, będący jedną z naczelnych zasad obiektowego paradygmatu programowania, w oparciu o który zaprojektowana została właśnie Java. Java pozwala na korzystanie z polimorfizmu obiektowego zarówno za pomocą mechanizmu dziedziczenia z klasy bazowej jak i poprzez implementację tzw. interfejsów, które są swoistymi klasami czysto abstrakcyjnymi. Mechanizmy te są w zasadzie niemal identyczne — możliwość deklarowania i implementacji interfejsów wynika bezpośrednio z zakazu wielodziedziczenia. Polimorfizm obiektowy ma w Javie charakter niejako podstawowego mechanizmu, przy wykorzystaniu którego powinno tworzyć się oprogramowanie. Fakt ten jest doskonale podkreślony poprzez automatyczne traktowanie wszystkich metod jako metod wirtualnych, co stanowi nota bene dodatkową zachętę do jego stosowania8 . • Polimorfizm parametryczny, dostępny w Javie dopiero od wersji 1.5, funkcjonuje w niej pod nazwą Typów Ogólnych (ang. Generics). Mechanizm ten stanowi jedną z bardziej kontrowersyjnych i gorzej dopracowanych cech języka. Z powodu chęci zachowania kompatybilności wstecznej, informacje o typach zastosowanych jako parametry, usuwane są jeszcze na etapie kompilacji — podczas procesu nazywanego wymazywaniem typów (ang. Type Erasure). W efekcie mechanizm typów ogólnych w Javie trudno nazwać prawdziwym polimorfizmem parametrycznym, choć ewidentnie bliżej jest mu do tej koncepcji niż chociażby systemowi szablonów znanemu z języka C++. • Dynamiczne rzutowanie typów. Kontrola legalności dynamicznego rzutowania odbywa się dopiero w czasie wykonania. • Dynamiczną kontrolę poprawności wykonania realizowaną za pomocą asercji. Więcej na 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 skonstruowana w postaci biblioteki tegoż języka9 . Jest to podejście zupełnie odmienne od zastosowanego w przypadku propozycji PPK, gdzie poszczególne mechanizmy sprawdzania poprawności kontraktu, bądź obsługi automatów skończonych, znajdowały swoje odzwierciedlenie na poziomie 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 programiś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 utylizacją istniejących abstrakcji językowych, użycie dostarczonej implementacji nie różni się zbytnio od użycia typowej biblioteki. 8

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

60

Rysunek 3.3: Obiekt aktora i jego funkcjonalne reprezentacje w postaci stowarzyszonych z nim obiektów, będących w istocie odgrywanymi rolami. W tym miejscu należy wspomnieć o jeszcze jednym, bardzo istotnym aspekcie propozycji dynamicznych ról. O ile mianowicie składnia języka nie uległa zmianie, o tyle jego semantyka została już nieznacznie zmodyfikowana. Trudno jest co prawda jednoznacznie określić co należy a co nie do semantycznych właściwości języka (nawiasem mówiąc, sama specyfikacja języka Java nie definiuje wyraźnie tej granicy), jednakże w wypadku propozycji autora można zdecydowanie 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 pozwala na tworzenie obiektów aktorów, które posiadają z kolei możliwość odgrywania w sposób dynamiczny różnego rodzaju ról. Cała propozycja dynamicznych ról dla języka Java koncentruje się wokół dostarczonego przez bibliotekę interfejsu pl.edu.pw.akosicki.roles.Actor. Interfejs ten reprezentuje funkcjonalność rzeczywistej klasy aktora. W szczególności definiuje on metody pozwalające na adaptowanie się do danej roli, porzucanie danej roli oraz sprawdzanie czy obiekt aktora aktualnie odgrywa daną rolę. Biblioteka nie zawiera natomiast żadnego interfejsu reprezentującego pojedynczą rolę aktora. Funkcję ról, tj. swego rodzaju zewnętrznych przejawów aktywności aktora, pełnią zwyczajne klasy języka Java. Sama adaptacja aktora do pewnej roli polega po prostu na zwróceniu instancji obiektu danej klasy, który, choć z pozoru niezależny, działa na rzecz określonego aktora. Schemat ów został podsumowany na rysunku 3.3. Obok mającego centralne znaczenie interfejsu aktora funkcjonuje także szereg pomocniczych 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 sekcjach niniejszego rozdziału. Z racji tego, że to co zostało do tej pory w kwestii dynamicznych ról powiedziane może być, zdaniem autora, wciąż dosyć “mgliste”, najlepszym wprowadzeniem do omówienia propozycji dynamicznych ról będzie konkretna demonstracja użycia jej biblioteki. Listing 3.6 zawiera prosty przykład użycia ról. Funkcję definicji ról pełnią zwyczajne klasy — Ship i Car. Klasy te nie posiadają zbyt szerokich możliwości. Jedyne czego można od nich zażą61

dać, to przemieszczenie reprezentowanego obiektu (statku bądź samochodu). W roli aktora wykorzystana 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 playRole aktorowi przypisywana jest rola typu Ship. Przy okazji utworzenia roli, aktor zwraca referencję na obiekt danej roli. Obiekt ów, co bardzo istotne, nie jest zwyczajnym obiektem swojej klasy, jaki można by uzyskać przy pomocy wywołania operatora new. Jak zostało już na wstępie wspomniane, pełni on niejako funkcję agenta danego aktora (ang. proxy). Wszelkie wywoł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, gdzie następuje wywołanie pewnej właściwej dla obiektu Ship metody. W linijce 36 następuje przypisanie aktorowi kolejnej roli, tym razem reprezentowanej przez klasę Car. Obiekt owej roli zostaje następnie użyty. Oczywiście, jak i w wypadku poprzedniej roli, tak i w tej sytuacji wywołanie pewnej metody jest w rzeczywistości wywołaniem na rzecz aktora, z którym obiekt danej roli jest związany. Kluczem do wyjaśnienia zaproponowanego przez autora mechanizmu dynamicznych ról jest asercja z linijki 38. Otrzymany wynik nie jest zgodny z semantyką Javy, jest natomiast zgodny 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ługa synchronizacji jest zapewniana automatycznie przez obiekt aktora. 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27

import p l . edu . pw . a k o s i c k i . r o l e s . Actor ; import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s RoleExample1 { s t a t i c c l a s s Ship { private int p o s i t i o n = 0 ; public int g e t P o s i t i o n ( ) { return p o s i t i o n ; } public void f l e e t ( int d i s t a n c e ) { this . p o s i t i o n = distance ; } } s t a t i c c l a s s Car { private int p o s i t i o n = 0 ; public int g e t P o s i t i o n ( ) { return p o s i t i o n ; } public void move ( int d i s t a n c e ) { t h i s . p o s i t i o n += d i s t a n c e ; } }

62

28 29 30 31 32 33 34 35 36 37 38 39 40

public s t a t i c void main ( S t r i n g [ ] a r g s ) { Actor amphibian = A c t o r F a c t o r y . c r e a t e A c t o r ( ) ; Ship s h i p = amphibian . p l a y R o l e ( Ship . c l a s s ) ; s h i p . f l e e t ( 10 ) ; Car c a r = amphibian . p l a y R o l e ( Car . c l a s s ) ; c a r . move ( −15 ) ; assert s h i p . g e t P o s i t i o n ( ) == −5; } }

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ól było zachowanie możliwie największej prostoty oraz możliwości współdziałania ze zdefiniowanymi już komponentami (klasami). W wyniku takiego założenia biblioteka dynamicznych ról udostępnia bardzo ograniczony zbiór klas i interfejsów, które jednak okazują się być zestawem w zupełności wystarczającym. Wszystkie udostępniane przez bibliotekę klasy i interfejsy10 umieszczone są bezpośrednio w pakiecie pl.edu.pl.akosicki.roles. Przy dalszych odwołaniach do typów udostępnianych przez bibliotekę, nazwa pakietu będzie na ogół pomijana. Ta sama skrótowa konwencja zostanie 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 raptem jednego podstawowego interfejsu, tj. Actor, oraz typów o charakterze pomocniczym. Interfejs Actor służy, zgodnie ze swoją nazwą, do reprezentacji aktora. Referencję do interfejsu aktora można uzyskać za pomocą statycznych metod fabryki ActorFactory. Enumeracja InitialSynchronization służy do określania rodzaju jednorazowej synchronizacji, która dokonuje 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 formie 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 synchronizacji pomiędzy poszczególnymi rolami danego aktora. Stosowania adnotacji tych jest czysto opcjonalne. Oprócz typów przeznaczonych do bezpośredniego użycia, biblioteka definiuje także niewielką hierarchię wyjątków. Hierarchia ta została przedstawiona na rysunku 3.5. Wyjątki mogą być odrzucane przede wszystkim podczas bezpośredniego użycia aktora, niemniej istnieje możliwość ich odrzucania także podczas użycia obiektu reprezentującego rolę pewnego 10

Adnotacje języka Java są szczególnym rodzajem interfejsów, enumeracja natomiast – klas. Począ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 pewnej klasy bazowej dla klas mających być używanymi w formie ról. Pomysły te zostały, rzecz jasna, odrzucone. 11

63

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

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

64

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 publiczne metody pozwalające na uzyskanie referencji do interfejsu Actor: Actor createActor() — podstawowa metoda tworząca obiekt aktora. Metoda ta została użyta w programie zaprezentowanym na listingu 3.6. Actor createThreadSafeActor() — metoda tworząca obiekt aktora dostosowany do użycia w środowisku wielowątkowym. Wszelkie metody uzyskanej w ten sposób implementacji aktora są synchronizowane. Co więcej, wszystkie metody obiektów użytych w formie ról danego aktora, stają się także metodami synchronizowanymi. Dzięki temu zarówno sam aktor, jak i odgrywane przez niego role, stają się de facto monitorami. Użycie metody tworzącej tego rodzaju aktora może w określonej sytuacji wydatnie umniejszyć nakład pracy, 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 zupeł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 standardowym wyjściu następujących komunikatów: started finished started finished

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s SynchExample implements Runnable { public void run ( ) { System . out . p r i n t l n ( ” s t a r t e d ” ) ; try { Thread . s l e e p ( 10000 ) ; } catch ( I n t e r r u p t e d E x c e p t i o n e ) { } System . out . p r i n t l n ( ” f i n i s h e d ” ) ; } s t a t i c T c r e a t e S y n c h I n s t a n c e ( C l a s s c l a z z ) { return A c t o r F a c t o r y . c r e a t e T h r e a d S a f e A c t o r ( ) . p l a y R o l e ( c l a z z ) ; }

65

18 19 20 21 22 23 24 25

public s t a t i c void main ( S t r i n g [ ] a r g s ) { SynchExample o b j e c t = c r e a t e S y n c h I n s t a n c e ( SynchExample . c l a s s ) ; Thread t h r e a d = new Thread ( o b j e c t ) ; thread . s t a r t ( ) ; o b j e c t . run ( ) ; } }

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 aktora 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 metody T playRole(Class, Object...). Pierwszym argumentem metody jest obiekt reprezentujący klasę, której instancja ma posłużyć za rolę dla danego aktora. Kolejne argumenty zostaną przekazana dla konstruktora danej klasy. Nowo utworzony obiekt jest automatycznie wią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 statycznego wyboru konstruktora określonym w specyfikacji języka Java, z tą różnicą iż za typ argumentów przyjmowany jest nie deklarowany typ danej zmiennej lecz typ faktycznej wartości wskazywanej przez zmienną. Modyfikator dostępu do konkretnego konstruktora jest ignorowany, 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ć o określeniu explicite argumentów konstruktora wskazujących na klasy otaczające, które normalnie dodawane są przez kompilator w sposób niejawny. W sytuacji gdy przekazanych argumentów nie da się użyć w wypadku żadnego konstruktora lub gdy nie da się wybrać pojedynczego konstruktora dla którego dopasowanie parametró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 aktor nie ma możliwości odgrywania więcej niż jednej roli określonego typu na raz. Próba użycia metody 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 konstruktora są w takiej sytuacji ignorowane. 12

W opinii autora jest to najsensowniejsze rozwiązanie. Alternatywą byłoby tutaj respektowanie modyfikatorów dostępu, co nastręczyłoby sporo trudnych do rozstrzygnięcia problemów — np. kwestii tego gdzie i kiedy można uzyskać dostęp do konstruktora o dostępie pakietowym. 13 Strategię 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 nie mniejszy niż w fazie poprzedniej. W drugiej i trzeciej fazie dopuszczalne jest stosowanie automatycznego opakowywania 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 ów posiada element największy, zostaje on wybrany jako właściwy konstruktor. W przeciwnym razie rozpoczęta zostaje kolejna faza dopasowywania. W ostateczności odpowiedni konstruktor może nie być w ogóle znaleziony.

66

Listing 3.8 przedstawia przykład uzyskania instancji obiektu roli dla danego aktora. Wywołanie metody playRole w linijce 17 prowadzi do utworzenia nowej instancji obiektu RoleCtors i przypisania jej do aktora actor. Stworzenie obiektu odbywa się tutaj przy użyciu konstruktora RoleCtors(A, java.lang.Object). Kolejne dwa wywołania metody playRole nie prowadzą do żadnych, poza zwróceniem referencji, efektów — dany aktor odgrywa 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 w celu 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 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26

import p l . edu . pw . a k o s i c k i . r o l e s . Actor ; import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s R o l e C t o r s { s t a t i c c l a s s A{} s t a t i c c l a s s B{} R o l e C t o r s (A o1 , Object o2 ) { } R o l e C t o r s ( Object o1 , B o2 ) { } public s t a t i c void main ( S t r i n g [ ] a r g s ) { Actor a c t o r = A c t o r F a c t o r y . c r e a t e A c t o r ( ) ; R o l e C t o r s r1 , r2 , r 3 ; r 1 = a c t o r . p l a y R o l e ( R o l e C t o r s . c l a s s , new A( ) , new Object ( ) ) ; // 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 a r 2 = a c t o r . p l a y R o l e ( R o l e C t o r s . c l a s s , new A( ) , new B( ) ) ; r 3 = a c t o r . p l a y R o l e ( R o l e C t o r s . c l a s s , new Object ( ) , new Object ( ) ) ; assert r 1 == r 2 ; assert r 2 == r 3 ; } }

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, utworzone 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ć żadnych działań związanych bezpośrednio z mechanizmem dynamicznych ról. Co więcej, może on nawet 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

metoda

działanie

T playRole(Class,Object...)

tworzy nowy obiekt roli lub zwraca obiekt już istniejący usuwa istniejący obiekt roli stwierdza czy aktor odgrywa daną rolę

boolean detachRole(Class) boolean hasRole(Class)

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ęcia lub, 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 aktorów nie posiadałyby możliwości dynamicznego pozbywania się odgrywanych ról. Odłączanie roli od danego aktora odbywa się za pomocą metody boolean detachRole(Class). Jeśli z danym aktorem jest stowarzyszona rola o danym typie, wywołanie metody detachRole zwraca wartość true oraz powoduje odłączenie roli. W przeciwnym wypadku, wywołanie metody 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 ten może być w dalszym ciągu normalnie użytkowany, przy czym nie działa on już na rzecz swojego dawnego 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ży zauważ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 ponowne wywołanie metody playRole. W takiej sytuacji aktualny obiekt danej roli aktora oraz obiekt bę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ępnia także pomocniczą metodę boolean hasRole(Class), która pozwala stwierdzić czy aktor posiada rolę o określonym typie. Tablica 3.1 zawiera podsumowanie metod dotyczących obsługi ról.

Program z listingu 3.9 stanowi prostą demonstrację odłączania ról. Diagramy obiektów 3.6a, 3.6b i 3.6c z rysunku 3.6 obrazują sytuację, odpowiednio, z linijek 17, 23 i 28. 1 2 3 4 5 6 7 8

import p l . edu . pw . a k o s i c k i . r o l e s . Actor ; import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s A ct o rD et a ch i ng { s t a t i c c l a s s A{} s t a t i c c l a s s B{} public s t a t i c void main ( S t r i n g [ ] a r g s ) {

68

9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33

Actor a c t o r = A c t o r F a c t o r y . c r e a t e A c t o r ( ) ; A a1 , a2 , a3 ; B b; a1 = a c t o r . p l a y R o l e ( A. c l a s s ) ; b = actor . playRole ( B. class ) ; a2 = a c t o r . p l a y R o l e ( A. c l a s s ) ; assert a1 == a2 ; assert a c t o r . hasRole ( A. c l a s s ) ; assert a c t o r . hasRole ( B . c l a s s ) ; a c t o r . d e t a c h R o l e ( A. c l a s s ) ; assert ! a c t o r . hasRole ( A. c l a s s ) ; assert a c t o r . hasRole ( B . c l a s s ) ; a3 = a c t o r . p l a y R o l e ( A. c l a s s ) ; assert a c t o r . hasRole ( A. c l a s s ) ; assert a c t o r . hasRole ( B . c l a s s ) ; assert a2 != a3 ; } }

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ów odgrywanych ról. Synchronizacja polega na utrzymywaniu spójnych wartości pomiędzy polami poszczególnych obiektów ról, dzięki czemu aktor staje się czymś więcej niż zwyczajnym kontenerem obiektów. Po każdorazowym wykonaniu metody pewnej roli, wartości pól obiektów pozostałych ról zostają uaktualnione. W uściśleniu, ma to miejsce bezpośrednio przed zakończeniem wywołania danej metody lub konstruktora roli14 . 14 Jest to ten sam punkt wywołania metody, w którym propozycja PPK wymaga sprawdzenia warunków końcowych.

69

Dwa należące do różnych obiektów pola, zostają ze sobą zsynchronizowane, jeśli spełnione są 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 zdefiniowanymi 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 gdy nie zostanie spełnione dowolne z pierwszych czterech kryteriów, nie dochodzi po prostu do synchronizacji. W przypadku gdy pierwsze cztery warunki są spełnione, lecz typy poszczególnych pó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 przeprowadzenia 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 pustej nazwie). Sama synchronizacja nastąpiła bezpośrednio przed zakończeniem wywołania metody move obiektu car. Warto w tym punkcie zwrócić jeszcze uwagę na fakt, że pole może uczestniczyć w synchronizacji w dwojaki sposób: a) jako pole którego wartość przypisywana jest innym polom lub b) jako pole, któremu przypisywana jest wartość innego pola. Reguła dotycząca tej kwestii jest prosta — wartości pól obiektu roli, na rzecz którego nastąpiło aktualne wywołanie metody przypisywane są, przy uwzględnieniu odpowiednich kryteriów synchronizacji, polom pozostałych obiektów. Autor, określając wyżej przedstawiony schemat synchronizacji, kierował się przede wszystkim jego użytecznością. Ów sposób może okazać się jednak nieodpowiedni dla pewnych potrzeb. Może to mieć w szczególności miejsce, gdy dynamiczne role mają zostać zastosowane do istniejącego już zbioru klas, w wypadku których nie stosowano spójnego nazewnictwa pól. Czasami wreszcie, może zachodzić potrzeba zupełnego wykluczenia niektórych pól z mechanizmu 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 zmiany domyślnego sposobu synchronizacji. Adnotacja Scope może zostać użyta w nagłówku definicji typu. Służy ona do nadpisania nazwy pakietu używanego podczas synchronizacji ról aktora. Po opatrzeniu danej klasy ową adnotacją, mechanizm synchronizacji ról będzie traktował ją tak jakby została zadeklarowana w pakiecie o nazwie podanej w argumencie adnotacji. Definicja adnotacji Scope jest następująca: package p l . edu . pw . a k o s i c k i . r o l e s ;

70

@Retention ( R e t e n t i o n P o l i c y .RUNTIME ) @Target ( ElementType .TYPE ) public @ i n t e r f a c e Scope { String value ( ) ; }

Przeznaczona do użycia w przypadku pól adnotacja Name ma funkcję analogiczną do adnotacji Scope. Służy ona zmianie nazwy pola używanej podczas synchronizacji. Definicja adnotacji jest następująca: package p l . edu . pw . a k o s i c k i . r o l e s ; @Retention ( R e t e n t i o n P o l i c y .RUNTIME ) @Target ( ElementType . FIELD ) public @ i n t e r f a c e Name { String value ( ) ; }

Nieco inne zastosowanie posiada trzecia z dostarczonych adnotacji — Ignore. Adnotacja ta jest prostym znacznikiem służącym do wyróżniania pól nie biorących udziału w synchronizacji. Pole oznaczone adnotacją Ignore będzie zwyczajnie ignorowane — zarówno w sytuacji gdy na rzecz roli o klasie je zawierającej zostanie wywołana metoda, jak i w sytuacji gdy pewna metoda zostanie wywołana na rzecz innej roli z obrębu danego aktora. Definicja anotacji Ignore jest następujące: package p l . edu . pw . a k o s i c k i . r o l e s ; @Retention ( R e t e n t i o n P o l i c y .RUNTIME) @Target ( ElementType . FIELD ) public @ i n t e r f a c e I g n o r e { }

Oczywiście, zgodnie ze standardowym zachowaniem się adnotacji, zmiany wprowadzone do 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 dowolnej z adnotacji pozostawia trwały, choć bardzo nieznaczny, ślad w danej klasie — wszystkie z adnotacji opatrzone są meta-adnotacją java.lang.annotation.Retention z parametrem RUNTIME, 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 przedstawiony został na listingu 3.10. 1 2 3 4

import import import import

p l . edu . pw . p l . edu . pw . p l . edu . pw . p l . edu . pw .

akosicki akosicki akosicki akosicki

. . . .

roles roles roles roles

. Actor ; . ActorFactory ; . Scope ; . Name ;

15

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

71

5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57

import p l . edu . pw . a k o s i c k i . r o l e s . I g n o r e ; public c l a s s F i e l d s S y n c h r o n i z a t i o n { static class A { private int v a l u e = 1 ; public void s e t V a l u e ( int v a l u e ) { this . value = value ; } public int g e t V a l u e ( ) { return v a l u e ; } } @Scope ( ” p l . some package ” ) static class B { private int v a l u e = 2 ; public void s e t V a l u e ( int v a l u e ) { this . value = value ; } public int g e t V a l u e ( ) { return v a l u e ; } } static class C { @Name( ” v a l u e ” ) private int c o u n t e r = 3 ; @Ignore private int v a l u e = 4 ; public void s e t C o u n t e r ( int v a l u e ) { this . counter = value ; } public int g e t C o u n t e r ( ) { return c o u n t e r ; } public void s e t V a l u e ( int v a l u e ) { this . value = value ; } public int g e t V a l u e ( ) { return v a l u e ; } }

72

58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

public s t a t i c void main ( S t r i n g [ ] a r g s ) { Actor a c t o r = A c t o r F a c t o r y . c r e a t e A c t o r ( ) ; A a = a c t o r . p l a y R o l e ( A. c l a s s ) ; B b = actor . playRole ( B. class ) ; C c = acto r . playRole ( C. class ) ; a . setValue ( 5 ) ; assert assert assert assert

a . g e t V a l u e ( ) == 5 ; b . g e t V a l u e ( ) == 2 ; c . g e t C o u n t e r ( ) == 5 ; c . g e t V a l u e ( ) == 4 ;

} }

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 rolami jest przeprowadzana w dwóch przypadkach: tuż przed zakończeniem wywoływania metody obiektu roli oraz tuż przed zakończeniem tworzenia samego obiektu roli (tj. po wywołaniu jej konstruktora). O ile w pierwszym przypadku przyjęcie, że to zmiany powstałe w danym obiekcie powinny zostać skopiowane do pozostałych obiektów jest zupełnie sensowne i zrozumiałe, o tyle w przypadku wywołania konstruktora kwestia ta pozostaje dyskusyjna. Czy obiekt nowoutworzonej roli ma dostosować wartości swoich składowych do wartości zawartych w dotychczasowych rolach, czy też ma mieć miejsce synchronizacja zgoła odwrotna? Ponieważ odpowiedź na tak postawione pytanie może zależeć od kontekstu, w którym używany jest mechanizm dynamicznych ról, autor postanowił umożliwić użytkownikowi ustawienie wybranego sposobu początkowej synchronizacji. W tym celu interfejs aktora udostępnia dwie metody: InitialSynchronization getInitialiSynchronization() oraz void setInitialSynchronization( InitialSynchronization type ), służące odpowiednio do sprawdzenia aktualnego sposobu początkowej synchronizacji dla danego aktora oraz do ustawienia sposobu początkowej synchronizacji dla danego aktora. Typ Actor.InitialSynchronization jest enumeracją określającą cztery rodzaje początkowej synchronizacji: INHERIT jest domyślnym sposobem synchronizacji. Polega on na tym, że nowoutworzony obiekt 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 aktora 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ę tu jednak, że wartości pól w nowoutworzonym obiekcie będą takie same jak wartości odpowiadających im pól w obiektach odgrywanych już ról. W razie złamania tego nakazu, następuje odrzucenie wyjątku ConformationException.

73

3.2.7

Ograniczenia dotyczące użycia ról

Autor przygotował propozycję dynamicznych ról w taki sposób aby definiowanie klas przeznaczonych na użycie w formie ról było, w miarę możliwości, obarczone jak najmniejszymi ograniczeniami. Ograniczenia te nie zostały jednak zupełnie wyeliminowane. Klasy o pewnych szczególnych właściwościach nie mogą być w ogóle używane jako role. W wypadku części klas, 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ły one klasami finalnymi oraz aby żadna ze zdefiniowanych przezeń metod nie była metodą finalną. Każda dowolna klasa spełniająca to kryterium może zostać obiektem roli pewnego aktora16 . Należy zauważyć, że zgodnie z ową regułą, role mogą być odgrywane także przez klasy abstrakcyjne. Odbywa się to poprzez stworzenie klasy adaptera z zalążkami abstrakcyjnych metod, które zwracają wartość domyślną typu danej metody. Domyślne wartości typów zostały przedstawiona przy okazji warunków końcowych propozycji PPK w tablicy 2.4 (strona 36). Wpomniane ograniczenie wynika niestety z powodów czysto technicznych i jest wynikiem swego rodzaju kompromisu pomiędzy prostotą propozycji dynamicznych ról a jej możliwościami. Więcej informacji temat technicznych aspektów implementacji biblioteki dynamicznych ról można znaleźć w sekcji 3.4. Mimo że mechanizmu dynamicznych ról można używać w kombinacji z praktycznie dowolnymi klasami, intencją autora było jego użycie w przypadku komponentów udostępniających swoje właściwości jedynie za pomocą metod. Przykładem tego rodzaju komponentów są klasy utworzone zgodnie z konwencją JavaBeans. Jedną z zasad konwencji JavaBeans jest stosowanie tzw. akcesorów czyli specjalnych metod służących do pobrania bądź ustawienia wartości pola klasy. Same pola powinny być zabezpieczone przed bezpośrednim dostępem z zewnątrz za 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 to bardzo istotne gdyż, jak to zostało w poprzedniej sekcji zaznaczone, synchronizacja obiektów poszczególnych ról ma miejsce jedynie podczas wywołań metod. Umieszczenie w klasie pól o dostępie publicznym może prowadzić do rozspójnienia się wartości pól pomiędzy poszczególnymi rolami aktora. Wbrew pozorom restrykcja ta nie jest szczególnie uciążliwa. W dobrze zbudowanym systemie większość klas, o nieco bardziej rozbudowanej funkcjonalności, spełnia konwencję 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 w celach introspekcji może w najlepszym razie doprowadzić użytkownika do odkrycia, że obiekty ról nie są tym czym się wydają. W gorszym scenariuszu, np. przy próbie zmiany wartości prywatnego pola, może skończyć się to odrzuceniem wyjątku lub w ogóle doprowadzić do zaburzeń w funkcjonowaniu pewnych wewnętrznych mechanizmów dynamicznych ról. 16

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

74

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 dostarczona w postaci zwyczajnej biblioteki, ich użycie nie nastręcza od strony technicznej żadnych trudności. W szczególności, nie ma tu potrzeby wykonywania jakiegokolwiek dodatkowego kroku podczas fazy budowania oprogramowania, tak jak to miało miejsce chociażby w wypadku przedstawionej wcześniej propozycji PPK, gdzie przed właściwym procesem kompilacji należało dodatkowo uruchomiać translator. W związku z tym, po dołączeniu do projektu odpowiedniej 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ślony w 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 oferuje użytkownikowi dosyć wyrafinowanego rodzaju abstrakcję, której celem jest niejako uproszczenie, 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ż powodu moż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 dynamicznych ról w przypadku istniejących już dużych systemów. Odmiennie natomiast przedstawia się kwestia utylizacji istniejących już klas, o których da się powiedzieć, że reprezentują swego rodzaju role, czyli pewne aspekty funkcjonalne większych odeń bytów. Możliwość wykorzystania takiego zbioru klas w formie ról zależy od wielu czynników, w tym m.in. od a) jakości kodu istniejących klas, b) ich logicznego umiejscowienia w 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 kosztownych modyfikacji — może się okazać, że przy spójnym nazewnictwie pól klas, jedynym niezbędnym zabiegiem będzie użycie tu i ówdzie adnotacji Ignore. Taka sytuacja miałaby prawdopodobnie miejsce w przypadku zbioru klas przedstawionych na rysunku 3.2. Czasami oczywiście adaptacja istniejącego zbioru klas na użytek dynamicznych ról może pociągać za sobą konieczność wprowadzenia pewnych zmian w kodzie lub też, z powodu wewnętrznych niespójności pomiędzy klasami, może nie być w ogóle możliwa17 . Choć nie jest to bezpośrednim przedmiotem niniejszego punktu, warto zwrócić uwagę na to, że oprócz użycia zgodnego ze swoim zasadniczym przeznaczeniem, tj. modelowania bytów o dynamicznie zmieniającej się funkcjonalności, zaproponowana przez autora biblioteka może zostać wykorzystana także na szereg innych, w pewnym sensie obocznych, sposobów, w tym np.: • jako narzędzie służące do dynamicznego tworzenia synchronizowanych obiektów pewnej klasy, której pierwotna definicja nie zawierała informacji o potrzebie synchronizacji 17

Na dobrą sprawę praktycznie każdy dowolny zbiór klas może zostać użyty w formie ról pewnego bytu, co można uzyskać poprzez trywialne wykluczenie wszystkich pól z mechanizmu synchronizacji. Sęk w tym, że w takiej sytuacji pomiędzy poszczególnymi obiektami ról danego aktora nie ma de facto żadnego powiązania a sam 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

w postaci modyfikatorów synchronized. Stosowny przykład został przedstawiony na listingu 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 formie pojedynczej biblioteki tegoż języka. Biblioteka owa została zrealizowana w oparciu o bibliotekę Javassist[5], która dostarcza narzędzi pozwalających na strukturalne modyfikacje obiektów klas18 . Informacje zawarte w poprzednich podrozdziałach, a także w dokumentacji samej biblioteki, 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ę nieco wewnętrznym mechanizmom funkcjonowania implementacji. Pozwoli to na lepsze zrozumienie istoty 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 szczególności ich niejawnego wiązania z odpowiednimi obiektami aktorów. Podczas tworzenia obiektu roli za pomocą metody playRole obiektu aktora, tworzona i zwracana jest w rzeczywistości instancja bezpośredniej podklasy klasy przekazanej jako argument. Podklasę tą można nazwać klasą agenta. Klasa agenta nadpisuje wszystkie metody publiczne klasy bazowej, tj. klasy roli. Jest to bezpośrednim powodem dla którego implementacja zabrania odgrywania ról za pomocą klas finalnych lub zawierających metody finalne. Klasa agenta zawiera także pewne dodatkowe pola prywatne związane z mechanizmem synchronizacji. Nazwa klasy agenta 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 w sekcji 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 nawet podczas normalnego użytkowania klas, można spotkać się z pewnymi aspektami wewnętrznej implementacji biblioteki ról - np. podczas procesu debugowania aplikacji. W świetle powyższej dyskusji, wyjście jakie otrzyma się po wykonaniu programu z listingu 3.11, tj.: class Role$$__RoleAgent__ class Role nie powinno być już dla użytkownika zaskakujące. 1 2 3 4 5 6

import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s Role { public s t a t i c void main ( S t r i n g [ ] a r g s ) { C l a s s a g e n t C l a s s = A c t o r F a c t o r y . c r e a t e A c t o r ( ) . p l a y R o l e ( Role . c l a s s ) . g e t C l a s s ( ) ; 18 Innymi słowy, biblioteka Javassist stanowi w pewnym sensie dopełnienie mechanizmu refleksji, który sam w sobie służy jedynie badaniu struktury klas.

76

7 8 9 10 11

System . out . p r i n t l n ( a g e n t C l a s s ) ; System . out . p r i n t l n ( a g e n t C l a s s . g e t S u p e r c l a s s ( ) ) ; } }

Listing 3.11: Dynamiczne role a mechanizm refleksji. W punkcie tym należy przypomnieć, że w Javie istnieją dwa sposoby na uzyskanie obiektu metaklasy. Pierwszym z nich jest posłużenie się specjalnym operatorem .class, który zwraca obiekt metaklasy dla konkretnej klasy (np. Role.class). Drugim sposobem jest użycie zadeklarowanej w klasie java.lang.Object publicznej metody Class getClass(), która zwraca aktualny typ danego obiektu. W związku z tym, w wypadku obiektu reprezentującego pewną rolę, metoda getClass() zwróci obiekt metaklasy dynamicznie utworzonej klasy agenta. 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 metody playRole() obiektu aktora. Niestety metoda Class getClass() jest obsługiwana w sposó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 Java można uznać za dyskusyjny. Jest to w dużej mierze wynikiem eksperymentalnego charakteru tej części pracy. O ile w przypadku propozycji PPK zagadnienie było jasno i klarownie sformułowane, o tyle dynamiczne role stanowią pewną próbę przezwyciężenia problemu z zakresu modelowania bytów o dynamicznym charakterze. Poniżej znajduje się uzasadnienie autora 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 wsparcie ze strony składni, jednakże pomysł ten został prędko odrzucony. W odróżnieniu od propozycji PPK, koszt wprowadzenia nowych konstrukcji składniowych dla dynamicznych ról przekroczyłby zdecydowanie potencjalne korzyści jakie możnaby w ten sposób osiągnąć. W przypadku PPK specjalna składnia zapewniała konieczną separację pomiędzy właściwą logiką programu a definicjami warunków kontraktów. W przypadku dynamicznych ról konieczności takiej separacji już nie było. Definiowanie ról: Zgodnie z ostatnimi tendencjami panującymi w środowisku zrzeszonym wokół 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ż pozwala ono programiście na tworzenie komponentów ról w postaci zwykłych obiektów, które mogą 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, 19 Akronim ten pochodzi z angielskiego i może być rozwijany na różne sposoby, np. jako “Keep It Short and Simple”.

77

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 interfejsu. Każda z alternatyw wiązała się z pewnymi dodatkowymi restrykcjami, których narzucania autor wolał uniknąć. Obiekty aktorów: W związku z przyjętym sposobem definiowana ról, wprowadzenie specjalnych 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 analogicznych jak w poprzednim punkcie. Rozważaną alternatywą było tutaj wprowadzenie nowego 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 uzyskania jakichkolwiek korzyści. Współdziałanie ról: Mechanizmem zapewniającym integrację pomiędzy rolami jest synchronizacja 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 definiowaniem klas przeznaczonych do odgrywania ról. Stałoby to w sprzeczności z przyjętą wcześniej możliwością utylizacji jak najszerszej liczby klas. Warto zauważyć, że przyjęty model funkcjonowania ról przypomina nieco rozwiązanie zastosowane w niektórych implementacjach standardu JPA20 . W jednym i drugim przypadku mamy do czynienia z pewnymi obiektami typu POJO, których wartości są w pewien ukryty sposób synchronizowane. Komponenty encyjne z JPA synchronizowane są mianowicie z odpowiednimi rekordami w bazie danych, obiekty ról z propozycji autora z innymi obiektami ról w obrębie tego samego aktora. Porównanie to pozornie wypada na niekorzyść propozycji autora, w której zabronione jest bezpośrednie odwoływanie się do pól obiektu roli. Zakaz taki nie obowiązuje w JPA, gdzie działanie takie 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ąca JPA. Wiąże się to, z grubsza rzecz biorąc, z tym, że dostarczający usługę JPA kontener EJB kontroluje jednocześnie obiekt ładowacza klas21 używanego m.in. do wczytywania klas klas pewnych obiektów korzystających korzystających z danych komponentów encyjnych. Biblioteka dynamicznych ról, w odróżnieniu od kontenera EJB, nie sprawuje pieczy 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ą propozycji autora. Trudno sobie zresztą wyobrazić rozwiązanie wygodniejsze i bardziej, z tej perspektywy, elastyczne. 20

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

78

3.5.1

Możliwości dalszego rozwoju

Propozycja dynamicznych ról dla języka Java nie jest zdecydowanie propozycją idealną. Z racji eksperymentalnego charakteru, autor przy jej formułowaniu kierował się nie tylko przesłankami czysto racjonalnymi ale i też, trzeba to powiedzieć, dokonywał wyborów o arbitralnym 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ściach lepszych 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 mogł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 role bezużyteczne mogłoby być automatycznie usuwane. Działanie takie, analogiczne do oferowanego przez specjalny rodzaj mapy — java.util.WeakHashMap, pomagałoby zapobiegać ewentualnym wyciekom zasobów. Bliższe informacje na temat różnych klas referencji 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, tak aby możliwe było przepisanie przykładu z listingu 3.6 (strona 62) w postaci zaprezentowanej na listingu 3.12. Język Java nie oferuje co prawda bezpośredniej możliwości przeciążania operatorów, jednak żądane zachowanie można z powodzeniem osiągnąć za pomocą procesora adnotacji22 , którego działanie polegało by na zwyczajnym zastępowaniu operacji rzutowania wywołaniami metody playRole. Minusem takiego rozwiązania był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 od wywołań translatora w propozycji dla PPK. • Możliwość uzyskiwania od danego aktora zbioru klas aktualnie odgrywanych przez niego ról. Mogłoby się to odbywać np. za pomocą metody java.util.Collection> getRoles(), lub poprzez implementację interfejsu Iterable>. • Udostępnienie dodatkowych mechanizmów współpracy z instancjami obiektów odgrywających role, w tym np. możliwości stwierdzania, czy dany obiekt jest w rzeczywistości obiektem roli oraz pobierania obiektu aktora, na rzecz którego może on potencjalnie działać. Funkcjonalność tego rodzaju mogłaby być dostępna za pomocą dodatkowej klasy narzędziowej lub bezpośrednio za pomocą obiektów ról. W drugim przypadku obiekty 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ści agentem pewnego aktora mogłoby się odbywać za pomocą operatora instanceof. • Zmniejszenie czasu synchronizacji poprzez oparcie go na bezpośrednim kopiowaniu wartości odpowiednich pól. W dostarczonej przez autora bibliotece synchronizacja odbywa się za pomocą powolnego mechanizmu refleksji. Nie jest to co prawda uwaga związana bezpośrednio z funkcjonalnością propozycji, niemniej autor dostrzega bardzo wyraźną potrzebę zastosowania się doń. 22

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

79

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40

import p l . edu . pw . a k o s i c k i . r o l e s . Actor ; import p l . edu . pw . a k o s i c k i . r o l e s . A c t o r F a c t o r y ; public c l a s s RoleExample1 { s t a t i c c l a s s Ship { private int p o s i t i o n = 0 ; public int g e t P o s i t i o n ( ) { return p o s i t i o n ; } public void f l e e t ( int d i s t a n c e ) { this . p o s i t i o n = distance ; } } s t a t i c c l a s s Car { private int p o s i t i o n = 0 ; public int g e t P o s i t i o n ( ) { return p o s i t i o n ; } public void move ( int d i s t a n c e ) { t h i s . p o s i t i o n += d i s t a n c e ; } } public s t a t i c void main ( S t r i n g [ ] a r g s ) { Actor amphibian = A c t o r F a c t o r y . c r e a t e A c t o r ( ) ; Ship s h i p = ( Ship ) amphibian ; s h i p . f l e e t ( 10 ) ; Car c a r = ( Car ) amphibian ; c a r . move ( −15 ) ; assert s h i p . g e t P o s i t i o n ( ) == −5; } }

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

80

Dodatek A

Mechanizm PPK - przykład użycia W rozdziałach 2 oraz 3 zamieszczonych zostało wiele przykładów użycia dostarczonych przez autora implementacji PPK oraz dynamicznych ról dla języka Java. Na ogół przykłady te miały na celu jedynie prezentację składni i semantyki omawianych w danym kontekście elementów JavaBC. Nie zawierały one natomiast wykonujących konkretne i praktyczne zadania programów. Co prawda autor wspominał tu i ówdzie o praktycznych zastosowaniach zaproponowanych przez siebie implementacji, jednakże nigdzie nie zamieścił nieco bardziej konkretnego i wyczerpującego przykładu użycia. Przykładu, który stanowiłby uzasadnienie praktyczności zaproponowanych przez niego koncepcji i towarzyszących im narzędzi programistycznych. Brak ten zrekompensowany jest w postaci niniejszego dodatku, który zawiera praktyczny przykład użycia mechanizmu PPK, oraz dodatku B zawierającego podobny przykład dla dynamicznych ról. Jako przykład użycia zaproponowanych mechanizmów PPK, autor postanowił przytoczyć 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żąca do tworzenia obiektów niezmienników. Użycie fabryki ma miejsce bezpośrednio w trakcie rozbioru gramatycznego, podczas napotkania odpowiednich konstrukcji składniowych. Przykładowo, podczas wykrycia w kodzie źródłowym bloku niezmiennika następuje wywołanie metody 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 niezmiennika.

beginAssertion(...) — metoda wywoływana podczas natrafienia na warunek niezmiennika.

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

81

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

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

addAssertionMessage(...) — metoda wywoływana podczas natrafienia na komunikat warunku.

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

endInvariant(...) — metoda oznaczająca zakończenie przetwarzania aktualnego niezmiennka.

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ń metod 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ń metody endAssertion lub też wywołania metody beginInvariant nie sparowanego odpowiednim wywołaniem endInvariant. Przykładowo dla kodu: public c l a s s C l a s s { private invariant { ∗ : true ; ∗ : true | | f a l s e : ”Some message ” ; }

82

}

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ąc się 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 kontrolę 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ło poprzez przetłumaczenie jej kodu do języka JavaBC. W efekcie spora część kodu pierwornego została zastąpiona definicją automatu skończonego oraz prywatnego niezmiennika. Dzięki temu kod programu zyskał na czytelności i zwięzłości — objętość klasy spadła z 359 linijek do 235 linijek2 , czyli o blisko 35%. Oryginalną wersję klasy można znaleźć w kodzie źródłowym translatora. Szczegóły techniczne, w tym liczne pola fabryki oraz zagnieżdżona klasa InvariantFactory nie są, w gruncie rzeczy, dla zrozumienia sedna przykładu istotne. Najistotniejsze fragmenty kodu znajdują się mniej więcej w połowie listingu. package p l . edu . pw . a k o s i c k i . p a r s e r . u t i l s ; import j a v a . u t i l . A r r a y L i s t ; import j a v a . u t i l . HashMap ; import j a v a . u t i l . HashSet ; 1

Mimo że warunki kontraktu dotyczyły metod publicznych, kontrola poprawności została oparta na asercjach. Autor nie zdecydował się tu na to aby złamanie kontraktu prowadziło do odrzucenia wyjątków java.lang.IllegalStateException bądź java.lang.IllegalArgumentException, gdyż klasa fabryki, jako taka, nie była i nie miała być częścią żadnego zewnętrznego interfejsu programistycznego. 2 Wnikliwy 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 kodu na kilka krótszych wierszy.

83

import j a v a . u t i l . L i s t ; import j a v a . u t i l . Map ; import j a v a . u t i l . S e t ; import import import import

p l . edu . pw . p l . edu . pw . p l . edu . pw . p l . edu . pw .

akosicki akosicki akosicki akosicki

. p a r s e r . u t i l s . Automaton . AutomatonState ; . parser . u t i l s . Invariant . InvariantAssertion ; . p a r s e r . u t i l s . S e m a n t i c E r r o r . Type ; . u t i l s . Pair ;

public c l a s s I n v a r i a n t s F a c t o r y { private Map<Pair, I n v a r i a n t F a c t o r y > f a c t o r i e s = new HashMap<Pair, I n v a r i a n t F a c t o r y > ( ) ; private L i s t <SemanticError > e r r o r s = new A r r a y L i s t <SemanticError > ( ) ; private I n v a r i a n t F a c t o r y c u r r e n t F a c t o r y = null ; private f i n a l L i s t <Pair<S t r i n g , C o d e P o s i t i o n >> c u r r e n t A s s e r t i o n S t a t e s = new A r r a y L i s t <Pair<S t r i n g , C o d e P o s i t i o n > >(); private f i n a l L i s t <Pair<E A c c e s s M o d i f i e r , C o d e P o s i t i o n >> c u r r e n t M o d i f i e r s = new A r r a y L i s t <Pair<E A c c e s s M o d i f i e r , C o d e P o s i t i o n > >(); private Pair<S t r i n g , C o d e P o s i t i o n > 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<S t r i n g , C o d e P o s i t i o n > c u r r e n t A s s e r t i o n M e s s a g e = 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 = f a l s e ; private c l a s s I n v a r i a n t F a c t o r y { private ClassName className = null ; private E A c c e s s M o d i f i e r m o d i f i e r = null ; private Automaton automaton = null ; private C o d e P o s i t i o n p o s i t i o n = null ; private L i s t a s s e r t i o n s = new A r r a y L i s t ( ) ; public void i n i t ( C o d e P o s i t i o n p o s i t i o n , ClassName className , E A c c e s s M o d i f i e r m o d i f i e r , Automaton automaton ) { this . p o s i t i o n = p o s i t i o n ; t h i s . className = className ; this . modifier = modifier ; t h i s . automaton = automaton ; } public void a d d A s s e r t i o n ( Pair<S t r i n g , 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 , Pair<S t r i n g , C o d e P o s i t i o n > message , Pair<S t r i n g , CodePosition > . . . s t a t e s ) {

84

// 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 ( ) ; assertion . expressionLiteral = expressionLiteral . getFirst (); assertion . expressionLiteralCodePosition = 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 . m e s s a g e C o d e P o s i t i o n = message != null ? message . g e t S e c o n d ( ) : null ; Set<S t r i n g > u s e d S t a t e s = new HashSet<S t r i n g > ( ) ; f o r ( Pair<S t r i n g , C o d e P o s i t i o n > 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 ( ) . e q u a l s ( ” ∗ ” ) ; i f ( usedStates . contains ( state . getFirst () ) ) { e r r o r s . add ( new S e m a n t i c E r r o r ( Type . INV STATE REPEATED, s t a t e . g e t S e c o n d ( ) ) ) ; continue ; } u s e d S t a t e s . 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 S e m a n t i c E r r o r ( Type .INV UNKNOWN STATE USED, s t a t e . g e t S e c o n d ( ) ) ) ; 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 I n v a r i a n t g e t 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 = new I n v a r i a n 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 ’ p a r e n t ’ // has t o be a s s i g n e d manually assertion . invariant = invariant ; } invariant invariant invariant invariant invariant

. assertions = assertions ; . automaton = automaton ; . className = className ; . modifier = modifier ; . position = position ;

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

85

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 : c u r r e n t F a c t o r y == 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 : c u r r e n t A s s e r t i o n M e s s a g e != 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 : c u r r e n t A s s e r t i o n M e s s a g e == 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 | | c u r r e n t F a c t o r y != null ; ∗ : c u r r e n t M o d i f i e r s != null ; } public void a d d M o d i f i e r ( S t r i n g m o d i f i e r L i t e r a l , C o d e P o s i t i o n p o s i t i o n ) { i f ( ! transient ? OUTSIDE ) return ; EAccessModifier modifier = EAccessModifier . getModifier ( 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<E A c c e s s M o d i f i e r , C o d e P o s i t i o n >( m o d i 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 ; currentModifiers . clear ( ) ; } public void b e g i n I n v a r i a n t ( C o d e P o s i t i o n p o s i t i o n , ClassName className , Automaton automaton ) { transient : INVARIANT ; E A c c e s s M o d i f i e r m o d i f i e r = null ; f o r ( Pair<E A c c e s s M o d i f i e r , C o d e P o s i t i o n > p a i r : c u r r e n t M o d i f i e r s ) { i f ( m o d i f i e r != null ) { e r r o r s . add ( new S e m a n t i c E r r o r ( Type . INV MODIFIER REPEATED, p a i r . g e t S e c o n d ( ) ) ) ; continue ; } i f ( p a i r . g e t F i r s t ( ) == E A c c e s s M o d i f i e r .UNKNOWN ) { e r r o r s . add ( new S e m a n t i c E r r o r ( Type . INV INVAILD MODIFIER , p a i r . g e t S e c o n d ( ) ) ) ; continue ;

86

} assert m o d i f i e r == null ; modifier = pair . getFirst ( ) ; } i f ( m o d i f i e r == null ) { m o d i f i e r = E A c c e s s M o d i f i e r .PACKAGE; // d e f a u l t m o d i f i e r } Pair i n v a r i a n t K e y = new Pair(className , m o d i f i e r ) ; i f ( f a c t o r i e s . containsKey ( invariantKey ) ) { e r r o r s . add ( new S e m a n t i c E r r o r ( 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 ; } I n v a r i a n t F a c t o r y f a c t o r y = new I n v a r i a n t F a c t o r y ( ) ; f a c t o r y . i n i t ( p o s i t i o n , className , m o d i f i e r , automaton ) ; f a c t o r i e s . put ( i n v a r i a n t K e y , f a c t o r y ) ; currentFactory = factory ; } public void b e g i n A s s e r t i o n ( ) { if ( skipInvariantDeclaration ) return ; transient : ASSERT ; } public void a d d A s s e r t i o n S t a t e ( S t r i n g s t a t e , C o d e P o s i t i o n p o s i t i o n ) { if ( skipInvariantDeclaration ) 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<S t r i n g , C o d e P o s i t i o n >( s t a t e , p o s i t i o n ) ) ; } public void a d d A s s e r t i o n A l l S t a t e s ( C o d e P o s i t i o n p o s i t i o n ) { if ( skipInvariantDeclaration ) return ; transient : ASSERT AST ; } public void a d d A s s e r t i o n C o n d i t i o n ( S t r i n g e x p r e s s i o n L i t e r a l , CodePosition p o s i t i o n ) { if ( skipInvariantDeclaration ) 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<S t r i n g , C o d e P o s i t i o n >( expressionLiteral , position ); }

87

public void a d d A s s e r t i o n M e s s a g e ( S t r i n g message , C o d e P o s i t i o n p o s i t i o n ) { if ( skipInvariantDeclaration ) return ; transient : ASSERT MSG ; c u r r e n t A s s e r t i o n M e s s a g e = new Pair<S t r i n g , C o d e P o s i t i o n >( message , p o s i t i o n ) ; } @SuppressWarnings ( ” unchecked ” ) public void e n d A s s e r t i o n ( ) { if ( skipInvariantDeclaration ) 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 . addAssertion ( currentAssertionExpressionLiteral , currentAssertionMessage , c u r r e n t A s s e r t i o n S t a t e s . toArray ( new P a i r [ ] {} ) ) ; c u r r e n t A s s e r t i o n M e s s a g e = 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 ; currentAssertionStates . clear ( ) ; } public void e n d I n v a r i a n t ( ) { transient : OUTSIDE ; skipInvariantDeclaration = false ; c u r r e n t F a c t o r y = null ; } public Pair, L i s t <SemanticError >> g e t I n v a r i a n t s ( ) { L i s t i n v a r i a n t s = new A r r a y L i s t ( ) ; for ( InvariantFactory f a c t o r y : f a c t o r i e s . values ( ) ) { 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, L i s t <SemanticError >>( invariants , errors ) ; } }

Listing A.1: Efekt InvariantsFactory.

refaktoryzacji

pochodzącej

88

z

translatora

dla

PPK

klasy

Dodatek B

Dynamiczne role - przykład użycia Niech dana będzie potrzeba stworzenia systemu działającego w modelu usługodawca — usługobiorca. Ogólne wymagania dotyczące systemu można przedstawić w następujących punktach: • Zadaniem systemu ma być świadczenie usług na rzecz klientów systemu. • System ma umożliwiać danemu klientowi korzystanie z dowolnych oferowanych przez siebie 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 stanowi on jedynie szkic pewnego konkretnego rozwiązania1 . Niestety kontekst zastosowania dynamicznych ról, w odróżnieniu od mechanizmów PPK, wykracza znacznie poza obręb pojedynczej 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 programu konsoli pozwalającego na łączenie się zeń i wydawanie określonych poleceń. Dwa najistotniejsze z dostępnych poleceń pozwalają na tworzenie klientów oraz świadczenie na ich rzecz odpowiednich usług. Klienci systemu są reprezentowani przez aktorów, odgrywających role usługobiorców zgodnych z konkretnymi usługami. Jeśli w chwili skorzystania z danej usługi klient nie posiada roli właściwego usługobiorcy, rola ta zostanie mu w dynamiczny sposób przypisana. W celach demonstracyjnych autor przygotował dodatkowo parę klas usług i związanych z nimi 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 musi skorzystać każdy klient. Oprócz posiadania konta, klienci dostają możliwość wykupienia ubezpieczenia turystycznego oraz zaciągnięcia kredytu za pomocą karty kredytowej. Tablica B.1 zawiera 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 diagramie z rysunku B.1. 1

Przedstawiony 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ól za pomocą konstruktorów innych niż bezparametrowe czy przekazywanie parametrów wywołania usługi.

89

roles examples clients — pakiet zawierający klasy usługobiorców. Zawartości odpowiednich plików przedstawione zostały na stronach 98–99. AccountHolder.java — przykładowa klasa usługobiorcy (posiadacz rachunku bankowego). CreditCardHolder.java — przykładowa klasa usługobiorcy (posiadacz karty kredytowej). Klasa dziedziczy po AccountHolder.java. TravelInsuranceClient.java — przykładowa klasa usługobiorcy (ubezpieczony). Klasa dziedziczy po AccountHolder.java. services — pakiet zawierający klasy usług. Zawartości odpowiednich plików przedstawione zostały na stronach 100–102. ActivateCreditCard.java — przykładowa klasa usługi (aktywacja karty kredytowej). CheckAccountBalance.java — przykładowa klasa usługi (sprawdzenie stanu rachunku bankowego). CheckCreditCardBalance.java — przykładowa klasa usługi (sprawdzenie stanu karty kredytowej). CompenseTravelAccident.java — przykładowa klasa usługi (wypłacenie rekompensaty w ramach ubezpieczenia podróżnego). GetCredit.java — przykładowa klasa usługi (zaciągnięcie kredytu). InsureTravel.java — przykładowa klasa usługi (wykupienie ubezpieczenia podróżnego). RepayCredit.java — przykładowa klasa usługi (spłata zaciągniętego kredytu). Console.java — klasa konsoli, za pomocą której można zarządzać systemem. Zawartość pliku przedstawiona została na listingu B.1 (strona 93). IServiceProvider.java — generyczny interfejs usługi. Interfejs ten zawiera pojedynczą 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 na listingu B.3 (strona 93). ServicesList.java — klasa pomocnicza, której celem jest dostarczanie aktualnej listy 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ę klas umieszczonych w pakiecie roles examples.services — tak jak przedstawiono to na listingu B.15 (strona 102). Jeśli w trakcie działania systemu administrator zdecyduje się 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

Rysunek B.1: Diagram klas dla usługobiorców i usługodawców z przykładu użycia dynamicznych ról. Stereotyp “role” podkreśla, że dana klasa jest używana w formie roli. Na diagramie zostały przedstawione tylko niezbędne informacje. Poniżej przytoczony został fragment pewnej sesji programu konsoli: $ list clients <no clients defined> $ create client Aleksander Client Aleksander created. $ list services * ActivateCreditCard * CheckAccountBalance * CheckCreditCardBalance * CompenseTravelAccident * GetCredit * InsureTravel * RepayCredit $ list clients * Aleksander 91

- AccountHolder $ service client Aleksander Service called with result: $ list clients * Aleksander - AccountHolder $ service client Aleksander Service called with result: $ list clients * Aleksander - AccountHolder - TravelInsuranceClient $ service client Aleksander Service called with result: $ service client Aleksander Service called with result:

CheckAccountBalance "Aleksander’s balance is: 0."

InsureTravel "Aleksander has been insured"

CompenseTravelAccident "Compensation for Aleksander has been payed" CheckAccountBalance "Aleksander’s balance is: 9900."

Podczas sesji tej dodano nowego klienta, któremu następnie wyświadczone zostały pewne usługi. Klient ten początkowo posiadał jedynie rolę AccountHolder, jednakże skorzystanie z usł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ładu użycia.

92

package r o l e s e x a m p l e ; import j a v a . i o . ∗ ; import j a v a . n e t . ∗ ; public c l a s s C o n s o l e { public s t a t i c void main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n { S o c k e t s o c k e t = new S o c k e t ( I n e t A d d r e s s . g e t L o c a l H o s t ( ) , S e r v e r .PORT ) ; B u f f e r e d R e a d e r sReader = new B u f f e r e d R e a d e r ( new InputStreamReader ( s o c k e t . g e t I n p u t S t r e a m ( ) ) ) ; P r i n t W r i t e r s W r i t e r = new P r i n t W r i t e r ( new OutputStreamWriter ( s o c k e t . getOutputStream ( ) ) , true ) ; B u f f e r e d R e a d e r i n = new B u f f e r e d R e a d e r ( new InputStreamReader ( System . i n ) ) ; System . out . p r i n t ( ” $ ” ) ; for ( S t r i n g 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 ” . e q u a l s ( l i n e ) | | ” shutdown ” . e q u a l s ( l i n e ) ) break ; f o r ( S t r i n g s L i n e = sReader . r e a d L i n e ( ) ; s L i n e . l e n g t h ( ) != 0 ; s L i n e = sReader . r e a d L i n e ( ) ) System . out . p r i n t l n ( s L i n e ) ; System . out . p r i n t ( ” $ ” ) ; } } }

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

package r o l e s e x a m p l e ; public i n t e r f a c e I S e r v i c e P r o v i d e r { 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 r o l e s e x a m p l e ; import import import import import

java . io . ∗ ; java . net . ∗ ; java . u t i l . ∗ ; p l . edu . pw . a k o s i c k i . r o l e s . ∗ ; r o l e s e x a m p l e . c l i e n t s . AccountHolder ;

import s t a t i c r o l e s e x a m p l e . S e r v i c e s L i s t . ∗ ; public c l a s s S e r v e r extends Thread {

93

public s t a t i c f i n a l int PORT = 2 5 5 5 5 ; private s t a t i c L i s t a c t o r s = C o l l e c t i o n s . s y n c h r o n i z e d L i s t ( new L i n k e d L i s t () ) ; private f i n a l B u f f e r e d R e a d e r r e a d e r ; private f i n a l P r i n t W r i t e r w r i t e r ; S e r v e r ( S o c k e t s o c k e t ) throws E x c e p t i o n { r e a d e r = new B u f f e r e d R e a d e r ( new InputStreamReader ( s o c k e t . g e t I n p u t S t r e a m ( ) ) ) ; w r i t e r = new P r i n t W r i t e r ( new OutputStreamWriter ( s o c k e t . getOutputStream ( ) ) , true ) ; } @Override public void run ( ) { try { f o r ( S t r i n g l i n e = r e a d e r . r e a d L i n e ( ) ; l i n e != null ; l i n e = reader . readLine () ) { i f ( ” shutdown ” . e q u a l s ( l i n e ) ) { writer . p r i n t l n ( ” Server terminated . ” ) ; System . e x i t ( 0 ) ; } else i f ( ” e x i t ” . equals ( l i n e ) ) { w r i t e r . p r i n t l n ( ” Connection c l o s e d . ” ) ; writer . close ( ) ; break ; } else i f ( ” l i s t c l i e n t s ” . equals ( l i n e ) ) listClients (); else i f ( ” l i s t s e r v i c e s ” . equals ( l i n e ) ) listServices (); else { S t r i n g [ ] 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+” ) ) createClient ( lines [2] ); e l s e i f ( l i n e . matches ( ” d e l e t e c l i e n t \\w+” ) ) deleteClient ( lines [2] ); e l s e i f ( l i n e . matches ( ” s e r v i c e c l i e n t \\w+ \\w+” ) ) serviceClient ( lines [2] , lines [3] ); else w r i t e r . p r i n t l n ( ”Unknown command” ) ; } writer . println ( ) ; } } catch ( E x c e p t i o n e ) { e . printStackTrace ( ) ; } } private void l i s t S e r v i c e s ( ) { ServicesList services = getRecentServices ( ) ; 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

for ( ServiceType s e r v i c e : g e t R e c e n t S e r v 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 d e f i n e d >” ) ; return ; } ServicesList services = ServicesList . getRecentServices ( ) ; f o r ( Actor a c t o r : a c t o r s ) { writer . println ( ” ∗ ” + a c t o r . p l a y R o l e ( AccountHolder . c l a s s ) . getName ( ) ) ; Set> r o l e s = new HashSet>(); for ( ServiceType s e r v i c e : s e r v i c e s ) i f ( a c t o 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 ) ) writer . println ( ” − ” + 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 i n g name ) { i f ( f i n d C l i e n t ( name ) != null ) writer . println ( ” Client already exist . ” ) ; else { Actor a c t o r = A c t o r F a c t o r y . c r e a t e T h r e a d S a f e A c t o r ( ) ; a c t o r . p l a y R o l e ( AccountHolder . c l a s s , name ) ; a c t o r s . add ( a c t o r ) ; w r i t e r . p r i n t l n ( ” C l i e n t ” + name + ” c r e a t e d . ” ) ; } } private void d e l e t e C l i e n t ( S t r i n g name ) { Actor a c t o r = f i n d C l i e n t ( name ) ; i f ( a c t o r == null ) w r i t e r . p r i n t l n ( ” C l i e n t d o e s not e x i s t . ” ) ; else { a c t o r s . remove ( a c t o r ) ; w r i t e r . p r i n t l n ( ” C l i e n t ” + name + ” removed . ” ) ; } } private void s e r v i c e C l i e n t ( S t r i n g name , S t r i n g serviceName ) { Actor a c t o r = f i n d C l i e n t ( name ) ; i f ( a c t o r == null ) { w r i t e r . p r i n t l n ( ” C l i e n t d o e s 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 t R e c e n t S e r v i c e s ( ) . g e t S e r v i c e ( serviceName ) ;

95

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 d o e s not e x i s t . ” ) ; return ; } i f ( ! a c t o r . hasRole ( s e r v i c e . c l i e n t C l a s s ) ) { actor . 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 ( ) , actor . 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 ( E x c e p t i o n e ) { e . printStackTrace ( ) ; } } private s t a t i c Actor f i n d C l i e n t ( S t r i n g name ) { f o r ( Actor a c t o r : a c t o r s ) i f ( actor . playRole ( AccountHolder . c l a s s ) . getName ( ) . e q u a l s ( name ) ) return a c t o r ; return null ; } public s t a t i c void main ( S t r i n g [ ] a r g s ) throws E x c e p t i o n { f o r ( S e r v e r S o c k e t s S o c k e t = new S e r v e r S o c k e t ( PORT ) ; ; ) new S e r v e r ( s S o c k e t . a c c e p t ( ) ) . s t a r t ( ) ; } }

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

package r o l e s e x a m p l e ; import j a v a . l a n g . r e f l e c t . Method ; import j a v a . u t i l . ∗ ; public c l a s s S e r v i c e s L i s t extends L i n k e d L i s t <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 s t a t i c c l a s s S e r v i c e T y p e { public f i n a l S t r i n g serviceName ; public f i n a l C l a s s s e r v i c e C l a s s ; public f i n a l C l a s s c l i e n t C l a s s ;

96

public S e r v i c e T y p e ( C l a s s 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 ( ) ; f o r ( Method method : s e r v i c e C l a s s . getMethods ( ) ) i f ( ” s e r v i c e ” . e q u a l s ( method . getName ( ) ) && method . getParameterTypes ( ) . l e n g t h == 1 && ! method . getParameterTypes ( ) [ 0 ] . e q u a l s ( Object . c l a s s ) ) { c l i e n t C l a s s = method . getParameterTypes ( ) [ 0 ] ; return ; } throw new I l l e g a l A r g u m e n t E x c e p t i o n ( ) ; } } private s t a t i c f i n a l long s e r i a l V e r s i o n U I D = 1L ; private s t a t i c f i n a l long REFRESH TIME = 1 0 0 0 ; private s t a t i c f i n a l S t r i n g DATA FILE = ” s e r v i c e s . t x t ” ; private s t a t i c f i n a 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 s t a t i c long l a s t R e f r e s h e d = System . c u r r e n t T i m e M i l l i s ( ) ; static { updateServices ( ) ; } private s t a t i c void u p d a t e S e r v i c e s ( ) { try { services . clear (); Scanner s c a n n e r = new Scanner ( C o n s o l e . c l a s s . g e t C l a s s L o a d e r ( ) . getResourceAsStream ( DATA FILE ) ) ; while ( s c a n n e r . hasNext ( ) ) { ServiceType s e r v i c e = new S e r v i c e T y p e ( C l a s s . forName ( s c a n n e r . next ( ) ) ) ; s e r v i c e s . add ( s e r v i c e ) ; } } catch ( E x c e p t i o n e ) { e . printStackTrace ( ) ; } } public s t a t i c synchronized S e r v i c e s L i s t g e t R e c e n t S e r v i c e s ( ) { i f ( System . c u r r e n t T i m e M i 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 . c u r r e n t T i m e M i l l i s ( ) ; updateServices ( ) ; } return new S e r v i c e s L i s t ( s e r v i c e s ) ; }

97

public S e r v i c e T y p e g e t S e r v i c e ( S t r i n g serviceName ) { for ( ServiceType s e r v i c e : this ) i f ( s e r v i c e . serviceName . e q u a l s ( serviceName ) ) return s e r v i c e ; return null ; } }

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

package r o l e s e x a m p l e . c l i e n t s ; public c l a s s AccountHolder { protected S t r i n g name ; protected int b a l a n c e = 0 ; public AccountHolder ( ) { } public AccountHolder ( S t r i n g name ) { t h i s . name = name ; } public S t r i n g getName ( ) { return name ; } public void setName ( S t r i n g name ) { t h i s . name = name ; } public void s e t B a l a n c e ( int b a l a n c e ) { this . balance = balance ; } public int g e t B a l a n c e ( ) { return b a l a n c e ; } }

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

package r o l e s e x a m p l e . c l i e n t s ; public c l a s s C r e d i t C a r d H o l d e r extends AccountHolder { private s t a t i c int cardNumberCounter = 0 ; public f i n a l int cardNumber = ++cardNumberCounter ; private boolean a c t i v a t e d = f a l s e ; private int c r e d i t C a r d B a l a n c e = 0 ;

98

public int g e t C r e d i t C a r d B a l a n c e ( ) { return c r e d i t C a r d B a l a n c e ; } public void c r e d i t ( int sum ) { if ( ! isActivated () ) return ; c r e d i t C a r d B a l a n c e −= sum ; } public void p a y C r e d i t ( int sum ) { if ( ! isActivated () ) return ; b a l a n c e −= sum ; c r e d i t C a r d B a l a n c e += 0 . 8 ∗ sum ; } public void a c t i v a t e ( ) { a c t i v a t e d = true ; } public boolean i s A c t i v a t e d ( ) { return a c t i v a t e d ; } }

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

package r o l e s e x a m p l e . c l i e n t s ; import j a v a . u t i l . Calendar ; public c l a s s T r a v e l I n s u r a n c e C l i e n t extends AccountHolder { private Calendar s t a r t = null , end = null ; public void i n s u r e ( Calendar s t a r t , Calendar end ) { this . s t a r t = s t a r t ; t h i s . end = end ; b a l a n c e −= 1 0 0 ; } public boolean compense ( Calendar d a t e ) { i f ( s t a r t == null | | s t a r t . a f t e r ( d a t e ) | | end == null | | end . b e f o r e ( d a t e ) ) return f a l s e ; b a l a n c e += 1 0 0 0 0 ; return true ; } }

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

package r o l e s e x a m p l e . s e r v i c e s ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . C r e d i t C a r d H o l d e r ; public c l a s s A c t i v a t e C r e d i t C a r d implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( C r e d i t C a r d H o l d e r c l i e n t ) { if ( client . isActivated () ) return c l i e n t . getName ( ) + ” ’ s c r e d i t c a r d has a l r e a d y been a c t i v a t e d ” ; client . activate (); return c l i e n t . getName ( ) + ” ’ s c r e d i t c a r d has been a c t i v a t e d ” ; } }

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

package r o l e s e x a m p l e . s e r v i c e s ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . AccountHolder ; public c l a s s CheckAccountBalance implements I S e r v i c e P r o v i d e r { @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 balance i s : ” + c l i e n t . getBalance () + ” . ” ; } }

Listing B.9: Zawartości pliku CheckAccountBalance.java.

package r o l e s e x a m p l e . s e r v i c e s ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . C r e d i t C a r d H o l d e r ; public c l a s s CheckCreditCardBalance implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( C r e d i t C a r d H o l d e r 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

package r o l e s e x a m p l e . s e r v i c e s ; import j a v a . u t i l . Calendar ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . T r a v e l I n s u r a n c e C l i e n t ; public c l a s s CompenseTravelAccident implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( T r a v e l I n s u r a n c e C l i e n t c l i e n t ) { i f ( c l i e n t . compense ( Calendar . g e t I n s t a n c e ( ) ) ) 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 i n s u r e d ” ; } }

Listing B.11: Zawartości pliku CompenseTravelAccident.java.

package r o l e s e x a m p l e . s e r v i c e s ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . C r e d i t C a r d H o l d e r ; public c l a s s G e t C r e d i t implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( C r e d i t C a r d H o l d e r c l i e n t ) { if ( ! client . isActivated () ) return c l i e n t . getName ( ) + ” ’ s c r e d i t c a r d has not been a c t i v a t e d ” ; 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 c a r d has been c h a r g e d ” ; } }

Listing B.12: Zawartości pliku GetCredit.java.

package r o l e s e x a m p l e . s e r v i c e s ; import j a v a . u t i l . Calendar ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . T r a v e l I n s u r a n c e C l i e n t ; public c l a s s I n s u r e T r a v e l implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( T r a v e l I n s u r a n c e C l i e n t c l i e n t ) { Calendar endDate = Calendar . g e t I n s t a n c e ( ) ; endDate . add ( Calendar .MONTH, 1 ) ; c l i e n t . i n s u r e ( Calendar . g e t I n s t a n c e ( ) , endDate ) ; return c l i e n t . getName ( ) + ” has been i n s u r e d ” ; } }

Listing B.13: Zawartości pliku InsureTravel.java. 101

package r o l e s e x a m p l e . s e r v i c e s ; import r o l e s e x a m p l e . I S e r v i c e P r o v i d e r ; import r o l e s e x a m p l e . c l i e n t s . C r e d i t C a r d H o l d e r ; public c l a s s RepayCredit implements I S e r v i c e P r o v i d e r { @Override public Object s e r v i c e ( C r e d i t C a r d H o l d e r c l i e n t ) { if ( ! client . isActivated () ) return c l i e n t . getName ( ) + ” ’ s c r e d i t c a r d has not been a c t i v a t e d ” ; c l i e n t . p a y C r e d i t ( 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 r e p a i d ” ; } }

Listing B.14: Zawartości pliku RepayCredit.java.

roles roles roles roles roles roles roles

example . example . example . example . example . example . example .

services services services services services services services

. ActivateCreditCard . CheckAccountBalance . CheckCreditCardBalance . CompenseTravelAccident . GetCredit . InsureTravel . RepayCredit

Listing B.15: Początkowa zawartości pliku services.txt.

102

Dodatek C

Narzędzia programistyczne W niniejszym dodatku omówione zostały dwa proste narzędzia programistyczne związane z implementacją propozycji PPK - translator z języka JavaBC do języka Java oraz towarzyszące mu zadanie (ang. task) dla systemy automatycznego budowania oprogramowania Apache Ant. Autor nie stworzył natomiast żadnych narzędzi wspomagających programowanie w zaproponowanej przez niego implementacji dynamicznych ról dla języka Java z racji tego, że została ona dostarczona w postaci zwyczajnej biblioteki. Zarówno biblioteka dla dynamicznych ról jak i narzędzia związane z propozycją PPK został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ępniana na licencji GNU Lesser General Public License. Oprócz kodu stworzonego bezpośrednio przez autora, w jej skład wchodzą także fragmenty bibliotek ANTLR, Javassist, JUnit i Apache Ant. W celu użycia biblioteki wymagana jest maszyna wirtualna obsługująca pliki klas zgodne z formatem Javy 6.01 .

C.1

Translator

Translator dla PPK jest prostą aplikacją przyjmującą na wejściu program napisany w języku JavaBC i zwracającą jego funkcjonalny ekwiwalent, zawierający jedynie konstrukcje właściwe dla języka Java. Oprócz swojego głównego zadania, translator wykonuje dodatkowo sprawdzenia wejściowego programu pod kątem poprawności składniowej oraz semantycznej. Sprawdzenie poprawności składniowej dotyczy zarówno składni specyficznej dla propozycji autora jak i, w pewnym ograniczonym zakresie, składni odziedziczonej po języku Java. Sprawdzenie poprawności semantycznej dotyczy natomiast tylko i wyłącznie konstrukcji związanych bezpośrednio z PPK2 . W efekcie użycie translatora do przetłumaczenia błędnego programu może być czasami możliwe — w takiej sytuacji błędy zawarte w programie źródłowym przenoszą 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ściwej kompilacji. Nawiasem mówiąc, napisanie translatora sprawdzającego pełną poprawność języ1

Główny numer wersji (ang. major version number) plików klas zawartych w bibliotece wynosi 50. Translator 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 warunku niezmiennika. 2

103

kową otrzymanych programów musiałoby się siłą rzeczy wiązać ze sprawdzaniem zgodności wszelkich możliwych konstrukcji ze specyfikacją języka Java, co przekraczałoby zdecydowanie moż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łędy językowe i mogą w związku z tym pozwalać na kompilację wadliwych programów. Dobrym przykładem może tu być np. próba odwołania się do stałej enumeracji jeszcze przed jej konstrukcją (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 nie dopuścić do kompilacji wadliwego programu, o tyle już ecj nie znajduje tego rodzaju błędu i pozwala w efekcie na kompilację, co pozostawia programistę w mylnym przeświadczeniu o tym, że dany program jest faktycznie prawidłowy Należy jeszcze wspomnieć o tym, że translator, zgodnie z częstą praktyką, dzieli wszystkie błędy semantyczne na dwie grupy: błędy właściwe i ostrzeżenia. O klasyfikacji danego błędu decyduje jego bezpośredni wpływ na pomyślność całego procesu translacji. Ostrzeżenia mianowicie nie powodują przerwania procesu translacji, błędy właściwe — tak. Tablica C.1 zawiera listę błędów semantycznych wykrywanych przez translator.

typ1 komunikat translatora i jego wyjaśnienie • • • • • • ◦ ◦ ◦ ◦ ◦ ◦

• • 1

— 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 klasie

typ błędu: • — błąd właściwy, ◦ — ostrzeżenie

104

typ1 komunikat translatora i jego wyjaśnienie • • ◦

• • • ◦ ◦ 1

Invalid modifier used — użycie nieprawidłowego modyfikatora w nagłówku definicji niezmiennika Only one modifier allowed — użycie dwóch modyfikatorów w nagłówku definicji niezmiennika State already occured — dwukrotne użycie danego stanu w liście stanów pewnego warunku — 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 stanu

typ 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, jest pl.edu.pw.akosicki.translator.Tool. Klasa ta została wyszczególniona w manifeście pliku javaBC.jar jako klasa startowa i w związku z tym nie ma konieczności jej jawnego podawania podczas wywołania translatora. Sam sposób wywołania translatora jest następują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 -jar javaBC.jar lub java -cp javaBC.jar pl.edu.pw.akosicki.translator.Tool. Wszystkie elementy następujące po instrukcji wywołania programu są traktowane jako jego argumenty. options pozwala na skorzystanie z dodatkowych możliwości udostępnianych przez translator: -e prowadzi do wygenerowania pliku zawierającego odwzorowanie programu otrzymanego w wyniku translacji do programu w postaci pierwotnej. Odwzorowanie to zostanie umieszczone w pliku .mapping. Zawartość pliku odwzorowania zostanie omówiona w dalszej część niniejszego dodatku. -i prowadzi do wygenerowania plików zawierających powstałe podczas procesu translacji formy pośrednie programu wynikowego. Poszczególne pliki z formami pośrednimi translacji mają nazwy postaci .intermediate, 105

gdzie oznacza numer konkretnej formy pośredniej. Formy pośrednie numerowane są kolejnymi, począwszy od jedynki, liczbami naturalnymi. Forma z numerem jeden jest najbardziej zbliżona do programu pierwotnego. W większości przypadkó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 debugowych. inputFile jest argumentem obligatoryjnym, oznaczającym nazwę pliku z programem źródłowym w języku JavaBC. outputFile jest drugim argumentem obligatoryjnym, oznaczającym nazwę pliku w którym ma zostać umieszczony program wynikowy. Wywołanie translatora z nieodpowiednim parametrami prowadzi jedynie do wyświetlenie komunikatu o błędzie i instrukcji prawidłowego użycia. Jeśli argumenty wywołania określone są prawidłowo, translator spróbuje przeprowadzić translację. Niech dla przykładu w danym katalogu znajduje się plik biblioteki oraz plik Test.javabc o treści przedstawionej na listingu C.1. Uruchomienie translatora może wyglądać następująco: $java -jar javaBC.jar -e Test.javabc Test.java Cannot translate Test.javabc - program contains errors Errors: 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łędy semantyczne, 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ńczonego, drugim próba odwołania się do wartości zwracanej przez metodę typu void. Po usunię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.java File 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 o nieosią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żącym katalogu pojawiły się dwa nowe pliki: Test.java oraz Test.java.mapping. Pierwszy z plików zawiera wynikowy program, drugi natomiast jest plikiem odwzorowania pomiędzy pozycjami z programu wynikowego a pozycjami z programu źródłowego.

106

1 2 3 4 5 6 7 8 9 10

c l a s s Test { automaton { S0 : S1 ; accepting S1 ; S2 ; } void method ( int a r g ) out ( return != 0 ) { } }

Listing C.1: Zawartość pliku Test.javabc.

1 2 3 4 5 6 7 8 9 10

c l a s s Test { automaton { i n i t i a l S0 : S1 ; accepting S1 ; S2 ; } void method ( int a r g ) out ( true ) { } }

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 wykrycia błędu dopiero na etapie analizy programu wynikowego (otrzymanego w wyniku translacji). Ponieważ ewentualna poprawka owego błędu musi być ze zrozumiałych przyczyn wprowadzona 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że mieć pochodzenie dwojakiego rodzaju. Może być on mianowicie pewnym, przeniesionym lub 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 plik odwzorowania będzie zawierał wskazanie na odpowiednie miejsce w kodzie źródłowym. W drugim przypadku plik odwzorowania może odsyłać do pewnej abstrakcji odpowiedzialnej bezpośrednio za wygenerowanie danego fragmentu kodu lub ewentualnie nie wskazywać w ogóle na cokolwiek. Sam plik odwzorowania jest plikiem tekstowym o mocno uproszczonej składni (autor przedłożył tu czytelność nad zwięzłość). Każda linijka pliku odwzorowania odpowiada bezpoś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 kodu zawartego w owej linijce. Przykładowy fragment pliku odwzorowania może przedstawiać się następująco:

107

33: 34: 35: 36:

1=>7:1 1-24=>8:1 25=>8:37 1-7=>8:6x6 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 blok zaczynają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 za jego 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 odwzorowań jest mechanizmem czysto pomocniczym i w większości przypadków uciekanie się doń nie powinno być koniecznego. Schemat translacji zaproponowany przez autora jest na tyle prosty, że na ogół nie powinno być żadnych wątpliwości co do tego skąd dany fragment pliku wynikowego rzeczywiście pochodzi.

C.2

Zadanie dla systemu Apache Ant

W celu ułatwnienia integracji z istniającymi systemami budowy oprogramowania, autor przygotował proste zadanie (ang. task) systemu Apache Ant[8]. Zadanie zadefiniowane jest w klasie pl.edu.pw.akosicki.translator.AntTask i służy do uruchamiania translatora na grupie 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 obligatoryjnym. 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 ten jest opcjonalny a jego domyślną wartością jest .javabc. mappings jest przełącznikiem określającym, czy obok plików wynikowych mają zostać umieszczone także pliki odwzorowania. Po uruchomieniu zadania przeglądana jest struktura podkatalogów wskazywanych przez srcdir. 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 odwzorowań, umieszczane są w strukturze katalogów wskazywanych przez destdir, w taki sposób aby ich położenie względem danej struktury katalogów zostało zachowane. Jeśli przykładowo parametr srcdir wskazywał na javab src a parametr destdir na src, tłumaczenia plików znajdujących się w javab src/pl/edu/pw/akosicki zostaną umieszczone w src/pl/edu/pw/akosicki. Znaczenie parametrów destdir i src jest analogiczne jak w popularnym zadaniu javac.

108

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 przez autora zadania. Wyczerpujący opis systemu Apache Ant można znaleźć w podręczniku[14]. 1 2 3 4 5 6 7 8 9 10 11 12 13

<project name=” s a m p l e p r o j e c t ” d e f a u l t =” t r a n s l a t e ”>

Listing C.3: Przykładowy plik z projektem systemu Apache Ant, wykorzystujący przygotowane przez autora zadanie tegoż systemu.

109

Dodatek D

Opis formalny gramatyki dla PPK Niniejszy dodatek zawiera opis gramatyki JavaBC wraz z krótkim komentarzem dotyczącym intencji autora oraz niektórych dodatkowych obostrzeń składniowych nie dających wyrazić się za pomocą gramatyki bezkontekstowej. Ponieważ propozycja dynamicznych ról nie znalazła swojego odzwierciedlenia w składni, gramatyka zawiera jedynie produkcje dotyczące mechanizmów Programowania przez Kontrakt. Gramatyka ta jest pochodną gramatyki Javy opisanej w specyfikacji języka Java [9, rozdział 18] i jako taka nie będzie, z powodu swojej objętości, przytaczana z całości. W zamian autor zdecydował się na umieszczenie w dodatku jedynie reguł zmodyfikowanych w stosunku do specyfikacji Javy lub też reguł całkowicie nowych. Reguły bądź ich fragmenty istniejące w specyfikacji Javy zaznaczone zostały szarą, pochyłą czcionką. Z kolei fragmenty gramatyki dodane 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ą produkcję w tej samej regule, w której znajduje się produkcja pozwalająca na tworzenie statycznego inicjalizatora klasy z racji podobieństwa owych deklaracji. W skład obu produkcji wchodzi 110

pojedyncze słowo kluczowe oraz umieszczone pomiędzy nawiasami klamrowymi ciało danej deklaracji. Pewną różnicą, której nie da się wywnioskować z opisu gramatyki, jest tutaj natomiast fakt, iż ciąg automaton nie jest wbrew pozorom słowem kluczowym. Opcjonalne sł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 MemberDecl rozwija się w definicję niezmiennika. Reguły z serii Automaton... dotyczą definicji ciała automatu skończonego. Warty zauważenia jest tutaj fakt, iż podobnie jak ma to miejsce w wypadku słowa automaton, słowa initial 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(...) oraz out(...), które pozwalają odpowiednio na definicję warunku początkowego oraz końcowego metody. 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

| | | | | | | | |

’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ł autor polecenie 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 modyfikatora pola 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 pewien sposó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 automatu skończonego, mają równie elementarny charakter. Jest to szczególnie istotne w wypadku wyraż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. Motywacja dotycząca użycia słowa transient w wyrażeniu sprawdzenia stanu jest podobna do tej w przypadku instrukcji zmiany stanu. 112

Wyrażenie odwołania się do aktualnej wartości zwracanej ze zrozumiałych powodów zaczyna się od istniejącego już słowa kluczowego MemberDecl. Co prawda słowo to używane jest w Javie do określenia instrukcji powrotu z metody, jednak nie prowadzi to w żadnym wypadku do niejednoznaczności, gdyż użycie to ma miejsce w innym kontekście. Podobna sytuacja zachodzi w przypadku słowa transient. Wyżej przedstawiona modyfikacja gramatyki Javy została dokonana przez autora w sposób arbitralny, przy czym autor miał przy jej tworzeniu na uwadze: a) klarowność wynikowej gramatyki b) intuicyjne wynikanie semantyki z danej składni oraz c) ograniczenie do koniecznego minimum konieczności sprawdzania poprawności programów wykraczającej poza podane reguły gramatyki bezkontekstowej. Przykładowo definicja automatu skończonego mogła zostać równie dobrze umieszczona na poziomie reguły MemberDecl. Wtedy jednak zaistniałaby konieczność upewniania się, iż przed definicją automatu nie zostały użyte żadne modyfikatory, na co teoretycznie pozwalałaby gramatyka. Większość pozagramatycznych reguł poprawności nie zostały w tym dodatku wymienionych, aby uniknąć powtarzania informacji zawartych w sekcji 2.3. Na koniec wypada jeszcze wspomnieć, że przytoczona powyżej gramatyka nie została w identycznej formie wykorzystana podczas tworzenia translatora dla PPK. Otóż autor, przy pisaniu analizatora gramatycznego dla potrzeb translatora, skorzystał z biblioteki ANTLR, będącej w istocie tzw. kompilatorem kompilatorów[16], oraz z gramatyki Javy przygotowanej specjalnie na potrzeby owego narzędzia[17]. Wymogło to pewne modyfikacje, w stosunku do specyfikacji gramatyki, związane z możliwościami użytego narzędzia oraz koniecznością sprawdzania poprawności semantycznej.

113

Bibliografia [1] Contract4j website. http://www.contract4j.org, 2008. [2] A.V. Aho, R. Sethi, and Ł. Ullman. Kompilatory: reguły, metody i narzędzia. Wydawnictwa Naukowo-Techniczne, Warszawa, 2002. [3] K. Arnold, J. Gosling, and D. Holmes. Java (TM) Programming Language, The. AddisonWesley 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 Wydawnicza 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. ACM SIGPLAN 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

[16] T. Parr. The Definitive ANTLR Reference: Building Domain-specific Languages. Pragmatic Bookshelf, 2007. [17] T. Parr. Java 1.5 grammar for ANTLR v3. 1152141644268/Java.g, 2008.

115

http://www.antlr.org/grammar/

Warszawa, dnia ................

Oświadczenie Oświadczam, że pracę magisterską pod tytułem: ............................................................................, której promotorem jest ......................... wykonałem samodzielnie, co poświadczam własnoręcznym podpisem. .........................

More Documents from "Aleksander"