|
4.3 Progetto delle classi Le problematiche client-server discusse nel paragrafo precedente sono una prima analisi del problema discusso all’inizio del capitolo, esse sono le basi per iniziare a progettare le classi necessarie per costruire l’architettura client-server per il controllo remoto di dispositivi elettronici. La fase di analisi determina cosa deve fare l’implementazione, la fase di progetto a oggetti determina le definizioni complete delle classi e delle relazioni esistenti tra esse, nonché le interfacce e gli algoritmi dei metodi usati per implementare le operazioni. I blocchi funzionali scoperti e individuati durante l’analisi servono
come scheletro del progetto a oggetti, l’implementazione va fatta con un
occhio verso l’ottimizzazione; le operazioni identificate durante l’analisi
vanno in algoritmi, le operazioni complesse devono essere decomposte in
operazioni interne più semplici. Le classi, gli attributi e le associazioni
vanno individuate; probabilmente in fase di implementazione occorrerà
introdurre nuove classi, rispetto al previsto, per immagazzinare risultati
intermedi. Va fatta una rappresentazione grafica del progetto, in questo
caso si userà la rappresentazione grafica descritta dalla metodologia
OMT (Object Modelling Technique).
In questa fase si parla ancora di Dispositivo Controllabile ma con questa terminologia non si indica più il dispositivo fisico vero e proprio bensì una rappresentazione software del dispositivo (un oggetto) così come anche il client e il server, essi sono degli oggetti nel senso object-oriented.
Di solito il contesto risolve ogni ambiguità.
Si procede individuando le caratteristiche delle varie classi, in una prima fase si individuano quali sono gli attributi e le operazioni che caratterizzano ogni classe, individuare le associazioni è più complesso e inizialmente può essere fatto in modo sommario, anche perché porsi fin da subito il problema delle relazioni tra le varie classi può portare a risultati imprecisi. Delle classi client e server si conoscono le operazioni principali, quelle dello scambio di informazioni tra il client e il server, inoltre il server sarà in associazione con dispositivo controllabile quindi ne dovrà avere una "rappresentazione" che deve essere del tutto generale in quanto non dipendente dal particolare dispositivo. Occorre fare un’astrazione, prescindere dal dispositivo e individuare gli attributi e le operazioni che hanno in comune tutti i dispositivi controllabili, non è importante che le proprietà individuate siano tante, piuttosto che esse siano valide per tutti i dispositivi. L’idea di fare un’astrazione è valida proprio perché si vuole definire una classe astratta che sia una superclasse per tutti i dispositivi controllabili in modo che chiunque voglia usare questa architettura client-server non dovrà fare altro che definire una propria classe, sottoclasse di questa classe astratta, che ne rappresenta le proprietà generali. Possiamo considerare la classe dispositivo controllabile (d’ora in poi sarà la classe controllableDevice) come una generalizzazione: essa avrà come sottoclassi dispositivi di acquisizione dati, strumenti di misura, dispositivi di controllo automatici, ecc., come evidenziato in Figura 4.6.
Figura 4.6 Generalizzazione della classe ControllableDevice.
I puntini di sospensione in figura indicano che vi possono essere altre sottoclassi; ognuna delle sottoclassi Acquisizione Dati, Strumenti di Misura e Controlli Automatici, può avere altre sottoclassi, cioè può essere una generalizzazione e cosi via, a secondo del livello di specializzazione che si vuole raggiungere. Per l’astrazione che si vuole fare della classe ControllableDevice, il primo livello di specializzazione illustrato in figura è sufficiente per capire che qualunque sia il dispositivi controllabile su di esso saranno possibili delle azioni per fornire dei comandi, a cui potrà seguire o meno la restituzione dei risultati. Questa operazione la caratterizziamo con una generico metodo execute(command:object):object, dove command potrà essere una oggetto generico (o un oggetto da definire) e il tipo di risultato restituito può essere ancora un oggetto. D’altronde non poteva essere diversamente, non potendo fare assunzioni sui particolari di un dispositivi. C’è da dire che sicuramente la classe ControllableDevice, o meglio una sua sottoclasse concreta, non si interfaccerà direttamente con il dispositivo fisico (hardware) ma con un programma software (driver) proprio del dispositivo, in genere scritto in un linguaggio di programmazione diverso dal linguaggio Java. Quindi in realtà il metodo execute(command) sarà successivamente "filtrato" dal driver del dispositivo. Un dispositivo controllabile può essere visto come una risorsa a cui più utenti vorrebbero accedere, questo comporta l’aggiunta di un’altra operazione alla classe ControllableDevice che caratterizza se il dispositivo controllabile è condivisibile o meno; questa operazione la si realizza mediante un metodo del tipo: isShareble():boolean che restituisce un booleano indicante se condivisibile o meno. Sempre vedendo il dispositivo controllabile come una risorsa esso dovrà avere due generiche operazioni che consentono l’acquisizione del controllo della risorsa e il suo rilascio, questo può essere realizzato mediante due metodi open() e close(). Un’ultima operazione importante è quella che indica lo stato del dispositivo se in uso o meno, questa operazione la si esprime mediante un metodo del tipo: inUse():boolean che restituisce un risultato se in uso o meno. La rappresentazione grafica della classe ControllableDevice è riportata in Figura 4.7, la simbologia {abstract} accanto a un metodo (in questo caso accanto a tutti i metodi) indica che quel metodo è astratto cioè non ne è fornita l’implementazione, si impone così che ciascuna sottoclasse di ControllableDevice deve fornire una propria implementazione; inoltre la classe non ha nessun attributo, ciò caratterizza ancora di più il concetto di astrazione che si voleva raggiungere fin da principio, si noti anche che la classe non eredita da nessuna classe in particolare quindi eredita dalla classe Object come vuole il linguaggio Java. Figura 4.7 Rappresentazione grafica della classe ControllableDevice Le classi dell’applicazione Server Completato il progetto della classe ControllableDevice, si passa alla classe per il server che in parte farà uso di questa classe essendo il server a doversi interfacciare con il dispositivo da controllare e quindi la classe dell’applicazione Server a doversi interfacciare con la classe ControllableDevice. Progettare le classi per il server o per il client equivale a risolvere le varie problematiche (o filosofie) di una tipica architettura client-server descritta in precedenza. Si vuole che il server sia in grado di gestire più dispositivi
controllabili e quindi più client, la soluzione per un server multiclient
si trova nell’uso dei Thread.
La multiprogrammazione è la capacità di un sistema operativo di supportare l’esecuzione concomitante di più applicazioni; essa significa semplicemente che un sistema operativo può eseguire applicazioni simultaneamente. Una sola applicazione alla volta può effettivamente usufruire del processore, ma il sistema operativo in multiprogrammazione è responsabile della suddivisione del tempo di esecuzione del processore e della condivisione di quest’ultimo tra molte applicazioni. Un termine attualmente diffuso per designare la multiprogrammazione è multitasking. Il multitasking consente di eseguire più applicazioni contemporaneamente, volendo essere più precisi il multitasking consente di eseguire più processi simultaneamente. Nell’ambito del multitasking troviamo il multithreading: un thread è un processo leggero ovvero un processo che possiede una quantità minore di dati, all’interno dello stesso task possono esserci più thread in esecuzione. Essendo un processo leggero il thread può essere avviato e fermato con molto meno overhead rispetto ai processi veri e propri. I thread sono importanti perché possono aumentare la capacità di gestire le richieste generate dalle macchine client, essi vanno utilizzati per aumentare la capacità dell’applicazione server di gestire operazioni simultanee. In un sistema basato sui thread esiste la possibilità dal lato
server di associare un thread a ciascun client che richiede la connessione.
Questo è ciò che faremo nell’architettura client-server che
si sta progettando.
Per realizzare un server multiclient (o multithreading) è utile dividere l’applicazione server in due classi: la classe ServerDevice e la classe ConnectionDevice. La logica è la seguente: un’applicazione in esecuzione in un thread sta costantemente in ascolto su di una porta di ogni nuova richiesta di connessione da parte del client, nel momento in cui vi è una richiesta, questa applicazione crea un nuovo thread a cui affida la comunicazione con il client, e ritorna prontamente all’ascolto di una nuova richiesta di connessione. Questa logica viene realizzata mediante le due classi: ServerDevice che è l’applicazione in ascolto di ogni nuova connessione e ConnectionDevice che realizza la comunicazione con il client. Per la comunicazione di useranno i Socket questo perché offrono elevata affidabilità, e flessibilità idonea per l’architettura che si vuole realizzare. La classe ServerDevice oltre all’ascolto della richiesta di connessione su una determinata porta sarà anche responsabile di allocare la memoria necessaria per un’istanza, o per più istanze se ci sono più dispositivi da controllare, della classe ControllableDevice. In particolare la classe ServerDevice deve essere indipendente dal numero e dai particolari dei dispositivi da controllare, essa deve essere in grado di ricevere come input dei parametri che rappresentano le classi dei dispositivi da controllare e costruire un vettore contenente le istanze che li rappresentano, in modo che quando il client fa richiesta di controllo di un dispositivo si verifica se ne esiste un’istanza e gli si consente l’accesso a quell’istanza, così facendo i client accedono e rilasciano una risorsa ma nessuna nuova allocazione deve essere effettuata. Un’altra soluzione poteva essere quella di creare un’istanza della classe ControllableDevice solo quando il client fa la richiesta di controllo di un dispositivo, ma in questo si deve prevedere un meccanismo di allocazione e rilascio del dispositivo, un vettore da aggiornare dinamicamente quando un’istanza di un dispositivo viene allocata in modo che altri client che ne facciano richiesta possano informarsi sullo stato del dispositivo ed eventualmente accedere o meno ad esso, ma tutto ciò porta ad un’inutile spreco di risorse della macchina che funziona da server. In fase di inizializzazione di ServerDevice vengono allocate dinamicamente le istanze di ControllableDevice e ne viene costruito un vettore dinamicamente ma una sola volta all’avvio del server. ServerDevice deve anche prevedere un’operazione che consente di caricare dinamicamente le classi concrete di ControllableDevice (che rappresentano i diversi dispositivi controllabili) che gli vengono fornite come parametro, per esempio dalla riga di comando. Quando ServerDevice riceve una richiesta di connessione l’accetta e crea un nuovo thread in cui va in esecuzione ConnectionDevice che è la vera responsabile della comunicazione client-server, ConnectionDevice riceverà da ServerDevice i parametri necessari per la comunicazione con il client, in particolare il Socket restituito dalla connessione accettata e il vettore contenente le istanze dei dispositivi controllabili; il client la prima richiesta che fa è quella di controllo di un determinato dispositivo, ConnectionDevice ne verifica l’esistenza nel vettore e gli passerà il controllo o meno a seconda dello stato del dispositivo. Anche qui una soluzione diversa poteva essere di eseguire l’operazione di controllo sullo stato del dispositivo in ServerDevice e solo dopo generare un nuovo thread per la comunicazione, in modo che se il dispositivo non è disponibile il nuovo thread non viene generato a priori, con un conseguente risparmio di risorse del sistema server. Introdurre operazioni in più in ServerDevice non è una buona idea per il fatto che queste operazioni portano un maggiore impegno in generale, e a una minore attenzione verso la richiesta di nuove connessione ovvero se il server è impegnato ad eseguire altre operazione qualche richiesta di connessione potrebbe perdersi. Affidando a ServerDevice il solo compito di accettare ogni nuova connessione e per essa generare un nuovo thread esso sarà velocissimo quindi svolgerà al meglio il proprio compito di essere un Server. Inoltre ConnectionDevice in esecuzione in un thread indipendente per ogni client sarà pronto a ogni richiesta del client essendo il suo unico compito quello di comunicazione. Per quanto riguarda il tipo di dati da trasferire tra il client e il server e viceversa non si fa nessuna ipotesi su di essi, gli stream di input e di output sono visti come dei generici InputStream e OutputStream, i dati vengono incapsulati in un oggetto e trasferiti, in questo modo il server si disinteressa del tipo di dati trasferiti, il compito sarà solo quello di inoltrare i dati ricevuti dal client al dispositivo controllabile e viceversa. Questo discorso va visto nell’ottica di una completa generalizzazione e indipendenza dai particolari del dispositivo elettronico che si vuole controllare. Gli stream di input e output sono visti come un generico flusso di dati dall’architettura client-server, ma saranno correttamente interpretati dall’interfaccia con il dispositivo (driver) dal lato server e dall’interfaccia con l’utente dal lato client. Chi progetterà e implementerà l’interfaccia client dovrà avere una profonda conoscenza dell’interfaccia server-dispositivo in modo da far si che gli stessi dati siano correttamente interpretati; quest’ipotesi è realistica perché chi avrà l’esigenza di interfacciare un dispositivo con il server dovrà implementare il driver per dialogare con il dispositivo, quindi avere buona conoscenza di esso, di conseguenza saprà come vanno inoltrati i comandi ovvero come deve essere fatta l’interfaccia lato client. Ci sono, adesso, tutti gli elementi necessari per poter individuare
gli attributi e le operazioni delle due classi. Le classi sono in esecuzione
in un thread; nel linguaggio Java e abbastanza semplice realizzare il multithreading
bisogna ereditare dalla classe java.lang.Thread
e ridefinire, nella propria sottoclasse, il motodo run()
presente nella classe Thread, il corpo
del metodo run() sarà eseguito in
un thread indipendente.
La classe ServerDevice ha un attributo port:int che specifica quale è la porta di ascolto, l’attributo serverSocket:ServerSocket; SeverSocket e una classe del package java.net, che serve per l’ascolto della connessione su una porta specificata; e un attributo vectorDevice:Vector contenente la lista delle istanze dei dispositivi controllabili. Le operazioni in ServerDevice sono:
un metodo ServerDevice(port:int, args[]:String)
che inizializza l’oggetto con la porta specificata in port
e arg[] contiene la lista dei dispositivi
(in senso software) che devono essere caricati in vectorDevice;
un metodo run() come richiesto dalla classe
Thread che contiene il codice per l’ascolto
delle connessioni e per creare un nuovo thread; e un metodo che deve essere
in grado di caricare e istanziare dinamicamente gli oggetti di tipo ControllableDevice
a partire da stringhe contenute in arg[]:
loadSubClass (stringClass: String, superclass:
String): Object, questo metodo a partire da una stringa stringClass
crea un’istanza della classe rappresentata da questa stringa dopo aver
verificato che sia sottoclasse di superclass,
la verifica è fatta perché stringClass
non può essere una classe qualsiasi ma deve essere una classe che
rappresenti un dispositivo controllabile (quindi sottoclasse di ControllableDevice
) e che possa essere inserita nel vettore vectorDevice
che contiene solo istanze dei dispositivi controllabili.
La classe ConnectionDevice Nelle operazione della classe ConnectionDevice si trova: connectionDevice(socket:Socket, vector:Vector) cioè il costruttore che inizializza gli attributi. Ereditando dalla classe Thread anche ConnectionDevice deve definire un’operazione run() in cui è contenuto il codice che viene eseguito nel thread, ovvero la comunicazione client-server. La rappresentazione grafica è riportata in Figura 4.9.
Figura 4.9 Rappresentazione grafica della classe ConnectionDevice.
E’ completo a questo punto il progetto delle classi che costituiscono il server, ciascuna classe è stata rappresentata singolarmente, ciò fino ad ora è stato fatto solo per motivi di spazio, in realtà questo è impreciso perché tutte le classi appartenenti ad una stessa applicazione quindi in relazione tra loro devono essere rappresentate nello stesso modulo come vuole la metodologia OMT, questo lavoro è fatto in Figura 4.10 in cui sono anche riportare le associazioni tra le classi.
I puntini di sospensione in figura sottolineano che la classe ControllableDevice
va specializzata, essa contiene solo proprietà generali dei dispositivi
controllabili. In base al tipo di dispositivo che si vuole controllare
si costruirà una classe (o una gerarchia di classi) che lo rappresenta
e sarà questa classe ad essere inserita nella gerarchia di ControllableDevice.
Il server è stato progettato per caricare dinamicamente le classi
che rappresentano i dispositivi da controllare, dal lato client sarebbe
comodo un meccanismo analogo: un client che in base a dei parametri di
input carica la giusta classe per il controllo del dispositivo remoto;
ci si deve anche preoccupare, a questo punto, dell’interfaccia utente;
un’interfaccia grafica con cui l’utente può facilmente interagire
per inviare i comandi al dispositivo remoto. Si divide allora l’applicazione
client in due classi: un applet (AppletGUI)
che contiene gli attributi e le operazioni necessarie per caricare da remoto
l’applicazione client per il controllo dello strumento, e l’applicazione
client (DeviceGUI).
La classe AppletGUI, quindi, eredita dalla classe Applet e contiene i seguenti attributi: classGUI:String che è la stringa che contiene il nome della classe da caricare, questo attributo si inizializza dalla pagina HTML; button: Button cioè il punsante che si visualizza nella pagina HTML e label:Label un’etichetta che informa lo stato di caricamento della classe remota o eventuali messaggi di errore se la classe remota non è disponibile. Nelle operazioni si trova un metodo init() che inizializza gli attributi, un metodo action(event:Event):boolean che rileva se è stato premuto il pulsante, e un metodo LoadSubclass(classString:String, superclass:String) che carica la classe specificata da classGUI verificando che sia sottoclasse di superclass, questa verifica è utile in quanto l’applet fa alcune assunzioni sulla classe da caricare e queste assunzioni sono vere se la classe da caricare eredita dalla classe DeviceGUI.
Questa classe viene caricare sul terminale dell’utente dall’applet AppletGUI ma una volta in esecuzione essa diventa quasi indipendente dal browser Web che ha caricato AppletGUI; DeviceGUI comunica con il server del dispositivo (ServerDevice) mediante i socket quindi un meccanismo del tutto indipendente dal browser Web, anche la visualizzazione dell’interfaccia utente avviene in un frame indipendente dal browser Web; DeviceGUI rappresenta il limite tra applet e applicazione: non eredita dalla classe Applet come vorrebbero gli applet ma è comunque sottoposta alle restrizioni di sicurezza degli applet; è in esecuzione in un frame indipendente ma se l’esecuzione del browser Web termina il frame si chiude. In questa situazione DeviceGUI va ancora vista come un applet sia perché si scarica come un applet sia perché a tenerla in esecuzione e la Java Virtual Machine incorporata nel browser Web. La terminologia browser abilitato per Java stava proprio a sottolineare che il browser all’interno deve avere una propria implementazione della Java Virtual Machine ed è questa macchina virtuale ad eseguire il codice Java compilato (l’applet) che giunge dal Web. DeviceGUI può essere comunque vista come una classe qualsiasi, non sottoclasse di Applet, quindi se in locale l’utente ha una propria Java Virtual Machine ed ha la classe DeviceGUI (o le relative sottoclassi) magari scaricata da un sito ftp, può tranquillamente eseguirla come un’applicazione normale senza nessuna restrizione. Seguendo il discorso di astrazione che si è fatto fin dall’inizio DeviceGUI contiene gli attributi e le operazioni fondamentali per la gestione della connessione e per una visualizzazione GUI sul terminale dell’utente. è chiaro che chi andrà a specializzare la classe astratta ControllableDevice dovrà specializzare anche la classe DeviceGUI; per esempio, dal punto di vista GUI DeviceGUI contiene solo un frame "vuoto". Gli attibuti sono: un indirizzo url:Url e un port:int che rappresentano l’url dell’host remoto a cui ci si vuole connettere e la rispettiva porta a cui connettersi, un attributo socket:Socket per stabilire la connesione con il server e un is:InputStream e os:OutputStream per gli stream di dati in input e in output e per ultimo un attributo inAnApplet:boolean che differenzia il comportamento della classe a seconda se viene eseguita come applet o applicazione. Inoltre DeviceGUI crea un menu e un’area di testo vuota che saranno utilizzati dalle sottoclassi. Tra le operazioni c’è un metodo init(url:Url, port:int) per inizializzare i relativi attributi; nel caso che DeviceGUI viene eseguita come applet dovrà essere appletGUI a fornire i parametri giusti di url e port, se viene eseguita come applicazione bisogna fornire esplicitamente questi due attributi per esempio dalla riga di comando. La classe che si sta progettando è l’equivalente, dal lato client, di ciò che rappresenta la classe ControllableDevice; DeviceGUI deve essere generale e non deve dipendere dai particolari del dispositivi controllabile le operazioni devono essere generali. Due operazioni che sicuramente vanno eseguite sono la connessione con il server e quindi con il dispositivo e la disconnessione, di ciò si occupano i metodi startDevice(stringDevice:String):boolean che inizializza il socket e gli stream di I/O e inoltre riceve come parametro il nome del dispositivo da controllare e restituisce se il dispositivo è disponibile o meno, e stopDevice()che rilascia il dispositivo e chiude il socket e gli stream di I/O e infine action() per le azioni dell’utente. La rappresentazione grafica e riportata nella figura che segue.
Figura 4.12 Rappresentazione grafica della classe DeviceGUI.
Ci sono tutti gli elementi per rappresentare in un solo grafico l’applicazione client ed individuare le associazioni tra le classi sopra descritte: Figura 4.13
Si conclude il paragrafo con un riassunto mediante uno schema a blocchi funzionali di quanto fino ad ora progettato (Figura 4.14). Da una parte si riprende, se pur lontanamente, lo schema a blocchi in Figura 4.3 dall’altra si introducono le relazioni temporali esistenti tra i vari blocchi; relazioni temporali (eventi) che saranno approfondite nel prossimo paragrafo.
Figura 4.14 Schema funzionale dell'architettura client-server. In Figura 4.14 vi è un server Web a cui un browser Web si connette e fa delle richieste; tra le risposte il server Web invia un applet (AppletGUI) che consente di scaricare (LoadClass()) il client DeviceGUI, quest’ultimo, una volta in esecuzione, si connette al server del dispositivo (ServerDevice) il quale accetta la connessione e crea (new()) ConnectionDevice che va in esecuzione e invia un acknowledgment a DeviceGUI. D’ora in poi le operazioni di richieste/risposte avvengono tra DeviceGUI e ConnectionDevice che a sua volta inoltra i comandi ricevuti a ControllableDevice, attende un’eventuale risposta e la invia a DeviceGUI.
Il modello a oggetti rappresenta una struttura statica, cioè la struttura degli oggetti e delle loro reciproche relazioni in un determinato istante del tempo. Esso non è completo per descrivere un progetto, gli oggetti si stimolano l’uno con l’altro provocando una serie di cambiamenti ai loro stati. Al diagramma degli oggetti va quindi aggiunto quello degli eventi ed è l’insieme che caratterizza completamente l’architettura client-server in progetto. Punto di partenza è lo scenario, ovvero una sequenza di eventi che si verificano durante una particolare esecuzione di un sistema, per poi giungere al diagramma degli eventi vero e proprio. Uno scenario possibile per l’architettura client-server in progetto è il seguente: Il diagramma degli eventi in Figura 4.15 illustra graficamente lo scenario descritto.
Figura 4.15 Tracciato degli eventi per il controllo di un dispositivo elettronico. Nel caso in cui la sottoclasse di DeviceGUI preposta al controllo del dispositivo elettronico non viene caricata come applet ma viene eseguita come un’applicazione normale il tracciato degli eventi rimane sostanzialmente lo stesso tranne per la parte iniziale, in cui non vi è più la fase di connessione al server Web ma direttamente la fase di connessione a ServerDevice. Con il tracciato degli eventi e il progetto delle classi ci sono tutti gli elementi per poter implementare l’architettura client-server progettata. In fase di implementazione si è preferito fare uso dei package per suddividere le classi. Sono stati definiti tre package: il package device che contiene la classe rappresentante il dispositivo controllabile, il package device.server che contiene le classi dell’applicazione server e il package device.client che contiene le classi dell’applicazione client. L’implementazione è riportata in appendice.
|