Obiektowe spojrzenie

W pierwszej części tekstu poświęconego programowaniu obiektowemu przyjrzymy się relacjom między klasami i obiektami. Poznamy wewnętrzną strukturę klas oraz wynikające z niej nowe zasady projektowania aplikacji. Dowiemy się też, jak zadbać o prawidłowe tworzenie i usuwanie obiektów. A za miesiąc - łącząc klasy i obiekty ze sobą - pokażemy na czym polega prawdziwa siła programowania obiektowego.

W pierwszej części tekstu poświęconego programowaniu obiektowemu przyjrzymy się relacjom między klasami i obiektami. Poznamy wewnętrzną strukturę klas oraz wynikające z niej nowe zasady projektowania aplikacji. Dowiemy się też, jak zadbać o prawidłowe tworzenie i usuwanie obiektów. A za miesiąc - łącząc klasy i obiekty ze sobą - pokażemy na czym polega prawdziwa siła programowania obiektowego.

Szybkiemu rozwojowi techniki komputerowej, jaki nieprzerwanie trwa od lat pięćdziesiątych, niezmiennie towarzyszy równie gwałtowny rozwój metod tworzenia oprogramowania. Mimo że ten drugi nie jest tak spektakularny, gdyż programiści stanowią stosunkowe wąskie grono użytkowników komputerów, to niewątpliwie jest on zasadniczym czynnikiem wytyczającym kierunki rozwoju informatyki (nie miałoby sensu tworzenie coraz szybszych i wydajniejszych komputerów, gdyby były ograniczane przez nierozwijające się oprogramowanie).

Na przestrzeni ostatniego półwiecza powstało wiele modeli programowania, które w mniejszym lub większym stopniu starano się dopasować do opisywanej rzeczywistości. Jednak zazwyczaj to rzeczywistość trzeba było podporządkowywać tym modelom, co kłóciło się z ideą programu komputerowego, jako narzędzia pozwalającego przenieść do komputera procesy zachodzące w realnym świecie. Koncepcja programowania obiektowego była - na tle wcześniejszych doświadczeń - czymś zgoła rewolucyjnym, gdyż uzależniała model programu od świata, który był przez ten model opisywany, a nie na odwrót.

Do zrozumienia zasad programowania obiektowego nie jest konieczna znajomość jakiegoś konkretnego języka programowania, gdyż zasady te w mniejszym lub większym stopniu zostały zaimplementowane w różnych językach. Pewne zagadnienia teoretyczne przyswajane są lepiej, gdy możemy poprzeć je konkretnym fragmentem kodu. Z tego powodu przedstawiane tutaj koncepcje będą w języku C++. Przeniesienie takiego kodu do innego języka jest na ogół kwestią zmiany składni. (W gruncie rzeczy proces ten wymaga również podejmowania decyzji dotyczących tych cech obiektowych, które w języku docelowym nie zostały zaimplementowane lub są obsługiwane w inny sposób niż w języku źródłowym. Na przykład dostępne w C++ dziedziczenie wielokrotne nie jest obsługiwane w obiektowym Pascalu, natomiast w PHP nie jest w sposób zgodny z założeniami obiektowymi zaimplementowana hermetyzacja. Teraz chodzi jednak o przedstawienie ogólnych zasad programowania obiektowego, a nie konkretnej implementacji.)

Obiekt

Klasa i obiekty.

Klasa i obiekty.

Czym jest obiekt w świecie rzeczywistym, nietrudno się domyślić, wystarczy rozejrzeć się dookoła. Na stole stoi magnetofon. To właśnie obiekt - rzecz postrzegana jako całość, mająca pewne cechy i realizująca określone funkcje. Dokładnie tym samym jest obiekt w programie. Chcąc zasymulować w programie działanie magnetofonu, nie możemy rozpatrywać danych opisujących jego stan, w oderwaniu od funkcji zmieniających ten stan. Zarówno jedne, jak i drugie dotyczą jednego obiektu, a zatem powinny być przechowywane i rozpatrywane razem. Mówiąc "magnetofon", nie mamy na myśli pokrętła zmiany głośności, basów, włącznika, obudowy, przewijania taśmy, odtwarzania i setek innych elementów oraz funkcji charakterystycznych dla tego sprzętu. Myślimy o magnetofonie jako o obiekcie.

