Obiektowe interakcje

W poprzednim tekście poświęconym programowaniu obiektowemu przyjrzeliśmy się klasom i ich wewnętrznej strukturze. Nauczyliśmy się też kontrolować powstawanie i usuwanie obiektów. W tym artykule, analizując możliwe relacje i zasady współpracy obiektów, poznamy najważniejsze pojęcia programowania obiektowego: kompozycję, dziedziczenie, polimorfizm i abstrakcję.

W poprzednim tekście poświęconym programowaniu obiektowemu przyjrzeliśmy się klasom i ich wewnętrznej strukturze. Nauczyliśmy się też kontrolować powstawanie i usuwanie obiektów. W tym artykule, analizując możliwe relacje i zasady współpracy obiektów, poznamy najważniejsze pojęcia programowania obiektowego: kompozycję, dziedziczenie, polimorfizm i abstrakcję.

Pomiędzy klasami mogą zachodzić dwa rodzaje relacji: jedna z nich to relacja jest, druga to relacja ma. Mimo że w programowaniu obiektowym pierwszy z tych typów wykorzystywany jest znacznie częściej, to drugi jest bardziej intuicyjny i łatwiejszy do zaimplementowania, lecz - nie wiadomo dlaczego - niedoceniany. Zauważmy, że między klasami faktura i kontrahent zachodzi właśnie ten drugi typ relacji: ma.

Kontrahent musi być umieszczony na fakturze, a zatem można powiedzieć, że faktura posiada (zawiera) kontrahenta lub - korzystając z terminologii obiektowej - faktura ma kontrahenta. Zatem mamy tutaj następującą relację: istnieje obiekt zewnętrzny, w którym znajduje się obiekt wewnętrzny. Magnetofon ma silnik, długopis ma wkład, człowiek ma nazwisko - w świecie rzeczywistym występuje mnóstwo takich przykładów. W programowaniu obiektowym umieszczanie (zawieranie) jednych obiektów w drugich nazywamy obudowywaniem lub kompozycją.

Kompozycja

Należy trochę zmienić kod klasy faktura, umieszczając w jej sekcji prywatnej deklarację obiektu klasy kontrahent:

class faktura

{

private:

...

kontrahent kontr;

public:

...

};

Dlaczego obiekt klasy kontrahent znalazł się w części prywatnej? Jest to wewnętrzny obiekt klasy faktura, a zatem niedostępny dla kodu zewnętrznego (podobnie jak silnik w magnetofonie nie jest dostępny na zewnątrz obudowy). Zwróćmy uwagę, że składowe prywatne klasy kontrahent nie są dostępne nawet dla klasy faktura; jedynym sposobem, w jaki ta druga klasa może komunikować się z pierwszą, jest publiczny interfejs klasy kontrahent (klasa zewnętrzna ma dostęp do interfejsu klasy wewnętrznej, natomiast nie ma dostępu do jej implementacji).

Jak pamiętamy, w klasie kontrahent zdefiniowaliśmy konstruktor, za pomocą którego można było przypisać kontrahentowi nazwę i numer NIP. Powyższa deklaracja nie wywołuje tego konstruktora (wywołanie takie nie jest w tym miejscu możliwe, gdyż programista nie może przekazać danych kontrahenta do sekcji private klasy faktura). Z drugiej strony można przypuszczać, że obiekt kontrahent powinien zostać zainicjalizowany wcześniej niż obiekt faktura (gdyż ten drugi mógłby na przykład odwoływać się w swoim konstruktorze do pierwszego). Zakładamy, że wywołanie konstruktora klasy kontrahent powinno się odbyć bezpośrednio przed uruchomieniem kodu konstruktora klasy faktura. Język C++ udostępnia w tym celu specjalną strukturę, zwaną listą inicjatorów konstruktora. Są to umieszczone po deklaracji konstruktora obiektu zewnętrznego wywołania konstruktorów obiektów wewnętrznych. Popatrzmy zatem, jak przedstawia się prawidłowa postać konstruktora klasy faktura:

