I.
Wprowadzenie do  języków zapytań (1)
II.
Wprowadzenie do  języków zapytań (2)
III.
Pojęcia obiektowości w bazach danych (1)
IV.
Pojęcia obiektowości w bazach danych (2)
  Wstęp
  1. Klasy
  2. Polimorfizm
  3. Role
  4. Kolekcje
  5. Trwałość
  6. Moduły
  Podsumowanie
  Zadania
V.
Podstawy semantyczne języków zapytań
VI.
Modele składu obiektów
VII.
Stos środowisk, rezultaty zapytań, funkcja nested
VIII.
Język SBQL (Stack-Based Query Language) (1)
IX.
X.
Dalsze własności SBQL
XI.
Operatory order by i group by
XII.
Przetwarzanie struktur nieregularnych
XIII.
Rozszerzenie języków zapytań o konstrukcje imperatywne
XIV.
Procedury, procedury funkcyjne, metody, reguły zakresu
XV.
Parametry procedur i metod, procedury rekurencyjne, optymalizacja poprzez modyfikację zapytań

 

1. Klasy

Pojęcie klasy stanowi abstrakcję w myśleniu i programowaniu, której celem jest uchwycenie zarówno statycznych własności obiektów (ich struktury), jak i własności dynamicznych, w tym operacji, które można wykonywać na obiektach. Definicje pojęcia klasy starają się więc odwzorować niezmienne cechy obiektów zarówno w planie pewnej ich populacji (np. klasa obiektów OSOBA), jak i w planie cyklu życiowego - dopuszczalnych zmian pojedynczego obiektu.

Istnieje wiele definicji pojęcia klasy, jak również wiele nieporozumień co do tych definicji. Dość popularne skojarzenie (zwłaszcza u osób o preferencjach matematycznych - analogia z "klasami abstrakcji" pewnej relacji równoważności), każe widzieć klasę jako zbiór obiektów. Taka definicja pojęcia klasy jest niewłaściwa z powodów, które przedyskutujemy przy okazji omawiania ekstensji klasy. Możemy tu podać pewną ogólną definicję, co do której można mieć nadzieję, że odpowiadałaby większości sytuacji występujących w projektowaniu, programowaniu i bazach danych.

Klasa jest miejscem przechowywania tych informacji dotyczących grupy obiektów, które są dla nich niezmienne, wspólne lub dotyczą całej ich populacji. Takie informacje są nazywane inwariantami obiektów.

Inwarianty dotyczące jednego obiektu mogą być przechowywane w wielu klasach tworzących hierarchię lub inną strukturę dziedziczenia. Obiekt przypisany do klasy zawierającej jego inwarianty jest nazywany wystąpieniem lub członkiem tej klasy. Poprzez przypisanie obiektów do klas unika się przechowywania inwariantów wewnątrz każdego obiektu. Klasa stanowi więc coś w rodzaju "czynnika wyciągniętego przed nawias" dla pewnej populacji obiektów. Takie "wyciągnięcie przed nawias" ma ogromne znaczenie dla modelowania pojęciowego, pozwalając operować zestawem inwariantów jak abstrakcją zastępującą zarówno poszczególne egzemplarze obiektów, jak i pewną ich populację.


1.1. Rodzaje inwariantów przechowywanych w ramach klas

Dwie kategorie informacji wspólnej dla obiektów uzyskały dużą popularność i często właśnie je wymienia się jako atrybuty klasy:

  • Typ, czyli statyczna struktura obiektu. Zwykle typ określa zestaw atrybutów obiektu (ich nazwy oraz typ wartości, które mogą przybierać).

  • Metody lub inaczej operacje, które można wykonać na obiekcie.