Skoro wiemy już, jak powinien być reprezentowany obiekt, zastanówmy się nad jego typem. Jak wiadomo, każda dana istniejąca w programie powinna być jakiegoś typu. Określa się, że konkretna liczba jest typu całkowitego lub zmiennoprzecinkowego, a np. nazwisko jest typu łańcuchowego. Dzięki temu kompilator wie, jakie operacje może na tej danej wykonać. Nie będziemy przecież mnożyć łańcuchów znaków ani wycinać fragmentów liczby. Obiekty, które są identyczne (nie chodzi tu o ich aktualny stan podczas działania programu, lecz o strukturę) grupuje się w klasy.

A zatem klasa jest de facto odpowiednikiem typu. Na przykład konkretny magnetofon Panasonic jest obiektem klasy magnetofon. Klasa jest pewnym tworem abstrakcyjnym, którego ukonkretnieniem jest obiekt. Przyjmijmy, że klasa jest przepisem na tort, a stojący na stole tort to obiekt powstały na podstawie tego przepisu. Odnosząc pojęcie klasy i obiektu do tradycyjnych metod programowania, możemy powiedzieć, że klasa jest typem, a obiekt daną tego typu. Z tego założenia wyszli twórcy języka Java, zastępując typy proste obiektami. Weźmy na przykład liczbę 5. Jest to liczba całkowita, czyli reprezentant pewnego abstrakcyjnego tworu, jakim jest zbiór liczb całkowitych. Zbiór, o którym mowa, jest tutaj klasą, natomiast liczba 5 obiektem tej klasy. Każda inna liczba całkowita będzie również obiektem tej klasy, lecz niemającym nic wspólnego z pozostałymi obiektami. Wykonanie operacji na liczbie 5 nie zmieni w żaden sposób stanu liczby 6. Podobnie przewinięcie kasety w magnetofonie Panasonic nie wpłynie na stan kasety w magnetofonie Technics. Operowanie na zmiennych obiektowych określane jest mianem wysyłania komunikatów lub żądań. Dzięki żądaniom możemy zmieniać stan obiektów.

Podsumowując, klasa to twór gromadzący pewne cechy wspólne obiektów (każdy magnetofon ma ustawioną głośność, każdy człowiek ma nazwisko, każde konto ma saldo), natomiast obiekt to egzemplarz danej klasy, który charakteryzuje się ściśle określonymi cechami (każdy magnetofon może mieć inaczej ustawioną głośność, każdy człowiek może mieć inne nazwisko, każde konto może mieć inne saldo).

Klasa i obiekt to podstawowe pojęcia programowania obiektowego. Spójrzmy, jak tworzy się klasę w języku C++:

class magnetofon

{

enum {wl, wyl} stan;

int glosnosc;

double stan_tasmy;

void zmien_glosnosc(int stopien_glosnosci);

void przewijaj(double tasma);

}

A tak deklaruje się obiekt tej klasy:

magnetofon panasonic;

Zmienne deklarowane w ciele klasy określane są mianem pól klasy, natomiast znajdujące się tu funkcje to metody klasy. W przedstawionej klasie umieszczone są jedynie deklaracje metod (interfejs); ich definicje (implementacja) mogą znajdować się poza klasą.

Chcąc jeszcze lepiej uzmysłowić sobie różnicę między tradycyjnym programowaniem strukturalnym a techniką obiektową, rozważmy kolejny przykład.

Musimy napisać prosty program fakturujący, który po wprowadzeniu danych kontrahenta (nazwa i NIP) oraz podatku, ilości i ceny sprzedawanego towaru, drukowałby (wyświetlał na ekranie) te dane oraz wartość do zapłaty. Stosując podejście strukturalne, z pewnością analizę problemu rozpoczęlibyśmy od rozbicia go na czynniki pierwsze. Oto typowy tok myślenia programisty strukturalnego: "Skoro zadaniem programu ma być wyliczenie i wyświetlenie faktury oraz danych kontrahenta, należy umożliwić użytkownikowi wprowadzenie tych danych. Na podstawie podanej ceny, ilości i stawki VAT, program powinien wyliczyć wartość brutto. Tę wartość wraz z danymi, na podstawie których została policzona oraz informację o nazwie i NIP-ie kontrahenta, należy wyświetlić na ekranie (wydrukować). To tyle na temat danych. Teraz należy zastanowić się, jak to wszystko zaimplementować w programie. Najlepiej podzielić wszystko na funkcje. Jedna funkcja posłuży do wprowadzenia danych kontrahenta, inna do pobrania od użytkownika ceny, ilości i stawki podatku, a jeszcze inna..., zaraz, zaraz, niech dane kontrahenta oraz dane do faktury pobiera jedna funkcja. Druga funkcja będzie miała za zadanie wyświetlenie faktury. Aha, będzie jeszcze jedna funkcja, która opierając się na wprowadzonych danych, wyliczy wartość brutto. Dla ułatwienia, dostęp do danych będzie możliwy z dowolnego miejsca programu. Zatem najlepiej zadeklarować zmienne globalnie, unikając problemów związanych z koniecznością przekazywania argumentów do funkcji i ze zwracaniem wartości. Całością będzie sterowała główna funkcja main(), która w zależności od wybranej opcji, będzie wywoływała odpowiednią funkcję: wprowadzanie danych kontrahenta i faktury lub wyświetlanie danych. Ale co się stanie, gdy w przyszłości trzeba będzie zmienić sposób drukowania faktury? Kod funkcji odpowiedzialnej za drukowanie zostanie zmieniony. A jeśli potrzebnych będzie kilka wzorów faktur? To utworzy się kilka instrukcji warunkowych, pozwalających na wybór odpowiedniego wzoru. Co prawda kod trochę urośnie, ale wszystko da się zrobić..."