faktura(int cena, int ilosc, int vat, char * nazwa, char * nip) : kontr(nazwa, nip) {

...

};

Wywołując taki konstruktor w kodzie funkcji main(), przekażemy do wewnętrznego obiektu klasy kontrahent nazwę oraz NIP kontrahenta:

main()

{

...

faktura fakt(6.84,10,22,"TRANSKOM","558-125-75-35");

...

}

Możemy to jeszcze uprościć. Ponieważ dane potrzebne do wyliczenia wartości brutto faktury będą przykazywane za pomocą argumentów metody dane_do_faktury (zob. deklaracja klasy faktura), dlatego należy usunąć te informacje z konstruktora. Oto jego nowa postać:

faktura(char* nazwa,char* nip):kontr(nazwa,nip) {};

Ciało przedstawionego konstruktora jest puste, gdyż jego jedynym zadaniem jest w tej chwili wywołanie konstruktora klasy kontrahent. Skoro konstruktor klasy faktura nie wprowadza zmian w środowisku działania programu, wobec tego możemy zrezygnować z destruktora tej klasy, usuwając z kodu jego deklarację oraz definicję. Ponadto w klasie faktura drobnej zmianie ulegnie metoda wyswietl(). Oprócz danych faktury będzie wyświetlała dane kontrahenta:

void faktura::wyswietl()

{

kontr.wyswietl();

cout << "Cena: " << k_cena << "\n"

<< "Ilosc: " << k_ilosc << "\n"

<< "VAT: " << k_vat << "\n"

<< "Wartosc: " << k_wartosc << "\n";

}

Jak widać, dzięki wykorzystaniu obiektu wewnętrznego, następuje wywołanie metody wyswietl() klasy kontrahent. Nie odwołujemy się bezpośrednio do pól klasy kontrahent, przechowujących nazwę i NIP, gdyż jest to niemożliwe ze względu na miejsce ich deklaracji (sekcja private w klasie kontrahent); korzystamy natomiast z interfejsu tej klasy, który zapewnia pośredni dostęp do jej implementacji.

Nasz kod jest gotowy do wykorzystania. Wystarczy powołać obiekt klasy faktura:

faktura fakt("TRANSKOM","558-125-75-35");

wywołać jego metodę, pozwalającą na wprowadzenie danych do faktury:

fakt.dane_do_faktury(6.84,10,22);

i wyświetlić rezultaty obliczeń:

fakt.wyswietl();

Wszystko działa, jak trzeba, jednak dociekliwy programista powinien teraz zadać parę pytań: "Czy jest jakiś prosty sposób na rozbudowę takiego programu? Czy jeśli pojawi się nowy wzór faktury, to trzeba zmieniać kod metody wyswietl() klasy faktura? Jeśli tak, to co zrobić w sytuacji, gdy klasa ta zostanie dołączona w postaci biblioteki wykorzystywanej również przez inne programy?". Odpowiedź jest jedna: w kodzie nic nie trzeba zmieniać. Z pomocą przychodzi drugi typ relacji, jakie są pomiędzy klasami, a mianowicie relacja jest. Na jej podstawie realizowane jest jedno z kluczowych zagadnień programowania obiektowego: dziedziczenie.

Dziedziczenie

Załóżmy, że dostajemy polecenie takiego zmodyfikowania programu, aby możliwe było drukowanie trzech rodzajów faktur: zwykłej bez nagłówka (takiej jak dotychczas), z logo firmy A oraz z logo firmy B. Nie chcemy nic zmieniać w istniejącym kodzie, a zatem jedynym rozwiązaniem jest dopisanie dodatkowego kodu i połączenie go w jakiś sposób z klasą faktura. Skoro należy drukować dwie dodatkowe, lecz inne faktury, a zatem - myśląc obiektowo - musimy utworzyć dwie dodatkowe klasy faktura. Jednak większość metod i pól tych dwóch klas będzie taka sama, jak te w klasie faktura. Różnice będą występowały jedynie w metodzie wyswietl(), drukującej różne logo firm. W związku z powyższym rozważaniem, od razu nasuwa się pytanie, czy nie można odziedziczyć cech i funkcji klasy faktura, pomijając przy tym metodę wyswietl()? Przecież faktura z logo firmy A jest typową fakturą. Podobnie fakturą jest faktura z logo firmy B. Relacje typu jest występują w rzeczywistym świecie niezwykle często. Mówimy, że jabłko jest owocem, magnetofon szpulowy jest magnetofonem, użytkownik komputera jest człowiekiem itd. Na tej relacji można oprzeć coś, co w terminologii obiektowej określane jest mianem dziedziczenia.

