Programowanie obiektowe Wstęp Programowanie obiektowe (ang. object-oriented programming) metodyka tworzenia programów komputerowych, która definiuje programy za pomocą obiektów elementów łączących stan (czyli dane) i zachowanie (czyli procedury, tu: metody). Obiektowy program komputerowy wyrażony jest jako zbiór takich obiektów, komunikujących się pomiędzy sobą w celu wykonywania zadań. Podejście to różni się od tradycyjnego programowania proceduralnego, gdzie dane i procedury nie są ze sobą bezpośrednio związane. Programowanie obiektowe ma ułatwić pisanie, konserwację i wielokrotne użycie programów lub ich fragmentów. Programowanie obiektowe a rzeczywistość Formie w programowaniu obiektowym odpowiada Klasa, materii - instancja - obiekt. Jest to najbardziej naturalny sposób rozumienia rzeczywistości - podstawową cechą mózgu ludzkiego jest klasyfikacja - łączenie występujących w rzeczywistości obiektów w grupy - klasy. Podstawowe założenia paradygmatu obiektowego Cechy języków programowania, które czynią je obiektowymi: Abstrakcja - Każdy obiekt w systemie służy jako model abstrakcyjnego "wykonawcy", który może wykonywać pracę, opisywać i zmieniać swój stan, oraz komunikować się z innymi obiektami w systemie, bez ujawniania, w jaki sposób zaimplementowano dane cechy. Enkapsulacja - Czyli ukrywanie implementacji, hermetyzacja. Zapewnia, że obiekt nie może zmieniać stanu wewnętrznego innych obiektów w nieoczekiwany sposób. Tylko wewnętrzne metody obiektu są uprawnione do zmiany jego stanu. Polimorfizm - Referencje i kolekcje obiektów mogą dotyczyć obiektów różnego typu, a wywołanie metody dla referencji spowoduje zachowanie odpowiednie dla pełnego typu obiektu wywoływanego. Jeśli dzieje się to w czasie działania programu, to nazywa się to póznym wiązaniem lub wiązaniem dynamicznym. Niektóre języki udostępniają bardziej statyczne (w trakcie kompilacji) rozwiązania polimorfizmu - na przykład szablony i przeciążanie operatorów w C++. Dziedziczenie - Porządkuje i wspomaga polimorfizm i enkapsulację dzięki umożliwieniu definiowania i tworzenia specjalizowanych obiektów na podstawie bardziej ogólnych. Historia programowania obiektowego W punktach kto wymyślił, pierwsze języki i terazniejsze. pierwotna koncepcja programowania obiektowego: Simuli 67, (Ole-Johana Dahla i Kristena Nygaarda z Norsk Regnesentral w Oslo kolejna koncepcja została dopracowana w języku Smalltalk, stworzonym w Simuli w Xerox PARC, Programowanie obiektowe zyskało status techniki dominującej w połowie lat 80., głównie ze względu na wpływ C++, w tym okresie cechy obiektowe dodano do wielu języków programowania, w tym Ady, BASIC-a, Lisp-a, Pascala i innych. Eiffel Bertranda Meyera był wczesnym przykładem w miarę udanego języka spełniającego te założenia; obecnie został on w zasadzie całkowicie zastąpiony przez Javę, głównie za sprawą pojawienia się Internetu, 1. Pojęcia klasy i obiektu. Przykład klasy i kilku obiektów tej klasy. Obiekt to podstawowe pojęcie wchodzące w skład paradygmatu obiektowości w analizie i projektowaniu oprogramowania oraz w programowaniu. Klasa to zbiór obiektów wyróżniających się wspólnymi cechami takimi jak struktura i zachowanie; częściowa lub całkowita definicja dla obiektów. Obiekt jest to struktura zawierająca: " dane " metody, czyli funkcje służące do wykonywania na tych danych określonych zadań. Z reguły obiekty (a właściwie klasy, do których te obiekty należą) są konstruowane tak, aby dane przez nie przenoszone były dostępne wyłącznie przez odpowiednie metody, co zabezpiecza je przed niechcianymi modyfikacjami. Takie zamknięcie danych nazywa się enkapsulacją czyli jakby zamknięcie ich w kapsule. W istocie obiekty są rozwinięciem koncepcji programowania z kontrolą typów zmiennych. W programowaniu obiektowym obiekty tworzone są dynamicznie jako podstawowy element konstrukcji programu. Każdy obiekt ma trzy cechy: " tożsamość, czyli cechę umożliwiającą jego identyfikację i odróżnienie od innych obiektów; " stan, czyli aktualny stan danych składowych; " zachowanie (ang. behaviour), czyli zestaw metod wykonujących operacje na tych danych. 2 . Dzi edzi cz e ni e . Przykł ad hi er ar chii kl a s . Dziedziczenie (ang. inheritance) to w programowaniu obiektowym operacja polegająca na stworzeniu nowej klasy na bazie klasy już istniejącej. Na przykład, jeśli mamy klasę (w C++): class Punkt { public: float x,y; Punkt(float _x, float _y); virtual void wypisz(); virtual void przesuń(float przesuniecie_x, float przesuniecie_y); }; Użycie dodatkowej klasy, która różni się od tej jedynie w kilku szczegółach-> wykorzystane dziedziczenie: Dziedziczona klasa może zostać zdefiniowana w następujący sposób: class NazwanyPunkt: public Punkt { //lista pochodzenia public: string nazwa; NazwanyPunkt(float _x=0,float _y=0,string _nazwa=NULL); virtual void wypisz(); }; Nowa klasa o nazwie NazwanyPunkt wywodzi się od klasy Punkt (klasa podstawowa, nadklasa, rodzic). W C++ klasie pochodnej możemy zdefiniować: " dodatkowe dane składowe (w naszym przykładzie: string nazwa;) " dodatkowe funkcje składowe (w naszym przykładzie zmieniliśmy funkcję wypisz()) " nową treść funkcji wirtualnej W innych językach szczegóły dziedziczenia mogą wyglądać odmiennie, np. w CLOS klasa pochodna może wpływać na metody odziedziczone po klasie podstawowej, ogólna zasada dziedziczenia pozostaje jednak taka sama. Dziedziczenie wielokrotne (ang. multiple inheritance) nazywane także dziedziczeniem wielobazowym to operacja polegająca na dziedziczeniu po więcej niż jednej klasie bazowej. Dziedziczenie wielokrotne stosowane jest np. w języku C++: class Samochod { public: int iloscKol; void jedz() { /* ciało metody */ } }; class Lodz { public: float wypornosc; void plyn() { /* ciało metody */ } }; class Amfibia : public Samochod , public Lodz { public: string nrRejestracyjny; }; W efekcie wielokrotnego dziedziczenia Klasa Amfibia posiada wszystkie pola i metody swoich klas bazowych. ... void napompujKolo( Samochod& samochod ) { /* ciało metody */ } void naprawKadlub( Lodz& lodz ) { /* ciało metody */ } int main() { Amfibia amfibia; napompujKolo( amfibia ); naprawKadlub( amfibia ); return 0; } Widzimy, że obiekt amfibia jest jednocześnie typu Samochod i Lodz. Wielokrotne dziedziczenie a interfejsy Zarówno dziedziczenie wielokrotne, jak i interfejsy pozwalają na uzyskanie równoważnego efektu - możliwości traktowania obiektu polimorficznie ze względu na wiele, niespokrewnionych ze sobą typów. W przypadku użycia interfejsów, czynności dziedziczenia (współdzielenia implementacji) i dzielenia interfejsu (czyli zewnętrznego kontraktu) są celowo rozdzielone. 3 . Metody wir tu aln e . Przykł ad ilu s trując y ich użyt e c zn ość. Przykład w C++ #include const float pi = 3.14159; class Figura { public: virtual float pole() const { // deklaracja metody wirtualnej return -1.0; } }; class Kwadrat : public Figura { public: Kwadrat( const float bok ) : a( bok ) {} float pole() const { return a * a; } private: float a; // bok kwadratu }; class Kolo : public Figura { public: Kolo( const float promien ) : r( promien ) {} float pole() const { return pi * r * r; } private: float r; // promien kola }; void wyswietlPole( Figura& figura ) { std::cout << figura.pole() << endl; return; } int main() { // deklaracje obiektow: Figura jakasFigura; Kwadrat jakisKwadrat( 5 ); Kolo jakiesKolo( 3 ); Figura* wskJakasFigura = 0; // deklaracja wskaznika // obiekty ------------------------------- std::cout << jakasFigura.pole() << endl; // wynik: -1 std::cout << jakisKwadrat.pole() << endl; // wynik: 25 std::cout << jakiesKolo.pole() << endl; // wynik: 28.274... // wskazniki ----------------------------- wskJakasFigura = &jakasFigura; std::cout << wskJakasFigura->pole() << endl; // wynik: -1 wskJakasFigura = &jakisKwadrat; std::cout << wskJakasFigura->pole() << endl; // wynik: 25 wskJakasFigura = &jakiesKolo; std::cout << wskJakasFigura->pole() << endl; // wynik: 28.274... // referencje ----------------------------- wyswietlPole( jakasFigura ); // wynik: -1 wyswietlPole( jakisKwadrat ); // wynik: 25 wyswietlPole( jakiesKolo ); // wynik: 28.274... return 0; } Wywołanie metod składowych dla każdego z obiektów powoduje wykonanie metody odpowiedniej dla klasy danego obiektu. Następnie wskaznikowi wskJakasFigura zostaje przypisany adres obiektu jakasFigura i zostaje wywołana metoda float pole(). Wynikiem jest "-1" zgodnie z treścią metody float pole() w klasie Figura. Następnie przypisujemy wskaznikowi adres obiektu klasy Kwadrat - możemy tak zrobić ponieważ klasa Kwadrat jest klasą pochodną od klasy Figura - jest to tzw. rzutowanie w górę. Wywołanie teraz metody float pole() dla wskaznika nie spowoduje wykonania metody zgodnej z typem wskaznika - który jest typu Figura* lecz zgodnie z aktualnie wskazywanym obiektem, a więc wykonana zostanie metoda float pole() z klasy Kwadrat (gdyż ostatnie przypisanie wskaznikowi wartości przypisywało mu adres obiektu klasy Kwadrat). Analogiczna sytuacja dzieje się gdy przypiszemy wskaznikowi adres obiektu klasy Kolo. Następnie zostaje wykonana funkcja void wyswietlPole(Figura&) która przyjmuje jako parametr obiekt klasy Figura przez referencję. Tutaj również zostały wykonane odpowiednie metody dla obiektów klas pochodnych a nie metoda zgodna z obiektem jaki jest zadeklarowany jako parametr funkcji czyli float Figura::pole(). Takie działanie jest spowodowane przez przyjmowanie obiektu klasy Figura przez referencję. Gdyby obiekty były przyjmowane przez wartość (parametr bez &) zostałaby wykonana 3 krotnie metoda float Figura::pole() i 3 krotnie wyświetlona wartość -1. Czysta wirtualność Określa to, że metoda z klasy bazowej deklarująca metodę wirtualną nigdy nie powinna się wykonać. W efekcie klasa taka staje się klasą abstrakcyjną. Oznacza to tyle, iż nie jest możliwe stworzenie obiektu tej klasy. Klasa taka służy jedynie temu, by zdefiniować pewnego rodzaju interfejs i jest przeznaczona jedynie po to, by od niej dziedziczyć. Metodę czysto wirtualną w języku C++ deklaruje się tak: class Figura { public: virtual float pole() = 0; }; Taka deklaracja metody wirtualnej zmusza jednocześnie do określenia metody float pole() na jednym z poziomów dziedziczenia. Nie jest możliwe pominięcie takiej implementacji. Jednocześnie taka deklaracja uniemożliwia stworzenie jakiegokolwiek obiektu klasy Figura np.: Figura mojObiekt;. Właściwości metod wirtualnych " nie może być zadeklarowana jako statyczna (static). " jeśli metoda wirtualna została zaimplementowana w jakimkolwiek wyższym poziomie dziedziczenia (w szczególności w klasie bazowej całej struktury dziedziczenia), nie jest konieczne podawanie implementacji w klasie pochodnej. " jeśli w klasie jest zadeklarowana jakakolwiek metoda wirtualna, zaleca się aby destruktor w tej klasie również określić jako wirtualny Java " W Javie domyślnie wszystkie metody są wirtualne. Aby jednak określić jakąś metodę jako niewirtualną należy zadeklarować metodę jako final. Zastosowania " Rozszerzalność kodu. Polimorfizm umożliwia rozszerzanie nawet skompilowanych fragmentów kodu. " Pozwala na rozszerzalność kodu również wtedy, gdy dostępna jest jedynie skompilowana wersja klasy bazowej. " Zwalnia programistę od niepotrzebnego wysiłku. " Programista nie musi przejmować się tym, którą z klas pochodnych aktualnie obsługuje, a jedynie tym, jakie operacje chce na tej klasie wykonać. " Programista myśli co ma wykonać a nie jak to coś wykonać - nie musi się przejmować szczegółami implementacyjnymi. 4. Konstruktory i destruktory. Zadania konstruktora Wywołanie konstruktora powoduje wykonanie następujących zadań: " obliczenie rozmiaru obiektu " alokacja obiektu w pamięci " wyczyszczenie (zerowanie) obszaru pamięci zarezerwowanej dla obiektu (tylko w niektórych językach) " wpisanie do obiektu informacji łączącej go z odpowiadającą mu klasą (połączenie z metodami klasy) " wykonanie kodu klasy bazowej (w niektórych językach nie wymagane) " wykonanie kodu wywołanego konstruktora Z wyjątkiem ostatniego punktu powyższe zadania są wykonywane wewnętrznie i są wszyte w kompilator lub interpreter języka, lub w niektórych językach stanowią kod klasy bazowej. W językach programowania w różny sposób oznacza się konstruktor: " w C++, Javie, C# - jest to metoda o nazwie zgodnej z nazwą klasy " w Pascalu - metoda której nazwę poprzedzono słowem kluczowym constructor. W języku C++ wyróżnia się następujące szczególne rodzaje konstruktorów: konstruktor domyślny, zwykły konstruktor, konstruktor kopiujący, konstruktor konwertujący, Zwykły konstruktor Konstruktor, który można wywołać, podając co najmniej jeden parametr. Jest to zwykły konstruktor stworzony przez twórcę klasy. Jego zadeklarowanie w C++ nie powoduje niejawnego generowania konstruktora domyślnego. Z reguły parametry takiego zwykłego konstruktora spełniają funkcję inicjalizatorów, które przypisują odpowiednie wartości wewnętrznym zmiennym tworzonego obiektu, np. (przykład w C++): class Wektor { public: Wektor( double x , double y ) { this->x = x; this->y = y; } private: double x; double y; }; int main () { Wektor mojWektor( 3 , 2 ); return 0; } Właściwości i ciekawostki " W większości języków konstruktor nie może być wirtualny(w efekcie czego nie może być metodą czysto wirtualną). " Konstruktor nie może być statyczny " W klasie, gdzie zadeklarowany jest konstruktor kopiujący, powinien być zadeklarowany dowolny inny konstruktor (domyślny lub inny), ponieważ nie byłoby możliwe stworzenie obiektu danej klasy. Aby stworzyć obiekt korzystając z konstruktora kopiującego, należałoby posiadać inny egzemplarz obiektu danej klasy, który nie może być utworzony, ponieważ jego stworzenie również wymagałoby egzemplarza danej klasy itd. " W klasie, gdzie wymagane jest istnienie: konstruktora kopiującego lub destruktora lub operatora przypisania, wymagane jest najczęściej istnienie wszystkich trzech. " Parametr konstruktora kopiującego nie może być przekazywany przez wartość, ponieważ powodowałoby to nieskończone wywołanie konstruktorów kopiujących. Dla potrzeb wywołania konstruktora należałoby wykonać kopię obiektu. Aby wykonać kopię obiektu należy wywołać jego konstruktor kopiujący, któremu również należy przekazać obiekt przez wartość, a więc wykonać jego kopię, itd. Błąd ten nie przejdzie procesu kompilacji, kompilator rozpoznaje taki przypadek i generuje sygnał błędu. Nie jest możliwe wygenerowanie nieskończonej pętli wywołań, ponieważ ciąg takich wywołań miałby teoretycznie nieskończoną długość i spowodowałby zablokowanie kompilatora. " Aby uniemożliwić stworzenie obiektu danej klasy należy: " zadeklarować wszystkie konstruktory w sekcji prywatnej (konstruktor kopiujący może ale nie musi spełniać tego warunku) " klasa nie może deklarować przyjazni z klasą ani funkcją Działanie takie stosuje się, gdy na przykład klasa ma służyć jako zbiór metod i pól statycznych i nie jest potrzebny jakikolwiek egzemplarz obiektu danej klasy (również jako klasy bazowej). Destruktor - w obiektowych językach programowania specjalna metoda, wywoływana przez program przed usunięciem obiektu i niemal nigdy nie jest wywoływana wprost w kodzie używającym obiektu. Pod względem funkcjonalnym jest to przeciwieństwo konstruktora. Destruktor ma za zadanie wykonać czynności składające się na jego "zniszczenie", inne niż zwolnienie pamięci zajmowanej przez sam obiekt, przygotowujące obiekt do fizycznego usunięcia. Po jego wykonaniu obiekt znajduje się w stanie osobliwym i zazwyczaj nie można już z tym obiektem zrobić nic poza fizycznym usunięciem. Destruktor zwykle wykonuje takie czynności, jak zamknięcie połączenia z plikiem/gniazdem/potokiem, odrejestrowanie się z innych obiektów, czasem również zanotowanie faktu usunięcia, a także usunięcie obiektów podległych, które obiekt utworzył lub zostały mu przydzielone jako podległe (jeśli jest ich jedynym właścicielem) lub wyrejestrowanie się z jego użytkowania (jeśli jest to obiekt przezeń współdzielony). W większości języków programowania (np C++, Object Pascal) destruktor jest dziedziczony jak każda inna metoda. Przykładowy destruktor (w składni C++): class Samochod{ public: string marka; //... (pewne dane i metody) ~Samochod() { //destructor std::cout << "Samochod " << marka << " zostal usuniety.\n"; } }; Finalizator W niektórych językach z wbudowanym odśmiecaczem (np. Java i C#) dostępna jest składnia finalizatora - specjalnej metody wywoływanej, gdy obiekt jest usuwany przy odśmiecaniu. W przeciwieństwie do destruktora nie wiadomo w którym dokładnie momencie działania programu to nastąpi.