Interface - Interfejs jako klasa abstrakcyjna. ( IWZ, wftg - 02XII2004)
Powyższy tytuł ma podkreślić fakt, że omawiany temat dotyczy jednej z konstrukcji języka programowania (VB .NET, Java, C#).
Inne pojęcia interfejsu to interfejs w sensie technologii Microsoft COM (Component Object Model), interfejs klasy (publiczne składowe klasy), intefejs warstwy w architekturze wielowarstwowej (projektowanie systemów inf.), tzw. n-tier architecture, oraz interfejs użytkownika systemu informatycznego - składowa projektu informatycznego.
W sensie użytkowym znaczenia tego terminu pokrywają się, interfejs mówi bowiem o kontaktcie/styku między czymś a czymś, sprzęgu między jakimiś dwoma składnikami, między usługodawcą i usługobiorcą widzianym tu z punktu widzenia usługobiorcy - interfejs określa jak jawi mu się usługodawca w sensie dostarczanych usług - niezależnie od implementacji (techniki i technologii realizacji), która powinna być nieistotna - najlepiej ukryta, by nie kusiło usługobiorcy korzystać z nieudostępnionych, a przez to niepewnych i niegwarantowanych usług.
Tak też jest z interfejsem jako pojęciem języka programowania obiektowego - dotyczy projektowanego sprzęgu klas ale wg mnie ma trochę inną rolę do spełnienia, bo bardziej chodzi tu o usługodawcę niż usługobiorcę! Chodzi o wymagania stawiane usługobiorcy.
Gdyby w projektowaniu nie rozpatrywało się systemów jako współpracujących części (dekompozycja problemu), interfejs jako klasa abstrakcyjna nie byłby potrzebny. Zupełnie inaczej jest, gdy projektuje się uniwersalny składnik systemu, mający współpracować z innymi składnikami, z których pewne jeszcze nie powstały, a które będą musiały korzystać z tego uniwersalnego składnika. Poprzednie stwierdzenie można przedstawić inaczej. Projektowana klasa uniwersalna jest tworzona po to, by wykorzystać ją do tworzenia systemu, czyli użyć w tworzeniu nowych klas. Jest to więc usługodawca, a te nowe przyszłościowe klasy będą w tym kontekście grać rolę usługobiorców (np. obiekty klasy korzystające z ArrayList jako pojemnika są jej usługobiorcami w wyżej ustalonym sensie). Usługodawca dostarcza składowych publicznych, by można korzystać z jego usług (ArrayList „daje” metody Add, Remove, właściwość Count itd.). Czasami, i tak jest w przypadku ArrayList, usługodawca (obiekt-kontener) nie musi nic wiedzieć o usługobiorcach, czyli w tym przypadku o obiektach, które będą w nim umieszczane. (Zauważmy skrót myślowy: rzeczywistym (aktywnym) usługobiorcą jest obiekt, który wywołując metody usługodawcy umieszcza obiekty (pasywni usługobiorcy, użytkownicy) w kontenerze, a nie te obiekty !!! Ja jednak skrótowo usługobiorcami nazywam biernych usługobiorców. Jak inaczej nazwać tu obiekty będące w relacji „jeden typ obiektu, projektowany, będzie współpracował z innymi obiektami - jeszcze w pełni nie zdefiniowanymi”)?.
Można powiedzieć jeszcze inaczej: nadsystem (reprezentowany przez projektowaną klasę) musi współpracować z podsystemami, tu: nowymi klasami. Rzecz polega na tym, że nadsystem definiuje swoje USŁUGI tu i teraz, a INFORMACJE (jeśli będą mu potrzebne) muszą dostarczyć te przyszłe, jeszcze nie zbudowane klasy (obiekty tych klas). Pojawia się relacja skierowana w drugą stronę - nadklasa dostarcza usług (znowu ten usług-odawca) ale żąda też informacji od obiektów z którymi ma pracować. Dobrze określonych informacji, np. mój obiekt klasy kontenerowej KluczArrayList chce, by umieszczany w nim jako pojemniku obiekt (użytkownik - o może lepsze określenie niż usługobiorca) umiał podać swój klucz. Usługodawcy nie obchodzi, w którym polu obiektu będzie ten klucz. Usługodawca nie może grzebać w użytkowniku, by ten klucz wydobyć, bo użytkownika być może jeszcze nie zdefiniowano!!! Relacja w jednym kierunku (usługi): metod do wywołania dostarcza usługodawca; relacja w drugą stronę (informacje): metod do wywołania przez usługodawcę dostarczy użytkownik. Raz metody są definiowane w usługodawcy, drugi raz w użytkowniku. Ponieważ usługodawca jest projektowany wcześniej, to właśnie on określa czego będzie żądać od przyszłego użytkownika. Jego żądania są wyrażone w formie sygnatur funkcji/procedur jakie będzie chciał wywoływać „w” użytkowniku, by otrzymać konieczne informacje. Jest to więc projektowe chciejstwo, bo formalnie semantyka tych funkcji/procedur nie może zostać zdefiniowana - potrzebny jest dodatkowo opis nieformalny, opis co one mają robić. Z puntu formalnego zaś, syntaktycznego, chciejstwo to jest obudowane w konstrukcję zwaną interfejsem - Interface. Użytkownik chcąc usług usługodawcy musi zgodzić się na jego żądania, czyli implementować interfejs - zdefiniować u siebie te wszystkie funkcje/procedury, które wymienione są w konstrukcji zwanej Interface ... End Interface.
Uogólnieniem tego mechanizmu jest spojrzenie na usługodawcę jako na nadsystem (warstwę nadrzędną), który chce pracować z jednym z wielu przyszłych podsystemów (warstwą podrzędną). Narzuca on więc podsystemowi, co powinien spełniać, by współpraca (usługi/informacje - dwukierunkowy kontakt) była możliwa. Interface umożliwia zdefiniowanie kierunku przepływu informacji od warstwy podrzędnej do nadrzędnej. Widać wyraźnie, że potrzeba definiowania interfejsu pojawia się w momencie, gdy określa się przyszłą współpracę projektowanej warstwy z warstwą podrzędną. Np. podczas tworzenia klasy KluczArrayList pojawiła się potrzeba podania klucza przez przyszłego użytkownika, co znalazło wyraz w zdefiniowaniu interfejsu IKluczArrayList. Tak powstała para: KluczArrayList - IKluczArrayList. By stać się użytkownikiem KluczArrayList (móc umieszczać obiekty w tym kontenerze), trzeba w klasie tych obiektów zaimplementować IKluczArrayList.
Języki programowania mają siłą rzeczy związek z językami naturalnymi. W nauce o języku rozróżnia się trzy aspekty języka: syntaktykę, semantykę i pragmatykę (strukturę, znaczenie, zastosowanie).
Pierwsza zajmuje się strukturą konstrukcji języka, odpowiada na pytanie „co” jest tą konstrukcją. Druga odpowiada na pytanie „jak” działa dana konstrukcja języka. Pragmatyka natomiast odpowiada na pytanie „po co/ do czego” można użyć danej konstrukcji.
Przykład:
syntaktyka: pięć
semantyka: konkretna liczba znana nieanalfabetom
pragmatyka: „przyjadę piątką” , „daj piątkę”, „przybij piątkę”, „zdałem na piątkę”
Pierwsze dwa aspekty konstrukcji „Interface” są jasno określone w definicji języka VB .NET. Niestety w literaturze programistycznej niejasno określa się pragmatykę interfejsu - a dla programisty właśnie to jest niezbędne. Stąd niniejszy wywód, w którym sam sobie uświadamiałem rolę interfejsu w projektowaniu i programowaniu.
Jak ma się Interface do klas i dziedziczenia, do hermetyzacji i polimorfizmu?
Dziedziczenie pozwala na relację jednokierunkową (od dziecka do rodzica/ od podklasy do klasy bazowej), zaś w drugą stronę klasa bazowa potrafi używać informacji z klas podrzędnych tylko w ograniczonym zakresie (i jest to wysoce niepożądane ze względu na naruszanie hermetyzacji) dzięki polimorfizmowi. Klasa może też dowiedzieć się czegoś o innej klasie (uzyskać informację) dzięki zaprzyjaźnieniu (fraza „Friend” w definicji klasy).
Jak pokazałem wcześniej, uzyskiwanie informacji od użytkownika jest typową cechą systemu, i hierarchiczna struktura klas nie rozwiązuje w pełni tego problemu - stąd mechanizm (konstrukcja) interfejsu.
Syntaktyka interfejsu jest zbliżona do syntaktyki klasy abstrakcyjnej (użyto innych słów kluczowych). Semantyka jest inna - klasa abstrakcyjna ma definiować hierarchię klas - hierarchię dziedziczenia, a interfejs ma zapewniać informację zwrotną, jest żądaniem kontraktu - narzuceniem wymogów. Natomiast pragmatyka pozwala na zastosowanie klasy abstrakcyjnej w roli interfejsu. Tak robi się np. w języku C++, w którym jest dodatkowo wielodziedziczenie a brak konstrukcji Interface. Bo klasa może wymagać implementacji wielu interfejsów - a ponieważ w VB .NET/Java/C# nie ma wielobazowości, to nie można wykorzystać wielu klas abstrakcyjnych w tej roli, tak jak w C++.
Kontynuując aspekty pragmatyki Interface'u, oto kilka zastosowań interfejsu, choć w każdym można doszukać się tych podstaw, które zaprezentowałem wcześniej:
Użycie wewnątrzprogramowe.
Dzięki implementacji tego samego interfejsu obiekty różnych klas mogą być traktowane jednolicie (w domyśle - przez usługodawcę), bo można definiować tablicę referencji typu interfejsowego, do których podczepia się obiekty klas implementujących interfejs. Wtedy można na tych obiektach wykonać operacje zadekretowane przez interfejs, a zrealizowane (zaimplementowane) w każdej klasie być może trochę inaczej, zgodnie ze specyfiką klasy. Uzyskuje się wtedy efekt polimorficznego wykonania metody na zestawie obiektów, co jest charakterystyczne dla obiektów podklas podczepionych pod tablicę referencji ich nadklasy, czyli typowe wykorzystanie dziedziczenia klas. Przykładem są klasy różnych gatunków zwierząt w sklepie zoologicznym, będące (pod)klasami klasy Zwierzę (to dziedziczenie nie jest tu istotne ani konieczne), a jednocześnie implementującymi interfejsy IŻywność oraz ITowar. Przez to dla obiektu każdej klasy możemy określić np. jego cenę (użyte np. przez podsystem kasowy sklepu) czy określić wartość odżywczą 100g mięsa (???!!! - powiedzmy, że to Azja ...).
Kontakt między programem a systemem zrealizowanymi w technologii obiektowej.
Z jednej strony program aktywnie żąda usług systemu (otwarcie pliku, pisanie do pliku), a z drugiej system asynchronicznie (aktywność systemu) przekazuje dane wołając funkcje/procedury programu. Ta druga strona jest możliwa, gdy program implementuje interfejs zdefiniowany przez konstruktorów systemu. Jest to „obiektowy lukier” dla tzw funkcji typu call-back.
Wymiana informacji między obiektami.
Mechanizm interakcji między klasami/podsystemami wzajemnie wywołującymi swoje funkcje. Definiując po każdej stronie interfejs dajemy środki do wzajemnej komunikacji.
Ta analiza doprowadza ostatecznie do powstania koncepcji nowego wzorca projektowego - Design Patterns (meta-konstrukcja projektowa), który nazwę DWUKIERUNKOWYM DYNAMICZNYM INTERFEJSEM WARSTWY. Dopiero ten wzorzec prezentuje właściwie rolę i sens Interface'u.
Podstawą jest spojrzenie na system jako współpracę dwóch warstw („cięcie” na warstwy jest umowne i zależy od bieżącej fazy projektowania systemu). Górną warstwę nazywam Usługodawcą, dolną Klientem. Usługodawca to przynajmniej jedna klasa, która realizuje usługę. Klient to przynajmniej dwie klasy: Zleceniodawca i Usługobiorca. Nie wykluczone jest, że obie te funkcje pełni jedna klasa. Zleceniodawca to podmiot usługi, (aktywizujący składnik) a przedmiotem usługi (biernym składnikiem) jest Usługobiorca. Wykonawcą jest Usługodawca - wykonuje zlecenie Zleceniodawcy używając Usługobiorców.
D. D INTERFEJS WARSTWY polega na dwukierunkowej wymianie informacji między już określoną warstwą nadrzędną a warstwą przyszłą, podrzędną: inicjatorem z jednej strony jest Zleceniodawca , a z drugiej Usługodawca. Mechanizmem Zleceniodawcy są dostarczane przez warstwę Usługodawcy metody. Mechanizmem Usługodawcy są dostarczane przez warstwę Klienta metody w postaci metod Usługobiorcy.
Usługodawca (in statu nascendi - właśnie projektowana warstwa!) dostarcza swoich metod (jeden kierunek wymiany) i zapewnia sobie możliwość użycia metod przyszłego Klienta (Usługobiorcy) definiując Interface, czyli zestaw wymagań, które musi spełnić warstwa podrzędna, by współpraca zapewniała drugi kierunek wymiany). Jeśli aktywność Usługodawcy jest potrzebna (drugi kierunek) to musi on zdefiniować Interface. Przyszły Klient (Usługobiorca) musi zaś go zaimplementować. W praktyce Usługodawca realizuje swoją aktywność w stosunku do warstwy podrzędnej (Klienta) definiując u siebie referencję typu Interface i po podłączeniu do niej obiektu Usługobiorcy wywołuje metody Usługobiorcy zdefiniowane przez ten Interface.
Interface to wymagania warstwy nadrzędnej w stosunku do warstwy podrzędnej, wymagania warstwy projektowanej w stosunku do warstwy z niej w przyszłości skorzystającej. Dlaczego podkreślam dynamiczny aspekt sprawy, warstwę podrzędna przyszłą a nie istniejącą? Gdyby chodziło o dwie dobrze zdefiniowane a więc nierozwojowe warstwy, to w obu można umieścić usługi na etapie projektu i będą mogły się wzajemnie, dwukierunkowo, wywoływać i żaden Interface nie jest tu potrzebny. Nie trzeba stawiać wymagań jak wszystko jest dogadane teraz.