Popatrzmy, jak wygląda kod klasy faktura_z_logo_A:

class faktura_z_logo_A : public faktura

{

public:

faktura_z_logo_A(char * nazwa

char * nip) : faktura(nazwa, nip) {}

void wyswietl();

};

void faktura_z_logo_A::wyswietl()

{

cout << "LOGO FIRMY A \n";

faktura::wyswietl();

}

Pierwsza widoczna zmiana dotyczy deklaracji klasy faktura_z_logo_A. Po nazwie klasy i dwukropku umieszczony jest specyfikator dostępu public oraz nazwa klasy faktura. Widząc taką deklarację, mówimy że klasa faktura_z_logo_A dziedziczy po klasie faktura. Innymi słowy, klasa faktura jest macierzysta dla klasy faktura_z_logo_A, która z kolei jest klasą potomną dla tej pierwszej. Dziedziczenie oznacza przejmowanie w klasie potomnej wszystkich publicznych danych oraz funkcji klasy macierzystej. Możemy powiedzieć, że gruszka dziedziczy po owocu, gdyż jest owocem, a więc ma cechy charakterystyczne dla każdego owocu (skórkę, miąższ itp.), a także jest wyposażona w indywidualne cechy, właściwe tylko klasie gruszek (na przykład charakterystyczny kształt i smak). Co dziedziczenie oznacza w praktyce? Możemy zadeklarować obiekt klasy faktura_z_logo_A i za jego pomocą wywoływać zarówno jego publiczne metody, jak i metody publiczne jego klasy macierzystej.

Dziedziczenie może być procesem wielostopniowym. Klasa faktura mogłaby być na przykład potomkiem jakiejś bardziej ogólnej klasy rachunek. W takim przypadku dziedzicząca po niej klasa faktura_z_logo_A, miałaby również publiczne metody i pola pierwszej klasy macierzystej rachunek. Gdy w klasie potomnej zostanie zadeklarowana metoda o takiej samej nazwie, jak w klasie macierzystej, wówczas obiekt klasy potomnej będzie wywoływał swoją metodę. W przeciwnym razie będzie szukał metody w klasie macierzystej. Zobaczmy, jak to wygląda w praktyce:

main()

{

faktura_z_logo_A fakt_A("TRANSKOM","558-125-75-35");

// wywołanie metody klasy faktura

fakt_A.dane_do_faktury(1.5, 100, 22);

// wywołanie metody klasy faktura_z_logo_A

fakt_A.wyswietl();

}

Zwróćmy jeszcze uwagę na konstruktor klasy faktura_z_logo_A. Jest on bardzo podobny do konstruktora klasy faktura; różnica dotyczy jedynie listy inicjatorów. Nie wywołujemy tutaj bezpośrednio konstruktora klasy kontrahent (na tym poziomie nie ma nawet dostępu do owego konstruktora, gdyż jest zdefiniowany w sekcji prywatnej klasy macierzystej), lecz konstruktor klasy macierzystej faktura, który z kolei inicjuje obiekt klasy kontrahent. Mamy tu do czynienia ze swego rodzaju łańcuchem wywołań. Podobnie jest z destruktorami; wywoływane są sekwencyjnie, w momencie gdy obiekty przestają być potrzebne.