Mówi się, że klasa łączy w sobie informację o strukturze obiektów i ich zachowaniu (behaviour). Tego rodzaju definicja pojęcia klasy jest jednak bardzo uproszczona. Bardziej uważne spojrzenie na obiektowe języki programowania oraz systemy obiektowe pozwala wyodrębnić więcej rodzajów inwariantów, które mogą znaleźć się w obrębie klasy. W wielu przypadkach ich znaczenie semantyczne i pragmatyczne nie da się wyrazić wyłącznie w terminach specyfikacji atrybutów lub metod. Spośród nich wymienimy następujące:

  • Nazwa, czyli językowy identyfikator obiektu używany w tekstach programu lub w zapytaniach. Zwróćmy uwagę, że nazwa obiektu może być inwariantem, ale nie musi. W językach C++ i Java nazwa nowo tworzonego obiektu nie jest określona przez jego klasę, natomiast w standardzie OMG CORBA i ODMG (z koncepcyjnego punktu widzenia) obiekt ma nazwy określone przez klasy, do których jest podłączony.

  • Specyfikacje powiązań (links, relationships) obiektów danej klasy z obiektami innej lub tej samej klasy.

  • Interfejs, lista eksportowa lub inny środek określający, które atrybuty czy metody są dostępne z zewnątrz klasy lub obiektu, a które są prywatne, czyli dostępne wyłącznie dla metod i innych abstrakcji proceduralnych z wnętrza danej klasy.

  • Wartości wspólne dla wszystkich elementów klasy, np. pewne stałe lub wspólne atrybuty.

  • Informacja o dopuszczalności wartości zerowych (null values).

  • Wartości domyślne (default values) używane przez system w momencie tworzenia nowego obiektu lub podstawiane w sytuacji, gdy dany atrybut dla pewnego obiektu przyjmuje wartość zerową.

  • Zdarzenia lub wyjątki, które mogą mieć miejsce podczas wykonywania operacji na obiekcie.

  • Obsługa zdarzeń lub wyjątków, czyli fragmenty kodu określające czynności, które mają być wykonane po wystąpieniu zdarzenia lub wyjątku; w bazach danych noszą one nazwę aktywnych reguł (active rules).

  • Lista importowa lub inny środek ustalający zestaw cech obiektów innych klas, które w sposób "wirtualny" (bez kopiowania) mają być zaimportowane do wnętrza obiektów danej klasy, tak aby nie istniały różnice w sposobach dostępu do cech własnych i cech zaimportowanych. Lista importowa (znana np. z Modula-2) jest nieco niedocenionym środkiem ograniczenia efektów ubocznych metod zdefiniowanych w ramach danej klasy.

  • Ograniczenia, więzy integralności (integrity constraints), którym musi podlegać każdy obiekt będący członkiem danej klasy.

  • Atrybuty pochodne (wyliczalne) wraz z ich algorytmami; można je traktować jako szczególny przypadek metod.

  • Ikony do reprezentacji graficznej elementów klas; można je traktować jako atrybuty lokalne niezbędne dla pewnych metod.

  • Reguły bezpieczeństwa i prywatności ustalające dozwolone operacje na elementach obiektów i klasy oraz wiążące użytkowników z prawami dostępu i uprawnieniami do dokonywania operacji na obiektach.

  • Informacje katalogowe, pomoce. Klasa może zawierać pasywną lub aktywną metainformację o przeznaczeniu, znaczeniu i regułach użycia obiektów lub ich atrybutów. Ta informacja może podlegać innym zasadom użycia niż normalne atrybuty obiektu. Informacja ta może obejmować fizyczne własności obiektów: ich reprezentację, metody dostępu, obecność indeksów itp.


Podana lista wszystkich możliwych inwariantów obiektów przechowywanych w obrębie klasy prawdopodobnie nie jest kompletna i może włączać elementy specyficzne dla pewnego systemu obiektowego.


1.2. Interfejs

Standardy i języki, takie jak CORBA, ODMG, Java i COM/DCOM, wprowadzają pojęcie interfejsu. Interfejs zawiera komplet informacji o tych własnościach klasy, które są niezbędne do poprawnego manipulowania obiektami tej klasy (w danym kontekście ich zastosowań). Interfejs posiada znaczenie pojęciowe dla użytkownika lub programisty i pozwala na wystarczająco dokładne przedstawienie tego, co obiekt zawiera w swoim wnętrzu (tj. interfejs określa odpowiedni fragment schematu obiektowego). Znajomość interfejsu nie oznacza pełnej wiedzy o klasie; np. interfejs może być opublikowany w podręczniku, co oczywiście nie oznacza, że posiadamy odpowiadającą mu klasę. Jeden interfejs może być powiązany z wieloma implementacjami (np. podobnymi klasami oferowanymi przez różne firmy), jak również jedna klasa może być wyposażona w wiele interfejsów.

W niektórych opracowaniach można znaleźć zdania stwierdzające, że interfejs oznacza specyficzną klasę (lub jest tym samym co typ). Praktycznym kryterium rozróżnienia pomiędzy klasą a interfejsem może być dość istotny fakt, że klasa może być przedmiotem obrotu handlowego (bowiem zawiera w sobie implementację metod i innych inwariantów obiektów), podczas gdy interfejs takiemu obrotowi nie podlega i zwykle jest publikowany w powszechnie dostępnych podręcznikach danego systemu lub standardu. Niestety, to banalne kryterium rozróżnienia pomiędzy klasą a interfejsem nie jest dostrzegane przez niektórych autorów publikacji z zakresu obiektowości.

