Języki i Środowiska Programowania Baz Danych | |||||||||||||
|
1. Rozszerzenie SBQL w modelu M1 Dotychczasowe definicje operatorów języka SBQL nie uwzględniają pojęć klasy i dziedziczenia. Pokażemy, w jaki sposób można je łatwo rozszerzyć na powyższe własności. W następnych podrozdziałach omówimy SBQL dla modeli składu M1 i M2. Konsekwencje pojęcia hermetyzacji dla SBQL (określonego modelem M3) musimy odłożyć na nieco później, kiedy będziemy omawiać zanurzenie zapytań w zdania imperatywne i abstrakcje programistyczne, takie jak procedury i metody, ponieważ pojęcie hermetyzacji daje się wykorzystać dopiero wtedy, gdy definiowany język obejmuje metody. Model składu M1 wprowadza pojęcie klasy jako specjalnej struktury danych przechowywanej w ramach składu obiektów. Klasy przechowują inwarianty obiektów. Istotnym inwariantem przechowywanym w ramach klasy jest nazwa obiektów tej klasy. Klasy i obiekty są połączone związkami dziedziczenia. Model M1 powoduje większą złożoność wprowadzanych przez nas definicji. Jak zobaczymy, z tego punktu widzenia model M2 jest bardziej korzystny, ale też mniej popularny niż M1. Powodem zwiększonej złożoności definicji dla modelu M1 jest to, że jest on przede wszystkim podstawą języków i systemów obiektowych zakładających wczesne (statyczne) wiązanie, tj. wiązanie w czasie analizy składniowej i kompilacji. Często w takiej sytuacji klasa jest bytem drugiej kategorii programistycznej, tj. nie jest istotna w czasie wykonania programu; jest własnością źródłowego tekstu programu, a nie jego wykonywalnego kodu. Ta własność zwykle komplikuje rozważania semantyczne. W naszym podejściu model M1 został przystosowany do koncepcji, w której wiązanie jest późne (dynamiczne). Dodatkowo, w klasycznych modelach języków programowania nazwy obiektów nie są inwariantami klas; np. w C++ i Java dla danej klasy można powołać obiekt o dowolnej nazwie. Te okoliczności powodują, że semantyka języków opartych na modelu M1 wykazuje anomalie; dotyczy to nie tylko języków zapytań, ale również języków programowania (np. anomalie związane z wielokrotnym dziedziczeniem). Aby przystosować model M1 dla potrzeb rozwijanej przez nas semantyki języków zapytań, należy zmodyfikować regułę wiązania nazw. Chodzi o zachowanie zasady zamienialności (substitutability), która mówi, że obiekt klasy podrzędnej jest jednocześnie obiektem klasy nadrzędnej. Przykładowo, obiekt Prac jest jednocześnie obiektem Osoba. Informacja o tej zależności jest przechowywana w modelu M1 w dwóch miejscach, Rys.28 i Rys.29. Pierwszym z nich są klasy, które przechowują nazwy należących do nich obiektów. Drugim miejscem przechowywania tej informacji są związki dziedziczenia zaznaczone na Rys.28 jako relacje KK i OK. W terminach operacji na stosie zmieniona reguła wiązania oznacza, że o ile w pewnej sekcji stosu znajduje się binder o nazwie Prac, zaś wiązana jest nazwa Osoba, to binder ten uczestniczy w procesie wiązania tak samo, jakby był to binder o nazwie Osoba. Alternatywnie możemy uważać, że w danej sekcji stosu środowiskowego wraz z binderem Prac(v) pojawia się w tej samej sekcji binder Osoba(v). Ta zasada powinna być uogólniona na dowolną liczbę poziomów hierarchii dziedziczenia oraz na wielokrotne dziedziczenie. Klasy wprowadzają nowe zasady otwierania i usuwania sekcji na stosie środowisk. Jeżeli operator niealgebraiczny przetwarza obiekt posiadający identyfikator i, to na wierzchołek stosu środowisk wkładane są, jak poprzednio, bindery określone przez nested(i). Oprócz tego, dla umożliwienia wiązania własności przechowywanych w ramach klas, poniżej wierzchołka stosu są ulokowane sekcje z binderami do własności klas tego obiektu w odpowiedniej kolejności, Rys.57. Po zakończeniu przetwarzania wszystkie te sekcje są usuwane ze stosu. Zwróćmy uwagę, że podana koncepcja automatycznie uwzględnia własność przesłaniania (overriding). Jeżeli np. klasa K1 zawiera pewną metodę m, która przesłania metodę m zawartą w klasie K2, to zgodnie z przedstawioną kolejnością przeszukiwania stosu przy wiązaniu nazwy m związana zostanie metoda m z klasy K1; metoda m z klasy K2 będzie niewidoczna.
Rys.57 przedstawia wizję stanu stosu od strony koncepcji semantyki języka zapytań. Wizja ta może być bezpośrednio zaimplementowana, ale jest ona dość rozrzutna, w związku z czym w staranniejszej implementacji i po zabiegach optymalizacyjnych organizacja tego stosu oraz operacje na tym stosie mogą wyglądać zupełnie inaczej. W tym wykładzie nie będziemy jednak zajmować się zbyt szczegółowo zagadnieniami implementacyjnymi. Rys.58 ilustruje tę sytuację dla przypadku, gdy operator niealgebraiczny, np. where w zapytaniu:
przetwarza obiekt Nowaka dla bazy danych przedstawionej na Rys.28 i Rys.29. Sekcja binderów do obiektów bazy danych zawiera bindery do obiektów i4 oraz i9 opatrzone zarówno nazwą Prac, jak i nazwą Osoba. Jest to jeden z wariantów uwzględnienia omawianej wcześniej zasady zamienialności. Dodatkowe bindery Osoba pojawiają się na podstawie nazw obiektów będących inwariantami klas oraz związku KK (patrz Rys.28).
Zwykle w różnych językach obiektowych zakłada się, że jeżeli wiązana jest nazwa Osoba, to kontrola typologiczna ograniczy możliwość użycia własności obiektu Prac nie będących własnościami obiektów Osoba. Dla języków pozbawionych mocnej kontroli typów nazwy Osoba i Prac w takim podejściu są traktowane de facto jako synonimy, co zdaniem niektórych autorów może prowadzić do anomalii, np. wiążąc nazwę Osoba mamy jednocześnie dostęp do takich własności, jak Zar i ZarNetto. Powstaje pytanie, czy jest to sprzeczne z jakimiś wypracowanymi zasadami obiektowości. Jak się okazuje z przykładów, pozbawienie możliwości dostępu do niektórych atrybutów obiektu Prac z tego powodu, że dostaliśmy się do niego poprzez nazwę Osoba, jest niewygodne i powoduje znaczne skomplikowanie niektórych zapytań. Jakkolwiek systemy bez kontroli typów są z kilku punktów widzenia niekorzystne i przez to niezbyt dla nas interesujące, przyjmiemy jednak, że muszą istnieć środki językowe pozwalające programiście na stwierdzenie, z jakim obiektem ma do czynienia (Osoba czy Prac) oraz odwołanie się do atrybutów, które zostały sztucznie "ukryte" ze względu na kontrolę typologiczną. Można to rozwiązać na kilka sposobów. Najprostszy polega na tym, że funkcja nested zwraca bindery do wszystkich własności obiektu, niezależnie od tego, w jaki sposób referencja do tego obiektu została odzyskana, zaś programista ma środki, aby ustalić w programie typ, klasę lub nazwę obiektu. Np. można wprowadzić funkcję objectName(i), która dla zadanego identyfikatora obiektu i (ustalonego poprzez zapytanie) zwróci nazwę tego obiektu w postaci stringu. Podaną wyżej dyskusję można też potraktować jako zarzut pod adresem modelu M1 i anomalii semantycznych, które on powoduje. Anomalie te radykalnie usuwa model M2.
|
P10.2. | procedure met(p1: T1, p2: T2 ): T { |
Niech ta metoda będzie wywoływana w zapytaniu:
P10.3. | q q ...met(q1, q2) ... |
w kontekście operatora niealgebraicznego q; q, q1, q2 są podzapytaniami.
Rys.59. Stany stosów ENVS i QRES podczas przetwarzania metody
Niech eval(q) zwróci bag{ r1, r2, ....}, gdzie r1, r2, .... są referencjami do obiektów będących członkami klasy K. Rys.59 przedstawia kroki przetwarzania dla r1. Górna część rysunku przedstawia stany ENVS, dolna - stany QRES. Najpierw ewaluowane jest q; w wyniku powstaje nowa sekcja na QRES. Mechanizm następnie wchodzi w pętlę iteracjyjną po r1, r2, .... W każdym obrocie tej pętli następuje włożenie na ENVS binderów do publicznych własności klasy K oraz binderów do wnętrza aktualnie przetwarzanego obiektu (rysunek przedstawia przetwarzanie dla obiektu r1). W tym środowisku następuje wiązanie nazwy met (wśród publicznych własności klasy K), ewaluacja parametrów metody oraz wywołanie metody. W wyniku wywołania na stos ENVS wkłada się sekcję z binderami do prywatnych własności klasy K oraz rekord aktywacyjny metody met, składający się z binderów zawierających obliczone parametry oraz binderów do lokalnych obiektów x1 i x2. Podczas ewaluacji sekcje inne niż wymienione oraz inne niż sekcje globalne powinny być przesłonięte (czarny prostokąt na ENVS) ze względu na reguły zakresu (będą omówione później). Po zakończeniu działania metody sterowanie wraca do przetwarzania zapytania; sekcje lokalne metody zostają usunięte ze stosu ENVS, rezultat metody jest ulokowany na wierzchołku QRES. Zwrócimy uwagę, że ciało metody ma dostęp do aktualnie przetwarzanego obiektu poprzez sekcję zawierającą nested(ri). Wynika stąd, że wewnątrz tego ciała programista może bezpośrednio używać nazw podobiektów tego obiektu. W niektórych językach identyfikacja aktualnie przetwarzanego obiektu odbywa się poprzez specjalne słowo kluczowe, np. self lub this. Przyjęcie tego założenia ma minimalne konsekwencje dla objaśnionego wyżej mechanizmu. Oznacza wyłącznie to, że powyżej sekcji zawierającej bindery obliczone poprzez nested(ri) będzie także sekcja (skojarzona z wywołaniem metody met) zawierająca pojedynczy binder o postaci self(ri). Traktujemy w tym przypadku self jako pomocniczą nazwę identyfikującą dany obiekt, podobnie jak w zapytaniu q as self.
Niekiedy może nam zależeć na pobraniu identyfikatora metody, a nie na jej wywołaniu, np. w celu przekazania metody jako parametru. W tym celu potrzebna jest specjalna składnia, np. ref Wiek. Podsumowując:
Jeżeli pewna klasa dziedziczy z większej liczby klas, to na stosie pojawią się sekcje klas nadrzędnych w pewnej (niekiedy dowolnej) kolejności. Możliwe są dwa przypadki:
Rys.60. Konflikt nazw przy wielodziedziczeniu
Podane wady wielokrotnego dziedziczenia są nieuchronne dla modelu M1 i nie dadzą się usunąć w jakikolwiek sposób. Wynikają one z faktu zmieszania w jednym środowisku własności różnych klas, często niekompatybilnych. Jest to przyczyna tego, że w niektórych językach i systemach zrezygnowano z wielokrotnego dziedziczenia. Jednakże nie usuwa to problemu, ponieważ w ten sposób zwiększa się dystans pomiędzy modelowaniem pojęciowym (dla którego wielokrotne dziedziczenie jest naturalne) a modelem implementacyjnym. Jest to niekorzystne zarówno z punktu widzenia efektywności programowania, czytelności programu, jak i jego pielęgnacyjności. Brak wielokrotnego dziedziczenia ogranicza zasadę obiektowości znaną jako "zasada otwarte-zamknięte" (open-close principle). Zasada ta mówi, że klasy na pewnym etapie powinny być zamknięte dla modyfikacji, ale otwarte dla rozszerzeń poprzez możliwość utworzenia ich specjalizacji. Zasadę tę również łamie zakaz umieszczania w klasach nadrzędnych własności o tych samych nazwach. Brak wielokrotnego dziedziczenia ogranicza możliwość tworzenia specjalizacji klas. Zasada otwarte-zamknięte jest uważana za podstawową z powodu możliwości ponownego użycia (reuse).
Model M1 jest stosowany w większości języków i systemów obiektowych (z różnymi mutacjami i pod różnymi nazwami). Niezależnie od tego, czy zezwala on na wielodziedziczenie (C++, OMG CORBA, standard ODMG, standard SQL-99) czy też nie (Smalltalk, Java), zawsze pojawią się w nim pewne wady bądź w zakresie modelowania pojęciowego, bądź też w postaci anomalii technicznych tworzących rafy dla programistów. Radykalnym sposobem usunięcia tych wad jest przyjęcie modelu M2 (i modeli pochodnych), zakładających koncepcję dynamicznych ról obiektów.
Rys.61. Schemat obiektowej bazy danych
Rys.61 przedstawia schemat obiektowej bazy danych zapisany w modyfikowanym UML, w którym liczności są podane nie tylko dla asocjacji, lecz dla dowolnych obiektów. Asocjacje PracujeW/Zatrudnia oraz Kieruje/Szef są zrealizowane jako bliźniacze obiekty pointerowe przechowywane wewnątrz odpowiednich obiektów. Atrybut Zar może nie wystąpić, atrybuty Stan i Lokacja mogą wystąpić dowolną liczbę razy, poczynając od 1.
P10.4. | Podaj nazwę działu i średni wiek jego pracowników (dziedziczenie metody Wiek przez klasę Prac): |
P10.5. | Podaj nazwiska, zarobek netto i nazwisko szefa dla programistów pracujących w Radomiu (dziedziczenie atrybutu Nazwisko przez klasę Prac): |
P10.6. | Dla każdego pracownika podaj nazwisko oraz procent budżetu przeznaczony na jego uposażenie. |