Kompletny przewodnik po języku ograniczeń obiektowych (OCL)

3 dni temu

Opierając się na podanych materiałach wykładowych, ten przewodnik obejmuje podstawowe pojęcia, składnię oraz zastosowania praktyczne języka ograniczeń obiektowych (OCL) w kontekście modelowania UML.


1. Wprowadzenie do OCL

Dlaczego potrzebujemy OCL?

Choć diagramy UML (takie jak diagramy klas) są doskonałe do wizualizacji struktury, nie mogą wyrażać wszystkich istotnych aspektów specyfikacji.

  • Język naturalny jest niejednoznaczny.

  • Tradycyjne języki formalne są jednoznaczne, ale zbyt trudne do odczytania i pisania dla przeciętnych modelistów systemów.

  • OCL mostkuje tę przerwę: jest to język formalny używany do wyrażania dokładnych ograniczeń, a mimo to przez cały czas pozostaje łatwy do odczytania i pisania.

Kluczowe cechy OCL

  1. Czysty język wyrażeń: Wyrażenia nie mają skutków ubocznych. Ocena wyrażenia OCL zwraca wartość, ale nie może zmienić stan systemu. Może jednak opisać zmianę stanu (np. w warunku po).

  2. Nie jest językiem programowania: Nie możesz pisać logiki programu, przepływu sterowania ani wywoływać procesów/operacji, które zmieniają stan.

  3. Język typów: Każde wyrażenie ma typ. Poprawnie zapisane wyrażenia muszą spełniać zasady zgodności typów. Każdy klasifikator UML jest typem OCL.

  4. Ocena natychmiastowa: Stan obiektów nie może się zmieniać podczas oceny wyrażenia OCL.


2. Podstawowa składnia i semantyka

Kontekst i self

Każdy wyrażenie OCL jest oceniane w kontekście określonego klasyfikatora UML (klasa, interfejs itp.).

  • kontekst: Określa klasę, do której stosuje się ograniczenie.

  • self: Słowo najważniejsze oznaczające konkretny egzemplarz klasy kontekstu, który jest oceniany.

kontekst Osoba -- 'self' odnosi się do konkretnego egzemplarza Osoba

Komentarze i operatory

  • Komentarze: Oznaczane przez -- (np. -- to jest komentarz).

  • Operatory infiksowe: Można używać standardowych operatorów matematycznych/logicznych (+, -, =, <). Koncepcyjnie, a + b jest identyczne z a.+(b).

Dostęp do adekwatności

Użyj notacji kropkowej, aby uzyskać dostęp do atrybutów lub operacji. Nawiasy są wymagane dla operacji, choćby jeżeli nie mają parametrów.

kontekst Osoba self.wiek -- Dostęp do atrybutu 'wiek' self.cenaAkcji() -- Wywołanie operacji 'cenaAkcji'

3. Typy i kolekcje

Podstawowe zdefiniowane typy

OCL zawiera typy standardowe z określonymi operacjami:

  • Logiczny: prawda, fałsz (Operacje: i, lub, nie, oznacza, xor)

  • Liczba całkowita / Liczba rzeczywista: Liczby (Operacje: +, -, *, /, abs, maks, min, dzi, mod)

  • Ciąg: Tekst (Operacje: rozmiar, koniugacja, podciąg, naLiczbeCalkowita)

Zbiory

Zbiory to typy abstrakcyjne używane do przechowywania wielu elementów. Dostęp do nich uzyskuje się dzięki -> notacji.

  1. Zbiór: Zbiór matematyczny (bez powtórzeń, nieuporządkowany). Zbiór {1, 2, 5}

  2. UporządkowanyZbiór: Bez powtórzeń, ale uporządkowane według pozycji. UporządkowanyZbiór {5, 4, 3}

  3. Pojemnik: Zezwala na duplikaty, nieuporządkowane. Pojemnik {1, 2, 2, 5}

  4. Sequencja: Zezwala na duplikaty, uporządkowane. Sequencja {1, 2, 5, 10}

    • Uwaga: Zapis .. zapis tworzy sekwencję kolejnych liczb całkowitych: Sequencja {1..5}.

Kluczowe operacje na zbiorach

  • Wspólne: rozmiar(), jestPusty(), nieJestPusty(), zawiera(v), nieZawiera(v), liczba(v), suma().

  • Iteratory (kluczowe dla OCL):

    • wybierz(expr): Zwraca elementy, gdzie wyrażenie jest prawdziwe.

    • odrzuc(wyrażenie): Zwraca elementy, gdzie wyrażenie jest fałszywe.

    • zbierz(wyrażenie): Wyodrębnia określoną adekwatność ze wszystkich elementów do nowej kolekcji.

    • dlaWszystkich(wyrażenie): Zwraca prawda jeżeli wyrażenie jest prawdziwe dla wszystkich elementów.

    • istnieje(wyrażenie): Zwraca prawda jeżeli wyrażenie jest prawdziwe dla przynajmniej jednego elementu.

    • iteruj(elem; acc = init | wyrażenie): Ogólna podstawa dla wszystkich iteratorów.