Interfejs jest również pojęciem różnym od pojęcia typu, jakkolwiek dla wielu systemów różnica ta jest mniej uchwytna. Typ jest specyfikacją klasy ograniczająca kontekst, w którym obiekty tej klasy mogą być użyte w wyrażeniach, zapytaniach lub programach. Jednocześnie typ określa reprezentację wartości przechowywanych wewnątrz obiektu, umożliwiając ich poprawną interpretację. Niekiedy typ określa również wewnętrzną budowę obiektów. Podobnie jak w przypadku interfejsu, typ jest pojęciem różnym od klasy: znajomość typu obiektów nie oznacza, że kupiliśmy lub napisaliśmy odpowiadającą mu klasę. Role pojęcia interfejsu i pojęcia typu są różne. W szczególności, interfejs może zawierać informację o wyjątkach powodowanych przez pewne metody, informację o obsłudze tych wyjątków, jak również informację o efektach ubocznych (działaniach na obiektach innych klas). Informacje takie są nieistotne dla typu. Różnice występują również w warstwie definicyjnej: typy mogą być zdefiniowane za pomocyą definicji rekurencyjnych (np. typ "drzewo genealogiczne"), natomiast tego rodzaju definicje są trudne do wyobrażenia dla interfejsów. Również typy zwykle dotyczą nie tylko obiektów, ale również wartości (zwracanych przez wyrażenia, zapytania lub procedury) oraz specyfikacji parametrów metod i procedur (nie będących obiektami).

Semantyka pojęcia interfejsu jest definiowana poprzez pojęcie klasy. Inaczej mówiąc, interfejs jest tylko napisem, zaś znaczenie tego napisu jest niedefiniowalne, o ile nie zdefiniujemy pojęcia klasy. Dziedziczenie interfejsów można uważać za pewną operację na napisach: jeżeli interfejs A dziedziczy z interfejsu B, to oznacza to nic więcej poza tym, że napis B można wstawić do napisu A poprzez prostą operację na tekście. Powyższe okoliczności stawiają w złym świetle standard ODMG, który definiuje pojęcie interfejsu, ale nie definiuje (a raczej źle definiuje) pojęcie klasy.


1.3. Hierarchia klasdziedziczenie

Klasę można budować wyłącznie na zasadzie formalistycznego "wyciągnięcia przed nawias" pewnego zestawu inwariantów. Częściej jednak klasa posiada niezależne znaczenie dla modelowania pojęciowego jako ogólna abstrakcja budowana przez projektanta lub programistę w celu myślowego odwzorowania pewnego zbioru obiektów. Dla przykładu, jeżeli projektant utworzył klasę STUDENT z inwariantami w postaci definicji atrybutów Nazwisko, Imię, RokUrodz, NrIndeksu, RokStudiów, Wydział oraz klasę PRACOWNIK, z inwariantami w postaci definicji atrybutów Nazwisko, Imię, RokUrodz, Zarobek, Firma, Zdjęcie, to inwarianty Nazwisko, Imię i RokUrodz można "wyciągnąć przed nawias" tworząc klasę OSOBA. Powstała w ten sposób klasa przenosi wyobrażenie co do jej znaczenia i roli w świecie zewnętrznym.

Opisana powyżej sytuacja została przedstawiona na Rys.14, gdzie z jednej strony, hierarchia klas pozwala na ustalenie, w jaki sposób inwarianty obiektów są "wyciągane przed nawias" (zgodnie z kierunkiem strzałek), a z drugiej strony, jaki jest pojęciowy obraz zakresu znaczeniowego tych klas. Rysunek przedstawia inwarianty w postaci atrybutów obiektów (Nazwisko Imię itp.) oraz metod (Wiek, ZarobekNetto itp.).

Dziedziczenie oznacza, że dla przetwarzania obiektu programista może wykorzystywać dowolne inwarianty z klasy, której dany obiekt jest członkiem, lub z dowolnych klas stojących wyżej w hierarchii w stosunku do danej klasy.