Jeszcze jednym nowym elementem jest wywołanie faktura::wyswietl(), umieszczone w definicji metody wyswietl() klasy faktura_z_logo_A. W ten sposób możemy w klasach potomnych wywoływać obiekty klas macierzystych. Gdybyśmy wywołali metodę wyswietl() bez operatora zasięgu faktura::, wówczas nastąpiłby ciąg rekurencyjnych wywołań metody wyswietl() z klasy faktura_z_logo_A, co w efekcie prędzej czy później doprowadziłoby do zawieszenia programu. Może pojawić się pytanie, po co wywołujemy metodę wyświetl klasy macierzystej w klasie potomnej? Otóż chcemy zmodyfikować logo faktury, pozostawiając resztę informacji bez zmian. Dokładamy w metodzie wyswietl() klasy faktura_z_logo_A linię drukującą logo, a dalszą część faktury wyświetlamy dzięki odziedziczonej metodzie wyswietl() klasy faktura. Zresztą nie mamy tutaj innej możliwości, gdyż informacje o cenie, ilości, podatku VAT oraz wartości umieszczone są w polach prywatnych klasy faktura, a tym samym są niedostępne poza tą klasą.

Klasa faktura_z_logo_B będzie wyglądała podobnie, jak klasa omówiona przed chwilą. Spójrzmy zatem na program główny i sposób wyświetlania różnych wzorów faktur:

main()

{

char wzor;

faktura fakt("TRANSKOM","558-125-75-35");

faktura_z_logo_A fakt_A("TRANSKOM","558-125-75-35");

faktura_z_logo_B fakt_B("TRANSKOM","558-125-75-35");

cout << "Wybierz wzor faktury: ";

cout << "o-ogolny, a-logo A, b-logo B: \n";

cin >> wzor;

switch(wzor)

{

case 'O':

case 'o': fakt.dane_do_faktury(8.5, 9, 22);

fakt.wyswietl();

break;

case 'A':

case 'a': fakt_A.dane_do_faktury(8.5, 9, 22);

fakt_A.wyswietl();

break;

case 'B':

case 'b': fakt_B.dane_do_faktury(8.5, 9, 22);

fakt_B.wyswietl();

break;

}

}

Mimo że wszystko działa poprawnie, to jednak daje się zauważyć pewną nadmiarowość. W każdym z warunków case wywoływane są te same metody, jednak dotyczą one różnych klas. Zastanówmy się zatem, czy nie moglibyśmy utworzyć jednej funkcji o nazwie na przykład fakturuj() i w niej wywoływać wszystkie te metody? Do funkcji tej przekazywalibyśmy argument typu... no właśnie, jaki musiałby być typ takiego argumentu? Powiedzieliśmy, że faktura z logo jest fakturą. A zatem skoro zachodzi tu relacja typu jest, to przekazując do funkcji fakturuj parametr formalny typu faktura, prawdopodobnie zmusilibyśmy kompilator do wykonania rzutowania argumentu aktualnego do typu faktura. Jednak jaka metoda wyswietl() zostanie wówczas wywołana? Ta z klasy faktura() czy może ta z klas potomnych? Najlepiej przekonajmy się sami. Oto kod zmienionej funkcji main():

main()

{

char wzor;

faktura fakt("TRANSKOM","558-125-75-35");

faktura_z_logo_A fakt_A("TRANSKOM","558-125-75-35");

faktura_z_logo_B fakt_B("TRANSKOM","558-125-75-35");

cout << "Wybierz wzor faktury: ";

cout << "o-ogolny, a-logo A, b-logo B: \n";

cin >> wzor;

switch(wzor)

{

case 'O':

case 'o': fakturuj(fakt);

break;

case 'A':

case 'a': fakturuj(fakt_A);

break;

case 'B':

case 'b': fakturuj(fakt_B);

break;

}

}

A to kod funkcji fakturuj():

void fakturuj(faktura f)

{

f.dane_do_faktury(8.5, 9, 22);

f.wyswietl();

}