Wariacje składni iteratora:

self.employee->select(wiek > 50) self.employee->select(p | p.wiek > 50) self.employee->select(p: Osoba | p.wiek > 50)

4. Przechodzenie po modelach UML

OCL jest intensywnie wykorzystywany jako język nawigacji do przemieszczania się po powiązaniach.

Standardowa nawigacja

Nawiguj używając nazwy roli przeciwnej. Typ zwracany zależy od wielokrotności:

  • Wielokrotność 1 lub 0..1: Zwraca pojedynczy obiekt.

  • Wielokrotność *: Zwraca Zbiór (lub Uporządkowany zbiór jeżeli oznaczony {uporządkowany}).

kontekst Company self.manager.age > 40 -- manager to 0..1, zwraca obiekt Person self.employee->notEmpty() -- employee to 0..*, zwraca Zbiór

Jeśli nazwa roli jest brakująca, użyj małej litery nazwy klasy po drugiej stronie (np. self.bank).

Klasy powiązań

Przejdź do klasy powiązania używając małej litery nazwy klasy powiązania.

kontekst Person self.job->size() >= 1 -- 'job' to klasa powiązania między Person i Company

Powiązania rekurencyjne

Gdy powiązanie wraca do tej samej klasy (np. Małżeństwo między dwoma osobami), musisz określić kierunek dzięki nawiasów kwadratowych [nazwaRoli].

kontekst Osoba self.marszyna[żona]->select(m | m.zakończona = false)->size() = 1

Zaawansowane powiązania

Użyj atrybutów jako wyróżnika w nawiasach kwadratowych, aby wybrać konkretny obiekt.

kontekst Bank self.klient[12345] -- Zwraca konkretną osobę z numerem konta 12345 self.klient -- Zwraca zbiór wszystkich klientów

Przypisywanie typu (rzutowanie)

Użyj oclAsType() aby uzyskać dostęp do adekwatności podtypu lub ukrytej adekwatności nadklasy.

self.oclAsType(Pod).p -- Dostęp do adekwatności 'p' zdefiniowanej w podtypie self.oclAsType(Nad).p -- Dostęp do adekwatności 'p' zdefiniowanej w nadklasie

5. Pisanie ograniczeń

1. Inwarianty (inv)

Warunki, które muszą zawsze być prawdziwe dla wszystkich instancji klasy.

kontekst Firma inv: self.liczbaPracownikow <= 50 inv SME: self.cenaAkcyji() > 0 -- 'SME' to opcjonalna nazwa ograniczenia

2. Warunki wstępne i końcowe (pre, post)

Ograniczenia związane z operacjami.

  • pre: Musi być prawdziwe przed wykonaniem.

  • post: Musi być prawdziwe po wykonanie. Użyj wynik dla wartości zwracanej.

  • @pre: Używane w warunkach po, aby odwołać się do wartości adekwatności przed rozpoczęcia operacji.

kontekst Osoba::dochod(): Integer pre dorosly: self.wiek >= 18 post wynikOK: wynik < 5000 kontekst Firma::zatrudnijPracownika(p: Osoba) post: pracownik = pracownik@pre->włączając(p)

3. Wyrażenia ciała, początkowe i wyprowadzone

  • ciało: Definiuje wynik operacji zapytania (może być rekurencyjne).

  • init: Definiuje wartość początkową atrybutu.

  • wyprowadź: Definiuje atrybut wyprowadzony.

kontekst Osoba::dochod(): Integer ciało: self.praca.pensja->suma() kontekst Osoba::zamężna: Logiczna init: fałsz kontekst Firma::liczbaPracownikow: Integer wyprowadź: self.pracownik->rozmiar()

6. Zaawansowane cechy OCL

Wyrażenia let i def

  • let: Definiuje zmienną lokalną wewnątrz pojedynczego wyrażenia.

    let sumaGodzin: Integer = self.pracujeNa->zbieraj(godziny)->suma() w sumaGodzin >= 30
  • def: Definiuje ponownie używane zmienne lub operacje przypisane do klasyfikatora.

    def: maTytul(t: String): Logiczna = self.praca->istnieje(tytul = t)

Nieokreślone wartości (logika trójwartościowa)

Jeśli podwyrażenie jest nieokreślone (np. przeglądanie nullowego związku), całe wyrażenie jest nieokreślone. Jednak operatory logiczne mają wyjątki skrótu:

  • prawda lub <nieokreślone> = prawda

  • fałsz i <nieokreślone> = fałsz

  • fałsz implikuje <undefined> = prawda

  • <undefined> implikuje prawdę = prawda

