OOAD - Seconda parte.Design
(progettazione)
Progettazione ad oggetti
Cosa significa progettare un sistema ad oggetti?
La progettazione è una fase fondamentale, durante la quale vengono presi in
considerazione tutti i requisiti non funzionali del sistema. Così come durante
l’analisi ci si concentra su cosa debba fare il sistema, durante il design ci si
concentra su come debba farlo, considerando gli obiettivi di riusabilità,
estendibilità, testabilità, manutenibilità, che costituiscono le grandi promesse
della tecnologia ad oggetti.
Come fa un buon progettista a raggiungere tutti gli obiettivi di cui sopra?
Nonostante le promesse dei vari linguaggi e (soprattutto) dei produttori di tool,
non è affatto sufficiente "adottare gli oggetti" per ottenere i risultati sperati.
Passare direttamente da un modello di analisi ad oggetti ad una implementazione,
sempre ad oggetti, difficilmente porterà ad un sistema semplice da estendere: l’analisi
modella il problema, e senza opportuni accorgimenti (introdotti proprio in fase di design)
i naturali cambiamenti nel problema da risolvere potrebbero avere un impatto notevole
sulla struttura complessiva della soluzione.
Occorre quindi uno sforzo ulteriore, deliberatamente orientato ad ottenere benefici a
medio-lungo termine; questo è peraltro uno dei nodi centrali che vanno ben compresi
anche dal management: i vantaggi della tecnologia ad oggetti si notano principalmente
sulla distanza, e non nella rapidità di consegna.
Un buon progettista ha probabilmente imparato l’arte del design leggendo, studiando,
osservando il lavoro altrui, pagando sulla propria pelle il prezzo di scelte poco oculate, e
conservando invece la struttura delle soluzioni vincenti. Un percorso difficile, non
particolarmente efficiente, e che non garantisce neppure una futura ripetibilità dei
successi ottenuti.
Obiettivi della fase di Design
•
•
•
Trasformare i concetti dell’ analisi di alto livello, usati per descrivere il contesto
del problema, in una forma implementabile
Specificare i vincoli HW e SW
Bilanciare tra concetti di riutilizzo, modificabilità ed efficienza
Passi della fase di Design
System Design
–Architettura tecnica: Configurazione HW e SW adatta all’ ambiente target
–Architettura d’ applicazione: suddivisione del sistema in sottosistemi e loro relativa
associazione ai processi
Object Design
–Rifiniture e estensioni del modello di analisi
System Design produce :
• l’ architettura del sistema di alto livello, dove si specificano la configurazione HW e
SW e l’ ambiente operativo.
• l’architettura dell’ applicazione, che implica prendere decisioni riguardo l’
organizzazione del sistema in sottosistemi, il modo di allocazione dei sottosistemi sull’ HW
Object Design produce:
• rifiniture ed estensioni del modello di analisi per permettere un’ implementazione efficiente
• La definitiva definizione delle classi e delle associazioni usate nell’ implementazione,
interfacce, algoritmi e metodi da usare.
System Design.
Bisogna comprendere che costruire un’ applicazione con un’ appropriata
architettura e’ un fattore critico di successo per qualsiasi progetto OO.
Bisogna essere in grado di applicare i piu’ importanti criteri per costruire
una buona architettura.
Scopo di questa sessione e’ rendere consapevoli che lavorare con gli
oggetti non e’ sufficiente per costruire applicazioni OO, gli oggetti devono
essere sapientemente organizzati in moduli, sottosistemi, nel contesto del
Sistema completo. Il risultato di questo processo e’ l’ archirettura dell’
applicazione.
Object Design.
• Essere in grado di trasformare concetti di analisi di altolivello (associazioni, vincoli,
ereditarieta’ …) in concetti facilmente implementabili.
• Essere in grado di applicare ottimizzazioni ad un Object Model inefficiente.
• Essere in grado di mappare un OM in un DB per risolvere problematiche di
persistenza dati
Lo scopo di questa sessione e’ discutere in dettaglio i passi che devono essere risolti
durante la fase di Object Design e applicare tecniche per risolverli.
Qualche regola utile per affrontare la fase di progettazione, ossia ‘come’
realizzare quanto e’ stato specificato nella fase di analisi:
Occorre individuare:
• un insieme di condizioni, verificabili in modo sistematico ed oggettivo, che
possono portare a problemi di vario tipo (impossibilità di riusare una classe, o di
supportare nuove funzionalità, o di estendere una gerarchia, eccetera). Ogni
condizione viene normalmente fatta risalire alla violazione di uno o più princìpi di
design, ma non pretende di catturare una casistica troppo ampia: si preoccupa
invece di essere verificabile e costruttiva. Il progettista verifica l'insorgere di
queste condizioni durante la normale attività di progettazione. Inoltre, ogni
regola spiega chiaramente quali sono le conseguenze di una sua violazione: il
progettista può benissimo decidere che tali conseguenze sono perfettamente
ragionevoli ed accettabili nel suo caso concreto. L’importante, come sempre, è
che questo tipo di decisioni siano prese consciamente in fase di design, e non
succeda invece di scoprire una conseguenza inattesa molto più avanti, ad
esempio in fase di estensione e/o di manutenzione.
•
Un insieme di tecniche di trasformazione. Per ogni condizione problematica,
esistono diverse tecniche di trasformazione che possono essere applicate, in
modo sistematico, per eliminare il problema e riportare il progetto entro il
rispetto dei migliori principi di design. La scelta della migliore tecnica dipende
ovviamente dai molti fattori al contorno, e richiede quindi una valutazione del
progettista che mantiene un ruolo molto importante.
•
Un insieme di criteri per valutare le qualità relative di design alternativi. Spesso
ci troviamo di fronte a delle scelte di progetto e non abbiamo basi concrete sulle
quali decidere. Esistono però dei criteri fondamentali, come stratificazione, e l’
accoppiamento, che possono essere adattati anche al paradigma ad oggetti e
possono fornire una guida oggettiva in molte situazioni .
Consideriamo la figura; qui e’ stata modellata una applicazione che gestisce due tipi di
documento, su ognuno dei quali esistono due tipi di viste. Le viste concrete accedono al
documento concreto per leggerne e modificarne i dati. I documenti concreti parlano con
le viste concrete per mantenerle sincronizzate dopo ogni modifica. Ne consegue
un’ovvia dipendenza circolare. Analogamente, il document manager crea i documenti, e
ogni documento notifica al manager la propria distruzione. Di nuovo, abbiamo una
dipendenza circolare.
Nessuna di queste dipendenze circolari è accettabile: noi vogliamo poter riusare
il document manager con altri documenti, ed i documenti con altre viste.
Vogliamo anche una estendibilità sul versante documenti e viste.La figura
seguente, dove la classe View disaccoppia i documenti dalle viste.
Ora possiamo applicare la stessa trasformazione tra Document e Document
Manager. Il risultato è visibile nella figura seguente: si e’ partiti da un design
banale e applicato in modo sistematico regole di trasformazione. Non solo,
strada facendo abbiamo anche capito meglio il ruolo di ogni classe: ad esempio,
se nella nostra applicazione avessimo un solo tipo di documento (es.
Document1), e non prevedessimo un riuso od una estensione di tale classe,
potremmo evitare di introdurre la classe astratta Document: questa emerge
infatti come elemento di disaccoppiamento tra DocumentManager ed i
documenti reali.
Architettura
Definizione dell'architettura
Al termine della fase di analisi, abbiamo identificato un certo numero di
classi, relazioni, ed eventi tipici del sistema; ci si potrebbe chiedere se
tale struttura non identifichi immediatamente l'architettura del sistema
stesso, proprio perche' ne cattura gia’ gli elementi essenziali. Cio‘
sarebbe auspicabile, e di fatto costituirebbe un enorme punto
a favore dell'approccio object oriented, ma purtroppo in genere non
corrisponde a verita'. Se il sistema e’ relativamente semplice, come nel
caso degli esempi necessariamente ridotti che si trovano in libri ed
articoli, si ha in effetti una stretta corrispondenza tra il risultato dell‘
analisi e l'architettura del sistema; ma non appena si esce dai
confini ridotti della divulgazione, tuttavia, la complessita' dei sistemi
richiede metodi piu' completi.
Architettura tecnica
•
Definizione della piattaforma (o delle piattaforme) che dovranno
ospitare il sistema. Questo include l'architettura hardware, almeno negli
aspetti piu' rilevanti, ossia quegli aspetti visibili dagli altri team di progetto. Il
lato hardware si estende inoltre a tutte le periferiche che dovranno essere
controllate dal sistema, aspetto particolamente importante in software di
controllo industriale. La piattaforma comprende inoltre il sistema operativo, gli
eventuali gestori di basi dati con i quali e' necessario interfacciarsi, eventuali
sistemi GUI per i quali il sistema dovra' essere disponibile.
La scelta della piattaforma andrebbe eseguita piuttosto rapidamente; anche se cio' puo'
sembrare limitativo, in quanto future scelte architetturali potrebbero suggerire
piattaforme alternative, in pratica la scelta non di rado e' abbastanza semplice.
Spesso, infatti, la scelta di una piattaforma e' motivata piu' da ragioni economiche o
strategiche che non tecniche: talvolta un sistema operativo poco diffuso o
sperimentale(privo di licenze gravose) potrebbe forse essere la piattaforma ideale per l’
applicazione, ma se si vuole vendere il prodotto bisogna orientarci verso uno standard di
mercato(per problemi di compatibilita’). Oppure, la scelta di una particolare architettura
hardware e' motivata da precedenti esperienze interne all’ azienda, o dalla richiesta del
committente.
Nel caso di un sistema di misura ho gia’ avuto modo di fare cenno alla
complessita’ dei componenti coinvolti.
Molto genericamente quasi tutte le aziende di questo settore hanno adottato
Microsoft come fornitore, Questo significa MSWindows (da ed. 95 XP) come
s.o. base per i loro sistemi, OFFICE per i programmi accessori, in particolare
Access per i DB.
Si e’ ormai standardizzata la comunicazione tra applicazione di misura e controllo
(HW e Firmware) via USB, in passato la comunicazione avveniva via RS232.
Gli utensili sono invece standard di mercato (quasi tutti), hanno un prorpio
protocollo di comunicazione.
I controlli, quindi il cassetto elettronico, rimane uno sviluppo proprietario, qualche
azienda sta iniziando a produrli in modo standard. Quindi anche i protocolli di
comunicazione sono proprietari. Facile capire perche’: in questo modo si osteggia
la concorrenza nella guerra al retrofit.
Architettura dell’ applicazione
•
La struttura statica. Questa parte sara' in effetti largamente basata sul risultato
dell'analisi, ed identifica le componenti statiche del problema, ovvero le astrazioni
di piu' alto livello.Il punto di partenza per definire la parte statica dell'architettura
sono proprio le "categorie di classi" o "i soggetti" , ovvero insiemi di classi
omogenei e coesivi, e debolmente accoppiati con il resto del sistema.
•
La struttura dinamica. Questa parte, per la cui definizione sara' fondamentale
ma non sufficiente identifica le componenti attive del sistema, il comportamento
(behaviour) del sistema stesso, la concorrenza interna, le interazioni con il mondo
esterno. La struttura dinamica puo' essere molto semplice in sistemi mono-task,
ad esempio un programma di data-entry, ma puo' arrivare a livelli di complessita'
enormi in sistemi dinamici e distribuiti.
•
Una pianificazione di consegna. Troppo spesso nella trattazione teorica dello
sviluppo si dimenticano le esigenze contingenti, come la realizzazione di un
prodotto semi-funzionanti (α release, β release) a scopo dimostrativo, ma che
sono veri e propri sotto-sistemi completamente operanti. Tali vincoli sono invece
fondamentali per la definizione di una architettura adeguata per il sistema da
realizzare: se e' necessario avere una versione intermedia che possa colloquiare
con una base dati esistente, partire con una architettura ambiziosa totalmente
basata su oggetti persistenti puo' portare a problemi piuttosto difficili da risolvere
e ad imbarazzanti ritardi di consegna. La realizzazione di sistemi complessi include
un buon processo di pianificazione, che influisce sulle componenti tecniche del
progetto: puo' non piacere, ma e' vero.
La pianificazione di consegna e' nuovamente un fattore extra-tecnico, anche se
un management illuminato dovrebbe sempre tenere in considerazione gli aspetti
tecnici di alto livello, durante la stesura dei tempi per il design e
l'implementazione. Nuovamente, di rado esiste una grande liberta' di scelta:
trascurando le situazioni del tipo "tutto e subito", sintomo caratteristico di un
approccio errato alla pianificazione dei progetti, esistono priorita' ben definite
che devono essere rispettate da un punto di vista contrattuale.
Far combaciare tali esigenze con la definizione architetturale del sistema non
sempre e' semplice o possibile (non a caso i ritardi di consegna non stupiscono
piu' di tanto nel mondo del software, ma vengono sempre rinfacciati), ed e'
realmente necessario che i progettisti interagiscano e cooperino con il project
manager (e viceversa) alla stesura di un piano realistico che non imponga scelte
architetturali troppo limitative.
La definizione della struttura statica e della struttura dinamica costituiscono
invece il nucleo tecnico del design architetturale, e come tali sono le piu'
interessanti.
Design Architetturale
Tutti gli elementi fondamentali di un problema, che sono stati evidenziati dalla fase di
analisi saranno modellati come classi; allo stesso modo, anche la gestione files e la
memoria condivisa sono classi, ed essendo implicito nella descrizione informale che dal
punto di vista della rappresentazione la provenienza dei dati e' irrilevante, le
caratteristiche comuni di file e memoria condivisa possono essere astratte in una
classe base "stream".
Inoltre, l'utente viene inserito come parte del dominio del problema: anche se non era
esplicitamente nominato nella descrizione informale(ma negli use cases della fase di
analisi compare), e' evidente che i "click" ed i "doppi click“, cioe’ tutte le interazioniutente hanno un'origine. E' buona norma rappresentare nel modello analitico anche gli
elementi del mondo esterno che interagiranno con il sistema informatico: cio' consente
una migliore vista di insieme, e permette di individuare a colpo d'occhio (con un po' di
esperienza) le situazioni in cui l'interazione tra alcuni elementi dell'ambiente ed il
sistema e' troppo debole o troppo forte.
Le scelte architetturali tendono ad essere pesantemente influenzate dalla piattaforma
software sulla quale intendiamo implementare l'applicazione: ad esempio, una
piattaforma nella quale esista gia' una astrazione dei file e della memoria condivisa, in
termini di memory-mapped-file, porterebbe ad una struttura molto diversa da quella
richiesta da una piattaforma con una distinzione netta tra memoria condivisa e file, e
dove sia quindi compito del progettista fornire la necessaria astrazione. Per questo e'
cosi' frequente che il lavoro degli analisti sia ignorato dai progettisti, ed il lavoro di
questi ultimi sia ignorato dai programmatori: un grado di astrazione cosi' alto da
consentirci di rimanere totalmente indipendenti dalla piattaforma, sino alla fase di
implementazione, e' altamente desiderabile ma anche molto rischioso: un buon
progettista capisce quando e' necessario rinunciare all'astrazione per evitare ai
programmatori il compito di ri-progettare il sistema.
L'architettura banale
Si puo’ essere tentati di trasferire direttamente il risultato dell'analisi dallo spazio
del problema a quello della soluzione. in questo caso il modello del design e'
identico al modello ottenuto al termine dell'analisi.
Molto spesso l'architettura "banale", cosi' chiamata in quanto deriva in modo
immediato e (appunto) banale dal risultato dell'analisi, e' in effetti l'architettura
che viene scelta per l'applicazione; e' altrettanto vero che molto spesso non
corrisponde alla migliore architettura possibile.
Vi sono tuttavia molte ragioni che portano alla scelta della soluzione banale: la
mancanza di tempo per considerare strutture alternative, la mancanza di
esperienza dei progettisti, un ciclo di sviluppo piu' orientato all'implementazione
che al design, o troppo guidato dall'interfaccia utente.
Quest'ultimo caso si presenta non di rado, in questi tempi in cui il "Rapid
Application Development" viene venduto come la nuova promessa
dell'informatica; come il paradigma a prototipi, di cui costituisce una nuova
formulazione ma di cui condivide pregi, difetti e campi di applicabilita', il RAD
tende spesso a generare prodotti under-architectured, ovvero con una
architettura banale, ricavata di norma mediando tra l'interfaccia utente ed il
dominio del problema.
Il problema maggiore della soluzione banale e' in genere proprio questo:
l'eccessivo accoppiamento tra le classi, che si ripercuote in una mancanza di
flessibilita' e di riusabilita' del codice e del design dettagliato. Cio' dovrebbe far
seriamente riflettere chi sostiene che le tecniche object oriented non
mantengono "per se'" la promessa del riutilizzo: nessuna tecnica puo' riparare
grossolani errori di progettazione dei componenti, ed una architettura carente
impedira' il riutilizzo del codice in qualunque paradigma di programmazione.
In tal senso, un programma object oriented dove i componenti siano
accoppiati troppo strettamente, non e' diverso da un programma nel
paradigma strutturato dove le procedure siano troppo interdipendenti: il
codice non potra' essere riutilizzato perche' non e' stato progettato in modo
appropriato.
Meno accoppiamento con le Classi Interfaccia
Un passaggio del paragrafo precedente rivela in realta' una possibile alternativa:
possiamo studiare una architettura che riduca l'accoppiamento,
indipendentemente dal linguaggio, introducendo delle classi piu' astratte che
operino da interfaccia rispetto all'implementazione. per ogni classe
dell'architettura banale, introduciamo una corrispondente classe interfaccia, il cui
compito e' appunto di astrarre l'interfaccia della classe (i suoi metodi pubblici)
dall'implementazione concreta dei metodi stessi. A livello implementativo, le
classi interfaccia non implementano i metodi, ma (usando gli appositi costrutti
del linguaggio) delegano alla classe implementazione la gestione degli stessi.
CUSB
CDriverProtocolUSB
End1
CDriverCMM
CSerial
CDriverProtocolSerial
End2
CMainCMM
Scalabilita' con gli Advise Sink
Quando ci troviamo di fronte a numerosi oggetti, anche la soluzione precedente
non e' particolarmente indicata: la tecnica delle classi interfaccia farebbe
raddoppiare un gia' consistente numero di classi, e non risolverebbe
completamente il problema dell'accoppiamento sulle istanze; dimensioni, e
difficolta' di testing.
Concetto non semplice.
Di norma vengono evidenziate tutte le classi che hanno tra di loro rapporti di
client-server. A fronte di cio’ si devono tenere in considerazione i meccanismi di
comunicazione tra loro. Quindi per queste classi non sara’ necessario applicare
una tecnica di disaccoppiamento, ma sara’ sufficiente implementare una buona
struttura di advise-sink.
Stratificazione
Una delle caratteristiche di un buon design è quella di separare elementi posti a diversi
livelli di astrazione; questa caratteristica viene comunemente indicata come
stratificazione, intendendo così che ogni sottosistema può essere visto come un insieme
di strati, di livello sempre più astratto, ognuno dei quali utilizza, almeno in linea di
principio, solo i servizi dello strato sottostante.
Un esempio ben noto di architettura stratificata, indicata in questo caso anche come "a
cipolla", è quella di molti sistemi operativi: al più basso livello di astrazione abbiamo
l'hardware, al più alto le applicazioni, ed in mezzo diversi strati che isolano dai dettagli dei
livelli sottostanti: gestione dei task, della memoria, del file system, e così via.
La stratificazione mira a ridurre le dipendenze tra gli elementi del sistema, e quindi il
fenomeno delle modifiche a cascata; notiamo la differenza tra stratificazione ed
incapsulazione: in un sistema object oriented, l'incapsulazione nasconde i dettagli
implementativi della classe, mentre la stratificazione riduce gli accoppiamenti sui
messaggi. L'incapsulazione in sé non è sufficiente a proteggere dalle modifiche
a cascata, poiché modifiche all'interfaccia di una classe (che possono
comunque avvenire) richiedono necessariamente un adattamento nelle classi
client. L'interposizione di diversi livelli di astrazione dovrebbe, se ben
strutturata, smorzare la propagazione delle modifiche verso i livelli successivi.
Come si applica il concetto di stratificazione a livello di design object oriented? In gran
parte, osservando le connessioni di messaggio, a livello delle categorie di classi, ed
all'interno di queste al livello dei singoli oggetti. Da tali connessioni possiamo determinare
un grafo diretto, dove due nodi (classi o categorie di classi) sono connessi se e solo se
esiste una connessione di messaggio tra un oggetto di una classe (o categoria di classi) e
l'altra
Design del dominio del problema
Dettagli della struttura statica e definizione della struttura dinamica
del modello.
Le componenti essenziali di un modello OO:
1.
Dominio del problema
2.
Interfaccia utente
3.
Gestione dei task
4.
Gestione dei dati
Struttura statica
Il dominio del problema è stato in gran parte identificato durante il
processo di analisi, che ha evidenziato le classi rilevanti ai fini del
modello, le relazioni tra esse esistenti, gli scambi dei messaggi, e così
via. Perché dunque riconsiderarlo a proposito del design? Ricordiamo che
attraverso l'analisi abbiamo stabilito cosa debba fare il sistema; è ora
importante definire, all'interno del dominio del problema, come tali
compiti verranno svolti. Ciò comporterà, l'estensione o la parziale
revisione delle gerarchie di classi coinvolte, in quanto dovremo
necessariamente introdurre nuovi elementi appartenenti al dominio della
soluzione (o più esattamente, di una possibile soluzione tra le molteplici
scelte possibili).
Passare da una descrizione del problema ad una definizione della soluzione,
nell'ambito di un modello object oriented, comporta i seguenti passi:
• la definizione degli algoritmi fondamentali, con le strutture dati o le
classi associate, quando necessario.
• l'introduzione delle cosiddette classi di infrastruttura.
• l'introduzione di classi aggiuntive nel dominio della soluzione.
• l'eventuale introduzione di classi di controllo
Definizione degli algoritmi.
Indipendentemente dal paradigma utilizzato, la soluzione di un problema comporta
sempre la ricerca e l'utilizzo di un appropriato processo di calcolo o di manipolazione
dell'informazione, ovvero un opportuno algoritmo. La formulazione concreta
dell'algoritmo può essere diversa, in funzione del paradigma ed anche delle
potenzialità del linguaggio target, ma una formulazione astratta dell'algoritmo
stesso prescinde in gran parte dal formalismo utilizzato: quicksort resta comunque
quicksort, sia esso realizzato attraverso una funzione che opera su array, da un
predicato in programmazione logica, o con l'ausilio di una gerarchia di classi aventi
una funzione virtuale per il calcolo del minore tra due elementi.
La conoscenza degli algoritmi e delle principali strutture dati (alberi, grafi,
Hash table, e così via) deve far parte del bagaglio culturale di ogni programmatore o
progettista: naturalmente, conoscere una formulazione nell'ambito del paradigma
object oriented può essere più vantaggioso, ma è spesso sufficiente una buona
conoscenza anche di una formulazione "classica", basata su abstract data types o su
una visione funzionale. Scegliere un algoritmo può essere banale in alcuni casi,
mentre può diventare un vero e proprio progetto di ricerca, con lo studio e la
definizione di nuovi procedimenti, qualora le esigenze siano particolarmente ardue
da soddisfare.
La scelta di un algoritmo dipende sia da motivazioni tecnologiche che dalla singola
applicazione.
Le “motivazioni tecnologiche" includono ad esempio l'architettura hardware a disposizione:
per citare un esempio semplice, spesso occorre decidere se un dato vada memorizzato o
ricalcolato ogni volta. D'altra parte, è l'applicazione stessa che determina la frequenza delle
singole operazioni: ciò che potrebbe essere conveniente con una determinata distribuzione
della frequenza delle operazioni, può non esserlo con un'altra. Un albero perfettamente
bilanciato può avere senso se le operazioni di ricerca sono molto più frequenti di quelle di
inserimento/cancellazione, ma non viceversa.
Quando definiamo o comunque decidiamo l'utilizzo di un determinato algoritmo, facciamo
spesso riferimento a strutture dati (ovvero, nel nostro contesto di OOD, a classi) ben note e
presenti in quasi ogni programma: liste, alberi, tabelle, matrici, eccetera. Queste classi
"base", che infatti sono spesso contenute nelle librerie di base dei linguaggi, formano le
cosiddette classi di infrastruttura; notiamo che in alcuni casi sarà necessario arrivare
comunque al design dettagliato di classi di infrastruttura, o fare riferimento ad una particolare
implementazione: se la cancellazione di elementi random di una lista è un'operazione
frequente, un'implementazione della lista con doppi puntatori sarà più indicata di una con
singoli puntatori. In molti casi, tuttavia, possiamo introdurre nel dominio della soluzione le
necessarie classi di infrastruttura senza preoccuparci eccessivamente dell'implementazione,
ma definendo semplicemente opportuni vincoli che verranno poi passati al programmatore.
Oltre a queste classi "generiche", la nostra particolare soluzione potrà richiedere
anche la definizione di nuove classi, non facenti parti a priori del dominio del
problema, e che sono specializzate per la soluzione identificata.
Tra le classi specializzate, un ruolo particolare è spesso riconosciuto alle
cosiddette classi di controllo, che interfacciano l'applicazione con il mondo
esterno, inteso sia come sistemi di I/O che come interfaccia utente. Per
esempio, nel paradigma model/view/controller, i "dati" dell'applicazione e
l'interfaccia utente non sono direttamente collegati, ma l'interazione passa
attraverso classi intermedie (controller); questo approccio ha il vantaggio di
diminuire l'accoppiamento con l'interfaccia utente, al prezzo di una codifica più
complessa. Per questa ragione, molti framework commerciali si basano su un
paradigma model/view , con controller fissato o embedded , piuttosto che sul
model/view/controller.
Specificare le classi
Tutte le classi di cui sopra verranno aggiunte al nostro modello object oriented,
arricchendo di conseguenza il risultato dell'analisi; il design non si ferma
naturalmente qui, in quanto è ora necessario scendere nel dettaglio di ogni
singola classe e definire gli elementi lasciati in sospeso durante la fase di analisi
o i passi precedenti.
•
•
In particolare, il risultato finale del design deve comprendere:
la definizione completa dell'interfaccia di ogni classe. Questo significa
definire, per ogni metodo esportato dalla classe stessa, il tipo del risultato e dei
parametri, se si tratta di un metodo const, se il metodo è virtuale o meno.
la definizione completa dei dati privati di ogni classe. Questo può
includere una descrizione dettagliata di strutture dati complesse o inusuali,
definizione di algoritmi di manipolazione, e così via.
•
la definizione dei metodi privati e protetti delle classi, non
•
La descrizione per ogni metodo(almeno i piu’ complessi), dell‘ algoritmo
necessariamente completa dei dettagli implementativi, ma comprendente
almeno gli elementi fondamentali per la comprensione della soluzione proposta.
associato. Ciò può essere ottenuto in modo più o meno formale, sia utilizzando
pseudocodice che formalismi grafici o linguaggi di specifica. In genere, ha poco
senso utilizzare flowcharts: se avete chiaro un linguaggio target, utilizzate uno
pseudocodice basato su di esso. L'uso di linguaggi di specifica è di solito limitato
ai progetti con requisiti di correttezza molto stringenti.
Tutti i vincoli che ha senso introdurre nel modello, incluse precondizioni e
postcondizioni relative ai singoli metodi o invarianti relativi allo stato dell'oggetto
o ai singoli passi di un processo di calcolo. Precondizioni, postcondizioni ed
invarianti sono espressioni logiche, utilizzate per formalizzare proprietà di oggetti
o funzioni: le precondizioni si utilizzano spesso per delimitare maggiormente lo
spazio dei valori che possono essere assunti dai parametri dei metodi, mentre le
postcondizioni sono di norma riferite allo stato di un oggetto dopo l'invocazione
di un metodo, per quanto si possano utilizzare anche internamente alla specifica
di un algoritmo, per chiarire lo stato di alcuni stadi intermedi.. I vincoli possono
essere espressi in modo più o meno formale, spaziando dal linguaggio naturale
allo pseudocodice fino all'uso di un linguaggio di specifica
Infine, va ricordato che il livello di dettaglio a cui potete arrivare in fase di design è spesso
arbitrario: in molti casi, una definizione dell'interfaccia pubblica e una visione ad alto livello degli
algoritmi sarà più che sufficiente, salvo i casi più complessi in cui è necessario definire in modo
più preciso le strutture dati coinvolte e formulare un algoritmo con maggiore precisione. Come
sempre, è difficile dire "quando fermarsi", poiché dipende molto dall'organizzazione: se chi
esegue il design è anche incaricato di implementare il codice relativo, può trovare utile lasciare
il design dettagliato ad un livello piuttosto astratto, e passare all'implementazione. D'altra parte,
se il design e l'implementazione vengono eseguiti in tempi e/o da persone differenti, puo’
essere necessario fornire maggiori informazioni in fase di progettazione, onde evitare errate
interpretazioni. Non è insolito che, nel caso del singolo sviluppatore incaricato di un progetto, il
design e l'implementazione si fondano in un processo unico, in cui è difficile identificare dove
termini uno e cominci l'altro; ciò non costituisce in sé un problema, ammesso che esistano
comunque dei "documenti di design" cui fare riferimento, e non si debba invece estrarre e
ricostruire tutta la conoscenza dal codice. Molte scelte, non solo di livello architetturale, ma
anche riferite alle singole decisioni a livello di strutture utilizzate, sono difficili o impossibili da
ricostruire dal codice: non è quindi raro che decisioni iniziali, mai documentate, vengano
implicitamente mantenute in future versioni, anche a fronte di modifiche dei requisiti,
semplicemente perché nessuno ne ricorda le motivazioni. Se le frequenze delle operazioni
cambiano, ad esempio, potrebbe avere senso riconsiderare le strutture dati coinvolte: non di
rado, tutttavia, non esiste traccia di come esse siano state scelte nella prima implementazione.
Se nel vostro processo di sviluppo vi è poco spazio per il design, cercate almeno di motivare le
scelte eseguite come guida per chi dovrà comprendere e modificare in seguito il vostro codice.
Struttura dinamica
Una volta definite le interfacce delle classi ad un adeguato livello di dettaglio, il che
può includere la definizione delle strutture dati e degli algoritmi fondamentali che
dovranno svolgere i compiti relativi, abbiamo terminato il progetto della parte
statica del sistema. Ovviamente vi saranno in seguito dei raffinamenti, in quanto il
processo di sviluppo è per natura iterativo, ed è spesso necessario riconsiderare
alcune scelte mentre il sistema evolve.
D'altra parte, la definizione statica del sistema non è sufficiente a caratterizzare il
comportamento del sistema stesso: essa si concentra troppo sulla singola classe,
con un'enfasi particolare sulla visione esterna della classe stessa, e non pone nel
giusto rilievo le interazioni tra le classi e gli eventuali vincoli temporali che esse
comportano. Inoltre, talvolta un oggetto può trovarsi in diversi stati ben
determinati, che influenzano in modo molto forte il suo comportamento. In genere,
è difficile comprendere le transizioni di stato da una descrizione statica del sistema,
anche se con un adeguato livello di dettaglio è spesso possibile. Si ricorre pertanto
a descrizioni più espressive, sia per le interazioni tra oggetti che per la
rappresentazione degli stati di un oggetto: queste fanno parte del dominio della
cosiddetta struttura dinamica del sistema, che tuttavia non si limita a questo. Una
parte della struttura dinamica è rappresentata dalla gestione dei task, che vedremo
in seguito.
Diagramma degli stati
Ad ogni istante, un oggetto si trova in uno stato ben determinato, definito dal
valore delle sue variabili di istanza. In alcuni casi, il comportamento dell'oggetto
dipende pesantemente dallo stato in cui si trova, o da alcune delle sue
componenti.
Notiamo che la distinzione tra risultato e comportamento può essere molto
sottile in alcuni casi, e dipende strettamente da come vengono definiti l'uno e
l'altro.
Quando un oggetto si comporta in modo molto diverso in funzione del suo
stato, lo stato diventa un elemento molto rilevante per comprendere il
funzionamento dell'oggetto, ed è in genere importante documentare in modo
preciso come avvengono le transizioni di stato, e quali conseguenze tali
transizioni abbiano sull'oggetto stesso e sul mondo esterno.
In tal caso si utilizza in genere un diagramma degli stati, ovvero un grafo
diretto dove i nodi rappresentano gli stati e gli archi le possibili transizioni tra gli
stati. Gli archi sono in genere etichettati per rappresentare lo stimolo che
scatena la transizione (proveniente in genere dal mondo esterno, anche se
talvolta ha senso considerare eventi interni al sistema).
Gestione dei dati
Modifiche che la fase di design introduce a livello dei dati: normalizzazione ed
ottimizzazione delle relazioni, persistenza dei dati e delle diverse soluzioni per
ottenerla, Stutture database object oriented a tecniche più tradizionali di
serializzazione dei dati.
Persistenza dei dati
Le considerazioni e le scelte di persistenza dati si applicano al momento
della definizione degli attributi e delle classi, all'interno della fase di
design; esse influenzeranno direttamente sia l'esistenza di alcune classi
che il layout interno di altre. Rimane comunque in sospeso un problema
molto sentito, ovvero come memorizzare gli oggetti su disco e come
recuperarli: questo problema, che viene in genere detto di "persistenza
dei dati", è di soluzione tutt'altro che immediata, e può essere affrontato
a due differenti livelli:
1.
Utilizzando un database orientato agli oggetti; in questo caso, la
persistenza degli oggetti è garantita dal database stesso. I database object
oriented sono relativamente nuovi, e non vi sono molti prodotti commerciali
disponibili; in ogni caso, esistono sia prodotti stand-alone (equiparabili ad un
tradizionale package con funzionalità di database) sia librerie che possono
essere utilizzate con linguaggi tradizionali.
2.
Implementando una soluzione all'interno di un normale linguaggio
object oriented; in questo caso, dovremo in genere definire manualmente,
per ogni classe, alcuni metodi che si occuperanno della gestione specifica dei
nostri dati ed appoggiarci a meccanismi gia’ presenti (es. Objectlibrary ADO).
Serializzazione
•
•
•
•
Rimanendo all'interno dei linguaggi tradizionali, un metodo molto semplice per memorizzare un
oggetto consiste nel "serializzarlo", ovvero emettere in un formato memorizzabile su file o DB la
sequenza (serie) dei suoi campi. L'operazione è invertibile per recuperare gli oggetti. Questa
soluzione lascia scoperti numerosi problemi, che possono essere affrontati in modi diversi:
come gestire i puntatori e lo sharing di oggetti in fase di memorizzazione
come gestire i puntatori e lo sharing di oggetti in fase di lettura
I puntatori non possono in genere essere memorizzati come tali, in quanto non vi è nessuna
garanzia che ricaricando gli oggetti il precendente valore del puntatore sia valido e punti alla
corretta area di memoria. A questo proposito esistono diverse soluzioni, tra cui:
utilizzare uno pseudo-puntatore, ovvero un indice di riferimento all'oggetto puntato che viene
memorizzato altrove.
utilizzare uno schema pseudo-relazionale, memorizzando la chiave dell'oggetto puntato.
Osserviamo che i due approcci coincidono se consideriamo l'indice come chiave dell'oggetto
puntato; in entrambi i casi, è comunque necessario avere a disposizione un meccanismo di
ricerca di un oggetto dato un indice o una chiave; per esigenze di prestazioni, ciò comporterà
l'uso di tutto l'apparato ben noto di tecniche di indicizzazione, cache, e così via, oltre agli
eventuali problemi di accesso concorrente e di sharing dei dati Si tratta comunque in genere di
codice che può essere posto in una libreria di classi e riutilizzato senza sforzo nei singoli casi.
Task & Threads
I principali fattori che influenzano la progettazione orientata agli oggetti,
quando la piattaforma target disponga di funzionalità di multitasking o
multithreading. La migliore architettura può essere ottenuta solo valutando con
attenzione i possibili vantaggi offerti dal sistema operativo sottostante.
I sistemi operativi multi-tasking sono ormai molto diffusi; in molti casi, è anche
disponibile una forma di "task all'interno del task", chiamato normalmente
thread. Esistono anche librerie per lo sviluppo di programmi multi-threaded su
piattaforme single-task come l'ormai vetusto MS-DOS. L'utilizzo di queste
funzionalità del sistema operativo prescinde in gran parte dal paradigma di
sviluppo utilizzato; tuttavia, se progettiamo un sistema che verrà ospitato su una
piattaforma che consente il multitasking e/o il multithreading, possiamo ottenere
la migliore architettura solo valutando con attenzione i possibili vantaggi offerti
dal sistema operativo sottostante.
Oggetti, Task e Thread
La decisione più difficile da prendere correttamente è in effetti il corretto
partizionamento degli oggetti, ovvero quali apparterranno ad un certo task e
quali ad un altro, quali oggetti apparterranno ad un thread (o viceversa), e
così via.
Vi sono al proposito diverse opzioni, nonché diverse opinioni: ad esempio, non
pochi suggeriscono di avere un thread per ogni oggetto. Da un punto di vista
astratto, in effetti, questa posizione è largamente condivisibile: gli "oggetti" del
mondo reale sono entità autonome, ed ognuna "agisce" in modo
sostanzialmente indipendente, anche se talvolta in coordinazione e
cooperazione con altri oggetti.
Ciò non significa che sia sempre la scelta migliore: anzi, in molte occasioni è
possibile arrivare ad un design più efficiente, o più semplice da capire e da
mantenere, solo compiendo lo sforzo necessario per individuare i livelli di task,
thread, e gli oggetti che devono agire ai diversi livelli. Pertanto, vedremo ora
alcuni dei fattori che influenzano la scelta; ovviamente, come in altri casi, è
impossibile restare completamente indipendenti dall'architettura
hardware/software su cui il sistema stesso verrà poi implementato: ad
esempio, esistono grandi differenze, sia a livello delle prestazioni nelle diverse
situazioni, sia a livello dei criteri da utilizzare nel progetto..
Fluidità dell'interazione.
Uno degli scopi da perseguire è una maggiore fluidità dell'interazione con
l'utente; pertanto, ogni volta che e’ possibile isolare un insieme di attività che
possono essere eseguite in modo concorrente, minimizzando così le attese
dell'utente, queste costituiscono dei candidati per il partizionamento in task o
thread.
Esempi tipici sono la stampa, l'acquisizione dati da una periferica, attività
"batch" come la compilazione o l'elaborazione di dati.
Uno scenario tipico nell’ ambito di un sistema di misura e’: l’ applicazione di
misura sta eseguendo il programma di misura, cio’ implica una stretta
comunicazione con il controllo della macchina di misura, per impartire i comandi
di movimentazione ed acquisire le coordinate-punti rilevare, nel contempo
verranno attivati gli algoritmi di elaborazione matematica per gli elementi
geometrici da calcolare, la visualizzazione dei risultati in formato report e l’
aggiornamento della finestra di visualizzazione coordinate, quindi anche una
forte interazione con l’ interfaccia utente
uso di linguaggi diversi: in un sistema multi-tasking ben progettato, i vari task
dovrebbero comunicare attraverso un protocollo ad alto livello. Ciò ha anche il
vantaggio di permettere la codifica di task diversi in linguaggi diversi; mentre
per piccole applicazioni ciò è spesso controproducente, in quanto richiede sforzi
"di adattamento" ai programmatori, su applicazioni medio/grandi può essere
un'ottima opportunità per utilizzare il linguaggio più adatto ad ogni compito (ad
esempio, un linguaggio pensato per lo sviluppo rapido per l'interfaccia utente,
un linguaggio efficiente per un back-end di calcolo numerico), nonché una
possibilità di impiegare programmatori con competenze e formazione diversa.
Notiamo che ciò non avviene, naturalmente, a livello di thread, che normalmente
hanno accoppiamento di comunicazione più alto, e vengono scritti nello stesso
linguaggio del task che li ospita.
tempi di compilazione: come conseguenza delle considerazioni viste al punto
precedente, anche se manteniamo lo stesso linguaggio di codifica, abbiamo un
completo isolamento delle unità di compilazione per i diversi task. Nuovamente,
su un grande sistema ciò può corrispondere a grandi risparmi nel tempo di
sviluppo, proprio perché ogni task può essere modificato e ricompilato in una
frazione del tempo necessario alla ricompilazione e soprattutto al linking
dell'intero sistema. Nuovamente, questo vantaggio si applica solo ai task, non ai
thread.
dimensione concettuale
se i task sono debolmente accoppiati ed altamente coesivi, come ci si aspetta
che siano, e se la comunicazione avviene attraverso un protocollo ad alto livello,
è ragionevole aspettarsi che gli sviluppatori (inclusi coloro che si occuperanno
della manutenzione) troveranno molto più semplice comprendere nella sua
interezza il task di cui si occupano. Viceversa, un sistema monolitico, molto
complesso, tende a superare le capacità dei singoli sviluppatori di comprenderlo
interamente. Anche se la struttura object oriented contribuisce (soprattutto
attraverso l'incapsulazione) a semplificare la comprensione del sistema, è
comunque molto più semplice raggiungere un buon grado di comprensione di un
programma quasi-autonomo che di una piccola parte di un software complesso
sviluppato in un unico task.
Un buon design di un sistema multi-tasking è spesso a sua volta stratificato, con
task di "basso livello" che eseguono compiti di back-end, come la lettura dei
dati, la stampa, il backup, e task via via più vicini alla visione esterna del
sistema.
protezione della memoria:
per quanto ci si sforzi di rendere robusti i propri programmi, questi tenderanno
comunque a contenere degli errori; ciò è sempre più probabile a mano a mano che
le dimensioni del software aumentano. In una applicazione monotask (sia singlethreaded che multi-threaded) un errore a run-time può compromettere l'integrità del
task stesso, che viene così terminato. D'altra parte, task diversi hanno spazi di
indirizzamento separati, e di norma gli errori non si propagano tra i task (a meno di
carenze del sistema operativo); pertanto, la suddivisione in più task può essere un
buon metodo per proteggere alcune parti critiche (ad esempio, la lettura e lo
storage dei dati) da altre meno critiche, ma più interattive, complesse, o soggette
ad errori run-time. In questo modo, ci si garantisce che i compiti fondamentali del
sistema non vengano interrotti da errori nei moduli secondari del sistema stesso.
librerie di sistema
occorre prestare grande attenzione nell'uso del multi-threading se vengono
utilizzare librerie di sistema o di terze parti, in quanto vi è la possibilità (se non
sono di recente sviluppo) che queste non siano thread-safe; un esempio molto
semplice può essere la funzione printf del C: se la sua implementazione non è
thread-safe, il buffer statico interno può essere sovrascritto da una seconda
invocazione prima che la precedente sia terminata. Di norma, è possibile
comunque ricavare delle primitive thread-safe interponendo delle funzioni
sincrone basate su semafori, o in alcuni casi sostituendo la funzione con una
classe che gestisca una coda di richieste; si tratta comunque di un problema
molto serio, da tenere in opportuna considerazione prima di definire una
struttura del sistema multi-threaded.
Note architetturali
In alcuni casi, la logica di sincronizzazione per i diversi thread o task non appartiene
a nessuna delle classi coinvolte, ma è solo motivata da una particolare politica di
gestione del sistema.
Inserire la logica di sincronizzazione all'interno delle classi riduce il potenziale
riutilizzo in un diverso sistema, esattamente come inserire le relazioni come membri
delle classi riduce la riusabilità delle stesse. Ciò non dovrebbe stupire, se
consideriamo che la sincronizzazione è un accoppiamento tra le classi, e che
l'accoppiamento riduce la possibilità di riuso.
Interfaccia Utente
Ultima fase del design, ovvero l'interfaccia con gli utenti del sistema.
Progettare un'efficace interfaccia utente per un programma non è molto diverso
dal progettare un cruscotto di automobile o i controlli di un apparato stereo.
In tutti i casi, esistono considerazioni di immediatezza(semplicita’ d’uso),
usabilità, idoneità, non-ambiguità, chiarezza, e così via, che vanno attentamente
vagliate se si desidera ottenere il miglior risultato.
Proprio per questa ragione, sarebbe utile per ogni progettista di interfacce, o
aspirante tale, estendere la propria conoscenza dell'interazione uomo-oggetti,
anche al di fuori del settore informatico
Modelli dell'interfaccia utente
•
•
•
•
Il progetto dell'interfaccia utente dovrebbe sempre essere ottenuto come mediazione
tra due modelli, spesso contrapposti: il modello concettuale dell'utente ed il modello
del programmatore; lo schema risultante viene definito modello del progettista.
Le caratteristiche dei diversi modelli.
Il modello concettuale dell'utente è un modello mentale, molto informale,
formato da un insieme di relazioni (di cui l'utente assume l'esistenza) tra un insieme di
elementi. Queste relazioni sono basate sia sull'esperienza quotidiana dell'utente, sia
sulla familiarità con il sistema e la somiglianza con altre applicazioni: in effetti, gli
esseri umani creano dei modelli mentali per le nuove situazioni in gran parte
basandosi sulla somiglianza con situazioni pregresse. Ad esempio, l'icona di un fax
verrà associata all'idea di un programma o di un device per l'invio di fax, in funzione
del background dell'osservatore (che ad esempio, potrebbe non avere mai visto un
fax). È molto importante cercare di avvicinarsi il più possibile allo schema mentale
dell'utente, perché ogniqualvolta si richiede l'adattamento dell'utilizzatore ad un nuovo
modello mentale, si rischia di vedere la propria applicazione etichettata come "troppo
complessa". Sfortunatamente, gli utenti non sono normalmente in grado di descrivere
il loro modello mentale: in effetti, di norma non siamo neppure consci di averlo;
esistono molte strategie per "scoprire" tale modello, tutte o quasi basate sui seguenti
punti:
classificazione degli utenti: background culturale, attitudini, aspettative nei confronti
del sistema, eccetera.
descrizione di un insieme di scenari reali di possibile utilizzo del sistema.
analisi delle procedure (manuali o automatizzate) attualmente in uso per lo
svolgimento di ogni operazione.
rispetto di standard a livello di ambiente operativo o aziendale.
Il modello del programmatore è normalmente ben codificato ed esplicito; fanno
parte del modello del programmatore le restrizioni tipiche della piattaforma target, le
convenzioni di sviluppo e le linee guida del produttore, gli elementi fondamentali di
human-computer interaction, e così via. Il programmatore riesce spesso a mappare un
modello object oriented nella sua interfaccia in modo quasi canonico, per quanto non
necessariamente ottimale. Un record di un database viene immediatamente
"immaginato" come una form a livello di interfaccia utente, con tanto di bottoni per la
navigazione del database. Purtroppo, la visione del programmatore non di rado si
rivela troppo tecnica per l'utente finale, ed è proprio in questo senso che nasce
l'esigenza di un modello che, in qualche modo, cerchi di mediare tra le esigenze,
talvolta contrastanti, della visione dell'utente e del programmatore.
Il modello del progettista rappresenta proprio questa mediazione; i suoi elementi principali
sono:
•gli oggetti a livello di interfaccia e le loro relazioni
•la rappresentazione visiva degli oggetti ("look")
•le tecniche ed i meccanismi di interazione ("feel")
Avendo a disposizione sia i documenti di design, sia una descrizione delle attività degli
utenti (come visto in precedenza), lo scopo del progettista è proprio quello di scegliere le
azioni e gli oggetti più rappresentativi (non tutti gli utenti avranno infatti lo stesso
modello), capire cosa va inserito nel progetto e cosa va escluso per le ragioni più varie
(difficoltà eccessiva, tempi di sviluppo, esistenza di prodotti analoghi, eccetera) ed infine
quali nuovi elementi debbano essere introdotti nel modello dell'utente (non
necessariamente l'automazione ricalca pedissequamente le procedure manuali).
Attraverso varie tecniche, il progettista deve far corrispondere ad oggetti del mondo reale
alcuni oggetti virtuali, ed esporre dei meccanismi per permettere l'interazione, il più
possibile semplice ed "indolore", tra l'utente e tali oggetti virtuali. Il momento
fondamentale, prima della scelta delle rappresentazioni visive, è quello delle tecniche e
dei meccanismi di interazione.
Modelli di interazione
Esistono due modelli fondamentali per l'interazione uomo-macchina, che non di
rado vengono utilizzati all'interno della stessa applicazione in momenti diversi. Il
primo modello, chiamato normalmente action-object, è il più indicato per gli
utenti inesperti od occasionali, anche se inerentemente meno flessibile. Il
secondo, detto object-action, lascia molta libertà operativa all'utente, ma proprio
per questa ragione è più indicato per gli esperti, che non necessitano di una
impostazione rigida ma semplice da seguire.
Il metodo action-object
Come suggerisce il nome, consiste nello scegliere l'azione da compiere, e poi
l'oggetto sul quale compierla; esistono molti esempi di applicazione del modello,
ad esempio l'apertura di un file all'interno di una applicazione: si seleziona prima
l'azione (File-Open) e poi l'oggetto (il file da aprire). Notiamo che in questo caso
il sistema ci può guidare, mostrando ad esempio i soli file corrispondenti
all'applicazione in uso, posizionandosi in una directory predefinita per quel tipo
di file, e così via. In generale, il metodo action-object guida il più possibile
l'utente, che in seguito alla selezione di una azione si trova di fronte una serie di
scelte, quasi tutte obbligate come la selezione di un oggetto o la compilazione di
una form, scelte che eseguite in sequenza portano al completamento di una
attività. Naturalmente, in questo caso il progettista costringe l'utente ad operare
secondo la propria visione dell'attività, ovvero il proprio percorso logico per
portarla a compimento; per un utente occasionale del sistema, o per utenti
inesperti, i benefici di una interazione guidata passo-passo superano spesso il
fastidio di trovarsi costretti in una sequenza preordinata. Ciò è tanto più vero
quanto meno arbitraria è la scelta dei singoli passi di interazione.
Il metodo object-action
Al contrario, consiste nello scegliere l'oggetto ed in seguito l'azione da compiere;
esempi tipici sono i browser/explorer o i programmi di disegno vettoriale. Nel
primo caso, possiamo navigare a piacere il disco, scegliendo in ogni momento
l'azione da applicare agli oggetti selezionati: aprire, cancellare, spostare,
eccetera. Nel secondo, possiamo selezionare un qualunque oggetto grafico ed
applicare qualche azione (ruotare, ingrandire, cambiare il colore, eccetera).
L'utente è totalmente libero da costrizioni: nei limiti delle funzionalità previste
dal sistema, non vi sono imposizioni di sorta sulla sequenza delle operazioni da
utilizzare per il completamento di una attività; l'utente esperto può adattare lo
strumento alla propria forma mentale ed alle proprie abitudini di lavoro, anziché
essere influenzato e guidato dallo strumento stesso. Viceversa, l'utente un pò
sprovveduto si troverà di fronte così tante scelte da esserne intimidito e
bloccato: non a caso, in Windows 95 esiste ora il famoso bottone "Start", che ha
fatto sorridere molti utenti con un minimo di disinvoltura, ma che ha fornito a
tutti i nuovi utenti un "aggancio" di partenza (action-object) verso un'interfaccia
che viceversa, nel momento iniziale, era proprio del tipo object-action.
Come scegliere il giusto modello? La scelta non può prescindere dal tipo di
applicazione e dall'abilità dell'utente; difficilmente saremo in grado di fornire
entrambe le possibilità, e dobbiamo quindi tenere presente anche la rapidità di
apprendimento dell'utente sin dall'inizio.
Object Design
Eccoci ancora alla fase di riscrittura diagrammi. Gli stessi diagrammi che sono
serviti nella fase di analisi, vengono riaffrontati a questo punto per la stesura ed
il completamento finale. Forti della conoscenza del sisema in tutte le sue parti si
ha chiaro in mente ora come effettuare l’ organizzazione delle classi, quali
saranno i metodi e gli attributi da attribuire, quali saranno gli stati delle classi e
dell’ intero sistema.
Per cui ora si incontrano le fasi di :
– Refined Object Model
– Optimisation
– Detailed Interaction Diagram
– Refined State Transition Diagram
– Persistence
Program
«interface»
IProgram
+getHeadProgramItem()
+getTailProgramItem()
+SetCurrentProgramItem()
+GetNextProgramItem()
+GetNumberItem()
+IsOutputOn()
+FindProgramItem()
+GetProgramItemIndex()
«interface»
IProgramEvent
+OnEdit()
+OnEditError()
+OnExec()
+OnExecError()
«interface»
IProgramExecution
+IsProgramItemExecutable()
+ExecCurrentProgramItem()
+ExecProgram()
+ExecProgramFromIndex()
+StopExec()
+ResumeExec()
+PauseExec()
+SetBreakPoint()
+ListBreakPoint()
+IsBreakPointSet()
+SetAutoMode()
+IsAutoMode()
+nableProgramOutput()
+EnableItemOutput()
«interface»
IEnumProgramItem
+Next()
+Skip()
+Reset()
+Clone()
«interface»
IProgramEdit
+AddProgramItemAbove()
+AddProgramItemBelow()
+DeleteProgramItem()
+DeleteAllProgramItem()
+UndoDelete()
+CopyItem()
+PasteItem()
+IsItemErasable()
«interface»
IProgramSerialize
+SerializeProgram()
IProtocol
CProtocol
CProtocol
«interface»
IProtocol
+Name()
+InterByte()
+TMO()
+InterBlockM()
+InterBlockC()
+TimeLow()
+TimeHigh()
+FastInit()
+BaudRateInit()
+DataBitInit()
+ParityBitInit()
+LenFakraCode()
+WakeUpByte()
+WakeUpMode()
+RunTimeMode()
+BaudRate()
+DataBit()
+ParityBit()
+Line()
+KW2000Type()
+MaxRetry()
+FormatByteInit()
+FormatByte()
+SourceAddress()
+TargetAddress()
+TesterPresent()
+SetTesterPresent()
Fasi Classiche in uno sviluppo SW
•
•
•
•
•
•
•
•
Raccolta e analisi requisiti
Stima dei tempi
Analisi
Design
Implementazione
Testing
• Unit Testing
• System testing
• Accettazione Test
Conversion (passaggio ad un sistema nuovo)
• Strategia parallela
• Eliminazione diretta
• Studio pilota
• Introduzione graduale
Manutenzione
Testing.
Esistono 3 tipi di test. Come da titoli: il test di piccole unita’ di progetto SW, test
dell’ applicazione (parti messe insieme integrando parti piu’ piccole del sistema),
test funzionale(ovvero secondo le aspettative dell’ utente), test dell’ applicazione
e dell’ intero sistema (test di stress, di sicurezza, di recovery da situazioni
critiche, di prestazione, velocta’, affidabilita’, ecc)
Conversion.
Passaggio da un sistema esistente ad uno nuovo(nuova versione).
Si puo’ effettuare in modo: parallelo(i sistemi coesistonoper un po’ di tempo);
eliminazione diretta(il vecchio sistema e’ completamente e definitavamente
sostituito)(es. sistema di misura mantiene l’ esecuzione di programmi scritti con
vecchi linguaggi); studio pilota (il nuo sistema e’ introdotto solo in certe aree);
introduzione graduale.
Maintenance.
Il sistema deve essere operativo anche nelle fasi di manutenzione.E’ molto
interessante sapere che: circa il 20% del tempo e’ relativo alla fase di
debug+soluzione bachi; un altro 20% riguarda cambiamenti dati e strutture, ma
il 60% riguarda nuovi sviluppi a seguito di suggerimenti utente,
documentazione, e ricodifica per migliorare l’ efficienza del sistema.