Un
oggetto remoto distribuito sulla rete può essere utilizzato contemporaneamente
da più client. Questo è uno dei fattori che rendono allettante
la programmazione distribuita poiché l’utilizzo in concorrenza di
una qualunque risorsa implica un suo migliore sfruttamento.
La
gestione della concorrenza in ambiente distribuito è strettamente
legata al design dell’applicazione e tipicamente va a ricadere in
uno dei tre approcci:
-
Unico
thread: il thread è unico, le richieste sono gestite in modo sequenziale
ed eventualmente accodate.
-
Un thread
per client: viene associato un thread alla connessione con il client; le
successive richieste del client sono a carico di questo thread.
-
Un thread
per request: esiste un pool di thread utilizzati in modo concorrente per
rispondere alle richieste dei client
Esisteranno
comunque situazioni in cui i differenti thread dovranno accedere ad una
risorsa condivisa (Es. connessione a DB, file di log, …); in questi casi
l’accesso alla risorsa andrà opportunamente sincronizzato.
Come
si vedrà nei prossimi articoli, con CORBA è possibile specificare
le politiche di gestione dei thread mediante l’utilizzo degli object adapter.
Nella pratica questi aspetti vengono spesso gestiti applicativamente con
l’adozione di opportuni pattern di programmazione.
Questo
permette di realizzare soluzioni complesse del tutto indipendenti dall’implementazione
e dalla tecnologia. Tipicamente queste soluzioni sono attuate con l’adozione
del pattern factory.
Il pattern Factory
Quando
si istanzia un oggetto è necessario fare un riferimento diretto
ed esplicito ad una precisa classe, fornendo gli eventuali parametri richiesti
dal costruttore. Questo vincola implicitamente l’oggetto utilizzatore all’oggetto
utilizzato. Se questo può essere accettabile nella maggior parte
dei casi, talvolta è un limite troppo forte.
Figura
1 — Pattern Factory
In
questi casi può essere molto utile incapsulare in una classe specializzata
tutte le valutazioni relative alla creazione dell’oggetto che si intende
utilizzare. Questa soluzione è il più noto tra i creational
patterns: il pattern Factory (anche detto Factory Method o Virtual Constructor).
Factory
letteralmente vuol dire Fabbrica ed è proprio questo il senso dell’oggetto
Factory: fabbricare sulla base di alcune valutazioni un determinato oggetto.
Più formalmente, facendo riferimento anche alla figura, un oggetto
Factory fabbrica oggetti ConcreteProduct appartenenti ad una determinata
famiglia specificata dalla sua interfaccia (o classe astratta) Product.
Un
client non crea mai in modo diretto un’istanza del Product (in un contesto
remoto probabilmente non ne conoscerà nemmeno la classe), ma ne
ottiene un’istanza valida attraverso l’invocazione del FactoyMethod sull’oggetto
Factory.
In
questo modo è possibile sostituire in ogni momento l’implementazione
ConcreteProductA con un’implementazione omologa ConcreteProductB senza
che un eventuale client se ne accorga.
In
realtà i vantaggi elencati in precedenza sono già impliciti
in uno scenario CORBA (il disaccoppiamento è garantito dalla funzionalità
Proxy dello stub). Nella programmazione distribuita il pattern Factory
ha però altri vantaggi, in particolare consente di implementare
meccanismi di load-balancing e fault-tolerance.
Poiché
è la Factory a determinare la creazione del Product, essa potrà:
-
istanziare
un oggetto per ogni client
-
applicare
un round-robin tra le differenti istanze già create, ottenendo un
semplice load-balancing
-
restituire
differenti tipologie di oggetti appartenenti alla famiglia Product sulla
base di valutazioni legate all’identità del client
-
implementare
un semplice fault-tolerance, escludendo dal pool di oggetti quelli non
più funzionanti o non più raggiungibili via rete.
Un esempio di
Factory
Per
sperimentare un tipico caso di applicazione del pattern Factory realizziamo
il classico “carrello della spesa”. Per l’esempio saranno implementate
la classe carrello (ShoppingCart) ed una sua Factory (ShoppingCartFactory).
La prima fornirà una funzionalità di acquisto ed una di restituzione
del contenuto, la seconda sarà dotata del solo metodo getShoppingCart.
Definiamo
l’IDL
//
IDL
module
shopping {
struct
Book {
string Author;
string Title;
};
typedef
sequence <Book> BookList;
interface
ShoppingCart {
void addBook(in Book book);
BookList getBookList();
};
interface
ShoppingCartFactory {
ShoppingCart getShoppingCart(in string userID);
};
};
Sono
definite come interface sia la Factory (con il Factory Method getShoppingCart),
sia il carrello vero e proprio. La Factory può essere considerata
una classe di infrastruttura, mentre tutti i metodi di business sono implementati
nello ShoppingCart.
La
classe ShoppingCartImpl implementa i due semplici metodi di business e
non presenta nulla di nuovo rispetto a quanto visto nei precedenti articoli
package
server;
import
shopping.*;
import
java.util.Vector;
public
class ShoppingCartImpl extends _ShoppingCartImplBase {
Vector v = new Vector();
public ShoppingCartImpl() {
super();
}
//
Aggiunge un libro al carrello
public
void addBook(Book book) {
v.add(book);
}
//
Restituisce l'elenco dei libri acquistati
public
Book[] getBookList() {
Book[] books = new Book[v.size()];
for (int i=0; i<v.size(); i++)
books[i] = (Book) v.elementAt(i);
return books;
}
}
Più
interessante è l’oggetto Factory che ha il compito di generare le
istanze di ShoppingCartImpl da assegnare ai client. Nel metodo getShoppingCart
viene stabilita la politica di creazione e restituzione delle istanze di
carrello; nel caso in esame la decisione è ovvia in quanto il carrello
ha evidentemente un rapporto uno a uno con i client e ad ogni client verrà
quindi assegnata una nuova istanza di ShoppingCart.
Per
memorizzare le varie istanze viene utilizzato un oggetto di tipo Dictionary.
Alla prima connessione dell’utente la Factory creerà
il carrello e lo “registrerà” sull’ORB. La Factory può ottenere
un riferimento valido all’ORB invocando il metodo _orb() fornito da org.omg.CORBA.Object.
Ecco
la classe Factory
package
server;
import
shopping.*;
import
java.util.*;
public
class ShoppingCartFactoryImpl extends
_ShoppingCartFactoryImplBase {
private
Dictionary allCarts = new Hashtable();
public ShoppingCartFactoryImpl() {
super();
}
public
synchronized ShoppingCart
getShoppingCart(String userID) {
// Cerca il carrello assegnato allo userID...
shopping.ShoppingCart cart =
(shopping.ShoppingCart) allCarts.get(userID);
// ...se non lo trova...
if(cart == null) {
// Crea un nuovo carrello...
cart = new ShoppingCartImpl();
// ...e lo attiva sull'ORB
_orb().connect(cart);
System.out.println("Created " + userID
+ "'s cart: " + cart);
// Salva nel dictionary associandolo allo userID
allCarts.put(userID, cart);
}
// Restituisce il carrello
return cart;
}
}
E’
da notare che la Factory sarà utilizzata in concorrenza da più
client, di conseguenza sarà opportuno sincronizzare il metodo getShoppingCart
per ottenere un’esecuzione consistente.
Per
quanto detto in precedenza, un client otterrà un oggetto remoto
di tipo ShoppingCart interagendo con la Factory. Pertanto, l’unico oggetto
registrato sul Naming Service sarà l’oggetto Factory. La registrazione
sarà effettuata dalla classe server con il nome “ShoppingCartFactory”
con le stesse modalità viste negli scorsi articoli (il codice qui
non viene mostrato).
Dopo
aver ottenuto il reference allo ShoppingCart assegnato, il client potrà
operare direttamente su questo senza interagire ulteriormente con la Factory
package
client;
import
shopping.*;
import
java.io.*;
import
org.omg.CosNaming.*;
import
org.omg.CORBA.*;
public
class ShoppingCartClient {
public
static void main(String args[]) {
if (args.length != 3) {
System.err.println("Uso corretto:
java ShoppingCartClient userId Autore Titolo");
return;
}
try{
// Crea ed inizializza l'ORB
ORB orb = ORB.init(args, null);
// Root naming context
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
NamingContext ncRef =
NamingContextHelper.narrow(objRef);
// Utilizzo il Naming per ottenere il
// riferimento all'oggetto Factory
NameComponent nc =
new NameComponent("ShoppingCartFactory", "");
NameComponent path[] = {nc};
ShoppingCartFactory factory =
ShoppingCartFactoryHelper.narrow
(ncRef.resolve(path));
// Ottengo dalla Factory un'oggetto ShoppingCart
ShoppingCart cart =
factory.getShoppingCart(args[0]);
// Aggiungo un libro
cart.addBook(new Book(args[1],args[2]));
// Ottengo la lista dei libri e la stampo
Book[] list = cart.getBookList();
for(int i=0; i<list.length; i++)
System.out.println("Autore " +
list[i].Author + " - Titolo "
+ list[i].Title);
} catch (Exception e) {
e.printStackTrace();
}
}
}
I passi
necessari per l’esecuzione sono i soliti (si veda [1]). Al client vanno
“passati” lo userId, l’autore ed il titolo del libro da aggiungere al carrello.
L’adozione
di un pattern è una scelta del tutto indipendente dalla tecnologia
e quindi adattabile a qualunque altro scenario (ad esempio RMI). Un design
come quello visto rende l’architettura più robusta ed adattabile,
consentendo di modificare e configurare il comportamento anche a start-up
avvenuto senza alcun impatto sui client. In ottica Enterprise la resistenza
ai cambiamenti è di massimo interesse.
Conclusioni
In
questo articolo abbiamo presentato il pattern Factory ed una semplice soluzione
CORBA che ne fa uso. Come si è già accennato, le politiche
relative alla gestione delle istanze sono configurabili anche utilizzando
gli Object Adapter. Il mese prossimo inizieremo a studiare il più
semplice tra questi: il Basic Object Adapter.
Bibliografia
[1]
G. Morello - "Corso CORBA IV Parte: CORBA Naming Service", Mokabyte N.
51, Aprile 2001
[2]
G. Morello - "Corso CORBA III Parte: un semplice esempio", Mokabyte N.
50, Marzo 2001
|