de_DEen_USes_ESfa_IRfr_FRhi_INid_IDjapl_PLpt_PTru_RUvizh_CNzh_TW

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.

Objct Constraint Language (OCL)


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 nadal 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 kluczowe 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 właściwości

Użyj notacji kropkowej, aby uzyskać dostęp do atrybutów lub operacji. Nawiasy są wymagane dla operacji, nawet jeśli 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: prawdafałsz (Operacje: ilubnieoznaczaxor)

  • Liczba całkowita / Liczba rzeczywista: Liczby (Operacje: +-*/absmaksmindzimod)

  • Ciąg: Tekst (Operacje: rozmiarkoniugacjapodciągnaLiczbeCalkowita)

Zbiory

Zbiory to typy abstrakcyjne używane do przechowywania wielu elementów. Dostęp do nich uzyskuje się za pomocą -> 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ą właściwość ze wszystkich elementów do nowej kolekcji.

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

    • istnieje(wyrażenie): Zwraca prawda jeśli 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śli 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 za pomocą 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 właściwości podtypu lub ukrytej właściwości nadklasy.

self.oclAsType(Pod).p       -- Dostęp do właściwości 'p' zdefiniowanej w podtypie
self.oclAsType(Nad).p     -- Dostęp do właściwoś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 (prepost)

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 właściwoś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 właściwoś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 każdego 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)