Cechy klasy i zdefiniowane adekwatności obiektów

  • allInstances(): Zwraca zbiór wszystkich istniejących instancji klasy.

    Person.allInstances()->size() <= 100
  • Właściwości obiektów: oclIsTypeOf(Typ), oclIsKindOf(Typ), oclInState(Stan), oclIsNew (używane w warunkach po).


7. Kompleksowy przykład badania: Model Firmy

Aby utwierdzić te koncepcje, przedstawiamy zestaw ograniczeń integralności stosowanych do modelu bazy danych Firmy (Pracownicy, Działy, Projekty, Zależni).

Podstawowe ograniczenia atrybutów

-- Wiek musi być >= 18 kontekst Pracownik inv: self.wiek() >= 18 -- Data zatrudnienia musi być późniejsza niż data urodzenia kontekst Pracownik inv: self.dataZatrudnienia > self.dataUrodzenia -- Data rozpoczęcia pracy menedżera musi być późniejsza niż data zatrudnienia kontekst Pracownik inv: self.zarzadza->niepuste() implikuje self.zarzadza.dataRozpoczecia > self.dataZatrudnienia

Ograniczenia nawigacji i relacji

-- Przypisany nadzorcy musi być starszy i zarabiać więcej niż pracownik kontekst Pracownik inv: self.nadzorowany->niepuste() implikuje self.wiek() > self.nadzorowany.wiek() inv: self.nadzorowany->niepuste() implikuje self.pensja < self.nadzorowany.pensja -- Nadzorca musi zostać zatrudniony wcześniej niż jego podwładni kontekst Pracownik inv: self.podwladni->niepuste() implikuje self.podwladni->forall(e | e.dataZatrudnienia > self.dataZatrudnienia) -- Menedżer działu musi być pracownikiem tego działu kontekst Dział inv: self.pracujeW->includes(self.zarzadza.pracownik) -- Lokalizacja projektu musi być jedną z lokalizacji działu kontekst Projekt inv: self.kontroluje.lokalizacje->includes(self.lokalizacja) -- Pracownik może pracować tylko nad projektami kontrolowanymi przez swój dział kontekst Pracownik inv: self.pracujeW.kontroluje->includesAll(self.pracujeNad.projekt)

Ograniczenia kolekcji i iteracji

-- Numer ubezpieczenia społecznego musi być unikalnym identyfikatorem dla wszystkich pracowników kontekst Pracownik inv: Employee.allInstances->forAll(e1, e2 | e1 <> e2 implikuje e1.SSN <> e2.SSN) -- Imię i relacja zależnego muszą być unikalne dla wszystkich pracownika kontekst Pracownik inv: self.zależni->forAll(e1, e2 | e1 <> e2 implikuje (e1.imie <> e2.imie lub e1.relacja <> e2.relacja)) -- Atrybut pochodny: liczba pracowników w dziale kontekst Dział inv: self.liczbaPracownikow = self.pracujeW->size() -- Pracownik może pracować nad maksymalnie 4 projektami kontekst Pracownik inv: self.pracujeNad->size() <= 4

Złożona logika przy użyciu niech i wybierz

-- Łączny czas pracy musi wynosić od 30 do 50 godzin tygodniowo kontekst Pracownik inv: niech totGodz: Integer = self.worksOn->zbierz(godziny)->suma() w totGodz >= 30 i totGodz <= 50 -- Na konkretnym projekcie może pracować maksymalnie 2 pracownicy przez mniej niż 10 godzin kontekst Projekt inv: self.worksOn->wybierz(godziny < 10)->rozmiar() <= 2 -- Na projekcie mogą pracować tylko kierownicy działów przez mniej niż 5 godzin kontekst Pracownik inv: self.worksOn->wybierz(godziny < 5)->niepusty() implikuje self.worksFor.manages.employee = self -- Pracownicy bez podwładnych muszą pracować >= 10 godzin na WSZYSTKICH swoich projektach kontekst Pracownik inv: self.subordinates->pusty() implikuje self.worksOn->dlaWszystkich(godziny >= 10) -- Kierownik działu musi pracować >= 5 godzin na wszystkich projektach kontrolowanych przez dział kontekst Dział inv: self.controls->dlaWszystkich(p: Projekt | self.manages.employee.worksOn->wybierz(godziny >= 5)->zawiera(p))

Zapobieganie cyklom (logika rekurencyjna)

-- Pracownik nie może nadzorować samego siebie kontekst Pracownik inv: self.subordinates->wyklucza(self) -- Hierarchia nadzoru nie może być cykliczna kontekst Pracownik def: wszystkiePodwladne = self.subordinates->suma( self.subordinates->zbierz(e: Pracownik | e.wszystkiePodwladne)) inv: self.wszystkiePodwladne->wyklucza(self)

Idź do oryginalnego materiału