Wykład 2
Hierarchia klas - klasy abstrakcyjne i konkretne, dziedziczenie metod
Nazwa przedmiotu: Inżynieria oprogramowania Egz.
Liczba godzin: 20 godz. wykładu
Punkt_2D protected: int x, y;
Przesuń
Figura_2D
Narysuj Ukryj
Okrąg int promień;
Skaluj
Trójkąt
Punkt_2D A, B, C;
Skaluj Obróć
Klasa
abstrakcyjna
Klasy konkretne
Rysunek na stronie poprzedniej przedstawia przykład hierarchii klas z dziedziczeniem prostym w notacji Coada- Yourdona.
Omówienie:
Klasa abstrakcyjna Figura_2D posiada dwie metody czysto wirtualne (abstrakcyjne) Narysuj i Ukryj, które mogą być dziedziczone do konkretnych klas pochodnych i wykorzystywane przez obiekty tych klas na zasadzie polimorfizmu.
Klasa Okrąg wykorzystuje odziedziczone z klasy Punkt_2D składowe x i y i metodę Przesuń do zapamiętania współrzędnych środka okręgu i przesuwania okręgu.
Klasa Trójkąt (poza hierarchią dziedziczenia) wykorzystuje klasę Punkt_2D do wytworzenia obiektów zagnieżdżonych (podobiektów) A, B i C w celu zapamiętania położeń wierzchołków trójkąta.
Wszystkie klasy konkretne posiadają (oprócz odziedziczonych) własne, niezbędne im metody.
Hierarchia klas - dziedziczenie i przesłanianie funkcji składowych
Base
Derived1Level1 Derived1Level1 DerivedLeveL2
Rysunek na stronie poprzedniej przestawia dziedziczenie wielopoziomowe z udziałem dziedziczenia wielobazowego (kratę), fragment kodu poniżej – implementację tej hierarchii z uwidocznieniem zasad dziedziczenia i przesłaniania metod class Base
{ void h( );
protected:
void g( );
public:
void f( ) { … h( ); …} // wywołanie metody prywatnej };
class Derived1Level1: public Base { public:
void f( ) { … g( ); h( ); …}
// przesłonięcie odziedziczonej metody Base::f( ), // wewnątrz definicji metody f( ) odwołanie do // odziedziczonej metody g( ) i własnej metody h( ) void h( );
// deklaracja własnej metody h( ) bez przesłaniania // metody Base::h( ), która nie jest dziedziczona };
class Derived2Level1: public Base { public:
void f( ) { g( );
h( ); // błąd kompilacji – Base::h( ) niedostępna }
};
class DerivedLevel2: public Derived1Level1, public Derived2Level1 { public:
void f( ) { g( );
h( ); // wywołanie odziedziczonej metody // Derived1Level1::h( )
Base::f( ); // wywołanie funkcji z klasy bazowej Base // z użyciem kwalifikatotra dostępu
} };
W rozpatrywanej hierarchii klas:
o Próba wywołania h( ) z metody f( ) w klasie Derived2Level1 powoduje błąd kompilacji z uwagi na rodzaj dziedziczenia,
o Dane i funkcje składowe chronione klasy bazowej dostępne są jedynie w klasach pochodnych. Dlatego też obie klasy pochodne mogą wywoływać metodę chronioną g( ), ale wywołanie tej metody z dowolnej funkcji zewnętrznej byłoby już niedopuszczalne.
o Wszystkie klasy pochodne definiują lokalne wersje metody f( ), przesłaniając wersje klas bazowych. Jednak dostęp do metody zdefiniowanej wyżej w hierarchii klas zawsze można sobie zapewnić za pomocą kwalifikatora dostępu.
Postać ogólna nagłówka definicji klasy pochodnej:
class oznacznik _klasy: lista_klas_bazowych Element listy klas bazowych ma postać:
< public | protected | private > ozn_klasy_bazowej
Dostępność składowych klas bazowych w klasach pochodnych:
Rodzaj dziedziczenia
Składowe klasy bazowej ...
w klasach
pochodnych są ...
public private niedostępne
protected protected
public public
protected private niedostępne
protected protected
public protected
private private niedostępne
protected prywatnymi klasy pochodnej public prywatnymi klasy
pochodnej Jeśli żadne słowo kluczowe nie wystąpiło przyjmuje się:
domyślnie private dla klas zdefiniowanych z użyciem słowa class,
domyślnie public dla klas zdefiniowanych z użyciem słów struct lub union.
Obiekty klasy pochodnej w roli obiektów klasy bazowej Przykład:
class PRACOWNIK {
protected:
char * Nazwisko, * Dział;
int Uposażenie;
. . . . };
class KIEROWNIK: public PRACOWNIK {
int Uprawnienia[ 8 ];
. . . . };
int main( ) {
KIEROWNIK kk;
PRACOWNIK * p = & kk;
// każdy kierownik jest jednocześnie pracownikiem,
PRACOWNIK pp;
KIEROWNIK * k = & pp;
// . . . ale nie każdy pracownik kierownikiem }
o Obiekty klasy pochodnej są traktowane jak obiekty klasy bazowej, jeśli sięgnie się do nich za pomocą wskaźnika.
Odwrotnie – NIE !
Polimorfizm składowych. Wiązanie statyczne i dynamiczne class Class1 {
public:
virtual void f( ) { … } void g( );
};
class Class2 { public:
virtual void f( ) { … } void g( );
};
Powyżej zdefiniowano dwie, nie powiązane dziedziczeniem, klasy z identycznymi deklaracjami funkcji składowych.
Jeżeli wymusimy, aby wskaźnik uprawniony do wskazywania obiektu klasy Class1 pokazywał na obiekt klasy Class2, to:
wywołanie poprzez ten wskaźnik metody g( ) spowoduje wywołanie metody Class1::g( ) - wiązanie statyczne,
wywołanie poprzez ten wskaźnik metody f( ) spowoduje wywołanie metody Class2::f( ) - wiązanie dynamiczne.
o Wiązanie statyczne wskaźnika z obiektem ma miejsce na etapie kompilacji. Późniejsze wiązanie wskaźnika z obiektem innej klasy nie ma już znaczenia.
o W przypadku wiązania dynamicznego decyzja o wiązaniu jest odkładana do czasu wykonania programu. Wiązanie dynamiczne można wymusić za pomocą słowa kluczowego virtual, poprzedzającego deklarację funkcji składowej. Wiąże ono wszystkie funkcje poprzedzone słowem virtual o tej samej nazwie w różnych klasach.
Zjawisko wywoływania funkcji, stosownej do klasy obiektu, z którym w danym momencie jest związany wskaźnik, nazywamy polimorfizmem.
Polimorfizm jest potężnym narzędziem programowania obiektowego. Wystarczy wysłać standardowy komunikat do obiektów wielu różnych klas, zawierających wirtualne funkcje o tej samej nazwie, ale różnych treściach. Wykonana zostanie funkcja z klasy, z której obiektem jest w danej chwili związany wskaźnik.
Dziedziczenie i polimorfizm. Abstrakcyjne typy danych.
W poniższym przykładzie zaprezentowane zostanie użycie:
klas abstrakcyjnych, funkcji wirtualnych i czysto wirtualnych (abstrakcyjnych), oraz omówiony mechanizm polimorfizmu.
Klasa FIGURE istnieje tylko po to, aby dostarczać interfejsu dla wyprowadzanych z niej klas – jest abstrakcyjnym typem danych (abstract data type – ADT). ADT jest zwykle typem bazowym dla hierarchii klas konkretnych, nie tworzy obiektów i zazwyczaj nie posiada danych składowych.
FIGURE
RECTANGLE CIRCLE
SQUARE
Klasa abstrakcyjna
class FIGURE {
public:
// poniżej deklaracje funkcji abstrakcyjnych,
// to jest czysto wirtualnych (nie wymagających definicji) virtual float GetArea( )=0;
virtual void Draw( )=0;
};
class CIRCLE: public FIGURE {
float x, y, radius;
public:
virtual float GetArea( ) { return 3.14*radius*radius;}
virtual void Draw( );
};
class RECTANGLE: public FIGURE {
float width;
float length;
public:
virtual float GetArea( ) { return width*length;}
virtual float GetLength( ) { return length;}
virtual float GetWidth( ) { return width;}
virtual void Draw( );
};
class SQUARE: public RECTANGLE {
public:
SQUARE::SQUARE( float len): RECTANGLE(len, len){ } // powyżej definicja konstruktora klasy SQUARE -
// faktycznie tworzony jest obiekt klasy RECTANGLE // wszystkie inne jawne metody klasy są dostępne jako // odziedziczone z klasy RECTANGLE
};
Dopuszczalne jest deklarowanie wskaźników do obiektów (nie istniejących przecież obiektów) klas abstrakcyjnych.
Spróbujmy to zrobić
FIGURE *sp;
Jeśli sp wskazywać będzie na dowolny obiekt klasy konkretnej, dziedziczącej na dowolnym poziomie z klasy abstrakcyjnej FIGURE, wtedy wywołanie funkcji składowej zadeklarowanej w klasie FIGURE, spowoduje wykonanie funkcji o definicji odpowiedniej do klasy obiektu, na który akurat wskazuje sp.
Przykłady
sp->Draw( ); // wystąpi polimorfizm !!!
float area=sp->GetArea( ); // wystąpi polimorfizm !!!
Podobnie delete sp; jest równoważne z wywołaniem destruktora odpowiedniej klasy pochodnej, i usunięciem obiektu, odpowiedniego dla klasy, na którą wskazuje w danej chwili sp. Destruktor w klasie FIGURE jest czysto wirtualny, ale jego definicje znajdują się w klasach pochodnych.
Zasady używania funkcji wirtualnych i czysto wirtualnych w warunkach dziedziczenia:
Można tworzyć obiekty klas zawierających funkcje wirtualne, ale jeśli chociaż jedną z tych funkcji uczynimy abstrakcyjną, cała klasa staje się abstrakcyjną.
Funkcje wirtualne dziedziczą się do klas pochodnych tak jak ”zwykłe” funkcje.
Funkcja wirtualna musi być zdefiniowana w pierwszej klasie konkretnej hierarchii klas.
Klasy pochodne nie muszą korzystać z funkcji wirtualnych klas bazowych.
Funkcja czysto wirtualna, która jest dziedziczona do klasy pochodnej i nie zostanie w tej klasie zdefiniowana, pozostaje funkcją czysto wirtualną, czyniąc klasę pochodną również klasą abstrakcyjną.
Jeśli wywołuje się funkcję wirtualną z kwalifikatorem zakresu, np. SQUARE:: Draw( ), mechanizm wirtualny nie działa.
Modelowanie przypadków użycia systemu
Diagram przypadków użycia systemu pełni pierwszą i podstawową rolę w projektowaniu systemu a także planowaniu jego powstawania.
Podstawowe definicje
Przypadek użycia systemu jest zbiorem scenariuszy, powiązanych ze sobą wspólnym celem użytkownika (aktora).
Scenariusz jest ciągiem kroków opisujących interakcję między aktorem a modelowanym systemem.
Aktor (rola) jest to funkcja, którą użytkownik pełni w stosunku do systemu.
Przykład scenariusza zakupu towaru w sklepie internetowym
Zakup towaru
Klient przegląda katalog i wybiera towar Klient przechodzi do kasy
Klient podaje adres i termin dostawy System podaje pełną informację cenową Klient podaje informacje o karcie kredytowej System autoryzuje sprzedaż
System wysyła e-mail do klienta z potwierdzeniem transakcji Alternatywa: niepowodzenie autoryzacji w kroku 6.
Umożliwić klientowi powtórzenie kroku 5. i przejść do kroku 6.
Alternatywa: stały klient
Wg. scenariusza głównego do punktu 2
3. System wyświetla: bieżące warunki dostawy, informacje o cenie, cztery ostatnie cyfry numeru karty kredytowej
4. Klient potwierdza, lub zmienia swoje dane domyślne Wg. scenariusza głównego od punktu 6
Diagram przypadków użycia dla systemu maklerskiego Zalecenie metodologiczne:
W scenariuszach i diagramie przypadków użycia nie należy dokumentować zbyt wielu szczegółów. Im wyższy stopień ryzyka niesionego przez przypadek użycia, tym dokładniejszy powinien być scenariusz. W dalszym procesie iteracyjnym i przyrostowym tworzenia systemu uszczegóławia się poszczególne scenariusze.
Ustal limity
Przeanalizuj ryzyko
Wyceń kontrakt
Zarejestruj transakcję
Limit
przekroczony
Zaktualizuj rachunki
Kierownik
sali
Makler
System księgowy
Sprzedawca Określ wartość
<<zawiera>>
<<zawiera>>
Przypadek użycia Aktor
Uogólnienie
Modelowanie związków między aktorami a przypadkami użycia:
1. Aktor to pełniona rola, a nie osoba fizyczna, zajmowane stanowisko, lub konkretny system – ta sama osoba może móc pełnić rolę kierownika sali, maklera i sprzedawcy. Odwrotnie – może być wiele osób pełniących rolę maklera.
2. Jeden aktor może występować w wielu przypadkach użycia i na odwrót – przypadek użycia może być wykonywany przez wielu aktorów.
3. Należy dążyć do wykrycia wszystkich przypadków użycia systemu.
4. Mogą być przypadki użycia nie mające związków z konkretnymi aktorami.
5. Najważniejszym jest zrozumienie przypadku użycia i celów użytkownika, które spełnia.
Modelowanie związków między poszczególnymi przypadkami użycia:
1. Do relacji zawierania dochodzi wtedy, gdy kilka przypadków użycia ma wspólną sekwencje podobnych kroków w scenariuszu.
2. Uogólnienie jest jak gdyby dziedziczeniem na poziomie przypadków użycia. Przypadek użycia podrzędny (zwany niekiedy specjalistycznym przypadkiem użycia) zwykle zawiera jeden ze scenariuszy alternatywnych podstawowego (uogólnionego) przypadku użycia. Chociaż specjalistyczny przypadek użycia przesłania niekiedy część podstawowego przypadku użycia, ale zawsze powinien dotyczyć tego samego celu użytkownika, co podstawowy przypadek użycia.
3. Relacja rozszerzenia (nie występuje w przykładowym diagramie przypadków użycia) wzbogaca podstawowy przypadek użycia o dodatkowe zachowania, które warto potraktować odrębnie. Jeśli używamy rozszerzeń, podstawowy przypadek użycia musi mieć wyspecyfikowane punkty rozszerzeń, a dodatkowy przypadek użycia może dodawać nowe zachowania tylko w tych punktach. Graficznie relację rozszerzenia w UML dokumentuje się podobnie, jak relację zawierania przy pomocy strzałki <<rozszerza>>
4. Podstawowy przypadek użycia może mieć wiele różnych rozszerzeń, podobnie jak określony specjalistyczny przypadek użycia może rozszerzać podstawowy w wielu różnych punktach.
Zalecenie metodologiczne:
Gdy trzeba powtórzyć coś w kilku różnych przypadkach użycia a jednocześnie chce się uniknąć powtórzeń, należy używać relacji zawierania.
Gdy trzeba opisać warianty typowego postępowania przy niezbyt jeszcze sformalizowanym scenariuszu, należy używać relacji uogólnienia.
Gdy trzeba opisać warianty typowego postępowania, ale jest już potrzebny bardziej precyzyjny scenariusz ze zdefiniowanymi punktami rozszerzeń, należy używać relacji rozszerzenia.
Rozbicie podstawowego przypadku użycia przy użyciu uogólnień i rozszerzeń powinno mieć rozsądny stopień, adekwatny do przyjętego w
całym diagramie przypadków użycia, stopnia szczegółowości.
Biznesowe i systemowe przypadki użycia
Systemowy przypadek użycia to interakcja użytkownika (lub innego systemu) z danym systemem, biznesowy przypadek użycia to reakcja przedsiębiorstwa (a nie systemu) na klientów i zdarzenia zewnętrzne. We wczesnych fazach rozwinięcia systemu należy koncentrować się bardziej na biznesowych przypadkach użycia. Pomagają one, na przykład, w wypracowaniu alternatywnych sposobów osiągnięcia celu przez aktora. Później dopiero można wypracowywać, spełniające poszczególne biznesowe przypadki użycia, systemowe przypadki użycia. Te ostatnie są niezbędne dla dalszego rozwijania systemu, począwszy od pierwszego jego prototypu.
Posumowanie:
Przypadki użycia są podstawowym narzędziem w uchwyceniu wymagań systemu a później w planowaniu i zarządzaniu iteracyjnym projektem tworzenia systemu informatycznego, zwłaszcza w metodologii programowania ekstremalnego.
Każdy przypadek użycia to potencjalne wymaganie, a dopóki nie określi się wymagań, nie można zaplanować
„co dalej”.
Większość przypadków użycia powstaje w fazie projektu, ale w miarę postępu prac pojawiają się nowe.
Przypadki użycia reprezentują spojrzenie z zewnątrz na tworzony system i dlatego nie istnieją korelacje między nimi a klasami wewnątrz systemu.
Modelowanie pojęciowe (poprzez biznesowe przypadki użycia) to najlepsza platforma porozumienia z przyszłym użytkownikiem tworzonego systemu informatycznego.
Model statyczny. Diagram klas
Diagram klas w perspektywie pojęciowej obrazuje klasy modelowanego systemu i związki (asocjacje) miedzy nimi. W perspektywie implementacyjnej diagram ten zwykle ulega zmianom (rozszerzeniu o nowe klasy i asocjacje).
Narzędzia implementujące język UML (np. Rational Rose) pozwalają oglądać diagram klas na różnym poziomie szczegółowości. Dlatego mówimy o warstwie klas i powiązań, warstwie atrybutów i usług oraz warstwie specyfikacyjnej.
Rysunek na stronie 33 prezentuje przykładowy fragment diagramu klas modelującego przypadek użycia „Zamawianie towaru” przedstawiony w warstwie atrybutów i usług.
Niektóre klasy na tym rysunku zostały przedstawione skrótowo, tak jak się to robi tworząc warstwę klas i powiązań.
Są to klasy Towar i Pracownik. Pozostałe klasy zostały wyspecyfikowane w sposób typowy dla warstwy atrybutów i usług, kiedy to podaje się tylko najważniejsze dla zrozumienia modelu i ważne z punktu widzenia modelowanego przypadku użycia, atrybuty i usługi. Typ wartości atrybutu, lub wartości zwracanej przez usługę, zwykle nie jest specyfikowany, chyba że jest to istotne dla zrozumienia modelu. Nie podaje się też argumentów usług i ich typów.
Natomiast w warstwie specyfikacyjnej podaje się pełną specyfikację atrybutów, wg. schematu
specyfikator_dostępu nazwa_atrybutu:typ=wartość_domyślna oraz specyfikacje usług wg. schematu
specyfikator_dostępu nazwa_usługi(lista_parametrów):
typ_wyniku {łańcuch_własności}
* 1
1 { A* }
{wiarygodnośćKredytowa() pozycje * == ”niska”}
*
0..1 przedstawiciel handlowy
* 1
{ A* } – { if( Zamówienie.Klient.wiarygodnośćKredytowa() = = ”niska” ) Zamówienie.przedpłata = = true } Asocjacje
W perspektywie pojęciowej asocjacje reprezentują stałe związki pojęciowe miedzy obiektami klas: Zamówienie musi przyjść od pojedynczego Klienta, Klient może złożyć kilka Zamówień, Klient firmowy może mieć co najwyżej jednego Pracownika w roli Przedstawiciela handlowego.
Należy zwrócić uwagę na dopuszczalne w UML przedstawienie krotności obiektów, tj. 1, *, 0 .. 1, m .. n, np. 1. .12. Gwiazdka * oznacza 0 . . . Ponadto w ostatniej specyfikacji musi zachodzić m n.
Zamówienie numer
dataOtrzymania przedpłata:Boolean wartość
wyślij() zamknij()
Klient nazwa
adres
wiarygodnośćKredytowa():String zamknij()
Klient firmowy wiarygodnośćKredytowa limitKredytowy
osobaDoKontaktów przypomnij() obciążZaMiesiąc()
Klient indywidualny nrKartyKredytowej
Pozycja zamówienia ilość
cena
zapewniona:Boolean
Pracownik
Towar
nazwa roli ograniczenie
Przedstawienie ról wygląda ogólnie tak jak na poniższym rysunku
rola B rola A
Nazwy ról można pominąć, jeśli pełnione role są oczywiste i zrozumiałe.
Z punktu widzenia perspektywy implementacyjnej asocjacje oznaczają zobowiązania klas i muszą znaleźć swoje odbicie w usługach klas i strukturze bazy danych, obsługiwanej przez obiekty obu klas.
Zobowiązania, o których powyżej, są czasem przedstawiane za pomocą strzałek
* 1
Mówimy wtedy, że mamy do czynienia z nawigowalnością, lub asocjacją jednokierunkową, czyli o możliwości przejścia z klasy do klasy. Realizowane jest to w ten sposób, że np.
obiekt klasy A ma wskaźnik do obiektu klasy B (mówimy czasem, że posiada obiekt klasy B), ale nie odwrotnie.
Nawigowalność powinna być specyfikowana w perspektywie implementacyjnej, natomiast w perspektywie pojęciowej nie musi.
Stosowanie ograniczeń
Ograniczenia, specyfikowane jako {opis ograniczenia}
dotyczą klas. Są to asercje, które są zdaniami logicznymi zawsze (dla wszystkich obiektów klas) prawdziwymi.
Wpisanie ograniczenia do diagramu klas zobowiązuje implementującego do takiego napisania kodu, aby prawdziwość ograniczenia była zawsze zapewniona.
Klasa_A Klasa_B
Końcowe uwagi metodologiczne, dotyczące diagramów klas:
1. Na etapie budowy modelu analizy należy stosować tylko perspektywę pojęciową.
2. Na początek używać tylko omawianych powyżej elementów: klas, asocjacji, istotnych dla zrozumienia modelu atrybutów i usług, ról, krotności obiektów, uogólnień i ograniczeń.
3. Później, ale tylko gdy będzie wyraźna potrzeba, można wprowadzać bardziej złożone elementy: nawigowalność, agregację, zawieranie, role uporządkowane, asocjacje kwalifikowane, powiązania w postaci zależności.
4. Lepiej jest mieć diagramy klas dla poszczególnych przypadków użycia systemu (nawet z pewnymi częściami wspólnymi), niż jeden wielki diagram, modelujący cały system, gdyż:
- pozwala to użytkownikowi lepiej zrozumieć system,
- ułatwia modyfikację (dodawanie funkcjonalności) i kontrolę poprawności modelu.
5. Na każdym etapie należy uważać, aby nie ugrzęznąć w szczegółach.