Ważnym aspektem tworzenia hierarchii klas jest unikanie redundancji, zarówno redundancji kodu, jak i redundancji koncepcyjnej. Przykładowo, metoda Wiek dla studenta i pracownika może być zaimplementowana tylko jeden raz. W sytuacji braku odpowiedniej nadklasy w modelu pojęciowym mogłoby się zdarzyć, że implementacja klas STUDENTPRACOWNIK byłaby wykonywana przez dwóch różnych programistów. Uniknięcie pisania przez nich redundantnego kodu tej metody byłoby uzależnione od pewnych nieformalnych (z reguły zawodnych) uzgodnień. Podobna kwestia dotyczy atrybutów, takich jak Nazwisko w rozważanym przykładzie. Brak nadklasy OSOBA mógłby spowodować, że każdy z naszych dwóch hipotetycznych programistów mógłby przyjąć różny rozmiar znakowy dla reprezentacji wartości, różny rozmiar pola na ekranie, gdzie taka wartość byłaby wyświetlana lub zapełniana, różną konwencję reprezentacji (wszystkie znaki lub wyłącznie wersaliki) itd. W większości przypadków tego rodzaju różnice zostałyby odebrane jako niespójność.

14
Rys.14. Przykad hierarchii klas

Odwołując się do poprzednio użytej analogii z "wyciąganiem czynnika przed nawias", załóżmy, że klasa A zawiera inwarianty obiektów OA, zaś klasa B zawiera inwarianty obiektów OB. Często okazuje się, że A i B zawiera część wspólną. W tej sytuacji można kontynuować proces "wyciągania przed nawias" wszystkich tych inwariantów, które są wspólne dla A i B. Proces ten prowadzi do utworzenia nowej klasy C, zawierającej inwarianty wspólne dla OA i OB. Klasę C określa się wtedy jako nadklasę (lub superklasę) w stosunku do klas A i B. Takie postępowanie można oczywiście kontynuować dla dowolnej liczby klas obiektów i dowolnej liczby poziomów hierarchii klas.

Hierarchia klas jest grafem zorientowanym bez cykli, zaś obiekty importują (dziedziczą) inwarianty z coraz bardziej ogólnych klas znajdujących się w tym drzewie, aż do korzenia (ewentualnie z wykorzystaniem efektu przesłaniania, omówionego nieco dalej). W przypadku wielodziedziczenia odpowiednia struktura jest określana jako "krata" (lattice) lub "półkrata" (semi-lattice). Jak wspomnieliśmy, nie ma obowiązku, aby każdy obiekt był przypisany do jakiejś klasy. Może istnieć obiekt lub ich grupa nie posiadająca istotnych inwariantów, które warto wyodrębniać w klasę. Nie ma również obowiązku, aby dwie różne klasy miały wspólną nadklasę (być może nie bezpośrednią). Taką nadklasę warto powoływać wyłącznie wtedy, jeżeli istnieje grupa inwariantów wspólna dla dwóch lub więcej klas i/lub wtedy, gdy taka nadklasa posiada znaczenie dla modelowania pojęciowego. Dla większości sytuacji takiej wspólnej klasy nie będzie. To oznacza, że hierarchia klas może mieć wiele "korzeni".

Należy zwrócić uwagę, że dziedziczenie może podlegać dodatkowemu kryterium zwanym (w UML) aspektem dziedziczenia. Przykładowo, pracowników można specjalizować według funkcji w organizacji (szef, sekretarka, księgowa, programista itd.), według płci (mężczyzna, kobieta), według kategorii wyszkolenia (uczeń, pracownik bez wykształcenia średniego, pracownik z wykształceniem średnim, pracownik z wykształceniem wyższym) itd. W wielu przypadkach nie istnieje jednoznaczne kryterium wyznaczające "najlepszy" aspekt specjalizacji i dziedziczenia. Często konieczne jest jednoczesne użycie wielu aspektów, co nieuchronnie prowadzi do wielokrotnego dziedziczenia. Analiza problemu aspektów dziedziczenia skłania do wniosku, że w wielu przypadkach dziedziczenie jest uproszczonym substytutem koncepcji dynamicznych ról.

Terminy "hierarchia", "krata" itd. należy uważać za konsekwencję dość powierzchownej formalizacji. Klasy mogą tworzyć system, który jest bardziej złożony niż hierarchia czy krata. Pojęcia takie jak delegacja, prototypy, role, wizjery, metaklasy, szablony, klasy parametryzowane, konstruktory typów masowych itd. mogą stworzyć zależności pomiędzy klasami, które będą znacznie bardziej wyrafinowane, niż te, które można objąć koncepcją zorientowanego grafu bez cykli.


1.4. Zasada zamienialności