Jak mogliśmy się domyślać, w przypadku wybrania dowolnego wzoru faktury zawsze wywoływana jest metoda wyswietl() z klasy faktura, dlatego nie osiągnęliśmy zamierzonego efektu. Nie było to zresztą możliwe, gdyż w chwili kompilacji programu nie wiadomo, jakiej klasy obiekt przyjdzie w argumencie funkcji fakturuj(). Kompilator dokonując skojarzenia wywołania funkcji z jej ciałem, realizuje tak zwane wiązanie. W przedstawionym przypadku rzutuje do typu faktura wszystko co jest przekazywane do funkcji fakturuj(). Dlatego też argument w powyższej funkcji jest obiektem klasy faktura. Wiersz f.wyswietl() wywołuje metodę wyswietl() z tej właśnie klasy. Chcąc rozwiązać ten problem, należałoby przenieść wiązanie z fazy kompilacji programu do fazy jego uruchomienia, gdzie jest już znany typ obiektu w funkcji fakturuj(). Czy jest jakiś sposób realizacji tego zadania? Oczywiście. Jest nim mechanizm uznawany za istotę programowania obiektowego: polimorfizm.

Polimorfizm

Polimorficzny znaczy wielopostaciowy. Mówiąc prościej, jest to kod, który w zależności od kontekstu może przyjmować różne postaci. Chcielibyśmy, aby po przekazaniu do funkcji fakturuj() obiektu klasy faktura wywołana została metoda wyswietl() tej klasy, natomiast po przekazaniu obiektu klasy faktura_z_logo_A wywoływana była metoda wyswietl(), pochodząca z klasy potomnej faktura_z_logo_A. Nie chcemy używać żadnych instrukcji warunkowych i innych tego typu półśrodków. W tym celu należy w klasie macierzystej zadeklarować funkcję wyswietl() jako wirtualną, umieszczając przed jej nazwą słowo kluczowe virtual, natomiast do funkcji fakturuj() trzeba przekazać adres obiektu klasy macierzystej. Oto zmiany, jakie należy wykonać w kodzie:

class faktura

{

...

public:

...

// dodanie słowa kluczowego virtual

virtual void wyswietl();

...

};

...

// argument przekazywany przez referencję

void fakturuj(faktura& f)

{

f.dane_do_faktury(8.5, 9, 22);

f.wyswietl();

}

...

Teraz program działa jak należy. Być może, zastanawiamy się, jak to się stało, że tak niewielkie zmiany w kodzie doprowadziły do uruchomienia tak zaawansowanego mechanizmu, jak polimorfizm. Cała tajemnica kryje się za słowem kluczowym virtual, którego wystąpienie zapobiega wspomnianemu wcześniej wiązaniu wywołania funkcji z jej ciałem, wykonywanemu podczas kompilacji programu. Jest to tak zwane późne wiązanie, kiedy znany jest już typ obiektu przekazywanego do funkcji fakturuj(). W wielkim uproszczeniu późne wiązanie polega na niejawnym umieszczeniu w klasach wskaźników do tablic, w których przechowywane są adresy znajdujących się w tych klasach funkcji wirtualnych. Podczas odwołania się do takiej funkcji uruchamiany jest specjalny kod, który korzystając ze wspomnianych wskaźników, wyszukuje w odpowiedniej tablicy adresu funkcji wirtualnej, po czym wywołuje tę funkcję. W jakiej tablicy będzie wykonywane wspomniane wyszukiwanie, zależy od typu (klasy) obiektu, którego będzie dotyczyło. Późne wiązanie może być odmiennie realizowane w różnych językach, jednak ogólna zasada jest taka, jak przedstawiona powyżej.

Nie ulega wątpliwości, że klasa faktura pełni dwa bardzo ważne zadania: dostarcza struktury macierzystej, na podstawie której mogą być budowane bardziej skonkretyzowane klasy potomne oraz gwarantuje możliwość korzystania w klasach potomnych z wywołań polimorficznych. Jednak poza tym klasa faktura raczej nie będzie wykorzystywana w żaden praktyczny sposób. Pozwala ona na wydrukowanie jakiejś bliżej nieokreślonej faktury, a nie faktury użytecznej, to jest takiej, która zawiera konkretne logo firmy. Czy istnieje jakaś możliwość ograniczenia roli tej klasy, wyłącznie do tych dwóch zadań, do których została zaprojektowana? Oczywiście. Możemy zadeklarować ją jako klasę abstrakcyjną.

