53
Modele etapów tworzenia oprogramowania
Już w latach sześćdziesiątych zauważono, że przebieg procesu
tworzenia oprogramowania informatycznego podlega takim
samym regułom, jak tworzenie dowolnego urządzenia np.
budowli. Wtedy właśnie wprowadzono pojęcie Inżynierii
Oprogramowania (Software Engineering).
Poniżej przedstawione zostaną trzy wybrane modele cyklu
życia systemu informatycznego:
- model kaskadowy,
- prototypowanie błyskawiczne,
- programowanie ekstremalne.
Model kaskadowy
przekazanie
Model KASKADOWY cyklu życia systemu
Ścisła
interpretacja
modelu
kaskadowego
traktuje
poszczególne
fazy
jako
niezależne
okresy
realizacji
przedsięwzięcia. Według tej interpretacji okresy te nie
Analiza
Projektowanie
Implementacja
Testowanie
Konserwacja
54
nakładają się na siebie, zaś ich wykonanie przebiega
sekwencyjnie, bez procesów iteracyjnych.
W rzeczywistości proces ten musi mieć charakter iteracyjny
(w postaci powrotów do wcześniejszych faz modelu w
przypadku wykrycia błędów powstałych w tychże fazach) i
przyrostowy (w każdej fazie nawrotu następuje wzbogacenie
modelu).
Do zalet modelu kaskadowego należy zaliczyć:
Łatwość zarządzania przedsięwzięciem,
Łatwość harmonogramowania poszczególnych etapów,
Łatwość określenia kosztów całego przedsięwzięcia,
Łatwość tworzenia dokumentacji.
Wadami tego modelu są;
Wysoki koszt błędów popełnionych we wstępnych fazach
projektu (błędy z fazy analizy i projektowania mogą wyjść
na jaw dopiero w fazie testowania lub konserwacji)
Długa przerwa w kontaktach z klientem (od określenia
wymagań – do przekazania).
Prototypowanie błyskawiczne
przekazanie
Prototypowanie błyskawiczne
Prototyp
1
Prototyp
2
Prototyp
N
Prototyp
N-1
55
Model prototypowy cyklu życia systemów powstał jako
antidotum na wymienione wyżej wady modelu kaskadowego,
z zwłaszcza duże koszty błędów popełnionych w fazie analizy
wymagań, co ma miejsce zwłaszcza w przypadku
nowatorskich i złożonych systemów. Model ten jest
przeciwieństwem modelu kaskadowego.
W modelu prototypowym wyróżnia się następujące fazy:
Ogólne określenie wymagań,
Budowa prototypu pierwszego,
Weryfikacja prototypu przez klienta, (!!!)
Budowa kolejnego prototypu,
. . . . . . . . .
Przekazanie systemu klientowi,
Dalsze doskonalenie systemu,
. . . . . . . . .
Głównym celem budowy prototypów jest lepsze określenie
wymagań, realizowane poprzez:
Wykrycie nieporozumień pomiędzy klientem a twórcami
systemu,
Wykrycie brakujących i trudnych usług.
Prócz tego pojawiają się dodatkowe zalety budowy
prototypów:
Możliwość
szybkiej
demonstracji
pracującej
wersji
systemu,
Możliwość szkoleń, zanim zbudowany zostanie pełen
system.
56
Model ten posiada oczywiście swoje wady. Zaliczyć do nich
należy:
Dodatkowo ponoszony i trudny do określenia koszt budowy
prototypów,
Trudny do określenia moment zakończenia całego
przedsięwzięcia (bardzo często proces ten nigdy się nie
kończy),
Pewne zaskoczenie klienta, który musi długo czekać na
odbiór systemu, którego „prawie całkowite” wykonanie
(demonstrowany prototyp) zajęło tak mało czasu.
Prototypowanie błyskawiczne jest często łączone z nasilonym
korzystaniem z gotowych komponentów.
Przedstawione
dwie
metodyki
(model
kaskadowy
i
prototypowanie błyskawiczne), stoją jak gdyby na dwóch
przeciwstawnych sobie biegunach koncepcji tworzenia
oprogramowania i w rzeczywistości żadne z nich w tak czystej
postaci nie jest stosowane. Dodatkowo ostatnio rozwijane jest
odbiegające jeszcze dalej od prototypowania błyskawicznego
podejście, zwane programowanie ekstremalnym.
Programowanie ekstremalne (eXtreme Programming - XP)
Ogólnie, podejście to, które pojawiło się w 1999 roku,
charakteryzuje:
• Stawianie programisty, a nie analityka systemowego i
projektanta, w centrum zainteresowania,
• Korzystanie z tzw. wzorców projektowych (nazwa jest
myląca, są to bowiem wzorce na poziomie kodu, a nie
projektu), przykłady wzorców projektowych: wzorzec
fasady, fabryka abstrakcyjna
• Uznanie kodu za dokumentację projektu,
57
• Ścisła
współpraca
programistów
z
przyszłym
użytkownikiem.
Podejście to ma wiele cech prototypowania błyskawicznego,
zatarła
się
jednak
granica
między
poszczególnymi
prototypami. Było to możliwe dzięki:
• Rozwojowi paradygmatu obiektowego, co wyraziło się
opracowaniem wielu bibliotek wzorców projektowych,
• Pojawieniu się nowych metodyk w zakresie architektury
oprogramowania, jak: refaktoryzacja, czy transformacje
architektury oprogramowania,
• Rozwój i wzrost znaczenia metodyk testowania, na
przykład
Test
-
Driven
Development(TDD)
–
programowanie sterowane testami,
Stosuje się tu, mniej lub bardziej świadomie, czteroetapowy
model sukcesu:
1. Określ cel, 2. Wykonaj działanie,
3. Odbierz informację zwrotną,
4. Skoryguj działanie tak, aby kolejny efekt był bliższy
sukcesowi.
Fasadę dla XP tworzą, również cztery, główne wartości:
1. Komunikacja w zespole (do stałej praktyki należy
programowanie w parach) i komunikacja z klientem,
2. Prostota (stałe utrzymywanie przejrzystości i spójności
projektu),
3. Informacja zwrotna (informacje te programiści uzyskują
zarówno od klienta, jak i na podstawie wyników
przeprowadzanych testów),
4. Odwaga w podejmowaniu i wdrażaniu kluczowych
decyzji, wynikająca z wysokiego profesjonalizmu, przy
58
pełnej świadomości odpowiedzialności za podjęte
decyzje.
Związki między klasami
Główne związki między klasami, widziane przez pryzmat
struktury programu (implementację):
1. dziedziczenie,
2. zawieranie
3. należenie, albo posiadanie
4. używanie,
5. związki zaprogramowane.
Związek zawierania
class X
{. . . . .
public:
X(int);
. . . . . };
class C1
{ X a; // klasyczne zawieranie
public:
C1(int i): a(i) { }
// niejawne wywołanie konstruktora klasy X
};
C1
X
59
Związek posiadania
obiekt C2
class C2
{ X *p;
// wskaźnik do obiektu posiadanego
obiekt typu X
public:
C2(int i): p( new X(i)) { }
// ten konstruktor kreuje i inicjuje obiekt posiadany
C2(X *q): p(q) { }
// ... a ten „dopina się” do obiektów istniejących,
// może się też dopinać do obiektów klas potomnych
~C2( ) {delete p;} // utrata obiektu posiadanego
X *udostępnij( ) { return p; }
X *zamien( X *q) { X *t = p; p = q; return t; }
};
Niech
class XX: public X { };
class XXX: public X { };
wtedy
void f( )
{
C2 *p1 = new C2( new X );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy X
C2 *p2 = new C2( new XX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XX
C2 *p3 = new C2( new XXX );
//obiekt dynamiczny klasy C2 „posiada” obiekt klasy XXX
}
p
60
Użycie referencji tworzy klasy operujące na obiektach klas X
bezpośrednio, bez konieczności używania wskaźników.
class C3
{
X &r; obiekt C3
public:
C3( X &q): r(q) { }; obiekt o nazwie q klasy X
. . . . . . . . . . . . . . . .
};
o
Związki posiadania z użyciem wskaźników i referencji
tworzą swoistą hierarchię obiektów a nie klas.
Związki używania
Cytaty: [ B. Stroustrup ]
1/
„Wiedza o tym, jakich klas używa dana klasa i w jaki sposób,
jest często decydująca do wyrażenia i zrozumienia
projektu.”
Związek, zwany na poziomie implementacji związkiem
użycia, jest na poziomie analizy obiektowej zwany związkiem
zależności.
RozkładZajęć
dodaj(p : Przedmiot)
usuń(p : Przedmiot)
Przedmiot
Iterator
<<friend>>
r
Zależność
Zależność
61
W tym przykładzie klasa RozkładZajęć używa klasy
Przedmiot
. Zmiany dokonane w specyfikacji klasy Przedmiot
mogą mieć wpływ na definicję klasy RozkładZajęć, ale nie na
odwrót.
Klasyfikacja sposobów, w jakich jedna klasa X może używać
innej klasy Y:
1. X używa nazwy klasy Y (jak w przykładzie powyżej),
2. X używa Y, ponieważ:
2.1. X czyta składową Y,
2.2. X zapisuje składową Y,
2.3. X wywołuje funkcje składową Y,
3. X tworzy obiekt klasy Y, tj. X przydziela pamięć dla
statycznego lub automatycznego obiektu klasy Y, lub
tworzy dynamiczny obiekt Y za pomocą operatora new,
4. X pobiera rozmiar Y
Związki zaprogramowane (inaczej ukryte)
Załóżmy,
że
w
projekcie
tworzonego
systemu
wyspecyfikowano, że każda operacja, która nie może być
obsłużona przez klasę A, powinna być obsłużona przez klasę
B, posiadaną przez klasę A.
class B
{ . . . . . .
void f( ); void g( ); void h( ); }
class A
{ B *p;
. . . . . .
void f( ); void ff( );
void g( ) { p→g( ); } // delegowanie g( )
void h( ) { p→h( ); } // delegowanie h( )
};
62
Związki zaprogramowane są głęboko ukryte w implementacji,
przez to mało widoczne a ich skutki są trudne do
przewidzenia.
Interfejsy i implementacje
Komponent jest fizyczną, wymienną, częścią systemu
informatycznego. Komponent, obok swojej implementacji,
wykorzystuje i realizuje pewien zbiór własnych interfejsów.
Interfejs komponentu jest zestawem operacji, zamkniętych w
klasie (lub klasach) interfejsowych, które to operacje
wyznaczają usługi oferowane przez komponent. Taki zestaw
usług określa tzw. szwy systemu.
Idealny interfejs:
• udostępnia wszystkie obowiązki komponentu „reszcie
świata” w sposób pełny i logicznie spójny,
• nie ujawnia użytkownikowi szczegółów implementacyj-
nych,
• jest wyrażony za pomocą typów z poziomu użytkownika,
• w ograniczony i dobrze zdefiniowany sposób zależy od
innych interfejsów.
// przykład interfejsu w złym stylu
class X {
Y a;
public:
void f( const char *, . . . );
// funkcja interfejsowa ze zmienną liczbą parametrów
implementacja
interfejsy
komponent
63
void ustaw_a( Y& );
// funkcja interfejsowa z parametrem w postaci referencji do
nieznanej klasy
Y& wez_a( );
// typ wyniku funkcji interfejsowej w postaci referencji do
nieznanej klasy
};
Zawarte w klasie interfejsowej powyższego przykładu metody
realizują interfejs na bardzo niskim poziomie abstrakcji,
ujawniając szczegóły implementacji. Nie są samoopisujące
się.
Zasady praktyczne projektowania
Motto:
„Nie
ma
jednej
„właściwej”
metody
projektowania.
Projektowanie
wymaga
wyczucia,
doświadczenia
i
inteligencji.”
1. Użyj publicznego dziedziczenia do reprezentowania relacji
bycia.
Jeśli class Y: public X { ... }; to klasa Y jest swego
rodzaju klasą X.
2. Użyj wskaźników, lub referencji, do reprezentowania relacji
posiadania.
3. Upewnij się, że zależności używania są zrozumiałe,
minimalne i niecykliczne.
4. Wyrażaj interfejsy w kategoriach typów z dziedziny
zastosowań.
64
Rozkład zobowiązań w modelowanym systemie
Ilościowy rozkład zobowiązań dla poszczególnych klas
powinien w modelowanym systemie być taki, aby
poszczególne klasy obejmowały porównywalne względem
siebie ilości atrybutów i usług. Poniższy rysunek przedstawia
poglądowo jako niedopuszczalne modele 1 i 3.
MODEL 1 – każde dana jest klasą
( klasy są jednopolowe )
MODEL 2 – model opcjonalny (ze zrównoważonym
rozkładem zobowiązań)
MODEL 3 – model z jedną tylko klasą
= model wg. paradygmatu strukturalnego
Modele 1 i 3 są nie do przyjęcia.
Aktualne
podejście
do
tworzenia
oprogramowania
charakteryzuje:
- zacieranie
podziałów
między
etapami
analizy,
projektowania i implementacji,
- rozdział
w
metodyce
tworzenia
systemów
informatycznych
korporacyjnych
(opartych
na
platformach typu: Windows, Unix, OS )i systemów
WEB-owych (opartych o Internet),
MODEL 1
MODEL 2
MODEL3
65
- ciągle wzrastający udział i specjalizacja gotowych
komponentów w procesie tworzenia oprogramowania,
- stosowanie wzorców projektowych.
Funkcje operatorowe
( czyli przeciążanie operatorów)
Dlaczego zmuszeni jesteśmy pisać funkcje operatorowe ?
• Aby rozszerzać liczbę operatorów działających na znanych
typach. Np. typowi int towarzyszą obsługujące go operatory
+ - * /. Każdy z tych operatorów możemy jednak
przeciążyć, nadając im nowe, dodatkowe znaczenia.
• Aby definiować nowe operatory dla nowych klas, takich
jak: liczby zespolone, obiekty algebry wektorów, macierzy i
wyznaczników, napisy, sygnały, itd.
Zadeklarujmy w klasie COMPLEX zaprzyjaźnienie z nią
dwóch funkcji operatorowych, implementujących wybrane
operacje na liczbach zespolonych.
class COMPLEX
{ double re, im;
public:
COMPLEX( double r, double i ): re(r), im(i) {}
friend COMPLEX operator+( COMPLEX, COMPLEX);
friend COMPLEX operator*( COMPLEX, COMPLEX);
};
Przykłady użycia:
COMPLEX a(1,2), b(0,1), c(0,0);
a = b + c;
// użyto operatora + dla typu COMPLEX, wymagana będzie
jednak definicja operatora przypisania =
66
b = b + c * a; // użyto operatorów „+” i „* ”
// zapis jest równoważny b = b + (c * a);
// bowiem przeciążone operatory zachowują swoją
// składnię ( priorytety i wiązania )
c = a * b + COMPLEX(10,20);
// w wyrażeniu wystąpił obiekt tymczasowy
// (część pogrubiona wyrażenia)
COMPLEX d = operator+( a ,b );
// jawne użycie funkcji operatorowej
// zapis jest równoważny COMPLEX d = a + b ;
Ogólne zasady przeciążania operatorów:
1. można przeciążać wszystkie operatory, za wyjątkiem
. .* :: ? :
2. nie można zmieniać składni operatorów, w szczególności:
priorytetów i wiązań, zamieniać operator unarny na
binarny, i odwrotnie, np. definiować binarny operator !
3. nie można wprowadzać nowych symboli operatorów,
np. operatora ** dla potęgowania (bo brak w C++ składni
dla takich operatorów).
Ogólnie funkcja operatora może być:
1. funkcja składową klasy, na obiektach której ma działać
operator,
2. funkcją
nie
będącą
składową
klasy
(funkcją
zaprzyjaźnioną).
Przeciążanie operatorów jednoargumentowych (unarnych)
Operator unarny można zdefiniować jako:
1. bezparametrową funkcję składową klasy,
2. funkcję o jednym argumencie, zaprzyjaźnioną z klasą.
67
Na przykład użycie ++ x będzie oznaczać wywołanie:
dla przypadku 1. - x . operator++( );
dla przypadku 2. - operator++( x );
Przeciążanie operatorów dwuargumentowych (binarnych)
Można je definiować jako:
1. jednoparametrowe, funkcje składowe klasy (podlegają
dziedziczeniu !), wówczas x @ y (gdzie @ jest symbolem
operatora) odpowiada wywołaniu funkcji operatorowej
x . operator@( y )
2. dwuparametrowe funkcje zaprzyjaźnioną z klasą, wówczas
x @ y
odpowiada
wywołaniu
funkcji
operatorowej
operator@( x, y )
Przykłady definicji funkcji operatorów unarnych:
• jako funkcji składowych klasy
COMPLEX COMPLEX:: operator−( )
{ re = − re; im = − im;
return * this;
}
• jako funkcji zaprzyjaźnionej z klasą
COMPLEX operator−( COMPLEX &c )
{ return COMPLEX(−c.re, −c.im ; }
68
Przykłady przeciążania operatorów binarnych:
class WEKTOR
{
double x, y;
public:
WEKTOR( ): x(0), y(0) { }
WEKTOR( double px, double py): x(px), y(py) { }
WEKTOR operator+ ( WEKTOR & );
WEKTOR operator+=( WEKTOR & );
friend WEKTOR operator−(WEKTOR &,WEKTOR &);
friend WEKTOR operator*(double, WEKTOR &);
friend ostream & operator<<( ostream &,WEKTOR &);
};
inline WEKTOR WEKTOR:: operator+
( WEKTOR & U );
{ return WEKTOR( this→x+U.x, this→y+U.y ); }
inline WEKTOR operator−( WEKTOR & U,
WEKTOR & V);
{ return WEKTOR( U.x−V.x, U.y−V.y ); }
inline WEKTOR operator*( double k, WEKTOR & U);
{ return WEKTOR( k * U.x, k * U.y ); }
inline ostream & operator<<( ostream & st,
WEKTOR & U);
{ st << ‘[‘ << U.x << ‘,’ << U.y << ‘]’ ; return st; }
69
Przykłady użycia:
WEKTOR A( 1, 1 ), B( 5, 5 ), C( -3, 3 );
cout << ‘\n’ << A + B << ‘\n’ << A − B << ‘\n’ <<
2 * C + A;
Otrzymane wyniki:
[ 6, 6 ]
[ - 4, - 4 ]
[ - 5, 7 ]
Przeciążanie złożonych operatorów przypisania op=
Złożone operatory przypisania to operatory typu: +=, *=, <<=.
W języku C++ zdefiniowano 11 takich operatorów. Pozwalają
one zapisać skrótowo wyrażenia, w których zmienne stojące
po lewej stronie operatora przypisania, występują również w
wyrażeniach stojących po prawej stronie tego operatora. Na
przykład zamiast x = x + y możemy zapisać x += y.
Ograniczenia w przeciążaniu operatorów przypisania:
• można używać wyłącznie funkcji składowych,
• operatorów przypisania nie można dziedziczyć.
Poniżej
przykład
przeciążenia
złożonego
operatora
przypisania dla klasy WEKTOR za pomocą funkcji składowej:
WEKTOR & WEKTOR:: operator+=( WEKTOR & U )
{ this→x += U.x; this→y += U.y; return * this;
}
Jest to sytuacja, w której użycie zmiennej this jest istotnie
konieczne – aby przekazać wektor po wykonaniu operacji
polegającej na jego zwiększeniu o wektor U.
70
Przykład użycia tak zdefiniowanego operatora C += A + B;
Zapis ten równoważny jest wywołaniu funkcji operatorowych
C.operator += ( A.operator+ (B) );
W wyniku działania operatora + z wyrażenia A + B
tworzony jest obiekt tymczasowy (użyty za chwilę jako
drugi argument operatora += po poddaniu go konwersji
trywialnej do WEKTOR &).
Obiekt tymczasowy jest po użyciu usuwany, chociaż można
tak zapisać funkcję operatorową, aby nie był usuwany.
Dzięki użyciu referencji w wyniku funkcji, pierwszy
argument operatora przypisania może być l-wartością i stać
po lewej stronie operatora przypisania.
Przeciążanie operatora indeksowania [ ]
Przystępując do studiowania tego rozdziału musimy mieć
świadomość, że wydobywając i-ty element tablicy t poprzez
zapis
t[i]
posługujemy
się
w
gruncie
rzeczy
dwuargumentowym operatorem indeksowania w sposób
następujący t[]i , gdzie lewym argumentem operatora
indeksowania jest nazwa tablicy, a prawym – położenie
interesującego nas jej elementu. Ponieważ wyrażenie t[i]
powinno być l-wyrażeniem (aby można było do niego
podstawiać wartości) deklaracja funkcji operatorowej dla
tablicy przechowującej liczby całkowite powinna mieć postać
int & operator[](int i);
Funkcje tę należy zgłosić w klasie, która obsługuje jakąś
kolekcję danych typu int, np. tablicę danych typu int.
W poniższym przykładzie rozważymy klasę STRING
posiadającą i obsługującą łańcuch znakowy
71
class STRING
{
char *str;
public:
STRING( void ): str( NULL ) { }
// poniżej konstruktor kopiujący
STRING( char *s ): str( strdup( s )) { }
// poniżej sprzątający po sobie destruktor
∼STRING( void ) { delete str; }
// poniżej definicja przeciążonego operatora indeksowania
char & operator[ ]( int index )
{ return *( str + index ); }
// poniżej definicja przeciążonego
// operatora wyprowadzenia do strumienia
friend ostream & operator<<( ostream & st, STRING & s)
{ return (st << s.str) ; }
Przykłady użycia obu operatorów:
STRING s( ”Adam” );
s[1] = ‘l’ ; cout << “ “<< s[0];
// powyżej wykorzystano przeciążony operator
// indeksowania dla obiektu s klasy STRING
cout << “ “ << s ;
// powyżej wykorzystano przeciążony operator
// wyprowadzenia do strumienia obiektu s klasy STRING
Wynik: A Alam
72
Tablica asocjacyjna
(czyli wykorzystanie operatora [ ] )
W poprzednim przykładzie do indeksowania tablicy użyto
danych typu int, ale można tu użyć w zasadzie dowolnego
typu ( sic ! ).
Tablica
asocjacyjna
(zwana
też
słownikiem
lub
odwzorowaniem) przechowuje pary wartości pozwalając
dotrzeć poprzez obiekt zwany kluczem do obiektu zwanego
wartością.
Przykład prostej tablicy asocjacyjnej:
#include <string>
using namespace std; klucz wartość
class TAB_ASOC 2
{
struct PARA 1
{
string klucz; 0
int wartosc;
} * tab;
int max;
int wolny;
public:
TAB_ASOC( int ); // konstruktor
int & operator[ ]( const string ); (1)
// operator indeksowania [ ] zwraca referencję do
// drugiej części pary
};
Wojtek
24
tab
wolny=1
max=3
73
Zdefiniowana powyżej tablica asocjacyjna przechowuje pary,
w których (służące do indeksowania) pole klucz, jest
dowolnym napisem, a wartością jest dana typu int.
// definicja konstruktora
TAB_ASOC:: TAB_ASOC( int r )
{ max = (r < 16 ) ? 16 : r ; wolny = 0 ;
wektor = new PARA[ max ];
}
Definicje operatora indeksowania tablic asocjacyjnych są
zwykle rozbudowane i obdarzone „pewną inteligencją”.
Potrafią zwykle:
- utrzymywać niezbędny rozmiar wektora par,
- wyszukiwać i przekazywać referencję do wyszukanej
drugiej części pary,
- dodawać do wektora nową parę, jeśli nie została ona tam
jeszcze dotychczas umieszczona.
Przykład użycia:
TAB_ASOC * zlicz_slowa( void )
{ const MAX = 256; // maksymalna długość słowa
char buf [MAX] ; // bufor pojedynczego słowa
TAB_ASOC wek( 512 );
while( cin >> buf ) wek[ buf ] ++ ;
return wek ;
}
74
Powyższa przykładowa funkcja zlicz_slowa( ) po wywołaniu:
1.
tworzy tablicę asocjacyjną o rozmiarze początkowym
512 par,
2. pobiera z wejścia do bufora słowa buf pojedyncze słowa
i wstawia je do tablicy asocjacyjnej wraz z wartością 1
(jeśli słowo nie zostało tam jeszcze umieszczone), lub
zwiększa o 1 drugą część pary (wartość) jeżeli słowo
zostało w tablicy asocjacyjnej umieszczone wcześniej,
3. zwiększa w miarę potrzeb rozmiar tablicy asocjacyjnej,
4. po dojściu do końca czytanego tekstu funkcja kończy
swoje działanie zwracając wskazanie do utworzonej
przez siebie tablicy asocjacyjnej.
Wykonywanie pk. 2 i 3 odbywa się w poleceniu wek[ buf ]++
i jest możliwe dzięki „zaszyciu” powyższej funkcjonalności w
funkcji operatorowej implementującej działanie operatora
indeksowania [ ].
Tego typu podejście jest już realizacją jednego z
paradygmatów programowania uogólnionego, które zakłada
jednolitą dla różnych struktur danych, metodę obsługi poleceń
(w tym przypadku polecenia wyszukiwania).
Iteratory
(czyli przeciążenie operatora wywołania funkcji ( )
Zadaniem iteratora jest zawsze dostarczanie obiektów w
określonym porządku. Aby iterator miał dostęp do
składowych struktury, którą iteruje, definiujemy go jako klasę
zaprzyjaźnioną z tą strukturą.
75
Zdefiniujemy przykładowo klasę iteratora dla tablicy
asocjacyjnej:
class ITERATR_ASOC
{
const TAB_ASOC * ta ; // wskazanie iterowanej tabl. asocj.
int ind; // indeks bieżący w tablicy asocjacyjnej * ta
public:
ITERATOR_ASOC( const TAB_ASOC & s )
{ ta = & s; ind = 0 ; } // konstruktor
PARA * operator( ) ( void ) // funkcja iteratora
} ;
W definicji klasy TAB_ASOC należy umieścić deklarację
zaprzyjaźnienia w postaci:
friend class ITERATOR_ASOC ;
Operator wywołania funkcji ( ) jest zwykle używany w
sposób, który ilustruje wywołanie sin(x), gdzie sin jest nazwą
funkcji, a x jej argumentem. Nic nie stoi na przeszkodzie aby
powyższe wyrażenie zapisać sin( )x i spróbować przeciążać
ten operator tak, jak to zrobiono w klasie ITERATOR_ASOC.
W klasie ITERATOR_ASOC zdefiniowaliśmy funkcje
iteratora w postaci funkcji operatorowej, wywoływanej na
rzecz obiektu klasy ITERATOR_ASOC (czyli iteratora) z
pustą listą argumentów. Funkcja ta dostarcza wskazanie
aktualnego elementu tablicy asocjacyjnej (pary). Jest to
możliwe, ponieważ obiekt klasy iteratora posiada swoją
prywatną daną (ind ), która posiada to aktualne wskazanie w
iterowanym obiekcie klasy TAB_ASOC.
76
Iterator ( tj. obiekt klasy iteratora ) inicjuje się w momencie
jego deklaracji. Natomiast każde użycie metody w postaci
funkcji operatorowej operator( ) zwraca zwykle wskazanie do
aktualnego
elementu
iterowanej
tablicy
asocjacyjnej,
(inkrementując jednocześnie indeks ind), lub 0 - po dojściu do
zajętego obszaru tablicy asocjacyjnej.
Przykłady użycia:
TAB_ASOC wek( 512 );
// deklaracja tablicy asocjacyjnej
ITERATOR_ASOC następny( wek );
// utworzenie obiektu klasy iteratora „przypiętego” do
// tablicy asocjacyjnej wek
TAB_ASOC:: PARA * p ;
// deklaracja wskazania pary jako zmiennej pomocniczej
while ( p = następny( ) )
cout << p → nazwa << ” : ” << p → wartosc << ‘\n’ ;
// następny jako nazwa obiektu klasy iteratora występuje
// w roli lewego argumentu operatora ( ),
// prawy argument nie występuje.
W
powyższym
przykładzie
każdorazowe
wywołanie
następny( ) zwraca wskazanie kolejnej pary w iterowanej
tablicy asocjacyjnej, co umożliwia wydrukowanie w pętli obu
składowych pary. Po dojściu do końca zajętego obszaru
tablicy asocjacyjnej zwrócona zostanie wartość 0 i działanie
pętli zakończy się.
o
Można jednocześnie aktywować wiele iteratorów tego
samego typu,
o
Można zdefiniować również inne iteratory, np.
pierwszy( ), ostatni( ), seek( parametr ). Muszą to być
jednak klasy iteratory a nie tylko funkcje.
Koniec części IV wykładu