Esplora E-book
Categorie
Esplora Audiolibri
Categorie
Esplora Riviste
Categorie
Esplora Documenti
Categorie
• Identificare gli obiettivi di progettazione. Gli sviluppatori identificare e dare la priorità alle qualità del
sistema che dovrebbero ottimizzare.
• Progetta la decomposizione iniziale del sottosistema. Gli sviluppatori scompongono il sistema in parti più
piccole in base ai modelli di caso d'uso e di analisi. Gli sviluppatori utilizzano gli stili architettonici standard
come punto di partenza durante questa attività.
• Perfezionare la decomposizione del sottosistema per affrontare gli obiettivi di progettazione. L'iniziale
scomposizione di solito non soddisfa tutti gli obiettivi di progettazione. Gli sviluppatori affinano fino a
quando tutti gli obiettivi sono soddisfatti.
In questo capitolo ci concentriamo sulle prime due attività. Nel prossimo capitolo, raffiniamo la
decomposizione del sistema e forniamo un esempio approfondito con il caso studio di ARENA.
2. La distanza complessiva che gli occupanti percorrono ogni giorno dovrebbe essere ridotta al
minimo.
Il modello ad analisi descrive il sistema completamente sotto il punto di vista degli attori e funge da
base di comunicazione tra il cliente e gli sviluppatori. Il modello ad analisi, comunque non contiene
informazioni sulla struttura interna del sistema, la sua configurazione hardware, o più generalmente
come il sistema dovrebbe essere realizzato. La progettazione del sistema è il primo step in questa
direzione. La progettazione del sistema si traduce nei seguenti prodotti:
- Design goals descrive le qualità del sistema che gli sviluppatori dovrebbero ottimizzare
- Software architeture descrive la scomposizione del sottosistema in termini di responsabilità del
sottosistema, dipendenze tra sottosistemi, mappatura del sottosistema sull'hardware e principali
decisioni strategiche quali flusso di controllo, controllo di accesso e memorizzazione dei dati
- Boundary use case descrive la configurazione del sistema, startup, shutdown, problemi di gestione
delle eccezioni
Gli scopi della progettazione derivano dai requisiti non funzionali. Gli obiettivi della progettazione guidano
le decisioni che devono essere prese dagli sviluppatori quando servono compromessi. La decomposizione
del sottosistema costituisce il grosso della progettazione del sistema. Gli sviluppatori dividono il sistema in
pezzi gestibili per affrontare la complessità: ogni sottosistema viene assegnato ad un team e realizzato in
modo indipendente. Perché questo sia possibile, gli sviluppatori devono affrontare problemi di sistema a
livello di scomposizione del sistema. In questo capitolo, descriviamo il concetto di decomposizione del
sottosistema e discutiamo esempi di scomposizioni generiche del sistema chiamate "stili architettonici." Nel
prossimo capitolo, descriviamo come la scomposizione del sistema viene perfezionata per raggiungere
obiettivi specifici di progettazione.
La figura 6-2 mostra la relazione tra la progettazione di sistemi e altre attività di ingegneria del software.
6.3 SYSTEM DESIGN CONCEPTS
In questa sezione, descriviamo più dettagliatamente le decomposizioni dei sottosistemi e le loro
proprietà. In primo luogo, definiamo il concetto di sottosistema e la sua relazione con le classi (sezione
6.3.1). Passiamo ora all'interfaccia dei sottosistemi (sezione 6.3.2): i sottosistemi forniscono servizi ad altri
sottosistemi.
Un servizio è un insieme di operazioni correlate che condividono uno scopo comune. Durante la
progettazione del sistema, definiamo i sottosistemi in termini di servizi che forniscono. Successivamente,
durante la progettazione degli oggetti, definiamo l'interfaccia del sottosistema in termini di operazioni che
fornisce. Esaminiamo poi due proprietà dei sottosistemi, l'accoppiamento e la coesione (sezione 6.3.3).
L'accoppiamento misura le dipendenze tra due sottosistemi, mentre la coesione misura le dipendenze tra
le classi all'interno di un sottosistema. La decomposizione ideale del sottosistema dovrebbe minimizzare
l'accoppiamento e massimizzare la coesione. Poi, guardiamo alla stratificazione e al partizionamento, due
tecniche per relazionare i sottosistemi tra loro (Sezione 6.3.4). La stratificazione consente di organizzare un
sistema come una gerarchia di sottosistemi, ciascuno dei quali fornisce servizi di livello superiore al
sottosistema sovrastante utilizzando servizi di livello inferiore dai sottosistemi sottostanti. Il
partizionamento organizza i sottosistemi come pari che si forniscono reciprocamente servizi diversi. Nella
Sezione 6.3.5, descriviamo una serie di architetture software tipiche che si trovano nella pratica.
Nel capitolo 2, Modellazione con UML, abbiamo introdotto la distinzione tra dominio dell'applicazione e
dominio della soluzione. Per ridurre la complessità del dominio dell'applicazione, abbiamo identificato parti
più piccole chiamate "classi" e le abbiamo organizzate in pacchetti. Allo stesso modo, per ridurre la
complessità del dominio delle soluzioni, scomponiamo un sistema in parti più semplici, chiamate
"sottosistemi", che sono composte da un certo numero di classi di domini delle soluzioni. Un sottosistema è
una parte sostituibile del sistema con interfacce ben definite che incapsulano lo stato e il comportamento
delle sue classi contenute. Un sottosistema corrisponde tipicamente alla quantità di lavoro che un singolo
sviluppatore o un singolo team di sviluppo può affrontare. Scomponendo il sistema in sottosistemi
relativamente indipendenti, i team concorrenti possono lavorare su singoli sottosistemi con un minimo
sovraccarico di comunicazione. Nel caso di sottosistemi complessi, applichiamo ricorsivamente questo
principio e scomponiamo un sottosistema in sottosistemi più semplici (vedi Figura 6-3).
Diversi linguaggi di programmazione (ad esempio Java e Modula-2) forniscono costrutti per sottosistemi di
modellazione (pacchetti in Java, moduli in Modula-2). In altri linguaggi, come C o C++, i sottosistemi non
sono esplicitamente modellati, quindi gli sviluppatori usano le convenzioni per raggruppare le classi (ad
esempio, un sottosistema può essere rappresentato come una directory contenente tutti i file che
implementano il sottosistema). Se i sottosistemi siano o meno esplicitamente rappresentati nel linguaggio
di programmazione, gli sviluppatori devono documentare attentamente la decomposizione del
sottosistema in quanto i sottosistemi sono di solito realizzati da diversi team.
Un sottosistema è caratterizzato dai servizi che fornisce ad altri sottosistemi. Un servizio è un insieme di
operazioni correlate che condividono uno scopo comune. Un sottosistema che fornisce un servizio di
notifica, ad esempio, definisce le operazioni per l'invio di avvisi, la ricerca di canali di notifica e la
sottoscrizione e la cancellazione di un canale. L'insieme delle operazioni di un sottosistema a disposizione di
altri sottosistemi costituisce l'interfaccia del sottosistema.
L'interfaccia del sottosistema include il nome delle operazioni, i loro parametri, i loro tipi e i loro valori di
ritorno. La progettazione del sistema si concentra sulla definizione dei servizi forniti da ciascun
sottosistema, ossia sulla enumerazione delle operazioni, dei loro parametri e del loro comportamento di
alto livello. La progettazione degli oggetti si concentrerà sull'interfaccia del programmatore di applicazioni
(API), che perfeziona ed estende le interfacce del sottosistema. L'API include anche il tipo di parametri e il
valore di ritorno di ogni operazione.
Le interfacce fornite e necessarie possono essere raffigurate in UML con connettori di assemblaggio,
chiamati anche connettori a sfera e presa. L'interfaccia fornita viene visualizzata come un'icona a sfera
(chiamata anche lollipop) con il suo nome accanto ad esso. L'interfaccia richiesta viene visualizzata come
icona socket. La dipendenza tra due sottosistemi è mostrata collegando la sfera e l'incavo corrispondenti
nello schema dei componenti.
La figura 6-5 mostra le dipendenze tra i sottosistemi Fieldofficerinterface, Dispatchterinterface e
Resourcemanagement. Il Fieldofficerinterface richiede al Resourceupdateservice di aggiornare lo status e la
posizione del Fieldofficer.
Il Dispatcherinterface richiede al Resourceallocationservice di identificare le risorse disponibili e assegnarle a
nuovi Incidenti.
Il sottosistema Resourcemanagement fornisce entrambi i servizi. Si noti che usiamo la notazione ball-and-
socket quando la decomposizione del sottosistema è già abbastanza stabile e che il nostro obiettivo si è
spostato dall'identificazione dei sottosistemi alla definizione dei servizi. Durante le prime fasi di
progettazione del sistema, potremmo non avere una comprensione così chiara dell'assegnazione delle
funzionalità ai sottosistemi, nel qual caso usiamo la notazione di dipendenza (frecce tratteggiate) della
Figura 6-4.
La definizione di un sottosistema in termini di servizi che fornisce aiuta a concentrarci sulla sua interfaccia
anziché sulla sua attuazione. Quando si scrive un'interfaccia del sottosistema, si dovrebbe cercare di
minimizzare la quantità di informazioni fornite sull'implementazione. Ad esempio, l'interfaccia di un
sottosistema non dovrebbe fare riferimento a strutture di dati interne, come elenchi collegati, array o
tabelle hash. Questo ci permette di minimizzare l'impatto del cambiamento quando revisioniamo
l'implementazione di un sottosistema. Più in generale, vogliamo minimizzare l'impatto del cambiamento
minimizzando le dipendenze tra i sottosistemi.
Se due sistemi sono liberamente accoppiati, essi sono relativamente indipendenti, quindi la modifica ad
uno dei sottosistemi avrà un piccolo impatto sull’altro. Se due sottosistemi sono fortemente accoppiati, le
modifiche ad un sottosistema avrà probabilmente un impatto sull’altro. Una proprietà desiderabile di una
decomposizione del sottosistema è che i sottosistemi sono accoppiati il più possibile in modo non corretto.
Questo minimizza l’impatto che gli errori o i cambiamenti futuri in un sottosistema hanno su un altro
sistema.
Consideriamo, per esempio, il sistema di risposta alle emergenze descritto nella figura 6-4. Durante la
progettazione del sistema, decidiamo di allocare tutti i dati persistenti (es: tutti i dati che sopravvivono ad
una singola esecuzione del programma) in un database relazionale. Questo porta ad un sottosistema
aggiuntivo chiamato Database (Figure 6-6). Inizialmente, noi progettiamo l’interfaccia del database e
sottosistema in modo che i sottosistemi che hanno bisogno di memorizzare i dati semplicemente eseguono
i comandi in un linguaggio di interrogazione adatto al database, come SQL.
Ad esempio, il sottosistema Incidentmanagement rilascia query SQL per memorizzare e recuperare i record
che rappresentano Incidents nel database. Questo porta ad una situazione con un elevato accoppiamento
tra il sottosistema Database e i tre sottosistemi client (vale a dire, Incidentmanagement,
Resourcemanagement, e Mapmanagement) che hanno bisogno di memorizzare e recuperare i dati, qualsiasi
cambiamento nel modo in cui i dati sono memorizzati richiederà modifiche nei sottosistemi client. Per
esempio, se cambiamo i fornitori della base di dati dovremo cambiare i sottosistemi per usare un dialetto
differente della lingua di richiesta. Per ridurre l'accoppiamento tra questi quattro sottosistemi, decidiamo di
creare un nuovo sottosistema, chiamato Storage, che protegge il Database dagli altri sottosistemi. In questa
alternativa, i tre sottosistemi client utilizzano i servizi forniti dal sottosistema Storage, che è poi
responsabile per l'emissione di interrogazioni in SQL al sottosistema Database. Se decidiamo di cambiare i
fornitori di database o di utilizzare un meccanismo di archiviazione diverso (ad esempio: file piatti),
abbiamo solo bisogno di cambiare il sottosistema Storage. Di conseguenza, l'accoppiamento complessivo
della decomposizione del sottosistema è stato ridotto. Si noti che la riduzione dell'accoppiamento non è
fine a se stessa. Nell'esempio precedente, la riduzione dell'accoppiamento ha portato a un'ulteriore
complessità. Riducendo l'accoppiamento, gli sviluppatori possono introdurre molti strati inutili di astrazione
che consumano tempo di sviluppo e tempo di elaborazione.
L'accoppiamento elevato è un problema solo se è probabile che un sottosistema cambi.
Un esempio di architettura chiusa è il Modello di Riferimento di Interconnessione dei Sistemi Aperti (in
breve, il modello OSI), che è composto da sette strati [Day & Zimmermann, 1983]. Ogni livello è
responsabile dell'esecuzione di una funzione ben definita. Inoltre, ogni livello fornisce i propri servizi
utilizzando i servizi del livello sottostante (Figura 6-10). Il livello fisico rappresenta l'interfaccia hardware
alla rete. È responsabile della trasmissione di bit su un canale di comunicazione. Il livello Datalink è
responsabile della trasmissione di frame di dati senza errori utilizzando i servizi del livello fisico. Il livello di
rete è responsabile della trasmissione e dell'instradamento dei pacchetti all'interno di una rete. Il livello di
trasporto è responsabile di garantire che i dati siano trasmessi in modo affidabile da un punto all'altro. Il
livello Transport è l'interfaccia che i programmatori Unix vedono quando trasmettono informazioni su
socket TCP/IP tra due processi. Il livello di sessione è responsabile dell'inizializzazione e dell'autenticazione
di una connessione. Il livello Presentazione esegue servizi di trasformazione dati, come lo swap di byte e la
crittografia. Il livello di applicazione è il sistema che si sta progettando (a meno che non si sta costruendo
un sistema operativo o uno stack di protocollo). Il livello di applicazione può anche consistere di
sottosistemi stratificati.
Fino a poco tempo fa, solo i quattro strati inferiori del modello OSI erano ben standardizzati. Unix e molti
sistemi operativi desktop, ad esempio, forniscono interfacce a TCP/IP che implementano i livelli Transport,
Network e Datalink. Lo sviluppatore dell'applicazione doveva ancora colmare il divario tra il livello
Transport e il livello Application. Con il crescente numero di applicazioni distribuite, questa lacuna ha
motivato lo sviluppo di middleware come CORBA [OMG, 2008] e Java RMI [RMI, 2009]. CORBA e Java RMI ci
consentono di accedere in modo trasparente agli oggetti remoti inviando loro messaggi mentre i messaggi
vengono inviati ad oggetti locali, implementare efficacemente i livelli Presentazione e Sessione (vedi Figura
6-11).
Un esempio di architettura aperta è il toolkit di interfaccia utente Swing per Java [JFC, 2009]. Il livello più
basso è fornito dal sistema operativo o da un sistema di finestre, come X11, e fornisce la gestione delle
finestre di base. AWT è un'interfaccia finestra astratta fornita da Java per schermare le applicazioni da
piattaforme di finestre specifiche. Swing è una libreria di oggetti interfaccia utente che fornisce una vasta
gamma di servizi, dai pulsanti alla gestione della geometria. Un'applicazione di solito accede solo
all'interfaccia Swing. Tuttavia, il livello Applicazione può bypassare il livello Swing e accedere direttamente
a AWT. In generale, l'apertura dell'architettura permette agli sviluppatori di bypassare i livelli più alti per
risolvere i colli di bottiglia delle prestazioni (Figura 6-12).
Le architetture a strati chiuse hanno proprietà desiderabili: portano a un basso accoppiamento tra
sottosistemi, e i sottosistemi possono essere integrati e testati in modo incrementale. Ogni livello, tuttavia,
introduce una velocità e lo stoccaggio overhead che può rendere difficile soddisfare i requisiti non
funzionali. Inoltre, l'aggiunta di funzionalità al sistema in revisioni successive può rivelarsi difficile,
soprattutto quando le aggiunte non erano previste. In pratica, un sistema viene raramente scomposto in
più di tre o cinque strati.
Un altro approccio per affrontare la complessità è quello di suddividere il sistema in sottosistemi peer,
ognuno responsabile di una diversa classe di servizi. Ad esempio, un sistema di bordo per una macchina
potrebbe essere scomposto in un servizio di viaggio che fornisce indicazioni in tempo reale per il
conducente, un servizio di preferenze individuali che ricorda la posizione del sedile del conducente e
stazione radio preferita, e il servizio del veicolo che traccia il consumo di benzina della vettura, le
riparazioni, e la manutenzione programmata. Ogni sottosistema dipende liberamente dagli altri, ma spesso
può funzionare in isolamento.
Con l'aumentare della complessità dei sistemi, la specifica della decomposizione del sistema è critica. È
difficile modificare o correggere la decomposizione debole una volta che lo sviluppo è iniziato, come la
maggior parte delle interfacce sottosistema dovrebbe cambiare. Riconoscendo l'importanza di questo
problema, è emerso il concetto di architettura del software. Un'architettura software include la
scomposizione del sistema, il flusso di controllo globale, la gestione delle condizioni di confine e i protocolli
di comunicazione intersubsystem [Shaw & Garlan, 1996]. In questa sezione, descriviamo diversi stili
architettonici che possono essere utilizzati come base per l'architettura di diversi sistemi. Questa non è
affatto un'esposizione sistematica o completa del soggetto. Vogliamo piuttosto fornire alcuni esempi
rappresentativi e rinviare il lettore alla letteratura per maggiori dettagli.
Repository
Nello stile architettonico del repository (vedi Figura 6-13), i sottosistemi accedono e modificano una
singola struttura di dati chiamata repository centrale. I sottosistemi sono relativamente indipendenti e
interagiscono solo attraverso il repository. Il flusso di controllo può essere dettato dal repository centrale
(es: trigger sui sistemi periferici di invocazione dati) o dai sottosistemi (es: flusso indipendente di controllo e
sincronizzazione attraverso le serrature nel repository).
I repository sono tipicamente utilizzati per i sistemi di gestione dei database, come un sistema di libro paga
o un sistema bancario. La posizione centrale dei dati facilita la gestione dei problemi di concorrenza e
integrità tra sottosistemi. Compilatori e ambienti di sviluppo software seguono anche uno stile
architettonico repository (Figura 6-14). I diversi sottosistemi di un compilatore accedono e aggiornano un
albero di analisi centrale e una tabella dei simboli. I debugger e gli editor di sintassi accedono anche alla
tabella dei simboli.
Il sottosistema repository può essere utilizzato anche per l'attuazione del flusso di controllo
globale. Nell'esempio del compilatore della Figura 6-14, ogni singolo strumento (ad esempio, il compilatore,
il debugger e l'editor) viene invocato dall'utente. Il repository assicura solo che gli accessi simultanei siano
serializzati. Al contrario, il repository può essere utilizzato per richiamare i sottosistemi in base allo stato
della struttura dati centrale. Questi sistemi sono chiamati "sistemi di lavagna." Il sistema di comprensione
vocale HEARSAY II [Erman et al., 1980], uno dei primi sistemi di lavagna, invocava strumenti basati sullo
stato attuale della lavagna.
I repository sono adatti per applicazioni con attività di elaborazione dati complesse e in continua
evoluzione. Una volta che un repository centrale è ben definito, possiamo facilmente aggiungere nuovi
servizi sotto forma di sottosistemi aggiuntivi. Lo svantaggio principale dei sistemi di repository è che il
repository centrale può diventare rapidamente un collo di bottiglia, sia dal punto di vista delle prestazioni
che da quello della modificabilità. L'accoppiamento tra ogni sottosistema e il repository è elevato,
rendendo così difficile cambiare il repository senza avere un impatto su tutti i sottosistemi.
Model/View/Controller
Nello stile architettonico Model/View/Controller (MVC) (Figura 6-15), i sottosistemi sono classificati in tre
diversi tipi: i sottosistemi modello mantengono la conoscenza del dominio, i sottosistemi vista lo
visualizzano all'utente e i sottosistemi controller gestiscono la sequenza di interazioni con l'utente. I
sottosistemi modello sono sviluppati in modo tale da non dipendere da alcun sottosistema di controllo o di
visualizzazione. Le modifiche del loro stato sono propagate al sottosistema vista tramite un protocollo di
sottoscrizione/notifica. Il MVC è un caso speciale del repository in cui Model implementa la struttura dati
centrale e gli oggetti di controllo dettano il flusso di controllo.
Ad esempio, le figure 6-16 e 6-17 illustrano la sequenza di eventi che si verificano in uno stile architettonico
MVC. Figura 6-16 visualizza due visualizzazioni di un file system. La finestra inferiore elenca il contenuto
della cartella Software Engineering basata su Comp, incluso il file 9DesignPatterns2.ppt. La finestra in alto
mostra le informazioni su questo file. Il nome del file 9DesignPatterns2.ppt appare in tre posizioni: in
entrambe le finestre e nel titolo della finestra superiore. Supponiamo ora di cambiare il nome del file in
9DesignPatterns.ppt. La Figura 6-17 mostra la sequenza di eventi:
La funzionalità di sottoscrizione e notifica associata a questa sequenza di eventi è di solito realizzata con un
modello di design Observer (vedi Sezione A.7). Il modello Observer permette agli oggetti Model e View di
essere ulteriormente disaccoppiati rimuovendo le dipendenze dirette dal modello alla vista. Per maggiori
dettagli, il lettore si riferisce a [Gamma et al., 1994] e alla Sezione A.7.
La logica tra la separazione di Model, View e Controller è che le interfacce utente, cioè la View e il
Controller, sono molto più spesso soggette a modifiche rispetto alla conoscenza del dominio, cioè il
Model. Inoltre, eliminando qualsiasi dipendenza dal Modello sulla Vista con il protocollo di
sottoscrizione/notifica, le modifiche delle viste (interfacce utente) non hanno alcun effetto sui sottosistemi
del modello. Nell'esempio della Figura 6-16, potremmo aggiungere una vista shell in stile Unix del file
system senza dover modificare il file system. Abbiamo descritto una scomposizione simile nel Capitolo 5,
Analisi, quando abbiamo identificato entità, confine e oggetti di controllo. Questa decomposizione è anche
motivata dalle stesse considerazioni sul cambiamento.
MVC è adatto per sistemi interattivi, soprattutto quando sono necessarie più visualizzazioni dello stesso
modello. MVC può essere utilizzato per mantenere la coerenza tra i dati distribuiti; tuttavia introduce la
stessa strozzatura delle prestazioni come per altri stili di repository.
Client/server
Nello stile architettonico client/server (Figura 6-18), un sottosistema, il server, fornisce servizi a istanze di
altri sottosistemi chiamati client, che sono responsabili dell'interazione con l'utente. La richiesta di un
servizio è di solito effettuata tramite un meccanismo di chiamata a procedura remota o un broker di oggetti
comune (ad esempio, CORBA, Java RMI, o HTTP). Il flusso di controllo nei client e nei server è indipendente
tranne che per la sincronizzazione per gestire le richieste o per ricevere i risultati.
Peer-to-peer
Uno stile architettonico peer-to-peer (vedi Figura 6-20) è una generalizzazione dello stile architettonico
client/ server in cui i sottosistemi possono agire sia come client che come server, nel senso che ogni
sottosistema può richiedere e fornire servizi. Il flusso di controllo all'interno di ciascun sottosistema è
indipendente dagli altri tranne che per le sincronizzazioni su richiesta.
Un esempio di stile architettonico peer-to-peer è un database che accetta richieste dall'applicazione e
notifica all'applicazione ogni volta che alcuni dati vengono modificati (Figura 6-21).
I sistemi peer-to-peer sono più difficili da progettare rispetto ai sistemi client/server perché introducono la
possibilità di blocchi e complicano il flusso di controllo.
I callback sono operazioni temporanee e personalizzate per uno scopo specifico. Per esempio, un Dbuser
peer in Figura 6-21 può dire al DBMS peer quale operazione invocare dopo una notifica di modifica. Il
Dbuser allora usa l'operazione di callback specificata da ogni Dbuser per la notifica quando un
cambiamento accade. I sistemi peer-to-peer in cui un peer "server" invoca i peer "client" solo attraverso i
callback sono spesso indicati come sistemi client/server, anche se questo è impreciso dal momento che il
"server" può anche avviare il flusso di controllo.
Three-Thier
Lo stile architettonico a tre livelli organizza i sottosistemi in tre strati (Figura 6-22):
• Il livello di interfaccia include tutti gli oggetti di confine che si occupano dell'utente, comprese le finestre,
i moduli, le pagine web e così via.
• Il livello logico dell'applicazione include tutti gli oggetti di controllo e entità, realizzando l'elaborazione, il
controllo delle regole e la notifica richiesta dall'applicazione.
• Lo storage layer realizza l'archiviazione, il recupero e la query di oggetti persistenti.
Lo stile architettonico a tre livelli è stato inizialmente descritto negli anni '70 per i sistemi di
informazione. Lo storage layer, un'analogia al sottosistema Repository nello stile architettonico del
repository, può essere condiviso da diverse applicazioni che operano sugli stessi dati. A sua volta, la
separazione tra il livello dell'interfaccia e il livello logico dell'applicazione consente lo sviluppo o la modifica
di diverse interfacce utente per la stessa logica applicativa.
Four-tier
Lo stile architettonico a quattro livelli è un'architettura a tre livelli in cui il livello Interface viene scomposto
in un livello Presentation Client e un livello Presentation Server (Figura 6-23). Il livello del client di
presentazione si trova sulle macchine utente, mentre il livello del server di presentazione può essere
posizionato su uno o più server. L'architettura a quattro livelli consente un'ampia gamma di client di
presentazione nell'applicazione, riutilizzando alcuni degli oggetti di presentazione tra i clienti. Ad esempio,
un sistema di informazioni bancarie può includere una serie di clienti diversi, come ad esempio
un'interfaccia browser Web per gli utenti domestici, una macchina automatica Teller, e un client di
applicazione per i dipendenti della banca. I moduli condivisi da tutti e tre i client possono quindi essere
definiti ed elaborati nel layer Presentation Server, eliminando così la ridondanza tra i client.
L'autista poi va alla macchina e inizia il viaggio, mentre il computer di bordo dà indicazioni in base alle
informazioni di viaggio dal servizio di pianificazione e la sua posizione attuale indicata da un sistema GPS di
bordo (Executetrip in Figura 6-27).
Eseguiamo l'analisi per il sistema Mytrip seguendo le tecniche descritte nel capitolo 5, Analisi, e ottenere il
modello in figura 6-28.
6.4.2 IDEMTIFYING DESIGN GOALS
La definizione degli obiettivi di progettazione è il primo passo della progettazione del sistema. Identifica le
qualità su cui il nostro sistema dovrebbe concentrarsi. Molti obiettivi di progettazione possono essere
dedotti dai requisiti non funzionali o dal dominio dell'applicazione. Altri dovranno essere sollecitati dal
client. È tuttavia necessario dichiararle esplicitamente in modo che ogni importante decisione di
progettazione possa essere presa coerentemente seguendo lo stesso insieme di criteri.
Ad esempio, alla luce dei requisiti non funzionali per Mytrip descritti nella Sezione 6.4.1, identifichiamo
l'affidabilità e la tolleranza di guasto alla perdita di connettività come obiettivi di
progettazione. Identifichiamo quindi la sicurezza come un obiettivo di progettazione, in quanto numerosi
driver avranno accesso allo stesso server di pianificazione del viaggio. Aggiungiamo la modificabilità come
obiettivo di progettazione, in quanto vogliamo fornire la possibilità per i conducenti di selezionare un
servizio di pianificazione del viaggio di loro scelta. Il riquadro seguente riassume gli obiettivi di
progettazione che abbiamo identificato.
In generale, possiamo selezionare gli obiettivi di progettazione da una lunga lista di qualità altamente
desiderabili. Le tabelle da 6-2 a 6-6 elencano una serie di possibili criteri di progettazione. Questi criteri
sono organizzati in cinque gruppi: prestazioni, affidabilità, costo, manutenzione e criteri per l'utente
finale. Le prestazioni, l'affidabilità e i criteri dell'utente finale sono solitamente specificati nei requisiti o
dedotti dal dominio dell'applicazione. I criteri di costo e manutenzione sono dettati dal cliente e dal
fornitore.
I criteri di prestazione (tabella 6-2) comprendono i requisiti di velocità e di spazio imposti al sistema. Il
sistema dovrebbe essere reattivo, o dovrebbe realizzare un numero massimo di compiti? Lo spazio di
memoria è disponibile per le ottimizzazioni della velocità, o la memoria dovrebbe essere usata con
parsimonia?
I criteri di affidabilità (tabella 6-3) determinano l'entità dello sforzo da compiere per minimizzare gli
incidenti di sistema e le loro conseguenze. Con quale frequenza il sistema può bloccarsi? Quanto a
disposizione dell'utente dovrebbe essere il sistema? Il sistema dovrebbe tollerare errori e guasti? I rischi
per la sicurezza sono associati all'ambiente del sistema? I problemi di sicurezza sono associati ai crash del
sistema?
I criteri di costo (tabella 6-4) comprendono il costo per sviluppare il sistema, per utilizzarlo e amministrarlo.
Si noti che i criteri di costo non includono solo considerazioni di progettazione, ma anche quelle gestionali.
Quando il sistema sostituisce uno più vecchio, si deve tener conto del costo della retrocompatibilità o del
passaggio al nuovo sistema. Ci sono anche compromessi tra diversi tipi di costi, come i costi di sviluppo, i
costi di formazione degli utenti finali, i costi di transizione e i costi di manutenzione. Mantenere la
retrocompatibilità con un sistema precedente può aumentare i costi di sviluppo riducendo i costi di
transizione.
I criteri di manutenzione (tabella 6-5) determinano quanto sia difficile cambiare il sistema dopo
l'implementazione. Quanto facilmente si possono aggiungere nuove funzionalità? Quanto facilmente le
funzioni esistenti possono essere riviste? Il sistema può essere adattato a un diverso dominio
applicativo? Quanto sforzo sarà necessario per portare il sistema su una piattaforma diversa? Questi criteri
sono più difficili da ottimizzare e pianificare, in quanto raramente è chiaro il successo del progetto e per
quanto tempo il sistema sarà operativo.
I criteri per l'utente finale (tabella 6-6) comprendono qualità auspicabili dal punto di vista dell'utente, ma
non ancora coperte dai criteri di prestazione e affidabilità. Il software è difficile da usare e da imparare? Gli
utenti possono eseguire le attività necessarie sul sistema?
Spesso questi criteri non ricevono molta attenzione, soprattutto quando il cliente che contrae il sistema è
diverso dai suoi utenti.
Quando si definiscono gli obiettivi di progettazione, solo un piccolo sottoinsieme di questi criteri può essere
preso in considerazione contemporaneamente. È, per esempio, irrealistico sviluppare software che sia
sicuro, sicuro ed economico. In genere, gli sviluppatori devono dare la priorità agli obiettivi di progettazione
e scambiarli uno contro l'altro, nonché contro gli obiettivi manageriali come il progetto corre in ritardo o
oltre il budget. La tabella 6-7 elenca diversi possibili compromessi.
Gli obiettivi manageriali possono essere scambiati con obiettivi tecnici (ad esempio, tempi di consegna vs.
funzionalità). Una volta che abbiamo una chiara idea degli obiettivi di progettazione, possiamo procedere a
progettare una decomposizione iniziale del sottosistema.
Trovare sottosistemi durante la progettazione del sistema è simile a trovare oggetti durante l'analisi. Ad
esempio, alcune delle tecniche di identificazione degli oggetti che abbiamo descritto nel Capitolo 5, Analisi,
come l'euristica di Abbotts, sono applicabili all'identificazione del sottosistema. Inoltre, la decomposizione
dei sottosistemi viene costantemente rivista ogni volta che vengono affrontate nuove questioni: diversi
sottosistemi sono fusi in un unico sottosistema, un sottosistema complesso è suddiviso in parti e alcuni
sottosistemi vengono aggiunti per affrontare nuove funzionalità. Le prime iterazioni sulla decomposizione
del sottosistema possono introdurre drastici cambiamenti nel modello di progettazione del sistema. Questi
sono spesso meglio gestiti attraverso il brainstorming.
La decomposizione iniziale del sottosistema deve essere derivata dai requisiti funzionali. Ad esempio, nel
sistema Mytrip, identifichiamo due grandi gruppi di oggetti: quelli coinvolti nel caso di utilizzo di Plantrip e
quelli coinvolti nel caso di utilizzo di Executetrip. Le classi Viaggio, Direzione, Attraversamento, Segmento e
Destinazione sono condivise tra entrambi i casi d'uso. Questo insieme di classi è strettamente accoppiato in
quanto viene utilizzato nel suo complesso per rappresentare un viaggio.
Decidiamo di assegnarli con Planningservice al Planningsubsystem, e il resto delle classi sono assegnate al
Routingsubsystem (Figura 6-29). Questo porta ad una sola associazione che attraversa i confini del
sottosistema. Si noti che questa decomposizione del sottosistema è un repository in cui il sistema
Planningsubsystem è responsabile della struttura centrale dei dati.
Un'altra euristica per l'identificazione del sottosistema è quella di tenere insieme oggetti funzionalmente
correlati. Un punto di partenza consiste nell'assegnare ai sottosistemi gli oggetti partecipanti che sono stati
identificati in ogni caso d'uso. Alcuni gruppi di oggetti, come il gruppo Trip a Mytrip, sono condivisi e
utilizzati per comunicare informazioni da un sottosistema all'altro. Possiamo creare un nuovo sottosistema
per ospitarli o assegnarli al sottosistema che crea questi oggetti.