Abstrakcja

Klasa abstrakcyjna jest pewnym modelem, stanowiącym budulec dla struktur potomnych. Klasa, o której mowa, udostępnia klasom wywodzącym się z niej swój interfejs, lecz nie pozwala na tej podstawie tworzyć obiektów. Gdyby klasa faktura była klasą abstrakcyjną, wówczas programista nie mógłby powołać obiektu tej klasy, lecz powstała na jej podstawie klasa faktura_z_logo_A, mogłaby bez problemów korzystać z publicznych metod i pól swojej klasy macierzystej. Aby utworzyć klasę abstrakcyjną, należy powołać w niej przynajmniej jedną metodę w pełni wirtualną, czyli taką, przed którą znajduje się słowo kluczowe virtual, a po niej umieszczony jest znak równości i zero (=0).

Deklaracja w pełni wirtualnej metody wyswietl():

class faktura

{

...

public:

virtual void wyswietl()=0;

...

}

Nie jest konieczne definiowanie (utworzenie ciała) metody wirtualnej, gdyż zazwyczaj nie miałoby to sensu, jednak należy zaimplementować tę funkcję w klasie pochodnej, ponieważ w przeciwnym przypadku ta ostatnia również stanie się klasą abstrakcyjną.

Jak pamiętamy, w klasach pochodnych faktura_z_logo_A oraz faktura_z_logo_B metoda wirtualna wyswietl() powodowała wyświetlenie odpowiedniego logo oraz wywołanie metody wyswietl() klasy macierzystej faktura, odpowiedzialnej za wyświetlenie danych faktury. Jednakże po zadeklarowaniu tej ostatniej jako w pełni wirtualnej i usunięciu jej definicji powyższa koncepcja przestaje mieć sens. Jak zatem wyświetlić wartości pól k_cena, k_ilosc, k_vat oraz k_wartosc z abstrakcyjnej klasy faktura, skoro są zadeklarowane w jej sekcji private, a zatem niedostępne poza tą klasą? Oczywiście moglibyśmy przesunąć je do sekcji public, lecz byłoby to naruszeniem zasad programowania obiektowego - te pola są wewnętrzne dla faktury, w związku z czym nie powinny być dostępne poza fakturą. Jednak pola, o których mowa, powinny być udostępnione w klasach dziedziczących po klasie faktura. Przecież faktura_z_logo_A to jest faktura, jak również faktura_z_logo_B to też jest faktura. W związku z tym pola k_cena, k_ilosc, k_vat oraz k_wartosc powinny być dla nich dostępne. W sukurs przychodzi tutaj specyfikator dostępu protected. Wszystkie metody i pola umieszczone w sekcji protected klasy są dostępne dla niej oraz wszystkich jej klas potomnych, natomiast nie są widziane poza kodem tych klas. Zmieńmy nasz kod, wprowadzając przy okazji kilka praktycznych przeróbek w funkcjach main() i faktura().

class faktura

{

private:

kontrahent kontr;

protected:

double k_cena;

int k_ilosc;

int k_vat;

double k_wartosc;

void oblicz_wartosc();

public:

faktura(char * nazwa, char * nip) : kontr(nazwa, nip) {};

virtual void wyswietl()=0;

void dane_do_faktury(double cena

int ilosc, int vat);

void wyswietl_kontr();

};

...

void faktura::wyswietl_kontr()

{

kontr.wyswietl();

}

...

class faktura_z_logo_A : public faktura

{

public:

faktura_z_logo_A(char* nazwa

char* nip):faktura(nazwa,nip) {};

void wyswietl();

};

/*-------------------------------*/

void faktura_z_logo_A::wyswietl()

