Paradygmaty obiektowe języka a paradygmaty obiektowe programowania
To nie to samo!
Można programować w języku nie obiektowym przestrzegając paradygmatów programowania zorientowanego obiektowo.
Można programować w języku obiektowym, wykorzystując obiektowe konstrukcje tego języka, które, zgodnie z paradygmatami obiektowych języków programowania, on posiada, istotnie naruszając paradygmaty programowania zorientowanego obiektowo.
Paradygmaty obiektowe języka programowania
Ma oferować konstrukcje wspierające paradygmaty
programowania zorientowanego obiektowo
Klasy (opakowanie danych i operacji na nich w abstrakcyjny typ danych)
Dziedziczenie
Możliwość dynamicznego wiązania wywołań metod z ich definicjami
Sterowanie prawami dostępu
Dziedziczenie pojedyncze i wielokrotne
Metody i klasy abstrakcyjne, interfejsy
Polimorfizm i dynamiczne wiązania, polimorfizm parametryczny (typy generyczne)
Sprawdzanie zgodności typów i polimorfizm, w jakim zakresie dynamiczne sprawdzanie typów: narzędzia decydowania przez programistę o statyczności i dynamiczności obliczania typów definiowanych wielkości
Alokacja i dealokacja obiektów
Na poziomie klasy (new, delete)
Na poziomie obiektu - konstrukcje języka ułatwiające dealokację całej
struktury powiązanych obiektow.
Paradygmaty programowania zorientowanego obiektowo
1. Identyfikacja klas i podział odpowiedzialności.
Adekwatnie modeluj przyjętymi klasami poziom konceptualny.
Hierarchia dziedziczenia ma właściwie oddać związki typu ogólniejszy - szczególny między pojęciami poziomu konceptualnego.
2. Programowanie na poziomie abstrakcyjnym (na poziomie interfejsu)
Adekwatnie podziel system na poziomy abstrakcji.
2.1 Programowanie na poziomie abstrakcyjnym (albo na poziomie interfejsu) ma odwzorować logikę poziomu analizy modelu (inaczej poziomu konceptualnego).
2.2 Pojęcia poziomu programowania abstrakcyjnego dobrane są właściwie, gdy
Po pierwsze, odwzorowują logikę poziomu konceptualnego.
Po drugie, architektura poziomu abstrakcyjnego nie wymusza żadnych decyzji implementacyjnych.
2.3 Programowanie na poziomie abstrakcyjnym ma ułatwiać prototypowanie analizowanego systemu. Prototyp jest tymczasową implementacją poziomu abstrakcyjnego, której używa się tylko do eksperymentalnego sprawdzenia, czy założona architektura poziomu abstrakcyjnego we właściwy sposób oddaje zamierzoną przez nas logikę poziomu konceptualnego.
3. Odwołania do obiektów poprzez modelujący kontrakt klasy interfejs (a nie bezpośrednio) poprawiają abstrakcję systemu, a co za tym idzie, elastyczność kodu.
4. Tworzone obiekty nie powinny być „ułomne”
Programuj sparametryzowane konstruktory tak, by tworzone obiekty zawierały pełną początkową informację.
5. Hermetyzacja
Ukrywanie danych i metod przed niepożądanym dostępem
Ukrywanie obszaru zmienności projektu (tzn. każdej decyzji projektowej, która może ulec zmianie (interfejsu, implementacji, zachowania metod, danych, struktury hierarchii dziedziczenia)) za pośredniczącymi interfejsami.
6. Prawidłowo zaprojektowana klasa jest spójna
Spójność obiektu jest miarą współpracy metod i pól klasy w celu wypełnienia nałożonej na nią odpowiedzialności (kontraktu). Klasa jest spójna, gdy jeżeli jej metody odwołują się w większości do pól tej samej klasy i innych jej metod. Brak spójności występuje, gdy metody klasy odwołują się do zewnętrznych obiektów lub wykonują niezwiązane ze sobą zadania.
Wysoka spójność obiektu jest wartością pożądaną, ponieważ jest dodatnio skorelowana z wieloma zewnętrznymi cechami jakości: pielęgnowalnością, czytelnością, testowalnością. Niska spójność prowadzi do ograniczenia ponownego użycia klasy oraz jej czytelności, a także zwiększenie pracochłonności testowania.
7. Stopień zależności między klasami powinien być możliwie niski
Klasa A jest zależna od klasy B, jeżeli posiada o niej jakąś wiedzę:
Posiada składową typu B lub
Implementuje typ B lub
Definiuje metodę, która zawiera klasę B w swojej sygnaturze („używa w interfejsie”) jako parametr, zwracaną wartość lub deklarowany wyjątek lub
Używa obiektów klasy B w implementacji swoich metod („używa w implementacji”)
Słabe powiązanie oznacza, że analizowana klasa w niewielkim stopniu zależy od zmian w innych klasach. Mają na to wpływ dwa czynniki: liczba powiązań oraz ich rodzaj. Siła powiązania jest odwrotnie proporcjonalna do abstrakcyjności klasy, tej zależnej: zależność od niezmiennego (stabilnego) interfejsu jest mniejsza niż zależność od szczegółowej klasy implementacyjnej.
Wysoki stopień powiązań ma negatywny wpływ na wiele zewnętrznych czynników jakości programu, m. in. jego pielęgnowalność, elastyczność i stopień abstrakcji. Dobry projekt obiektowy powinien minimalizować liczbę powiązań między klasami i interfejsami, i jednocześnie preferować zależności od stabilnych interfejsów.
Zależność między stopniem powiązań miedzy klasami a spójnością klas: na ogół wysoka spójność oznacza także niski stopień powiązań, a brak spójności stopień ten zwiększa.
Kilka uwag o dziedziczeniu
Dziedziczenie powoduje przeniesienie z klasy bazowej do klasy pochodnej dwóch elementów: typu (jeżeli nie ma w klasie bazowej wielkości prywatnych) i implementacji. Pozwala to na ponowne wykorzystanie kodu z klasy bazowej, ale ściśle wiąże ze sobą te klasy.
Implementacja interfejsu pełni tylko jedną z tych ról: przeniesienie typu. Dzięki temu klasy implementujące ten sam interfejs mogą być zróżnicowane w znacznie większym stopniu, niż podklasy tej samej klasy bazowej.
Stosowanie interfejsów jest na ogół lepszym rozwiązaniem, ponieważ tworzy słabsze powiązania miedzy klasami.
Dziedziczenie pozwala współdzielić kod między klasy pochodne, co w niektórych sytuacjach jest uzasadnione.
Obiekty klasy Punkt reprezentują punkty płaszczyzny poprzez udostępnianie współrzędnych punktu. Konstruktor klasy Punkt generuje obiekt z podanych współrzędnych.
Klient chce używać interfejsu Figura z metodą rysuj( ). Konkretnymi figurami mogą być albo Prostokąt albo Koło. Każdy obiekt klasy Prostokąt ma dwa atrybuty klasy Punkt reprezentujące przeciwległe punkty narożne prostokąta. Każdy obiekt klasy Koło ma atrybut klasy Punkt reprezentujący środek koła i atrybut typu float reprezentujący promień koła. Metoda rysuj( ) każdej figury konkretnej może być zaimplementowana przy pomocy metod oferowanych przez jedną z dwóch klas - bibliotek: WLib oraz XLib.
Wśród metod oferowanych przez klasę WLib są
• NarysujLinie(Punkt p, Punkt q) // narysowanie linii od punktu p do punktu q
• NarysujKoło(Punkt p, float r) // narysowanie koła o środku p i promieniu r.
Wśród metod oferowanych przez klasę XLib są
• rysuj_l(Punkt p, Punkt q) // narysowanie linii od punktu p do punktu q
• rysuj_k(Punkt p, float r) // narysowanie koła o środku p i promieniu r.
Z punktu widzenia klienta nie są ważne (są nieistotne) informacje
• do jakiej konkretnej figury klient się odwołuje,
• jakie metody i której biblioteki są używane do zaimplementowania metody rysuj( ).
Z punktu widzenia klienta istotne jest tylko to, że klient odwołuje się do obiektu klasy Figura i może wywołać metodę rysuj( ) tego obiektu.
Zaprojektować schemat rozwiązania tego zadania - tzw. diagram klas.
Katalog czytelników biblioteki przechowuje karty czytelników. Każda karta jest albo typu „młodzieżowa” albo typu „zwykła” i przechowuje dane czytelnika, w tym informacje o wypożyczeniach czytelnika.
Katalog czytelników ma oferować możliwości dodawania i usuwania karty z katalogu.
Katalog książek biblioteki przechowuje karty zawierające dane o książkach biblioteki, w tym kategorię tematyczną książki i jej numer identyfikacyjny.
Katalog książek ma oferować możliwości dodawania i usuwania karty książki z katalogu i podawać informacje o liczbie wypożyczeń książek danej kategorii tematycznej.
Zaproponować zestaw klas i funkcję główną testującą proponowany zestaw klas.
enum KategoriaTemat {historia, poezja, fantastyka};
class KartaKsiazki
{ public:
KategoriaTemat kat; //no i dużo innych danych o książce
};
class Wypozyczenie
{ public:
KartaKsiazki kartaks; //zamierzony błąd projektowy: kartaks powinna byc wskaźnikiem!
// dlaczego? W każdym razie w C++ to jest błąd projektowy.
int okres;
};
template <class T> class zbior
{ public:
virtual void zrob_pusty()=0;
virtual void wstaw(T)=0;
virtual void usun(T)=0;
virtual T pierwszy()=0;
virtual bool ostatni(T)=0;
virtual T nast(T)=0;
virtual T biezacy()=0;
};
// implementacja konkretna (ale nie instancja!) tego wzorca jest softwarem wielokrotnego
// użycia - dlaczego?
class KartaCzytelnika
{ public:
int nr_czyt;
int oplata;
zbior<Wypozyczenie>* Wypozyczenia;
};
class Zwykla : public KartaCzytelnika
{ public:
int limit;
int cena;
};
class Mlodziezowa : public KartaCzytelnika
{ public:
int limit;
int upust;
};
// z polami limit coś logicznie nie w porządku - dlaczego?
class KatalogCzytelnikow
{ public:
zbior<KartaCzytelnika>* katalogc;
void dodaj(KartaCzytelnika k)
{katalogc->wstaw(k);};
void usun(KartaCzytelnika k)
{katalogc->usun(k);};
KatalogCzytelnikow(){katalogc->zrob_pusty();};
};
class KatalogKsiazek
{ public:
KatalogCzytelnikow* KCz;
zbior<KartaKsiazki>* katalogk;
void dodaj(KartaKsiazki k)
{katalogk->wstaw(k);};
void ususn(KartaKsiazki k)
{katalogk->usun(k);};
KatalogKsiazek(){katalogk->zrob_pusty();};
int liczba_wypoz(KategoriaTemat kategoria)
{ KartaCzytelnika k; int licznik=0; Wypozyczenie w;
for(k=(KCz->katalogc)->pierwszy(); (KCz->katalogc)->ostatni(k)==false;
k=(KCz->katalogc)->nast(k))
{ for(w=k.Wypozyczenia->pierwszy();
k.Wypozyczenia->ostatni(w)==false; w=k.Wypozyczenia->nast(w))
if(w.kartaks.kat==kategoria)licznik++;
};
return licznik;
};
};
//zamierzony błąd projektowy: źle rozłożone odpowiedzialności. Odpowiedzialność za
//dostarczenie informacji o liczbie wypożyczeń książek danej kategorii tematycznej powinna //być nałożona na klasę KatalogCzytelników! Dlaczego?
//ten błąd ma źródło w sformułowaniu specyfikacji zadania w języku naturalnym
//teraz poziom programowania konkretnego, a mianowicie
// po pierwsze wzorzec klasy konkretnej elementu listy
// po drugie, wzorzec klasy konkretnej implementacji listowej zbioru elementow typu T
int main()
{ // testy na zaproponowany zestaw klas
cout<<endl;
system("PAUSE");
return 0;
}
Odpowiedzi:
1. Zamierzony błąd projektowy: źle rozłożone odpowiedzialności. Odpowiedzialność za
dostarczenie informacji o liczbie wypożyczeń książek danej kategorii tematycznej powinna być nałożona na klasę KatalogCzytelników! Dlaczego?
Bowiem metoda liczba_wypoz(KategoriaTemat kategoria) zależy tylko od informacji zawartej w klasie KatalogCzytelników. Tak jak jest, to jest dodatkowa zależność klasy KatalogKsiążek od klasy KatalogCzytelników. Zwróć uwagę, że specyfikacja zadania w języku naturalnym jest błędna i źle rozdziela odpowiedzialności między klasy. Już na poziomie konceptualnym można popełnić błędy.
2. Z polami limit coś logicznie nie w porządku - dlaczego?
Wspólne dane dla podklas powinny być zamieszczane w klasie bazowej.