Takie rozważania można kontynuować jeszcze długo, dochodząc do wniosku, że prowadzą do niczego. Wielkość kodu zaczyna gwałtownie przyrastać, zanim programista napisze choćby jeden wiersz. Program staje się stosunkowo trudny, niedostosowany do przyszłych zmian, a jego kod nie nadaje się do wykorzystania w innych projektach. To niestety typowe cechy programów proceduralnych.

Podejście obiektowe charakteryzuje się zupełnie innymi założeniami. Stając przed jakimś zadaniem, musimy się zastanowić, jak najdokładniej oddać w programie problem istniejący w świecie rzeczywistym. Dla porównania prześledźmy tok myślenia programisty obiektowego: "Podstawowym obiektem w programie jest faktura, a zatem należy ją postrzegać jako całość, która otrzymuje jakieś dane wejściowe (cenę, ilość, stawkę VAT) i produkuje dane wyjściowe (wartość brutto). Nie rozpatrujemy tutaj poszczególnych elementów faktury, ale jedną, spójną i nierozdzielną całość - obiekt faktura. Oczywiście potrzebne są pewne metody, pozwalające działać na tym obiekcie, a zatem niezbędna jest metoda umożliwiająca wprowadzenie danych, inna wyliczająca wartość brutto i jeszcze inna, drukująca fakturę. Wartość brutto powinna być obliczana automatycznie, bezpośrednio po wprowadzeniu danych, a zatem to obiekt musi uruchamiać odpowiadającą za to metodę, a nie kod zewnętrzny. Co więcej, poza obiektem metoda ta w ogóle nie powinna być widziana (bo nie jest tam potrzebna). Oczywiście niezbędny będzie jeszcze jeden obiekt, reprezentujący kontrahenta. Te dwa obiekty, kontrahent i faktura, powinny znajdować się względem siebie w określonej zależności". To mniej więcej tyle. Prosto i obiektowo. Rodzą się tu jednak dwa pytania. Po pierwsze, jak ukryć pewne fragmenty obiektów przed światem zewnętrznym (powiedzieliśmy przecież, że spoza obiektu nie powinien istnieć dostęp do metody wyliczającej wartość brutto)? Po drugie, w jakiej zależności pozostają klasy kontrahent oraz faktura?

Hermetyzacja

Hermetyzacja.

Hermetyzacja.

Powinno być już jasne, że obiekt to struktura zamknięta, do której dostęp realizowany jest za pomocą specjalnie przygotowanego interfejsu. Wróćmy do wspomnianego wcześniej magnetofonu. Na jego obudowie znajdują się przyciski i gałki, pozwalające ustawiać siłę głosu, wysokość basów, przewijać taśmę, włączać odtwarzanie, nagrywanie itp. Mało kto zastanawia się nad tym, że za przekręceniem gałki zwiększającej głośność kryje się zmiana rezystancji opornika. Obudowa jest tutaj czynnikiem, który ukrywa przed użytkownikiem wszelkie wewnętrzne mechanizmy. Nie ma potrzeby zdejmowania obudowy, aby ściszyć magnetofon. Jednak owe niedostępne, wewnętrzne mechanizmy są potrzebne, gdyż to właśnie one (a nie interfejs) decydują o stanie, w jakim znajduje się magnetofon.


Zobacz również