{

cout << "LOGO FIRMY A \n";

wyswietl_kontr();

cout << "Cena: " << k_cena << "\n"

<< "Ilosc: " << k_ilosc << "\n"

<< "VAT: " << k_vat << "\n"

<< "Wartosc: " << k_wartosc << "\n";

}

// klasa faktura_z_logo_B wygląda

// analogicznie jak powyższa

class faktura_z_logo_B : public faktura

{

...

};

...

void fakturuj(faktura& f)

{

double cena = 0;

int ilosc = 0;

int vat = 0;

cout << "Podaj dane do faktury: \n";

cout << "Cena: ";

cin >> cena;

cout << "Ilosc: ";

cin >> ilosc;

cout << "VAT: ";

cin >> vat;

f.dane_do_faktury(8.5, 9, 22);

cout << "\n\nF A K T U R A\n";

f.wyswietl();

cout << "\n\n";

}

int main()

{

char * nazwa = new char[100];

char * nip = new char[13];

char wzor;

cout << "Podaj nazwe firmy: ";

cin >> nazwa;

cout << "Podaj NIP firmy: ";

cin >> nip;

cout << "Wybierz wzor faktury: ";

cout << "a-logo A, b-logo B: \n";

cin >> wzor;

faktura_z_logo_A fakt_A(nazwa,nip);

faktura_z_logo_B fakt_B(nazwa, nip);

switch(wzor)

{

case 'A':

case 'a': fakturuj(fakt_A);

break;

case 'B':

case 'b': fakturuj(fakt_B);

break;

}

}

Warto zwrócić uwagę na umieszczenie w klasie faktura publicznej metody wyswietl_kontr(), wyświetlającej dane kontrahenta. Ponieważ obiekt klasy kontrahent pozostał w sekcji private klasy faktura, taka pośrednicząca metoda jest jedynym sposobem dotarcia do danych kontrahenta z poziomu klas potomnych. W programowaniu obiektowym metody tego typu spotykane są stosunkowo często. Ponadto zwróćmy uwagę na sposób jej wywołania w metodach wyswietl() klas pochodnych. Nie trzeba tu używać operatora zasięgu faktura:: (choć jego użycie nie zaszkodziło niczemu), gdyż kompilator automatycznie będzie poszukiwał wywoływanej metody w klasie macierzystej. Mimo że przedstawiony kod jest zbyt prosty, aby wykorzystać go w praktyce, to daje on dobre podstawy do zapoznania się z cechami programowania obiektowego.

Dążenie do natury

W 1735 roku niejaki Karol Lineé, bardziej znany jako Lineusz, zaproponował podział świata roślinnego i zwierzęcego na jednostki taksonomiczne, dając tym samym podwaliny systematyce, czyli nauce zajmującej się badaniem organizmów, sporządzaniem ich opisów, katalogowaniem oraz klasyfikacją. Klasyfikacja to nic innego, jak przyporządkowywanie obiektów (na przykład roślin, zwierząt) do grup (klas), które spełniają warunek zupełności i rozłączności, ze względu na podobieństwo ich cech. Od czasów pierwszego komputera trzeba było czekać wiele lat, zanim tak, zdawałoby się, naturalne myślenie obiektowe zaproponowane przez Lineusza znalazło odzwierciedlenie w metodach programowania. Dla programistów, którzy długo korzystali z technik strukturalnych, przestawienie się na myślenie obiektowe jest często barierą nie do pokonania. Nie wystarczy nauczyć się pewnych elementów składni, trzeba zupełnie zmienić sposób postrzegania programu komputerowego. Należy jak najbardziej zbliżyć projekt do opisywanej natury. Problem oraz jego rozwiązanie nie mogą istnieć w dwóch całkowicie odrębnych światach. Na zrozumienie programowania obiektowego jest tylko jedna recepta: starajmy się rozbić pojęcia na samodzielne jednostki - klasy, ustalić panujące między nimi zależności i dopiero wówczas usiąść przed komputerem.


Zobacz również