Przegląd konstrukcji języka OCL
Przegląd konstrukcji języka OCL Podstawowymi jednostkami, z których składa się wyrażenie OCL, są obiekty i ich właściwości. O każdym obiekcie można powiedzieć, że jest pewnego typu, definiującego wykonywane na nim operacje. Typy podzielono na następujące grupy: Typy predefiniowane, czyli: typy podstawowe (Integer, Real, String i Boolen) typy kolekcyjne (Collection, Set, Bag i Sequence) służące do precyzyjnego opisywania rezultatu nawigacji po powiązaniach w modelu klas. Typy modelowe, definiowane przez użytkownika. Wszystkie klasy, interfejsy lub typy z modelu UML stają się samoczynnie typami modelowymi w języku OCL. Odnosi się to także do typów wyliczeniowych, używanych przy definiowaniu atrybutów. Typami modelowymi są klasy Klient i ProgramLojalnegoKlienta zdefiniowane w naszym przykładowym modelu UML.
Typy wartości i typy obiektowe W języku OCL mianem typów określa się zarówno typy wartości (ang. value type), jak i typy obiektowe (ang. object type). Egzemplarz typu wrtości ma stałą wartość. Przykładowo „jedynka” typu Integer nigdy nie stanie się „dwójką”. Inaczej jest z typami obiektowymi, definiują one bowiem egzemplarze, które mogą zmieniać wartości. Dlatego, nawet gdy obiekt typu modelowego Osoba zmieni wartość atrybutu imię, pozostanie tym samym bytem. M. Fowler nazywa typy obiektowe obiektami referencyjnymi (reference object), a typy wartości – obiektami-wartościami (value object). Inną ważną cechą typów wartości jest fakt, że ich wartości są same w sobie zarówno obiektami, jak i ich nazwami. Dwa obiekty typu wartości, mające tę samą wartość, są z definicji tym samym egzemplarzem, w przeciwieństwie do obiektów typu obiektowego, które mogą mieć zarówno takie same, jak i różne wartości, pozostając odrębnymi egzemplarzami. Identyczne są tylko wówczas, gdy mają wspólną tożsamość. W języku OCL predefiniowane typy podstawowe i kolekcyjne należą do typów wartości, natomiast wprowadzane przez użytkownika typy modelowe mogą być zarówno typami wartości, jak i typami obiektowymi.
Wyrażenia OCL a ograniczenia OCL Wyrażenie OCL jest poprawne wtedy i tylko wtedy, gdy jest napisane zgodnie z regułami języka OCL. Wszystkie wyrażenia OCL są typu predefiniowanego lub modelowego (klasy albo typu zdefiniowanego w modelu UML). Każde zwraca wynik, czyli rezultat wyznaczenia jego wartości. Przykładowo, rezultatem wyrażenia 1 + 3 jest 4. Jego typem jest natomiast Integer, ponieważ typ wyrażenia to także typ wyniku. Ograniczenie jest restrykcją nałożono na jedną lub więcej wartości (części) modelu lub systemu obiektowego, przedstawiono za pomocą wyrażeń logicznych, przyjmujących wartość true, gdy restrykcja jest spełniona. Ograniczenie jest zatem wyrażeniem typu predefiniowanego Boolean. Istnieją jednak wyrażenia OCL innego typu. Przykładem niech będzie 1 + 3 – poprawne wyrażenie OCL typu Integer. Na podstawie tych rozważań można zapisać następującą definicję ograniczenia: Ograniczenie OCL jest poprawnym wyrażeniem OCL typu Boolean Wartością wynikową ograniczenia jest albo true, albo false.
Kontekst warunków początkowych i warunków końcowych Tylko operacja lub metoda może być kontekstem warunków początkowego i końcowego, a jej parametry mogą występować w wyrażeniach ustanawiających te warunki. Operację kontekstową zapisuje się w osobnym wierszu, poprzedzając ją nazwą odpowiedniego interfejsu, typu lub klasy (podkreślony krój pisma). Pełna sygnatura operacji powinna zawierać typ wyniku oraz listę parametrów i ich typów. Pod kontekstem, w wierszach oznaczonych etykietami pre: i post: zapisuje się warunki początkowe i końcowe. Typ1::operacja (argument :Typ2) : TypWyniku Pre : argument.atrybut = true Post: result = argument.atrybut xor self.atrybut2 Tylko napisy widoczne po etykietach pre: i post: są wyrażeniami OCL. Klasę, interfejs lub typ, do którego należy specyfikowana operacja lub metoda, nazywamy typem kontekstowym. Słowo kluczowe self wskazuje obiekt, na którym operacja będzie wykonywana.
Słowo kluczowe self Niekiedy zachodzi potrzeba bezpośredniego odwołania się do obiektu kontekstowego. Wskazuje nań słowo kluczowe self. Sposób zapisu wyrażeń może być następujący: Klient self.nazwisko = ‘Kowalski’ Przykład bezpośredniego odwołania z użyciem self Członkowstwo Klient.karta.członkowstwo.includes(self) Słowo self zawsze odnosi się do obiektu typu, który jest kontekstem ograniczenia OCL. Jeśli kontekstem jest na przykład klasa Klient, to self wskazuje jej egzemplarz. Dlatego self można traktować jak obiekt, od którego rozpoczyna się wyrażenie. Zazwyczaj nie ma potrzeby zapisywania go, ponieważ kontekst jest oczywisty: nazwisko = ‘Kowalski’
Typy podstawowe Typy podstawowe nie są skomplikowane. Różnią się nieco od swoich odpowiedników znanych z języków programowania. Uwaga: chociaż wyrażenia OCL są związane z modelem i jego diagramami, to typy podstawowe wartości są niezależne od jakichkolwiek diagramów i mogą wystąpić w dowolnym wyrażeniu. Ilustracją tego jest napis 1 +3, będący poprawnym wyrażeniem OCL, albo faza true = not false – ograniczenie OCL niezależnie od jakiegokolwiek modelu, lecz zgodne z regułami języka.
Typ Boolean Wyrażenie logiczne typu Boolean może przyjmować tylko jedną z dwu wartości: true lub false. Działania na typie Boolean, zestawiono w tabeli. Jedyną nietypową operacją standardową jest implikacja (implies). Może ona występować w językach specyfikacji lub w środowiskach teoretycznych, rzadko pojawia się w językach programowania. Zwraca ona wartość true, gdy oba argumenty występujące w wyrażeniu przyjmują wartość true lub gdy pierwszy argument ma wartość false. Jako przykład rozważymy klasę Usługi systemu LK, której model przedstawia rysunek. Zapisane poniżej wyrażenie przyjmuje wartość true tylko wówczas, gdy o każdej usłudze można powiedzieć, że jeśli klient zużywa na nią punkty, to nie może otrzymać za nią żadnych dodatkowych punktów. Innymi słowy, klient nie otrzyma punktów za zakup usługi, dokonany za zebrane punkty.
Operacje standardowe dla typu Boolean Notacja Typ wyniku Alternatywa Koniunkcja Alternatywa wykluczająca Negacja Równy Różny Implikacja If then else a or b a and b a xor b not a a = b a <> b a implies b if a then b else b’ endif Boolean Typ b i b’
Usługi kosztWPunktach : Integer zyskWPunktach : Integer opis : String warunki : Boolean Rys. Klasa Usługi z systemu „Lojalny Klient” Usługi self.kosztWPunktach > 0 implies self.zyskWPunktach = 0 Inną interesującą operacją na typie Boolean jest if-then-else. Zapisujemy ją w następujący sposób: if <wyrażenieOCL-typu-Boolean> then <wyrażenieOCL> else <wyrażenieOCL> endif
W zależności od wyniku wartościowania <wyrażeniaOCL-typu-boolean>, rezultatem konstrukcji if-then-else może być wartość wyrażenia OCL, zapisanego w klauzuli then albo klauzuli else. Klauzulę else trzeba umieszczać zawsze, ponieważ każde wyrażenie OCL musi zwracać jakąś wartość. Łatwo można zauważyć, że jeżeli wyrażenie po słowie kluczowym if nie będzie spełnione, to brak klauzuli else będzie przyczyną nieokreślonegostanu całego ograniczenia. Wyrażenia występujące po then i else muszą być tego samego typu. Klient nazwisko : String tytuł : String wiek : Integer jestMężczyzną : Boolean Rys. Klasa Usługi z systemu „Lojalny Klient”
Spójrzmy na poniższy niezmiennik dla klasy Klient pochodzącej z modelu LK. tytuł = (if jestMężczyzną = true then ‘Pan’ else ‘Pani’ endif Wynika z niego, że atrybut tytuł ma wartość ‘Pan’, jeśli atrybut jestMężczyzną przyjmuje wartość true. W przeciwnym wypadku tytuł ma wartość ‘Pani’. Oto inne przykłady poprawnych wyrażeń logicznych: not true wiek() > 21 and wiek() <65 wiek() <= 12 xor karty->size >3 tytuł = ’Jego Magnificencja’ or tytuł = ‘Najjaśniejszy Pan’ nazwisko = ‘Popiołek’ if standard = ‘UML’ then ‘Użycie standardu UML’ else ‘Uwaga! Cechy spoza standardu UML’
Typy Integer i Real Typ Integer reprezentuje liczby całkowite. Ze względu na to, że OCL jest językiem modelowania, nie ma żadnych ograniczeń co do ich wartości. W szczególności nie określono maksimum. Podobnie rzecz ma się z typem Real, reprezentującym matematyczne pojęcie liczb rzeczywistych. W języku OCL, tak jak w matematyce, typ Integer to podtyp typu Real. Na typach Integer i Real można wykonywać zwykłe operacje dodawania, odejmowania, mnożenia i dzielenia. Dodatkowo wprowadzono operator wartości bezwzględnej abs. Przykładowo, wartość -1.abs to 1, podobnie (2.4).abs wynosi 2.4. dla typu Real zdefiniowano ponadto operacje floor i round. Pierwsza zwraca największą liczbę całkowitą, mniejszą lub równą danej (np. (4.6)floor to 4 typu Integer). Druga zaokrągla daną liczbę do najbliższej liczby całkowitej (np. (4.6) round to 5 typu Integer).
Operacje standardowe dla typu Real Notacja Typ wyniku równy różny mniejszy większy nie większy nie mniejszy plus minus iloczyn iloraz modulo dzielenie całkowite moduł (wartość bezwzględna) maksimum z a i b minimum z a i b zaokrąglenie część całkowita (podłoga) a = b a <> b a < b a > b a <= b a + b a – b a * b a / b a.mod(b) a.div(b) a.abs a.max(b) a.min(b) a.round a.floor Boolean Integer lub Real Real Integer
Przykłady Poniższe przykłady ilustrują użycie typów Real i Integer – wszystkie dają wynik true 2654 * 4.3 + 101 = 11513.2 (3.2).floor / 3 = 1 1.175 * (-8.9).abs – 10 = 0.4575 12 > 22.7 = false 12.max(33) = 33 33.max(12) = 33 13.mod(2) = 1 13.div(2) = 6 33.7.min(12) = 12 -24.abs = 24 (-2.4).floor = -3
Typ String Typ String reprezentuje ciąg znaków (napis), zamknięty w pojedynczym cudzysłowie (np. ‘kamień’, ‘tajemniczy ogród’). Można na nim wykonywać m.in. operacje toUpper, toLower, size, substring i concat. Poniżej podamy przykłady użycia typu String – wszystkie wyrażenia zwracają true. ‘Ania’.size = 4 (‘Ania’ = ‘Jan’) = false ‘Ania’.concat (‘i Jan’) = ‘Ania i Jan’ ‘Ania’.toUpper = ‘ANIA’ ‘Ania’.toLower = ‘ania’ ‘Ania i Jan’.substring(8, 10) = ‘Jan’
Typy modelowe Ograniczenia definiuje się zawsze w odniesieniu do konkretnego modelu UML. Pochodzące zeń klasy, typy, interfejsy i typy danych uznaje się w języku OCL za klasy, które można konkretyzować. Innymi słowy, wszystkie wymienione elementy modelu UML są typami OCL. Wynika z tego, że utworzone przez projektanta nowe typy, nazywane typami modelowymi, są traktowane w kontekście opracowywanego modelu na równi z predefiniowanymi typami OCL. Do właściwości typów modelowych zaliczamy: Atrybuty Operacje i metody Nawigacje po powiązaniach (i agregacjach) Wyliczenia zdefiniowane jako typy atrybutów Typy modelowe oznacza się nazwami wziętymi z diagramu klas modelu UML. Klasy, interfejsy, klasy powiązane, aktorzy, przypadki użycia i typy danych zdefiniowane w modelu UML są poprawnymi typami modelowymi. Ich przykładami, pochodzącymi z modelu LK, są zarówno klasy Usługi, PartnerzyProgramu, Klient i inne, jak i wyliczenie {srebrny, złoty} zdefiniowany w klasie KartaKlienta. Pomocnicza klasa Data także jest typem modelowym, choć nie została w całości zdefiniowana na diagramie.
Atrybuty w modelu UML Ponieważ klasa Klient jest częścią modelu UML, jest także poprawnym typem OCL. Jej atrybuty są zatem atrybutami klasy Klient w kontekście języka OCL. Klient nazwisko : String tytuł : String jestMężczyzną : Boolean dataUrodzenia : Data wiek : Integer wiek() : Integer jestSpokrewnionyZ(p : Klient) : Boolean Rys. Klasa Usługi z systemu „Lojalny Klient” Nic nie stoi na przeszkodzie, aby użyć atrybutu nazwisko typu String w wyrażeniu OCL. Zgodnie z regułami składni należy poprzedzić jego nazwę kropką Klient self.nazwisko
Operacje standardowe dla typu String Notacja Typ wyniku konkatenacja rozmiar konwersja do małych liter konwersja do dużych liter podłańcuch równy różny string.concat(string) string.size string.toLower string.toUpper string.substring(int,int) string1 = string2 string1 <> string2 String Integer Boolean
self.jestSpokrewnionyZ(self) = true Operacje w modelu UML Podobnie jak atrybutów, również operacji oraz metod zdefiniowanych w modelu UML można używać w wyrażeniach OCL. Od tej reguły istnieje jednak ważne odstępstwo. Ponieważ OCL jest językiem bez skutków ubocznych (side-effect-free), niedopuszczalne są w nim operacje zmieniające stan jakiegokolwiek obiektu. Jednak tak zwane operacje odpytujące (query operation), zwracając określoną wartość, lecz niczego nie zmieniające, mogą stanowić składnik wyrażenia OCL. W języku UML każda operacja lub metoda jest opatrzona etykietą boolowską isQuery. Jeśli ta etykieta ma wartość true, to operacja może być stosowana w wyrażeniach OCL, gdyż na pewno nie wywołuje skutków ubocznych. W języku OCL można zapisywać warunki początkowe i końcowe dla operacji i metod odpytujących. Notację z kropką stosuje się zarówno do atrybutów, jak i do operacji. Po nazwach operacji piszemy ponadto otwierające i zamykające nawiasy zwykłe, ujmujące opcjonalne parametry. Mając do dyspozycji klasę Klient możemy napisać: Klient self.wiek() > = 0 self.jestSpokrewnionyZ(self) = true self.nazwisko = ‘Kowalski’ implies self.wiek() >21
Rys. Klasa z identyczną nazwą atrybutu i operacji Uwaga: Nawet wówczas, gdy operacja jest bezargumentowa, obowiązuje reguła dotycząca pisania nawiasów. Wynika to z potrzeby rozróżniania między atrybutami i operacjami, ponieważ w języku UML jest dopuszczalne, aby miały one takie same nazwy. Na rys. pokazano przykład. Inna Osoba wiek : String wiek() : Integer Rys. Klasa z identyczną nazwą atrybutu i operacji Poniższe ograniczenie jest prawidłowe: InnaOsoba self.wiek() >21 implies self.wiek = ‘dojrzały’ Jako inny przykład zapiszemy niezmiennik dla modelu LK, określający wartości zwracane przez operację program() Transakcja self.program() = karta.członkostwo.program
Operacje klasowe i atrybuty klasowe z modelu UML Operacje klasowe (class operation) i atrybuty klasowe (class attributes) klas z modelu UML są właściwościami typów OCL, odpowiadających tym klasom. Ich składnia niczym nie różni się od reguł zapisu dotyczących zwykłych operacji i atrybutów. Przykładem atrybutu klasowego, będącego częścią definicji klasy Data, jest teraźniejszość. Można go użyć w wyrażeniu, otrzymując w rezultacie obiekt typu Data: Data Data.teraźniejszość