Zasada zamienialności (substitutability principle) jest oznaczana również jako LSP (Liskov's Substitutability Principle). Jest ona następująca:

Zasada  zamienialności: w każdym miejscu programu, gdzie może być użyty pewien obiekt klasy K, może być także użyty obiekt, którego klasą jest podklasa klasy K.

Zasada ta często jest sformułowana w odniesieniu do typów i kontroli typologicznej: w każdym miejscu programu, gdzie może być użyty pewien obiekt typu T, może być także użyty obiekt, którego typ jest podtypem typu T. Przykładowo, wszędzie tam, gdzie można użyć liczby całkowitej, można także użyć liczby naturalnej; wszędzie tam, gdzie można użyć obiektu Osoba, można także użyć obiektu Pracownik. Ponieważ obiekt podklasy klasy K zawiera więcej atrybutów niż obiekt klasy K, zasada ta oznacza ignorowanie wszystkich tych atrybutów, które "wystają" poza typ oczekiwany w danym miejscu programu. Zasada ta obejmuje również metody zawarte w klasach.

Zasada zamienialności wydaje się niepodważalnym, absolutnie naturalnym aksjomatem. Jak się okazuje, prowadzi ona także do pewnych anomalii, np. anomalii podstawienia, anomalii wielodziedziczenia, dylematu "kowariancja czy kontrawariancja" i innych. Zasada zamienialności staje się kontrowersyjna, jeżeli przyjmiemy, że inwariantem obiektów jest ich nazwa. W takim przypadku zasada ta z definicji nie może być spelniona. Zasada zamienialności staje się również sprzeczna z zasadą otwarta-zamknięta w przypadku, gdy instancjami klas są kolekcje.

Alternatywą dla zasady zamienialności jest  model z dynamicznymi rolami obiektów. Model ten usuwa wszystkie problemy związane z zasadą zamienialności.


1.5. Obywatelstwo klasy

Pojęcie obywatelstwa klasy (class citizenship) jest istotne dla zrozumienia różnic występujących pomiędzy poszczególnymi obiektowymi językami programowania i systemami. W językach programowania obywatelem pierwszej kategorii nazywa się taki byt programistyczny, który istnieje i którym można manipulować w czasie wykonania. Obywatelem drugiej kategorii nazywa się ten byt, który istnieje tylko w fazie analizy tekstu programu (kompilacji), tzn. nie istnieje lub jest całkowicie niedostępny podczas wykonania. W klasycznych językach programowania większość pojęć ma obywatelstwo drugiej kategorii (np. nazwy zmiennych, typy, moduły itd.), gdyż programista nie posiada jakichkolwiek możliwości zarządzania nimi w czasie wykonania. Np. nie może w trakcie wykonywania programu zmienić nazwy zmiennej. Tego rodzaju operacja jest możliwa wyłącznie na tekście programu.

Istnienie pojęć o drugiej kategorii obywatelstwa sprzyja wydajności i niezawodności, gdyż wszystkie operacje związane z tymi pojęciami (np. kontrola typu) mogą być wykonane w trakcie kompilacji. Z drugiej strony, pojęcia programistyczne przesunięte do pierwszej kategorii obywatelstwa sprzyjają elastyczności, programowaniu ogólnemu (generic) i ponownemu użyciu.

Podwyższenie sprawności komputerów spowodowało tendencję przesuwania pojęć programistycznych do pierwszej kategorii obywatelstwa. Przykładem są relacyjne bazy danych, gdzie deklaracje relacji, ich nazwy, procedury, perspektywy, reguły i inne pojęcia są obywatelami pierwszej kategorii.

15
Rys.15. Stosunek pomidzy klasami i ich ekstensjami

Klasa może być obywatelem pierwszej kategorii (Smalltalk) lub drugiej kategorii (C++). Inaczej mówiąc, w Smalltalk'u klasa może być przetwarzana tak samo, jak dowolny inny obiekt. W C++ takie przetwarzanie jest niemożliwe, ponieważ klasa istnieje wyłącznie w fazie analizy programu. W zależności od kategorii obywatelstwa są możliwe lub niemożliwe niektóre operacje na klasie, takie jak:

  • Wysłanie komunikatu do klasy (jako obiektu). Smalltalk umożliwia taką operację, a w C++ jest to niemożliwe;

  • Zmiana nazwy obiektu: jest ona niemożliwa, jeżeli nazwa jest obywatelem drugiej kategorii;

  • Dynamiczna zmiana zestawu lub typu atrybutów obiektów (określana jako "ewolucja schematu");

  • Dynamiczna zmiana zestawu metod znajdujących się wewnątrz klasy.


Jeżeli klasy są obywatelami pierwszej kategorii, to mogą one być traktowane na takich samych zasadach jak normalne obiekty programistyczne lub obiekty bazy danych. Posiadają one wtedy swoją lokację w przestrzeni adresowej komputera, który (teoretycznie lub praktycznie) może podlegać manipulacjom podczas czasu wykonania. Do tych manipulacji (szczególnie często rozważanych) należy wstawienie nowej klasy do bazy danych, usunięcie klasy, dołożenie do klasy nowej metody (atrybutu wirtualnego), zmiana reguł bezpieczeństwa dla obiektów tej klasy, zmiana ograniczeń na obiekty itd.

Dla języków zapytań obywatelstwo klasy ma znaczenie drugorzędne. Jednakże przyjęcie założenia, że klasy są drugiej kategorii programistycznej, znacznie komplikuje wyjaśnienie semantyki tych języków. Powoduje też niejasność koncepcyjną: jeżeli klasy są drugiej kategorii, to są składnikami programów źródłowych, a nie elementami bazy danych. Jasność koncepcyjna wymagałaby założenia, że klasy obiektów bazy danych są również przechowywane w bazie danych i podlegają tej samej semantyce pielęgnacyjnej (tworzeniu, usuwaniu, zmianie itd.). W dalszej części tej książki będziemy więc uważać, że klasy są pierwszej kategorii programistycznej, mogą być przechowywane jako trwałe obiekty w bazie danych zaś ich ewentualne przesunięcie do drugiej kategorii będziemy traktować jako zabieg optymalizacyjny.


1.6. Ekstensja klasy

Terminem "ekstensja" (extent, extension) określa się aktualnie istniejący zbiór obiektów będących wystąpieniami danej klasy. Wystąpienia mogą być bezpośrednie (w przypadku klasy konkretnej) lub pośrednie (dla nadklas). W terminologii wielu opracowań z zakresu baz danych (np. standard ODMG) termin ekstensja oznacza specjalną strukturę danych przywiązaną do klasy. Struktura ta przechowuje wszystkie obiekty będące członkami tej klasy.

Pojęcie ekstensji jest kontynuacją koncepcji modelu relacyjnego, gdzie deklaracja typu tablicy jest nierozerwalnie związana z deklaracją samej tablicy. Dla każdej deklaracji typu w schemacie relacyjnym istnieje dokładnie jedna tablica przechowywana w bazie danych. Wielu specjalistów postulowało rozdzielenie tych dwóch deklaracji, co ostatecznie ma miejsce w standardzie SQL-99. Postulat uniezależnienia deklaracji typów/klas i odpowiadających im bytów programistycznych staje się jeszcze bardziej aktualny w przypadku obiektowości, której filozofią jest dekompozycja pojęć na niezależne składowe oraz ortogonalna ich kombinacja.

W przypadku omawianego problemu oznaczałoby to, że deklaracja klasy, deklaracje/tworzenie obiektów będących wystąpieniami tej klasy oraz deklaracje/tworzenie repozytoriów (kontenerów), w których mają znaleźć się obiekty tej klasy, powinny być niezależne.

Istnieje szereg argumentów przemawiających za tą tezą. Np. rozproszona baza danych może składać się z wielu modułów i każdy z nich może zawierać obiekty klasy X. W tym przypadku istnieje wiele ekstensji tej samej klasy. Inny przykład: można utworzyć klasę "zdjęcie", z określonymi metodami, takimi jak "wyświetl" lub "zmień format", natomiast kolekcje obiektów ze zdjęciami umieszczać wewnątrz obiektów PRACOWNIK, PIES lub ROŚLINA. Podobnie jak poprzednio, mamy do czynienia z wieloma ekstensjami tej samej klasy. Jedynym argumentem na rzecz istnienia ekstensji jako specjalnej struktury danych jest uproszczenie wyrażeń, które programista musi napisać w celu utworzenia nowego obiektu oraz ulokowania go w obiektowej strukturze danych. Ta zaleta jest nieco iluzoryczna, ponieważ można ją bardzo łatwo osiągnąć poprzez napisanie odpowiedniej metody.

Pojęcie ekstensji klasy implikuje problemy semantyczne dotyczące klas nadrzędnych. W hierarchii klas przedstawionej na Rys.15 ekstensja klasy OSOBA będzie rozszerzona o "obcięte" obiekty klasy PRACOWNIK. W istocie, spójna implementacja ekstensji musi założyć, że będą istnieć obiekty znajdujące się jednocześnie w kilku ekstensjach. Ten aspekt staje się bardziej skomplikowany w przypadku wielokrotnego dziedziczenia. Dodatkowo, obiekty są różnie widziane w różnych ekstensjach. Na Rys.15 ekstensja klasy OSOBA zawiera "cienie" obiektów PRACOWNIK, które nie zawierają atrybutów ZAROBEK i DZIAŁ. Nie są one oczywiście kopiami fragmentów tych obiektów, lecz są konsekwencją myślowej abstrakcji mającej skutki dla konstrukcji i semantyki języka programowania. Niestety, te niuanse powodują, że całość pomysłu ekstensji klasy staje się dość trudna i wewnętrznie niespójna.


1.7. Zmienne i inne cechy klasy jako całości

Dotychczas mówiliśmy o tym, że klasa jest miejscem przechowania inwariantów dla pewnej grupy podobnych obiektów; inwarianty są importowane (dziedziczone) przez każdy z obiektów. Oprócz tego rodzaju inwariantów klasa może także przechowywać pewne cechy właściwe dla klasy jako takiej lub cechy całej kolekcji obiektów będących członkami klasy (czyli jej ekstensji). Można podać szereg przykładów, kiedy takie wspólne cechy mogą okazać się potrzebne:

  • Metody, ograniczenia lub reguły przechowywane wewnątrz klasy mogą potrzebować pewnych lokalnych procedur lub funkcji. Np. metoda zarobek_netto może potrzebować lokalnej funkcji oblicz_podatek. Ta funkcja nie jest dziedziczona przez wystąpienia tej klasy.

  • Z tych samych powodów mogą się okazać potrzebne wewnątrz klasy pewne lokalne struktury danych, np. zmienne lub obiekty oraz ich typy. Mogą one być lokalne, dostępne tylko z wnętrza klasy, lub też mogą być publiczne, ale nie dziedziczone przez wystąpienia klasy.

  • Pewna grupa metod i zmiennych (obiektów) może odnosić się do zbioru wszystkich aktualnych wystąpień klasy. Np. liczba wystąpień tej klasy (wyliczana), informacja na temat ostatnich aktualizacji wystąpień klasy, minimalny i maksymalny zarobek (uczestniczący w pewnej regule lub ograniczeniu), średni zarobek (wyliczany), wartości początkowe dla nowo tworzonych obiektów, informacja o archiwalnych kopiach obiektów tej klasy itd. Takie metody, zmienne i obiekty mogą być również prywatne lub publiczne.

16
Rys.16. Różne aspekty klasy

Sytuacja jak powyżej została zilustrowana na Rys.16. Jak widać, klasa może mieć wiele koncepcyjnie zróżnicowanych cech, co może uczynić to pojęcie dość trudnym do zrozumienia i użycia.

Alternatywą jest przyjęcie założenia, że klasa może zawierać wyłącznie inwarianty dziedziczone przez jej członków oraz pewne dane lokalne, niewidoczne na zewnątrz, które są niezbędne do zdefiniowania tych inwariantów. Cechy wspólne dla kolekcji obiektów danej klasy rozpatrywanej jako całość nie powinny być cechami tej klasy, lecz innej klasy zdefiniowanej dla kolekcji elementów. W dalszym ciągu tej książki będziemy przyjmować, że klasy zawierają wyłącznie cechy dziedziczone przez jej członków oraz cechy prywatne niezbędne dla definicji cech dziedziczonych.


1.8. Wielokrotne dziedziczenie

Dziedziczenie może odbywać się nie tylko z jednej klasy, lecz z kilku, poprzez zsumowanie dziedziczonych inwariantów. Np. klasa AMFIBIA dziedziczy zarówno z klasy POJAZD_LĄDOWY, jak i z klasy POJAZD_WODNY (oraz pośrednio z klasy POJAZD). Oznacza to, że obiekty tej klasy mają wszystkie atrybuty zdefiniowane w klasach POJAZD, POJAZD_LĄDOWY i POJAZD_WODNY oraz można do nich zastosować wszystkie zdefiniowane w tych klasach metody (oraz ewentualnie inne inwarianty). Tę sytuację określa się jako wielokrotne dziedziczenie lub wielodziedziczenie (multiple inheritance, multi-inheritance). Przy wielodziedziczeniu struktura klas jest grafem bez cykli, Rys.17.


Rys.17. Przykad wielokrotnego dziedziczenia

Potencjalnym problemem wielodziedziczenia jest konieczność rozstrzygnięcia konfliktów nazw, jeżeli w dziedziczonych klasach znajdują się inwarianty o tych samych nazwach. Np. rozpatrując Rys.17 powstaje pytanie, który atrybut max_prędkość ma odziedziczyć amfibia i jaka będzie jego interpretacja? Może być różna, gdyż np. dla pojazdów lądowych prędkość zapisuje się
w km/godz., natomiast dla pojazdów wodnych w węzłach.

Istnieje kilka metod rozstrzygania tego rodzaju konfliktów, np. traktowanie konfliktu jak błędu (Eiffel), ustalenie priorytetu ścieżek dziedziczenia, lokalna zmiana nazwy inwariantu (O2) lub kwalifikowanie konfliktowych własności przez nazwę klasy (C++). Metody te powodują jednak dalsze wady lub anomalie. Nie ma sposobu uzyskania pełnej spójności przy wielokrotnym dziedziczeniu. Przyczyna tego jest prosta: wielokrotne dziedziczenie oznacza zmieszanie w środowisku jednej klasy inwariantów z dwóch lub więcej środowisk, które mogą być niekompatybilne. To zmieszanie oznacza utratę informacji o związkach pomiędzy własnościami poszczególnych klas, co musi powodować patologie.

18
Rys.18. Hierarchia klas dla osób, pracowników, studentów i emerytów


1.9. Abstrakcyjne typy danych

Abstrakcyjny typ danych(abstract data type, ADT) jest pojęciem opartym na założeniu, że typ struktury danych jest skojarzony z operacjami działającymi na elementach tego typu. Nie istnieje potrzeba i możliwość używania operacji nie należących do oferowanego zestawu; operacje są kompletne i wyłączne (patrz: hermetyzacja). Bezpośredni dostęp do składowych takiej struktury danych nie jest możliwy, dzięki czemu jej szczegóły implementacyjne (np. zestaw i reprezentacja atrybutów) są niewidoczne. Nie jest możliwe przetwarzanie struktur ADT za pomocą operacji generycznych (generic), np. za pomocą operacji odzyskania wartości atrybutu (dereferencji) lub operacji podstawienia. Klasycznym przykładem abstrakcyjnego typu danych jest stos, wraz z operatorami takimi jak push (połóż element na wierzchołku stosu), pop (zdejmij element z wierzchołka stosu), top (odczytaj element znajdujący się na wierzchołku stosu) i empty (sprawdź, czy stos jest pusty). Po zadeklarowaniu lub utworzeniu zmiennej X jako stosu wszelkie operacje na tej zmiennej odbywają się poprzez powyższe cztery operatory.

Mechanizm ADT może odnosić się do wartości lub do obiektów. W wielu propozycjach (np. w systemach obiektowo-relacyjnych) ADT jest kojarzony z obiektowością. ADT jest cechą nowego standardu SQL-99 i stanowi o jego obiektowości. Do pewnego stopnia jest to uzasadnione: element należący do ADT jest zwykle pewną strukturą złożoną z wielu wartości i w tym sensie przypomina obiekt, zaś manipulowanie takim elementem wyłącznie za pomocą operatorów realizuje tę zasadę hermetyzacji, którą przypisuje się pojęciu klasy.

Z drugiej jednak strony, ADT może być uważany za pojęcie węższe lub ortogonalne w stosunku do pojęcia klasy. W szczególności, w czystej postaci ADT nie zakłada dziedziczenia (nie dotyczy to SQL-99), nie uwzględnia powiązań pomiędzy obiektami (wystąpieniami ADT), ani też ich tożsamości. ADT nie zajmuje się innymi inwariantami klas niż operacje. ADT nie zakłada również operatorów działających jednocześnie na wszystkich aktualnych wystąpieniach ADT (czyli na ekstensji klasy).

Pojęcie ADT jest istotnie różne od pojęcia typu (konkretnego), m.in. ze względu na różnice celów pragmatycznych tych dwóch pojęć. Wielu autorów plącze pojęcie ADT z pojęciem typu, co często prowadzi do nieporozumień. W dalszym ciągu wykładu nie będziemy używać tego pojęcia, uważając go za odmianę pojęcia klasy.

Copyrights © 2006 PJWSTK
Materiały zostały opracowane w PJWSTK w projekcie współfinansowanym ze środków EFS.