Projektowanie i wytwarzanie aplikacji internetowych

Część 3

Sebastian Wyrwał

Porównując rozwój i działanie oprogramowania z działaniem
sprzętu można dojść do wniosku, że ten ostatni rozwija się lepiej i jest
bardziej niezawodny. Podczas gdy podzespoły elektroniczne stawały się coraz
szybsze, mniejsze i tańsze oprogramowanie stawało się wolniejsze, większe i
droższe. Rozwiązaniem tego problemu, dotyczącego również systemów
rozproszonych było podjęcie prac standaryzacyjnych. Nastąpił znaczny rozwój
inżynierii oprogramowania wraz z metodami specyfikacji i projektowania systemów
informatycznych. Powstały różne koncepcje rozwiązań dotyczących systemów
rozproszonych, ich projektowania i implementacji. takie jak:

  • Komponenty (ang. components) – Są one gotowe do użycia
    i dostarczają określonej funkcjonalności np. przeglądarki plików
    graficznych. Powinny mieć dobrze zdefiniowany sposób komunikowania się ze
    światem zewnętrznym, a ich implantacja pozostaje ukryta. Można przyjąć,
    że odpowiadają one obiektom w modelu RM-ODP. Na poziomie elementów
    elektronicznych analogią do nich są układy scalone.

  • Ramy (ang. framework) – są szkieletami aplikacji
    o określonej misji np. edytora tekstów. Rama definiuje strukturę
    aplikacji i sposób współdziałania jej składowych. Implementacja
    konkretnego rozwiązania polega na nadpisaniu części funkcji lub/i
    zdefiniowaniu pewnych obiektów. Przykładem mogło by być wygenerowanie
    „z palca” szkieletu aplikacji przy użyciu narzędzi takich jak MS VC++
    lub Borland C. Aplikacja taka przypomina mniej lub bardziej edytor tekstu.

  • Wzorce (ang. patterns) stanowią rozwiązania
    powtarzających się problemów z jakimi spotykają się projektanci. Dotyczą
    one np. tworzenia obiektów w aplikacjach, w których sposób prezentacji
    graficznej ma być konfigurowany w czasie działania aplikacji. Są bardziej
    ogólne niż ramy, i w mniejszym stopniu determinują projekt całej
    aplikacji. Przed użyciem muszą być zaimplementowane w języku
    programowania. Analogią do wzorca jest schemat obwodu elektronicznego w książce,
    który realizuje pewną funkcję np. jest filtrem, wzmacniaczem itp.

  • Architektury (ang. architectures) określają sposób
    współpracy wzorców i komponentów mających na celu zapewnienie określonej
    funkcjonalności np. dostępu do obiektów w systemach zdalnych.
    Architektura może być określana przez wiele składowych różnych typów.

Przedstawione tu koncepcje nie tworzą hierarchii, mają
natomiast pokazać różne, często przenikające się idee. Należy zdać sobie
sprawę, że podział jest umowny i zależy od przyjętego stopnia abstrakcji.
Czym innym jest komponent dla jego użytkownika, a czym innym dla zespołu
ludzi, którzy go projektują.

Ceną płaconą za ułatwienia w procesie tworzenia aplikacji
są ograniczenia z jakimi należy się pogodzić stosując daną technologię, język
lub bibliotekę.

W artykule przedstawiono architekturę CORBA jako przykład
standaryzacji w obrębie systemów rozproszonych i konkretne rozwiązanie oparte
o tę architekturę. Zaprezentowano zadanie, które polega na stworzeniu
prostego systemu informatycznego, służącego do rejestracji zdalnej umów o świadczenie
usług telekomunikacyjnych. Do realizacji tego zadania została użyta
architektura CORBA a językiem implementacji jest język Java, popularny i
wygodny w użyciu język obiektowy, używany do tworzenia aplikacji
rozproszonych. Trzeba pamiętać, że CORBA jest specyfikacją i jej użycie
zawsze wiąże się z konkretnymi narzędziami i językami programowania.
Prezentowany przykład jest zorientowany na zagadnienia związane z komunikacją
i pomija budowę interfejsu użytkownika oraz współpracę z bazą danych.
Celem przykładu jest również pokazanie schematu postępowania w celu użycia
architektury CORBA i przykładowych narzędzi z nią związanych. Poza
prezentacją przykładu pokazano prosty wzorzec – fabrykę obiektów (ang. Factory),
która jest często stosowana wraz z architekturą CORBA, zapewnia bowiem
wygodny mechanizm tworzenia obiektów w systemie zdalnym.

Java

Java jest językiem obiektowym i wywodzi się z języka C++,
z którego przejęła pojęcie klasy. Właściwości obiektów definiują klasy
i tylko przy ich pomocy można tworzyć nowe typy danych. W języku Java
deklaracja i definicja klasy są łączne i znajdują się w pliku z
rozszerzeniem *.java. Nazwa pliku musi być taka sama jak nazwa
publicznej klasy, która znajduje się w tym pliku. Plik z definicją publicznej
klasy TeSt1 musi nazywać się TeSt1.java, ponieważ duże i małe
litery są rozróżniane. Klasy mogą być grupowane w pakiety, co dostarcza
wygodnego mechanizmu strukturyzacji programu i tworzenia bibliotek. Prosta klasa
wygląda następująco:

import java.io.*; // włączenie biblioteki io, realizującej
operacje we/wy
public class A
{
public void do_it(String imie){
System.out.println("A: "+imie); // wyświetlenie łańcucha znaków
}
}

Nazywa się ona A, jest klasą publiczną zdefiniowaną
w pliku A.java.1  Posiada ona jedną metodę do_it, która
wypisuje łańcuch znaków, poprzedzony napisem A: . Ponieważ klasa ta
korzysta z mechanizmów wejścia wyjścia, niezbędne jest włączenie pakietu java.io
definiującego te operacje.

W celu bardziej szczegółowego scharakteryzowania
opisywanego języka potrzebny jest punkt odniesienia. Może nim być właśnie język
C++ jako uznany język implementacji systemów informatycznych i protoplasta języka
Java. Aby dokładniej przedstawić język Java, poniżej opisano wybrane jego
cechy, skupiając się bardziej na semantyce niż podobnej do C++ składni. Właściwości
języka są bowiem bardziej istotne, niż sposób zapisu konkretnych
konstrukcji, zwłaszcza przy wyborze języka, który będzie użyty do
implementacji.

  • Java jest językiem „czysto” obiektowym.
    Wymusza to staranniejsze projektowanie. Trudno bowiem „łatać” kod
    dopisywanymi „na prędce” funkcjami. Z drugiej strony może to prowadzić
    do nadmiarowości; nie wszystko bowiem musiałoby być obiektem gdyby nie
    wymagał tego sam język. Załóżmy, że w systemie potrzebna jest funkcja
    do wyznaczania maksimum dwóch liczb. W języku C++ można ją bez trudu
    zdefiniować, w języku Java trzeba natomiast zdefiniować klasę, która będzie
    posiadać taką metodę. Nie ma w niej bowiem globalnych metod lub danych a
    każdy definiowany przez użytkownika typ danych jest klasą, co implikuje
    czysto obiektowy styl programowania.

  • Hermetyzacja pozwala na ukrycie szczegółów
    implementacyjnych klasy i ich oddzielenie od definicji interfejsu klasy. Nie
    różni się ona w zasadzie od hermetyzacji w języku C++. W obu językach w
    obrębie klas mogą występować składowe publiczne, prywatne i chronione,
    określane odpowiednio słowami kluczowymi public, private, protected,
    z tym że w języku Java dostęp określa się osobno dla każdej składowej
    np.:

    class Kolekcja{
    //impementacja klasy
    private int[] dane; // tu przechowujemy dane
    //interfejs klasy
    public void Get(int gdzie){}
    public void Sort(){sort();}{}//używa sort()
    //chroniony
    protected void sort(){}
    protected void Set(int val, int gdzie){}//potrzebna przy sortowaniu
    }

    • Składowe prywatne nie są dostępne z zewnątrz i
      implementują one działanie klasy. W klasie Kolekcja występuje
      zmienna dane, jest ona jednak niewidoczna dla innych klas.

    • Składowe chronione są dostępne z zewnątrz tylko dla
      klasy, która dziedziczy z danej klasy i dla innych klas zdefiniowanych w
      tym samym pakiecie co dana klasa.2  Służą one zasadniczo do
      implementacji klas pochodnych.3  Niech metoda sort
      implementuje prosty algorytm sortowania; można zaimplementować własny
      algorytm tworząc klasę która dziedziczy z klasy Kolekcja i
      definiując metodę sort. Zmiany w obrębie składowych prywatnych
      i chronionych nie pociągają za sobą zmian w kodzie korzystającym z
      klasy.

    • Składowe publiczne są dostępne z zewnątrz klasy bez
      żadnych ograniczeń i definiują interfejs klasy; przykładowo interfejs
      klasy Kolekcja definiują funkcje Get, Sort. Zezwalamy użytkownikowi
      klasy na pobieranie danych (metoda Get)
      i ich sortowanie (metoda Sort) lecz nie zezwalamy na zmianę lub
      wstawianie własnych danych.

  • Dziedziczenie pozwala na modyfikację istniejącej
    już klasy oraz na grupowanie pewnych wspólnych właściwości klas – oznacza
    się je w języku Java słowem kluczowym extends. Zdefiniowana poniżej
    klasa B dziedziczy z klasy A. (jej definicja znajduje się w pliku
    B.java)
    import java.io.*;
    public class B extends A{
    public void do_it(String imie){
    System.out.println("B: "+imie);
    }
    }
    W języku Java klasa może dziedziczyć co najwyżej z jednej
    klasy, podczas gdy w języku C++ dopuszczalne jest dziedziczenie wielokrotne, z
    którego jednak w praktyce korzysta się rzadko. Zamiast niego, w języku Java
    istnieje możliwość zdefiniowania interfejsu, który odpowiada klasie
    abstrakcyjnej z C++. Interfejs deklaruje metody ale ich nie definiuje. Klasa może
    implementować wiele interfejsów, co oznacza że definiuje ona wszystkie metody
    zadeklarowane w danym interfejsie. Przykładowy interfejs myInt, który
    definiuje jedną operację f() wygląda następująco:
    public interface myInt{
    public void f();
    }
    Klasa C, która go implementuje przedstawiona jest
    poniżej:
    import java.io.*; // m.in. obsługa we/wy
    public class C implements myInt{
    public void f(){
    System.out.println("C f()");
    }
    }
    Ponieważ interfejs jest klasą (abstrakcyjną), między
    interfejsami może zachodzić relacja dziedziczenia.

  • Polimorfizm łączy się z dziedziczeniem i pozwala
    na jeszcze silniejsze niż hermetyzacja rozdzielenie interfejsu klasy od
    implementacji. Klasa pochodna jest zgodna z klasą bazową w sensie przypisania,
    co oznacza, że referencji do klasy bazowej można przypisać referencje do
    klasy pochodnej a wszędzie tam gdzie może wystąpić obiekt klasy A może
    również wystąpić obiekt klasy B. Pozwala to na traktowanie klas
    pochodnych i klasy bazowej jako jednego typu. W języku Java wywołania metod są
    domyślnie wirtualne; oznacza to, że nawet w przypadku referencji do klasy
    bazowej, wywoływana jest metoda klasy, której obiekt został przypisany do tej
    referencji a nie klasy bazowej. Ilustruje to poniższy program, który korzysta
    ze zdefiniowanych wcześniej klas A i B:

    import java.io.*;
    public class test{
    public static void main(String args[]){
    B b = new B(); //utworzenie obiektu klasy B
    A a = b; // przypisanie jego referencji do ref. do klasy A
    a.do_it("Ala"); //wywołanie metody do_it
    }
    }
    Wykonanie tego programu odpowiada wywołaniu metody main
    klasy test (zapisanej w pliku test.java) przez wirtualną maszynę
    Javy. Funkcja main zadeklarowana jest jako static co oznacza, że
    do jej wywołania nie jest potrzebny obiekt klasy. Powyższy program po
    uruchomieniu wypisze: B: Ala, świadczy to, że została wywołana metoda
    do_it klasy B, chociaż wywołano ją na rzecz referencji do
    bazowej klasy A. W C++ dla osiągnięcia takiego rezultatu
    należało użyć specyfikatora virtual, jako że metody w tym języku
    nie są domyślnie wirtualne.
    Klasa jest również zgodna, w sensie przypisania, z
    interfejsem jaki implementuje, co oznacza, że referencji do tego interfejsu można
    przypisać referencje do tej klasy. Np. referencji do interfejsu myInt można
    przypisać referencję do klasy C tzn: myInt mi = new C(); Wywołania
    funkcji interfejsu są również wirtualne.

  • Predefiniowana klasa Object jest
    korzeniem hierarchii wszystkich klas a referencja do obiektu każdej klasy może
    być przypisana referencji do obiektu tej klasy np.: Object o=new A(); Umożliwia
    to definiowanie abstrakcyjnych typów danych, a w szczególności korzystanie z
    kolekcji jakich dostarcza język Java (takich jak np. Vector). Trzeba
    bowiem pamiętać, że w języku Java w odróżnieniu od języka C++ nie można
    definiować typów abstrakcyjnych, które się parametryzuje w momencie użycia.

  •   Język Java zapewnia dynamiczną identyfikację typów.
    W czasie wykonania programu można sprawdzić, czy obiekt jest danego typu.
    Predefiniowana w języku klasa Vector jest kolekcją służącą do
    przechowywania obiektów różnych typów. Załóżmy, że klasa Kierownik
    dziedziczy z klasy Pracownik, implementując dodatkowo funkcję ilu_podwladnych,
    której nie ma klasa pracownik tzn.:
    class Kierownik extends Pracownik{
    public int ilu_podwladnych(){return 100;}
    }
    Aby móc ją bezpiecznie wywołać, należy przy pobieraniu
    danych z wektora sprawdzić ich typ, tzn. czy dany obiekt jest klasy Pracownik
    czy Kierownik. Ogólnie problem taki występuje zawsze przy
    rzutowaniu w dół tzn. od typu bardziej abstrakcyjnego to typu mniej
    abstrakcyjnego. W języku Java istnieje operator instanceof zawracający
    wartość true jeżeli obiekt jest danego typu; np.
    if (o instanceof Kierownik){
    Kierownik k = (Kierownik)o;
    System.out.println(k.ilu_podwladnych());
    }
    Jeżeli obiekt o jest klasy Kierownik to
    dokonuje się jawnej konwersji do tej klasy, a następnie wywołuje się metodę
    ilu_podwladnych. Język C++ nie zapewnia dynamicznej identyfikacji typów,
    a w popularnych bibliotekach napisanych w tym języku osiąga się ją kosztem
    dużego narzutu w sensie ilości kodu.

  • Manipulowanie obiektami w języku Java odbywa się
    za po-mocą referencji zwanych niekiedy odnośnikami. Obiekty mogą być
    tworzone wyłącznie dynamicznie, na stercie (ang. heap) przy użyciu operatora new.
    Nie istnieje natomiast operator delete, służący do niszczenia obiektów,
    ani pojęcie destruktora. Obiekty są niszczone, a pamięć zwalniana
    automatycznie. Pozwala to zmniejszyć ilość błędów związanych z
    przydzielaniem pamięci. Jeżeli klasa musi wykonać „porządek po sobie”
    zwalniając przydzielone zasoby, to implementuje się dla niej metodę finalize().
    Podczas wykonywania programu w języku Java, maszyna wirtualna nie ładuje na
    początku wszystkich klas występujących w programie, czyniąc to w miarę
    potrzeby gdy tworzone są obiekty tych klas. Definicję klasy można załadować
    również „ręcznie” przy pomocy instrukcji forName klasy Class
    np.: Class.forName(„sun.jdbc.odbc.JdbcOdbcDriver”);

  • Język Java zapewnia elegancką obsługę sytuacji wyjątkowych,
    która nieznacznie tylko różni się od znanej z języka C++. Do wydzielenia
    bloku, w którym mają być wyłapywane wyjątki służy instrukcja try,
    a następująca po niej instrukcja catch wprowadza blok obsługi wyjątku
    danego typu. W języku Java wprowadzono nie istniejącą w języku C++ instrukcję
    finally, która wprowadza blok wykonywany bez względu na to, czy wyjątek
    wystąpił czy nie. Jest to konieczne, ponieważ w języku Java nie istnieją
    destruktory. Przykładowy fragment kodu
    w języku Java z obsługą wyjątków wygląda następująco:
    try{
    d.do_sth();
    }
    catch(Exception e){
    e.printStackTrace(System.out);// wyświetla sekwencje wywołań metod, //które
    doprowadziły do wyjątku
    }
    finally{
    System.out.println("końcowe porządki");
    {
    zakładając, że d jest obiektem klasy D, która
    jest zdefiniowana następująco:
    class D{
    public int do_sth() throws Exception
    {
    // coś tu robimy, jak nie wyjdzie to rzucany jest wyjątek
    }
    }

Wymienione powyżej cechy języka odnoszą się do
projektowania i programowania. Z punktu widzenia systemów rozproszonych, bardzo
istotna jest przenośność między platformami sprzętowymi. Aplikacje
napisane w języku Java nie są (z założenia) wykonywane przez system
operacyjny, a przez wirtualną maszynę Javy, co zapewnia przenośność między
platformami sprzętowymi, czyniąc z niej wygodny język dla osób tworzących
aplikacje internetowe. Język Java jest dużo prostszy niż język C++, a jego
opanowanie praktyczne zajmuje mniej czasu.

Biorąc pod uwagę sposób wykonania programów napisanych w
języku Java możemy je podzielić na:

  • aplikacje stanowiące samodzielny byt

  • aplety wykonywane przez przeglądarkę internetową.

Prosta aplikacja została zaprezentowana przy okazji
polimorfizmu, kod najprostszego apletu wygląda następująco:

import java.awt.*; // programowanie graficzne
import java.applet.*; // programowanie apletów
public class Aplet1 extends Applet
{
public void paint(Graphics g)
{
g.drawString("Hello World", 20, 20);// wyrysowanie łańcucha znaków
}
}

Klasa Applet jest klasą bazową dla apletów, a jej
metoda paint, przedefiniowywana w klasach potomnych, odpowiada za
wyrysowanie apletu na urządzeniu graficznym reprezentowanym przez obiekt klasy Graphics.
Przykładowa strona w języku HTML „korzystająca” z tego apletu wygląda
następująco:

<HTML>
<HEAD>
<TITLE>Aplet1 Example1</TITLE>
</HEAD>
<BODY>
<H1>Aplet1</H1>
<HR>
<P>
<APPLET CODE="Aplet1.class" WIDTH="300" HEIGHT="300">
</APPLET>
</P>
<HR>
</BODY>
</HTML>

Corba

Architektura CORBA jest standardem stworzonym przez OMG (ang.
Object Management Group). Celem tej grupy było stworzenie specyfikacji umożliwiającej
integrację różnych systemów obiektowych i współpracę obiektów
stworzonych przez różnych dostawców.

Obiekty standardu CORBA, w odróżnieniu od obiektów znanych
z języków programowania mogą:

  • znajdować się gdziekolwiek w sieci,

  • na różnych platformach sprzętowych i systemowych,

  • być zaimplementowane w różnych językach programowania.

Ważnymi częściami składowymi specyfikacji CORBA są:

  • ORB (ang. Object Request Broker),

  • Interfejsy ORB,

  • Adaptery obiektowe (ang. Object adapter) np. BOA, POA,

  • Jądro ORB (ang. ORB Core),

  • Składnica interfejsów (and. Interface Repository) po
    stronie klienta,

  • Składnica informacji o serwerach (ang. Implementation
    Repository) po stronie serwera,

  • Usługa nazw.

ORB (ang. Object Request Borker), odpowiada za znalezienie
serwera, przygotowanie go do odebrania żądania i skomunikowania się z danymi
tworzącymi żądanie, oraz za jego interfejsy. ORB nie musi być
zaimplementowany jako pojedynczy komponent. Jest on definiowany przez swoje
interfejsy. Jądro ORB dostarcza mechanizmów do reprezentacji obiektów i
komunikacji. Takie rozdzielenie pozwala na współpracę z różnymi
mechanizmami obiektowymi. Funkcjonalność ORB widoczna jest przez interfejsy,
co przysłania szczegóły techniczne. Z interfejsami komunikują się klient i
serwer. Oczywiście inne są interfejsy po stronie serwera a inne po stronie
klienta.

Istnieją trzy kategorie interfejsów:

  • Operacje, które są takie same dla różnych
    implementacji ORB,

  • Operacje specyficzne dla konkretnych typów obiektów,

  • Operacje specyficzne dla określonego stylu implementacji
    obiektów.

Serwer korzysta z usług ORB przy pomocy adapterów
obiektowych. Istnieją adaptery obiektowe z interfejsami specyficznymi dla
konkretnych typów obiektów. Adapter obiektowy odpowiada m.in. za zarządzanie
referencjami do obiektów i rejestrację serwerów w ich składnicy.

Składnica interfejsów jest usługą dostarczającą
informacji na temat interfejsów serwerów. Informacje te mogą być używane
np. do dynamicznego generowania żądań. Można tu przechowywać także
dodatkowe informacje na temat interfejsów.

Składnica informacji o serwerach zawiera informacje pozwalające
na lokalizowanie serwerów przez ORB. Uruchamianie (instalacja) serwerów oraz
polityka odnosząca się do ich aktywacji i wykonywania ich usług, jest
realizowana poprzez operacje na tej składnicy.

Usługa nazw pozwala na identyfikowanie obiektów. Obiekt
jest identyfikowany przez łańcuch znaków nazywany IOR (ang. Interoperable
Object Reference). IOR zawiera m. in. informacje o typie obiektu, który
identyfikuje. Sam IOR jest jednak nieczytelny dla człowieka. Usługa nazwy
(ang. Name Service) ułatwia korzystanie z obiektów. Zamiast z referencji
korzysta się z czytelnej dla człowieka nazwy. Usługa nazwy odwzorowuje zatem
nazwę na referencję.

ORB może być zaimplementowany w:

  • kliencie i serwerze – do realizowania komunikacji używa
    się wtedy mechanizmów komunikacji między procesami lub usługi lokacji,

  • wyróżnionym serwerze, który przekazuje żądania do
    serwerów, które będą je obsługiwać. Wyróżniony serwer może być
    programem, z którym komunikuje się przy pomocy zwykłych mechanizmów
    komunikacji między procesami.

  • Systemie operacyjnym jako jego podstawowa usługa.
    Poprawia to bezpieczeństwo i wydajność.

  • Bibliotece – dla małych obiektów, które mogą być współdzielone.

Schematyczne działanie architektury CORBA pokazuje rysunek
1. Zaczerniona elipsa to obiekt-serwer, który dostarcza funkcjonalności, nie
zaczerniona elipsa, to referencja do tego obiektu pozwalająca używać go
lokalnie. Gruba strzałka odpowiada żądaniu.



(CORBA mówi o implementacji obiektu, chodzi jednak o obiekt
– dostawcę usług a więc o serwer).

ORB dostarcza informacji potrzebnych do identyfikacji obiektu
– referencji do obiektu. Różne implementacje ORB mogą inaczej obsługiwać
referencje do obiektów. ORB może być rozumiany jako rozproszona usługa
dostarczania żądań do obiektów zdalnych. Przykładowe produkty związane z
CORBĄ to np. The Java 2 ORB, VisiBroker for Java.

Interfejs widziany przez klienta jest całkowicie niezależny
od tego, gdzie znajduje się serwer i jak został zaimplementowany. Klient
komunikuje się z serwerem właśnie przy pomocy interfejsu statycznego
(dedykowanej stopki), który jest specyficzny dla danego serwera lub przy pomocy
interfejsu dynamicznego, który jest niezależny od serwera. Tak więc
interfejsy po stronie klienta mogą być definiowane statycznie przy użyciu języka
IDL, lub mogą być dodawane do usługi składnicy interfejsów (ang. Interface
Repository Service). Interfejsy są widziane w tym wypadku jako obiekty. Klient,
wykonując żądanie, zna typ obiektu i operację, posiada także referencję do
obiektu. Żądanie jest realizowane albo przez wywołanie procedur statycznej
stopki albo przez skonstruowanie dynamicznego żądania. Z punku widzenia
serwera nie ma znaczenia, jak operacja (tzn. dynamicznie lub statycznie) została
wywołana.

Za przesyłanie obiektów (począwszy do wersji 2.0
architektury CORBA) odpowiada protokół IIOP (ang. Internet Inter Orb
Protocol), zaimplementowany w oparciu TCP/IP z użyciem specyficznych dla
architektury CORBA komunikatów. Pozwala to na współpracę produktów pochodzących
od różnych dostawców (w sensie rozwiązań związanych z samą architekturą
CORBA).

Współpraca serwera z klientem w ramach architektury CORBA

Działanie serwera w najprostszym przypadku przebiega następująco
(zakładamy użycie JAVA 2.0 ORB):

  1. Zainicjowanie ORB,

  2. Utworzenie co najmniej jednego obiektu dostępnego
    zdalnie,

  3. Połączenie do ORB każdego obiektu, który ma być dostępny
    zdalnie,

  4. Dodanie obiektu do usługi nazw,

  5. Oczekiwanie na żądania klientów.

Działanie klienta dla serwera działającego jak powyżej
przebiega następująco:

  1. Zainicjowanie ORB,

  2. Pobranie referencji do obiektu, korzystając z usługi
    nazw,

  3. Wykonanie operacji na obiekcie (tak jakby był on w
    systemie lokalnym).

Oczywiście w ramach aplikacji można zarejestrować wiele
obiektów. Często do uzyskiwania referencji do obiektów używa się wzorca
projektowego typu Factory będącego uproszczoną wersją wzorca Abstract
Factory
opisanego
w [4]. Fabryka taka jest klasą, której metody „wytwarzają” obiekty innych
klas. Klient pobiera od serwera referencje do obiektu fabryki, przy pomocy której
może tworzyć inne obiekty. Przykładowa fabryka dla opisanych wcześniej klas A,B
oraz C wygląda następująco:

import java.awt.*; // m.in. obsługa we/wy
class Factory{
public A makeA(){return new A();}//utworzenie obiektu klasy A
public B makeB(){return new B();}
public C makeC(){return new C();}
}

Wykorzystanie takiej fabryki jest bardzo proste. Mając
referencję do niej, wywołujemy funkcję makeA() gdy chcemy stworzyć
obiekt klasy A tj. A a = f.makeA(); W wypadku fabryki nie jest
wymagane aby tworzone obiekty były spokrewnione. Możemy zdefiniować fabrykę
dla całego modelu biznesowego systemu (tj. dla encji występujących w
rzeczywistym świecie, który „informatyzujemy”). Można pójść dalej i
stworzyć fabrykę, w której tworzone obiekty będą implementowane poprzez
nazwę tzn.:

class SecondFactory{
public Object make(String name){
if (name=="A") // jeśli nazwa obiektu jest A
return new A(); //to go tworzymy i zwracamy referencję
else if (name=="B")//lub jeśli nazwa obiektu jest B
return new B();//tworzymy go
else if (name=="C")
return new C();
else return null;// jeśli nieprawidłowa nazwa to zwracamy null
}
}

Dodanie nowego obiektu nie zmienia interfejsu takiej fabryki,
a więc nie wymaga przegenerowywania definicji interfejsu (dla nowego obiektu
musimy oczywiście wygenerować stopkę i szkielet). Mając referencję do
fabryki, obiekt generujemy następująco: C c = (C)sf.make(„C”); Konieczna
jest jawna konwersja typów do C, bo fabryka zwraca obiekt typu Object.
Podsumowując, istotne jest to, że aby móc wykonywać operacje na obiekcie
zdalnym, trzeba mieć do niego referencje.

Praktyczne wykorzystanie specyfikacji CORBA – specyfikacja prostego systemu

W celu bliższego przedstawienia działania architektury
CORBA, zostanie zaprezentowany mały projekt. W ramce znajduje się opis
rzeczywistości tego systemu.

Misja systemu: System ma służyć do zdalnej
rejestracji umów o świadczenie usług telekomunikacyjnych telefonii komórkowej.
Rejestracja umowy poprzedzona jest weryfikacją legalności dostępu przez
dealera, który jest identyfikowany przez kod i hasło. Klient jest
jednoznacznie identyfikowany przez PESEL.

Przypadki użycia: a. rejestracja umowy, b. sprawdzenie
czy dokonano aktywacji

a. rejestracja umowy:

Wątek główny

  1. Wyświetlenie informacji o gotowości systemu,

  2. Prośba o podanie kodu i hasła dealera w celu
    autoryzacji,

  3. Hasło nie poprawne 10,

  4. Wpisanie przez dealera danych do umowy,

  5. Weryfikacja danych, w przypadku weryfikacji negatywnej
    10,

  6. Sprawdzenie czy klient, identyfikowany przez PESEL już
    istnieje; jeśli nie to 9,

  7. Stworzenie nowej umowy dla tego klienta 8,

  8. Klient wpisany do kolejki klientów oczekujących na
    aktywację 11,

  9. Utworzenie nowego klienta, 7,

  10. Wyświetlenie informacji o niepowodzeniu rejestracji lub
    braku dostępu,

  11. Wyświetlenie informacji o zakończeniu rejestracji
    zarejestrowana umowa ma unikalny numer, 1.

Wątek alternatywny

  1. Wyświetlenie informacji, że system nie jest dostępny
    lub o błędzie wykonania

b. sprawdzenie czy dokonano aktywacji:

Wątek główny

  1. Wyświetlenie informacji o aktywacji

Wątek alternatywny

  1. Wyświetlenie informacji, że system nie jest dostępny
    lub o błędzie wykonania

Użytkownicy systemu: dealerzy.

Błędy: system niedostępny, aktywacja nie możliwa,
rejestracja niemożliwa, dokumenty zastrzeżone, klient zastrzeżony.



Słownik systemu (tabela 1) opisuje pojęcia występujące
w opisie rzeczywistości i jest pomocny przy rozstrzyganiu wątpliwości przez
analityków i projektantów.

Tabela 1. Słownik systemu

Pojęcie  Opis
Klient  Osoba fizyczna chcąca zawrzeć umowę o świadczenie usług telekomunikacyjnych, klient identyfikowany jest przez: PESEL. Klient posiada imię, nazwisko oraz adres. Klient może posiadać wiele umów, zawartych
u jednego lub różnych dealerów.
Umowa  Upoważnia klienta do korzystania dokładnie z jednego telefonu.
Dane do umowy:
– PESEL, imię i nazwisko, adres klienta,
– Numer telefonu abonenta, numer karty SIM,
– Umowa jest identyfikowana przez numer nadawany przez system w momencie rejestracji.
Dealer  Osoba reprezentująca dostawcę usług telekomunikacyjnych, identyfikowana przez kod i hasło. 
Weryfikacja  Sprawdzenie, czy dokumenty klienta nie są zastrzeżone oraz czy z tym klientem można zawrzeć umowę, odbywa się to na poziomie funkcjonalnym poprzez współpracę z zewnętrznym systemem.
Rejestracja  Wprowadzenie danych dotyczących umowy do bazy; jeżeli klient nie istnieje poprzedzona jest rejestracją klienta.
Autoryzacja  Sprawdzenie, czy osoba jest uprawniona do rejestracji umów. Odbywa się ona przez podanie danych identyfikacyjnych dealera.
Aktywacja  Proces umożliwiający rozpoczęcie korzystania z usług telekomunikacyjnych; aktywowana może być wyłącznie zarejestrowana umowa.

System jest oczywiście bardzo prosty, niemniej jednak sposób
jego opisu odpowiada, w dużym uproszczeniu, opisom poprzedzającym
projektowanie dużych systemów informatycznych. Do zaimplementowania systemu
zostanie użyty język Java a do komunikacji, architektura CORBA.

W systemie dostępne są następujące operacje związane
z rejestracją umowy:

  • Autoryzacja dealera,

  • Sprawdzenie czy klient jest już zarejestrowany w
    systemie,

  • Dodanie nowego klienta,

  • Rejestracja umowy dla klienta,

  • Sprawdzenie czy umowa została aktywowana.

Rejestracja umowy jest realizowana przez jeden obiekt po
stronie serwera, który grupuje całą opisaną powyżej funkcjonalność. Założenie
takie przyjęto w celu uproszczenia projektu.

Szkielet aplikacji realizujący komunikację

Poniżej znajduje się specyfikacja pliku sr.idl,
definiującego interfejs i_rejestracja i operacje dostępne w systemie.
Definicja ta jest napisana w języku IDL. Można powiedzieć że interfejs w tej
definicji (ang. interface) odpowiada obiektowi, który istnieje po stronie
serwera.

module rejestracja_umow
{
interface i_rejestracja
{
boolean autoryzacja(in string kod, in string haslo);
boolean czy_klient_istnieje(in string PESEL);
boolean dodaj_nowego(in string PESEL, in string nazwisko, in string imie, in
string adres);
boolean rejestruj(in string PESEL,in string numer_tel, in string SIM,out
string informacje);
boolean czy_aktywna(in string numer_umowy);
boolean zakoncz();
};
};

Narzędzie dostępne w pakiecie JDK1.3, idlj pozwala
na wygenerowanie z takiej specyfikacji plików potrzebnych do budowy szkieletu
aplikacji. Wywołane z opcją -fclient, generuje pliki potrzebne do
budowy szkieletu aplikacji po stronie klienta.

Dla interfejsu i_rejestracja zdefiniowanego w powyższym
pliku zostały wygenerowane następujące pliki:

  • _i_rejestracjaStub – implementuje lokalny obiekt
    odpowiadający obiektowi zdalnemu, nie korzysta się jednak z niego bezpośrednio.
    Jest to tzw. stopka (zwana również namiastką).

  • i_rejestracjaHelper – implementacja operacji na
    typach danych tego interfejsu, są tam metody do zapisywania obiektu do
    strumienia i odczytywania go stamtąd.

  • i_rejestracjaHolder – używany dla parametrów in
    oraz out,

Narzędzie to wywołane z opcją -fserver generuje
pliki potrzebne do budowy aplikacji po stronie serwera; dla interfejsu i_dealer
został wygenerowany następujący plik:

  • _i_rejestracjaImplBase – klasa bazowa, z której
    dziedziczy klasa implementująca funkcjonalność obiektu po stronie serwera.
    Jest to tzw. szkielet.

Dodatkowo „po każdej ze stron” generowane są następujące
pliki z deklaracjami:

  • i_rejestracja – interfejs zadeklarowany w języku
    Java, do referencji do tego interfejsu przypisana będzie referencja do obiektu
    zdalnego po stronie klienta,

  • i_rejestracjaOperations – definicja interfejsu wraz
    z operacjami.

Idea działania tej aplikacji jest bardzo prosta. Program
serwera tworzy na maszynie zdalnej obiekt (klasy dziedziczącej z klasy _i_rejestracjaImplBase,
która została wygenerowana automatycznie i jest zdefiniowana w pliku _i_rejestracjaImplBase).
Obiekt ten będzie oczywiście występować
w ramach programu serwera. W tym momencie ograniczymy się do wypisywania
informacji o wywoływanych przez klienta funkcjach. Program klienta wygląda
następująco:

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;


public class klientR
{
public static void main(String args[]){
try
{
System.out.println("Klent Rejestracji Zdalnej");
ORB orb = ORB.init(args,null); // utworzenie ORB’a i jego inicjacja
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
//pobranie kontekstu korzenia usługi nazw
NamingContext ncRef = NamingContextHelper.narrow(objRef); //przekształcenie
typu
NameComponent nc = new NameComponent("ZdalnaRejestracjaUmow","");
NameComponent path[]= {nc};
i_rejestracja rejkl = i_rejestracjaHelper.narrow(ncRef.resolve(path));
//pobranie ref. do obiektu zdalnego
//na podstawie nazwy usługi
rejkl.autoryzacja ("Seb", "abc111");
//wywołanie metody obiektu zdalnego
rejkl.czy_aktywna ("123456789");
//wywołanie metody obiektu zdalnego
}
catch(Exception e)
{
System.out.println("Błąd: "+e);
e.printStackTrace(System.out);
}
}
}

Program serwera:

import java.awt.*;
import java.awt.event.*;
import java.io.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import org.omg.CORBA.*;


public class SerwerR
{
public static void main(String args[]){
try{
ORB orb=ORB.init(args,null);
// utworzenie ORB’a i jego inicjacja
rejestracjaImp rej = new rejestracjaImp();
//utworzenie obiektu implementującego funkcjonalność
//po stronie serwera
orb.connect(rej);//połączenie go z ORB’em
System.out.println("Stworzono: n"+orb.object_to_string(rej));
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
// pobranie referencji do usługi nazw
System.out.println("dodawanie obiektu do usługi nazw");
NamingContext ncRef = NamingContextHelper.narrow(objRef);
NameComponent nc =
new NameComponent("ZdalnaRejestracjaUmow","");
NameComponent path[] = {nc};
ncRef.rebind(path,rej);
//przypisanie nazwy usługi do referencji do obiektu
System.out.println("dodano");
java.lang.Object sync = new java.lang.Object();
synchronized (sync){
sync.wait(); //oczekiwanie na zgłoszenia klientów
}
}catch (Exception e)
{
e.printStackTrace(System.out);
}
}
}

Implementacja obiektu po stronie serwera, który dostarcza
funkcjonalności (ogranicza się ona do wypisania po stronie serwera, że
funkcja została wywołana i wypisania argumentów wywołania) wygląda następująco:

import java.io.*;


public class rejestracjaImp extends _i_rejestracjaImplBase
{
public boolean autoryzacja(String kod, String haslo){
System.out.println("autoryzacja " +kod+" "+haslo);
return true;
}
public boolean czy_klient_istnieje(String PESEL){
System.out.println("czy_klient_istnieje " +PESEL);
return true;
}
public boolean dodaj_nowego(String PESEL, String nazwisko, String imie,
String adres){
System.out.println("dodaj_nowego " +PESEL+" "+nazwisko+" "+imie+"
"+adres);
return true;
}
public boolean rejestruj(String PESEL, String numer_tel, String SIM,
org.omg.CORBA.StringHolder informacje){
System.out.println("rejestruj "+PESEL+" "+numer_tel+" "+SIM+"
"+informacje);
return true;
}
public boolean czy_aktywna(String numer_umowy){
System.out.println("czy_aktywna ");
return true;
}
public boolean zakoncz(){
System.out.println("zakoncz");
return true;
}
}

Aby przetestować powyższy szkielet aplikacji rozproszonej,
należy uruchomić serwer usługi nazw (ang. Name Service). Służy do tego narzędzie
tnameserv, znajdujące się w katalogu …/jdk1.3/bin pakietu JDK. Następnie
uruchamiamy program serwera, a po jego uruchomieniu program klienta. Reasumując:
utworzenie (działającego) szkieletu aplikacji polega na:

  • Zdefiniowaniu w języku IDL interfejsów,

  • Wygenerowaniu plików dla serwera i klienta przy użyciu
    narzędzia idlj,

  • Napisaniu klasy, która dziedziczy z klasy-szkieletu _i_rejestracjaImplBase
    oraz implementuje działanie obiektu po stronie serwera,

  • Inicjowaniu ORB-a po stronie klienta, pobraniu referencji
    do usługi NamingService a następnie przy jej pomocy do usługi serwera,
    z której będzie się korzystać. W tym przypadku usługa ta nazywa się:
    ZdalnaRejestracjaUmow

  • Pobraniu referencji do zarejestrowanego przez serwer
    obiektu, któremu odpowiada nazwa usługi,

  • Zainicjowaniu ORB-a po stronie serwera, utworzeniu obiektu
    klasy rejestracjaImp, zarejestrowaniu go do ORB-a, pobraniu referencji do
    usługi nazw, zarejestrowaniu nazwy usługi odpowiadającej obiektowi tj.
    ZdalnaRejestracjaUmow.

Przy dalszej implementacji aplikacji możemy zapomnieć, że
do implementacji użyto architektury CORBA i implementować rzeczywistą
funkcjonalność systemu. Po stronie klienta należy zaimplementować interfejs
użytkownika a po stronie serwera m. in. dostęp do bazy danych.

c.d.n.

Literatura

[1] B. Eckel Thinking in Java Prentice Hall PTR

[2] The Common Object Broker: Architecture and
Specification 2.4.
OMG 2000

[3] Professional Java Server Programming Wrox 1999

[4] E. Gamma, R. Helm, R. Johnson, J. Vlissides Design
Patterns
Addison-Wesley 1998

[5] D.C. Schmidt Developing Distributed Object Computing
Applications with CORBA