|
Dr. Carlo Pescio Introduzione della Tecnologia ad Oggetti in Azienda: Gestione e Prevenzione dei Rischi |
Presentato alla conferenza
"Tecnologia ad Oggetti per l'Industria", Firenze, 1996.
Alcune malattie al loro
inizio sono facili da curare, ma difficili da riconoscere... ma
nel corso del tempo... diventano facili da riconoscere ma difficili
da curare
Niccolò Machiavelli
Introduzione
La tecnologia object oriented
può portare benefici concreti, ma la sua introduzione nelle
aziende non è priva di rischi, spesso ignorati o sottovalutati.
Nella mia attività di consulente ho potuto notare come
molte delle difficoltà siano tutto sommato comuni ed abbiano
sintomi simili anche in realtà diverse; in più
occasioni ho avuto modo di sperimentare dei rimedi, delle strategie
di prevenzione e delle tattiche per gestire i problemi. In molti
casi i fattori sono di tipo umano, e si possono riscontrare in
altre situazioni di paradigm shift; in altri casi si tratta
di problemi più strettamente tecnici e legati in modo
specifico alla tecnologia ad oggetti. In quanto segue discuterò
alcuni tra i rischi più comuni e le strategie che ho
adottato con successo in diversi progetti; ho ritenuto opportuno
porre la giusta enfasi sulle cause, e non solo sui possibili
rimedi, in modo da stimolare anche la ricerca e la sperimentazione
di metodi alternativi di prevenzione, diagnosi, e gestione dei
rischi.
Superare il mito degli
oggetti, ovvero: scegliere la tecnologia ad oggetti per le giuste
ragioni.
Vi sono ottime ragioni per
passare al paradigma object oriented: una migliore integrazione
tra fase di analisi, design e codifica, sia a livello di terminologia
che di metodi di astrazione, la possibilità di incrementare
il riuso del codice e del design, una più ampia gamma
di tecniche per ridurre la complessità dello sviluppo,
minori problemi di manutenzione ed upgrade del software. Su queste
basi molti produttori di tool, ma anche professionisti e ricercatori,
hanno però talvolta costruito false promesse di immediati
salti di produttività, prospettive di riuso inverosimili,
ed il mito di un "object nirvana" che è responsabile
di fallimenti e delusioni. Non è inusuale vedere team di
sviluppo cui viene ordinato di passare all'OOP dall'upper
management, a seguito della lettura di un articolo o della dimostrazione
di un CASE tool e delle conseguenti, mirabolanti promesse. È
questo il primo rischio cui si va incontro nell'introduzione della
tecnologia object oriented nelle aziende: assumere che gli
oggetti portino un immediato beneficio sui tempi di sviluppo;
la sua diagnosi è relativamente semplice, essendo il sintomo
principale un generale scontento dell'upper management: gli oggetti
avrebbero dovuto far risparmiare tempo e risorse, mentre
gli sviluppatori non solo non stanno procedendo alla velocità
sperata, ma si trovano spesso bloccati in situazioni difficili
da districare. In realtà va chiarito sin dall'inizio che
gran parte dei benefici degli oggetti sono a medio/lungo termine:
i primi progetti richiederanno maggiore sforzo, e presumibilmente
anche maggiori investimenti economici in tempo di sviluppo, tool
e formazione, mentre più avanti gli sviluppatori saranno
in grado di riutilizzare codice e design precedenti, ed aumentare
così la produttività e la qualità del prodotto;
anche la manutenzione e la ristrutturazione verranno semplificate,
ma ciò non sarà visibile nelle prime fasi dello
sviluppo. Un effetto collaterale dell'eccessivo ottimismo iniziale
è anche una generale riluttanza ad investire in formazione,
o ad assumere personale già esperto nell'analisi, design
e programmazione object oriented; non di rado, questo approccio
è inizialmente supportato dagli stessi programmatori, che
per molte ragioni sono piuttosto portati ad affermazioni del tipo
"ovviamente sono in grado di imparare il C++ da solo, in
poco tempo". Se da un lato ciò è ragionevole,
perché il C++ è sostanzialmente un sopra-insieme
del C, dall'altro un simile approccio, diretto peraltro dal basso
(codifica) verso l'alto (design, analisi), si rivela quasi sempre
fallimentare: come consulente, ho spesso occasione di rivedere
il risultato dei team che hanno seguito tale percorso, e l'inevitabile
conclusione è che la struttura del programma non è
assolutamente orientata agli oggetti, e che ben pochi (o nessuno)
dei benefici promessi dalla tecnologia object oriented si realizzeranno
nel progetto senza le necessarie azioni correttive. I sintomi
di questo rischio, ovvero assumere che la transizione agli
oggetti sia ovvia ed immediata, sono spesso più riconoscibili
tra i programmatori e progettisti: un'enfasi eccessiva sugli aspetti
implementativi più che di progettazione, sui tool più
che sui metodi, e l'incapacità di spiegare in modo comprensibile
e completo anche i concetti più basilari dell'OOP, come
il binding dinamico o il polimorfismo. Il rimedio,
naturalmente, è l'adeguata formazione degli sviluppatori;
in generale, una buona formazione non può prescindere dall'esperienza
diretta, ed è quindi necessario affiancare ad uno studio/training
anche la possibilità di affrontare lo sviluppo di progetti
pilota, o lo sviluppo sotto la supervisione o con l'affiancamento
di persone esperte, in grado di guidare e formare il giudizio
degli sviluppatori; affidarsi ad alcuni giorni di training intensivo,
proposto magari da un produttore di tool, è raramente la
soluzione adeguata ai problemi della formazione. Da notare che
l'enfasi eccessiva sugli strumenti è un sintomo abbastanza
comune di un altro rischio, ovvero assumere che gli oggetti
eliminino automaticamente i tradizionali rischi di sviluppo,
e che ci si possa quindi concentrare solo sugli oggetti, dimenticando
tutti i problemi al contorno. In realtà non solo esistono
problemi extra-tecnici che vanno gestiti, ma la tecnologia object
oriented non è neppure completamente matura: ad esempio,
la distinzione tra attributi e classi, ma anche l'uso o meno degli
attributi in fase di analisi, sono ancora oggetto di dibattito
[EJW95], e se da un lato quasi tutte le metodologie di design
object oriented sono parzialmente basate su state transition diagram,
nessuna di esse spiega esattamente quali interrelazioni possono
esistere tra le transizioni lecite per una classe e quelle delle
sottoclassi [EE94]. La fiducia eccessiva in una metodologia ("Qui
usiamo solo il metodo di Booch!") è un fenomeno tipico
nel mondo del software, ricco di profeti e di "guerre di
religione"; in realtà, nessuna metodologia è
in grado di garantire il successo di un progetto, che è
più spesso basato sui fattori umani [Bro79], [DML87],
[McC95], [Wei71] che su quelli strettamente tecnologici.
Gestire gli oggetti, ovvero:
l'influenza del management nel successo della tecnologia ad oggetti.
Non sempre la spinta alla
transizione verso la tecnologia ad oggetti viene dall'alto: anzi,
non di rado è un gruppo di programmatori e progettisti
entusiasti (o stanchi, o desiderosi di rinnovare le proprie conoscenze)
che cercano di introdurre nei progetti dapprima un linguaggio,
poi un metodo di design, più raramente un metodo di analisi
orientato agli oggetti. Nel fare questo, spesso si rendono involontariamente
colpevoli di propagandare in modo eccessivo i benefici della tecnologia,
aumentando le probabilità dei rischi precedenti; purtroppo,
senza l'adeguato sostegno del management, è realmente difficile
ottenere dei risultati di rilievo nell'introduzione degli oggetti
nelle aziende. La mancanza di formazione e supporto del management
è di per sé abbastanza ragionevole, dal momento
che nel migliore dei casi la dirigenza avrà assistito ad
un seminario o letto un articolo in cui si vantavano i prodigiosi
risultati degli oggetti, la possibilità di modellare esattamente
il dominio del problema, e la naturalezza dei metodi di astrazione
utilizzati. Tuttavia tali fattori sono anche la causa prima dei
miti degli oggetti visti sopra, della mancanza di formazione
degli sviluppatori (se i metodi di astrazione sono naturali,
perché dovremmo spendere denaro per formare il personale?)
e di un generale clima di scontento non appena i miracoli tanto
attesi non vengono realizzati. E non verranno sicuramente realizzati,
perché con tali premesse si tenderà in modo naturale
a riporre aspettative eccessive nei primi progetti. I sintomi
sono anche in questo caso piuttosto riconoscibili: dietro la
promessa di venditori e degli sviluppatori più entusiasti,
l'azienda tenta di fare troppo, troppo in fretta, e troppo presto:
ad esempio, cominciare a sviluppare in C++ sotto Windows usando
una sofisticata libreria commerciale di classi, prospettando inoltre
un ciclo di sviluppo abbreviato rispetto alle precedenti esperienze
in MS-DOS e Pascal. I fattori correttivi più importanti
in questi casi sono due: cercare di educare il management
sui reali benefici a lungo termine degli oggetti, ma anche ascoltare
le ragioni del management per vedere se, ed in che modo, gli oggetti
possono essere realmente introdotti in azienda. Molte piccole
software house, o piccoli gruppi di sviluppo interni alle aziende,
non hanno le problematiche di riuso e manutenzione o le necessità
di accurata progettazione che più traggono beneficio dagli
oggetti. In alcuni casi un programma viene sviluppato per fare
fronte ad un'esigenza che dura al più qualche mese, e
poi diventa inutile perché cambiano le regole del business;
allora un approccio RAD, con strumenti come il Visual Basic, può
anche essere più adatto di una metodologia object oriented
che richieda grandi investimenti nella progettazione architetturale.
La mancanza di comunicazione tra dirigenza e sviluppo è
infatti la fonte principale di richieste contradditorie (es.
prototipazione, qualità e design); da un lato il marketing
richiede un ciclo di sviluppo rapido, che si adegui velocemente
ai nuovi requisiti utenti, dall'altro la dirigenza richiede di
stabilire un controllo di qualità, magari nello sforzo
di adeguarsi alle normative ISO 9000, ed infine si insiste sul
design sperando in un futuro riuso. Il sintomo più comune
è in questi casi l'insoddisfazione e la confusione dei
programmatori, che essendo in fondo alla catena vengono raggiunti
da richieste assolutamente contraddittorie, come l'esigenza di
sviluppare in fretta e di modificare rapidamente il codice, mantenendo
però una adeguata documentazione (senza che venga allocato
il tempo necessario) e minimizzando l'impatto delle modifiche
sul design. Anche in questo caso, trattandosi di un problema largamente
extra-tecnico, l'unica soluzione reale passa attraverso la comunicazione
e la formazione a tutti i livelli, il riconoscimento effettivo
delle necessità aziendali, la valutazione degli obiettivi
da raggiungere ed una onesta pianificazione degli interventi.
All'interno di tale pianificazione, è importante prendere
in considerazione l'eventuale resistenza da parte degli sviluppatori:
lo sviluppo del software è una attività umana e
sociale, e vi sono fattori tipicamente umani che possono entrare
in gioco, come la resistenza ai cambiamenti, la paura di perdere
lo status di esperto, il timore di non riuscire a completare
la transizione ad un nuovo paradigma. Gestire le innovazioni significa
anche individuare le possibili cause di resistenza e porvi rimedio,
ad esempio evitando di assegnare al "team C++" chi ha
investito anni di studio e di lavoro per diventare l'esperto aziendale
di Fortran, senza aver avuto da questi una onesta ed ampia assicurazione
del suo desiderio di passare i prossimi anni ad imparare un nuovo
paradigma. Viceversa, alle prime difficoltà, chi ha avuto
successo in un diverso paradigma tenderà in modo naturale
a tornare ad esso, vanificando i benefici degli oggetti ed innescando
una spirale denigratoria, in quanto la forzatura del paradigma
non porterà che ad ulteriori problemi. Molti ritengono
che un background in un linguaggio di terza generazione, o nel
paradigma strutturato, sia controproduttivo nell'utilizzo della
tecnologia object oriented; è interessante notare che simili
affermazioni vengono spesso da persone con un ampio background
di programmazione strutturata. In realtà, ciò che
conta sono le attitudini personali ed il desiderio di imparare,
la capacità di mettersi in discussione e di autocritica;
le doti essenziali di un buon architetto nel paradigma strutturato
sono le stesse nel paradigma object oriented. Una ulteriore forma
di resistenza si manifesta spesso come tendenza a sottostimare
l'importanza e la difficoltà di analisi e design. Utenti,
manager e sviluppatori che "vogliono il risultato" tenderanno
a non allocare risorse per queste fasi (il che è facilmente
diagnosticabile osservando il planning, che dovrebbe allocare
almeno il 25% del tempo per analisi e design), anzi in
molti casi verranno vissute come una perdita di tempo; questa
è una tipica mentalità da hacker, molto comune tra
i programmatori che non hanno avuto una formazione da software
engineer, ma è spesso avallata e supportata dai project
manager, che si illudono che "codice scritto" equivalga
a "progresso del progetto". Peter Coad [CN93] chiama
questo fenomeno WHISKEY ("Why in the Hell Isn't Someone 'Koding'
Everything Yet?"), ed è molto comune che, in mancanza
di adeguata preparazione, il management diventi nervoso all'idea
che non vi sia ancora del codice "che gira". Ricordiamo
invece che i benefici degli oggetti sono a medio/lungo termine,
e che in mancanza di una attenta analisi e di un buon design,
molti di questi benefici non si verificheranno. Nuovamente, oltre
alla giusta informazione, è necessario anche adottare il
giusto ciclo di vita in funzione del tipo di software sviluppato
e della mentalità aziendale, preferendo ad esempio un modello
a spirale o a prototipi se ciò può eliminare alla
fonte alcuni contrasti sociali. È anche importante avere
alcuni sviluppatori in grado di fungere da esempio e da riferimento
per gli altri: se in precedenza il ciclo di vita del software
si esauriva in "scrivi il codice e correggilo finché
funziona", difficilmente avremo un passaggio indolore ad
un metodo più formale. In molte occasioni, una convincente
introduzione di concetti fondamentali di software engineering,
come accoppiamento ed coesione, centrata su esempi concreti e
con riferimenti precisi al paradigma object oriented, può
fornire il necessario background, ma anche il necessario entusiasmo,
per passare ad un ciclo di vita più pianificato ed ordinato.
Delimitare il problema,
ovvero: l'importanza dell'analisi.
Uno dei tratti psicologici
più comuni nel settore scientifico e tecnologico è
indubbiamente la spinta a trovare soluzioni. Non appena
si vede un problema, scatta un meccanismo inconscio che ci spinge
ad ideare delle possibili soluzioni; in molti casi, in assenza
di problemi ne vengono cercati dei nuovi (gran parte della ricerca
è proprio fondata su questa tensione interna). La predisposizione
e la capacità di individuare soluzioni ai problemi che
si presentano sono ovviamente di grande beneficio durante lo sviluppo:
vi è però una fase in cui la naturale tendenza a
ragionare "per soluzioni" diventa pericolosa: il momento
dell'analisi. Idealmente, l'analisi dovrebbe individuare il modello
del problema, definire cosa il sistema debba fare, senza
inquinamenti dovuti alle soluzioni, ovvero a come tale
sistema debba portare a termine i suoi compiti, senza quindi confondere
problema e soluzione. In realtà, ottenere un simile
risultato è possibile solo a fronte di uno sforzo cosciente
e continuo: non si può semplicemente usare una metodologia
di analisi, incluse quelle basate su use case [JCJO92], e pensare
di essere al riparo da ogni rischio. Ricordiamo infatti che anche
l'utente, quando ci espone i suoi problemi, tende spesso a proporci
inconsciamente la sua personale soluzione ai problemi aziendali;
questo fenomeno è costantemente scoperto durante il business
reengineering, quando le procedure aziendali vengono messe
in discussione per trovare nuove soluzioni ai reali problemi dell'azienda.
A cosa può portare la confusione tra analisi e design?
Di solito a soluzioni troppo generali o inefficienti, oppure
a soluzioni troppo ristrette o rigide: dipende in gran
parte dalle attitudini di chi sceglie, consciamente o meno, il
percorso adottato. Un atteggiamento tipicamente "matematico",
portato all'astrazione, tenderà ad introdurre elementi
di eccessiva generalità nel modello, costringendo poi ad
implementare soluzioni eccessivamente flessibili, con dispendio
di tempo e talvolta perdita di efficienza: chi ha lavorato in
grandi progetti riconoscerà quasi sicuramente la "sindrome
dell'applicazione universale", ovvero la sensazione di sviluppatori
e progettisti che l'applicazione stia degenerando verso un sistema
che, nelle intenzioni degli analisti, dovrebbe essere in grado
di fare qualunque cosa con modifiche minimali. Viceversa, un atteggiamento
tipicamente "ingegneristico", pragmatico e portato al
risultato, tenderà ad introdurre semplificazioni ed assunzioni,
spesso guidate dalla tecnologia, all'interno del dominio del problema.
La conseguenza diretta è l'incapacità del sistema
di assorbire l'impatto di modifiche ai requisiti senza ristrutturazioni
significative, ma anche la difficoltà di scalare le soluzioni
adottate verso altri tipi di tecnologia: un fenomeno molto sentito
nel passaggio, ad esempio, da applicazione modellate su un terminale
ad applicazioni document-centric in ambienti grafici. Molto spesso
si finisce per riportare nel nuovo ambiente una serie di assunzioni
anacronistiche, come la presenza di pagine a dimensioni fisse,
quando una rivalutazione del problema porterebbe a soluzioni più
adatte alla nuova tecnologia. I sintomi dei rischi qui esposti
sono difficili da notare dall'interno; nessuna delle metodologie
di analisi dà una base formale per la fase di analisi,
per quanto si possa trovare in [CEW93] un tentativo di formalizzare
la nozione di "bontà" di una classificazione
di oggetti, simile in un certo senso alla teoria della normalizzazione
relazionale. In generale, senza un aiuto esterno non ci si rende
conto dei problemi su esposti sino a quando non si arriva in fase
di codifica (soluzione troppo generale) o di manutenzione o revisione
dei requisiti (soluzione troppo restrittiva); in alcuni casi,
non si identifica mai il problema come tale, ma si ha una cattiva
ricezione del prodotto da parte del mercato. Una buona tecnica
preventiva, che ho sperimentato con successo in diversi progetti,
è stato l'affiancamento di personale "estraneo"
alle precendenti soluzioni aziendali, anche se non necessariamente
esterno all'azienda; il rischio è infatti più forte
quando si inizi lo sviluppo di un progetto "simile"
ad altri già trattati, o la reingegnerizzazione di un prodotto
già sviluppato. In questo caso, affiancare all'analista
una figura che chieda le motivazione delle scelte che non appaiono
naturali, che esponga la sua visione del dominio del problema,
e che più in generale rallenti la corsa verso la "soluzione
ideale" dell'analista, può evitare gran parte dei
problemi. Naturalmente, esistono altre considerazioni di carattere
politico (l'analista può subire la situazione come un insulto
alle proprie capacità, la figura aggiuntiva dovrebbe avere
una autorità sufficiente a non essere considerata un peso
morto, eccetera) che in generale fanno propendere per l'affiancamento
di una persona piuttosto esperta ed in grado di muovere delle
critiche sensate, viceversa il suo ruolo verrà -di fatto-
ignorato dal resto del team. In alcuni casi, un ottimo architetto
(vedere punto seguente) può essere in grado di riconoscere
un inquinamento da soluzione nel prodotto dell'analisi, svolgendo
di fatto il ruolo di verifica su esposto; l'architetto ha in genere
autorità sufficiente a muovere critiche e proporre schemi
alternativi, quindi anche i fattori politici sono più
semplici da gestire. Purtroppo, gli ottimi architetti sono pochi,
nel software come in altri settori; in mancanza di supporto umano,
una buona tecnica per evitare le soluzioni troppo generali è
di richiedere all'analista di motivare la presenza di ogni classe
nel dominio del problema, e valutare in un meeting di revisione
[ES93], [Fag76] l'effettiva rilevanza di ognuna di esse. Per evitare
le soluzioni troppo rigide, è spesso possibile stabilire
sin dall'inizio una serie di linee evolutive del prodotto,
e richiedere che esse siano percorribili senza eccessive variazioni:
nuovamente, riunioni di revisione di analisi e design sono necessarie
per verificare l'aderenza del risultato ai requisiti.
L'integrità concettuale,
ovvero: l'importanza dell'architettura nel design orientato agli
oggetti.
Possiamo definire l'architettura
di una applicazione come la struttura del sistema, che comprende
i moduli attivi, i meccanismi di interazione tra i moduli, ed
un insieme di regole che governano tale interazione. La scelta
della struttura non è immediata, anzi, terminata la fase
di analisi, esistono di norma un gran numero di scelte possibili;
la scelta operata riflette spesso una "visione globale"
di come suddividere le responsabilità, un piano di evoluzione,
una visione a lungo termine. Per questo non bisogna assumere
che vi sia un'unica "struttura naturale" per il progetto:
occorre anzi avere ben chiara l'importanza della scelta architetturale,
sia dal punto di vista tecnico che politico. Tecnicamente, la
struttura ideale tende all'integrità concettuale, ovvero
a garantire che l'insieme di regole che governano il sistema sia
piccolo, che a problemi simili corrispondano soluzioni simili,
che non vengano applicate soluzioni locali ma soluzioni omogenee
con il resto del sistema. Osserviamo il contrasto con un sistema
sviluppato in modo anarchico: in molti casi, le regole di interazione
sono diverse per ogni interfaccia. È importante capire
che l'interfaccia è ben più di quanto si
possa leggere nella dichiarazione di una classe: esistono precondizioni
e postcondizioni che non vengono documentate; esistono pattern
di accesso, dipendenze temporali, problemi di ownership degli
oggetti, restrizioni sul subclassing, e molti altri fattori di
cui, in pratica, non si può non tenere conto, ma che non
sono immediatamente riconoscibili. È difficile comprendere
veramente un sistema quando in ogni punto valgono regole diverse:
chi ha avuto esperienza di integrazione di prodotti e librerie
di terze parti, o ha utilizzato librerie complesse che mancavano
di uniformità concettuale, riconosce sicuramente il fenomeno,
analogo alla vista di un palazzo con parti barocche, parti post-moderne,
parti in stile liberty. Il ruolo dell'architetto è proprio
questo: garantire l'uniformità e l'integrità concettuale;
è un ruolo difficile, che richiede anche una buona capacità
di gestire gli inevitabili contrasti con chi si vede rifiutare
una brillante (o meno) soluzione locale che mal si adatta alla
struttura del progetto, o con chi vuole usare una tecnica solo
per dimostrare le proprie capacità e "lasciare il
segno" nel progetto. Tuttavia, nella mia esperienza, la presenza
di un buon architetto è una delle condizioni fondamentali
per il successo di un progetto. Naturalmente, la sola presenza
di un architetto non garantisce il successo, né impedisce
che vengano adottate soluzione che, a lungo termine, si rivelano
errate o comunque non ottimali; è allora necessario accettare
il costo di una ristrutturazione del sistema. Anche in questo
caso, è importante avere l'esperienza per evitare di ristrutturare
troppo o troppo poco: il primo caso, tutto sommato poco frequente
in grandi progetti, dove mancano le effettive risorse per modificare
spesso l'intera struttura, si nota facilmente perché gli
sviluppatori sono costantemente costretti a modificare le loro
classi, che non sono mai stabili, ma in un continuo stato "di
flusso". Molto spesso, la soluzione correttiva è proprio
quella di introdurre (o di cambiare!) un architetto. Il secondo
caso è invece molto comune, proprio per la mancanza di
risorse che spinge a dire "lo faremo alla prossima versione",
ben sapendo che mai, nella storia dell'azienda, c'é stato
il tempo di rivedere pesantemente la struttura prima di iniziare
ad implementare tutte le novità della versione successiva.
Ricordiamo però che l'entropia del codice tenderà
comunque ad aumentare in assenza di azioni correttive: è
necessario rivedere periodicamente le soluzioni adottate, identificare
potenziali aree problematiche, e studiare alternative migliori.
Nuovamente, si tratta di una scelta tra un costo presente ed un
costo futuro, e non sempre si ha la possibilità di accettare
il costo immediato; è tuttavia importante essere almeno
a conoscenza dei punti deboli della struttura, se non altro per
evitare di sovraccaricarli in versioni future; in realtà,
ciò richiede anche la giusta cultura aziendale, dove sia
possibile esporre un problema senza il timore di ripercussioni.
In molti casi, gli sviluppatori hanno imparato a nascondere le
aree problematiche, adducendo magari a difficoltà implementative
per evitare di introdurre modifiche in tali aree, perché
ammettere pubblicamente un errore strutturale sarebbe controproducente
nel meccanismo aziendale. Questo è in realtà un
doppio sintomo, della necessità di ristrutturare il codice
e di rivedere le regole sociali all'interno dell'azienda. Vediamo
infine un ultimo rischio collegato all'architettura: lasciare
che l'integrità architetturale prenda il sopravvento sull'usabilità
del prodotto, o in altri termini, spostare l'enfasi dall'utente
agli oggetti. Non è raro che si adottino certe soluzioni,
che hanno un impatto non trascurabile sull'interfaccia utente,
semplicemente perché sembra più consono al paradigma
object oriented o all'architettura del sistema. Prendiamo ad esempio
l'uso sempre più diffuso dei menu pop-up attivabili su
ogni oggetto visibile, ad esempio con il tasto destro del mouse.
Certamente, si tratta di una tecnica per esporre la natura di
"oggetto" dei vari elementi grafici, rappresentando
in un menu le azioni che l'oggetto può compiere; ma si
tratta realmente di un modello di interazione naturale per la
massa degli utenti? In generale, la progettazione dell'interazione
uomo-macchina deve seguire altri criteri [Nor88], [Tog96] e non
esporre il modello ingegneristico dell'applicazione.
Distinguere i principi
dalle tecniche, ovvero: non dobbiamo abbandonare gli insegnamenti
dell'ingegneria del software.
Un nuovo paradigma nasce spesso
da un confronto piuttosto aspro con le metodologie preesistenti:
è quindi ragionevole aspettarsi un rifiuto, o almeno una
forte critica, dei concetti precedenti. Ciò porta però
a trascurare anche gli aspetti positivi e gli insegnamenti più
importanti, al punto che molti sviluppatori che si sono convertiti
al paradigma object oriented, o che hanno iniziato a lavorare
professionalmente con un linguaggio orientato agli oggetti, ignorano
completamente i capisaldi dell'ingegneria del software. Consideriamo
ad esempio due degli errori di progettazione più comuni:
non minimizzare le dipendenze tra le classi e non isolare
le parti soggette a variazioni; il primo è addirittura
così comune che, di fatto, il problema più grande
nella manutenzione dei programmi object oriented [WCMH92] sembra
proprio essere la comprensione delle interazioni e dipendenze
tra le classi. Eppure lo studio di entrambi i problemi risale
agli albori dell'ingegneria del software [Par72], [Par79]. Come
accorgersi del problema? Qui si vede il valore dell'analisi e
del design, e della rappresentazione grafica dei modelli; se da
un lato è infatti piuttosto complesso valutare questi fattori
esclusivamente in base al codice, dall'altro le dipendenze tra
le classi appaiono lampanti in un diagramma delle classi. Senza
un modello, ci si accorgerà del problema più avanti,
tipicamente in fase di debugging e manutenzione. Come si può
porre rimedio al problema? Riportando l'attenzione dello sviluppatore
ai principi fondamentali dell'information hiding (o insegnandoli,
se necessario): distinguendo le tecniche, come l'incapsulazione
e la creazione di classi protocollo, dai principi fondamentali,
come l'information hiding e la necessità di presentare
interfacce resilienti rispetto alle variazioni nei requisiti e
nell'implementazione. Analogamente, dovrebbe essere compito dell'analisi
indicare la stabilità dei requisiti, semplificando il compito
del progettista, o la revisione del design. La mancanza di precisione
nei requisiti è un caso particolare di un problema ben
noto, ovvero non documentare le assunzioni e le decisioni;
soprattutto nella fase di design, che fa da ponte tra analisi
e codifica, il progettista assume numerosi fattori al contorno:
ad esempio, che le ricerche siano più frequenti degli
inserimenti, che gli attributi di una classe siano stabili, che
alcune classi vengano sempre riutilizzate in connessione, e così
via. Molto spesso, queste assunzioni restano solo nella mente
del progettista: anche per questo il ruolo dell'architetto è
così critico, come depositario delle assunzioni nascoste;
si tratta però di un evidente punto debole dell'intero
processo di sviluppo. La diagnosi è piuttosto difficile,
e prevede la revisione dei documenti di design; una buona tecnica
è anche chiedere periodicamente di spiegare le ragioni
di una certa decisione (ad esempio, l'utilizzo o meno di una libreria)
presa alcuni mesi prima: se non si può rispondere semplicemente
cercando tra la documentazione di design, è evidente che
non viene tenuta traccia delle decisioni prese. La cura è
possibile ma costosa, e passa per la necessaria allocazione del
tempo utile per documentare le assunzioni e le decisioni: talvolta,
basta però dare loro la giusta enfasi, magari a scapito
di altro tipo di documentazione che invece è già
catturato dai diagrammi prodotti. Tornando alla differenza tra
principi e tecniche, professionalmente mi capita spesso di assistere
a dibattiti su quale design sia "più object oriented"
o "più incapsulato", eccetera; per quanto esistano
diversi tentativi di introdurre misure quantitative specifiche
nel paradigma object oriented, ad es. [CK94], [LK94] (e le rispettive
critiche [BBM95], [BS96]), molto spesso il dibattito si può
esaurire cercando di evitare i due errori più gravi: non
massimizzare la coesione e non minimizzare l'accoppiamento.
Consideriamo ad esempio uno dei casi più controversi:
l'utilizzo di classi "manager", che gestiscono insiemi
di altre classi; si tratta di un caso molto frequente, tanto da
essere nominato tra i pattern strutturali [GHJV95] come "mediator".
Vi è un forte dibattito sull'uso delle classi manager:
Coad e Nicola [CN93] le sconsigliano caldamente, articoli recenti
le ritengono candidate al "cancro del software" [Hay96],
ed in effetti nella mia esperienza ho visto numerosi esempi di
classi manager cresciute a dismisura, ed utilizzate in modo tale
da trasformare gli altri oggetti in elementi passivi. D'altra
parte, alcuni autori [Sou94] le ritengono molto utili per "aumentare
l'incapsulazione". Come risolvere il dilemma? Proprio distinguendo
tra i principi e le tecniche: le classi manager sono utili quanto
diminuiscono l'accoppiamento, dannose quando diminuiscono la coesione;
la scelta migliore è spesso frutto di considerazioni di
trade/off. Tutti i casi di "cancro del software" succitati
sono chiari esempi di sistemi a bassa coesione, ed i casi in cui
le classi manager "aumentano l'incapsulazione" sono
quelli in cui, di fatto, diminuiscono l'accoppiamento [Pes95a].
È però fondamentale che a progettisti e programmatori
venga insegnato (o re-insegnato) ad applicare le tecniche (incapsulazione,
ereditarietà, polimorfismo) per soddisfare ai principi
(separation of concern, information hiding, alta coesione, basso
accoppiamento), non in base ad una personale opinione di cosa
sia "più object oriented"; nuovamente, posso
dire per esperienza che questo è possibile in tempi brevi
e con risultati di tutto rilievo: tra l'altro, la coesione e l'accoppiamento
sono facilmente misurabili, e non sono necessarie metriche molto
sofisticate per raggiungere buoni risultati.
Progettare gli oggetti,
ovvero: spostare l'enfasi dalla programmazione al design.
Vi sono due modi di utilizzare
i costrutti di un linguaggio: per quello che fanno, o per quello
che esprimono. Nel primo caso stiamo spostando l'attenzione alla
codifica, ed intendiamo la programmazione come un colloquio uomo-macchina;
nel secondo caso stiamo spostando l'attenzione sulla progettazione,
ed intendiamo la programmazione come un colloquio uomo-macchina
e uomo-uomo. Per chi proviene dal paradigma strutturato, o per
chi si è sempre concentrato sui soli aspetti pragmatici
dell'implementazione, viene però naturale utilizzare meccanismi
come l'ereditarietà o l'incapsulazione per il loro effetto,
non per la semantica che trasmettono. La conseguenza più
comune è che si tende a mantenere una struttura imperativa,
con classi assolutamente non autonome (molto facile da capire,
attraverso la presenza di metodi di get/set) e con un controllo
centralizzato dell'esecuzione (le "classi manager" di
cui sopra). Non vi è un serio tentativo di identificare
ruoli e responsabilità delle diverse classi, che vengono
usate esattamente come strutture dati del C o del Pascal.
Un problema più sottile,
ma non meno diffuso, è di confondere ereditarietà
e contenimento; in più di una occasione, ho visto
sviluppatori derivare da una classe, quando logicamente avrebbero
dovuto aggregarla, "perché così ho accesso
diretto ai metodi". Questo approccio è assolutamente
perdente in fase di manutenzione ed anche di estensione, dove
le condizioni che consentivano di implementare l'aggregazione
tramite ereditarietà si sfaldano e ci si trova a dover
modificare l'interfaccia della classe, e quindi tutto il codice
che chiama i metodi della classe. Un approccio più mirato
al design, e che utilizzi l'ereditarietà pubblica solo
per esprimere il concetto di IS-A, previene sin dall'inizio l'insorgere
del problema. La distinzione tra is-a, has-a, ed is-implemented-using
è molto importante, ed in alcuni metodi di design (es.
Booch) ed alcuni linguaggi (es. C++) è esprimibile direttamente,
a tutto vantaggio della chiarezza, dei controlli statici, e della
manutenibilità del codice. Purtroppo è anche molto
comune confondere ereditarietà di interfaccia e di implementazione:
nella mia esperienza, ho avuto modo di vedere che in molti progetti,
anche di dimensione ragguardevole, nonché in librerie commerciali
di un certo peso (in tutti i casi ben sopra le 100.000 righe di
codice) l'ereditarietà di implementazione (ereditarietà
privata in C++) non è mai utilizzata. L'ereditarietà
viene sempre espressa come ereditarietà di interfaccia,
tranne che le classi derivate vengono, di fatto, accoppiate sull'implementazione.
questa è una delle ragioni che portano al cosiddetto problema
della fragile base class [Pes95b], dove modifiche alla
sola implementazione di una classe base si ripercuotono su tutte
le classi derivate. L'errore qui è pensare che l'incapsulazione
protegga da ogni difetto nel design: l'incapsulazione è
una tecnica per separare le implementazioni, e quindi ha
anche il compito di proteggere dai difetti interni di una classe,
ma non protegge dai difetti strutturali, di interfaccia e di progetto.
Un ulteriore errore di design che è molto semplice da verificare
su un diagramma delle classi è di consentire accoppiamenti
transitivi tra le classi: ad esempio, avendo una classe che
accede ai sotto-oggetti di un'altra classe. Il rispetto di un
criterio di design come la Legge di Demeter [LH93] [Pes95c]
può in questi casi prevenire il problema alla radice; notiamo
che come sempre, vi sono in casi in cui la legge può e
deve essere violata: di norma, si tratta anche di punti in cui
un commento o una documentazione aggiuntiva sulle assunzioni al
contorno (vedi sopra) si rivela fondamentale. È naturale
chiedersi se la necessità di progettare l'applicazione,
e non semplicemente le singole classi, non sia un "tradimento"
dei principi dell'OOP: in effetti, su questa riflessione alcuni
autori [Ude94] hanno sommariamente concluso che l'OOP è
fallita. Anche questo è un errore piuttosto grave: confondere
classi e componenti software; le tecnologie ad oggetti non
portano necessariamente alla creazione di componenti, anzi, la
struttura più comune per le librerie è quella di
white-box framework [JF91] e non di black-box framework. Stroustrup
stesso, in una conversazione [Pes96] ha ammesso che la creazione
di componenti non è uno degli obiettivi del C++ e richiede
uno sforzo addizionale; altri autori [Höl93] [TNG92] hanno
investigato a fondo sulle difficoltà insite nella produzione
e nella integrazione di componenti software. Le conseguenze sono
piuttosto ovvie: ogni volta che qualcuno dice "possiamo sviluppare
questa parte in modo del tutto indipendente perché è
un componente separato", sia questi un manager, un progettista,
o un produttore di tool, chiedetegli su quali basi può
garantire che il protocollo tra quel "componente" ed
il resto del sistema sia sufficientemente stabile ed universalmente
accettato all'interno del progetto. In mancanza di una risposta
convincente, è necessario dedicare un pò di tempo
a definire e raffinare l'interfaccia del componente, prima che
insorgano gli inevitabili problemi di integrazione.
Il riuso non è gratuito,
ovvero: gestire i costi ed i benefici del riuso.
Fa parte del folklore degli
oggetti che la tecnologia object oriented "aumenti il riuso";
come per tutte le grandi promesse, la conseguenza immediata è
di sottostimare la difficoltà del riuso. Il riuso
è possibile all'interno di ogni paradigma e linguaggio,
ed anche se gli oggetti aiutano indiscutibilmente ad ottenere
codice riusabile, il riuso va progettato all'interno degli
oggetti: come visto sopra, l'uso degli oggetti non protegge da
errori di progettazione che possono seriamente compromettere le
possibilità di riuso. L'errore più comune è
lasciare che classi "generali" si trovino a dipendere
da classi specializzate, impedendo di fatto il riuso delle classi
generali al di fuori del contesto specializzato. L'introduzione
di criteri di design come la stratificazione, unitamente
alle tecniche per trasformare un modello non stratificato in uno
stratificato, è forse il correttivo tecnico più
efficace. Va fatto comunque notare che i maggiori impedimenti
al riuso sono gli stessi, indipendentemente dal paradigma, e sono
sostanzialmente fattori umani: i manager tendono spesso a sottostimare
le resistenze degli sviluppatori, ma in molte organizzazioni
esistono due fenomeni endemici che riducono il riuso al minimo
indispensabile: la mentalità da macho-programmer e la mancanza
di tempo per progettare un componente riusabile. La prima è
assai più diffusa di quanto si voglia ammettere - molti
programmatori pensano che riusare il codice scritto da altri sia
da principianti, e che tutto sommato nessun altro programmatore
sia capace di scrivere codice migliore del loro. Nelle parole
di un programmatore Microsoft [You96] "Ogni programmatore
qui è fermamente convinto di essere l'unico programmatore
competente - gli altri sono idioti. Perché dovrebbe riusare
il codice scritto da idioti?". Questo atteggiamento è
molto diffuso fra i team composti di programmatori molto selezionati
e qualificati, dove uno dei rischi maggiori è proprio la
difficoltà nell'ottenere una reale cooperazione tra gli
sviluppatori; in questi casi, è spesso indispensabile un
vivo incoraggiamento da parte dell'upper management, anche basato
su incentivi economici [Faf94], per superare l'ostacolo dell'eccessivo
protagonismo. Viceversa, in aziende dove gli sviluppatori sono
meno inclini all'autoesaltazione, è la mancanza di tempo
il fattore più determinante; il problema del programmatore
diventa allora: Chi paga il costo del riuso? Se il riuso
non è pianificato, accettato ed incoraggiato dalla dirigenza,
il programmatore deve scegliere tra l'investimento di tempo per
la progettazione, lo sviluppo ed il testing di un componente realmente
riusabile, oppure l'immediata realizzazione di una classe custom,
ritagliata sulle proprie esigenze. Pochi programmatori accettano
l'idea di rimanere indietro nel planning, o di investire parte
del loro tempo libero, pur di creare una soluzione generale; non
viene quindi mai a crearsi quella famosa "libreria di classi"
che dovrebbe portare all'accelerazione dei tempi di sviluppo in
futuri progetti. Anche in questi casi, solo la buona comunicazione
tra programmatori, progettisti e manager può portare al
giusto compromesso, scegliendo le classi più importanti
e significative e investendo per la creazione di elementi riusabili.
Va infatti scongiurato anche il rischio opposto, ovvero di investire
troppo nel riuso: ovvero, investire grandi quantità
di tempo e denaro per creare componenti teoricamente riusabili
ma che, in pratica, non verranno mai riutilizzati. Questo fenomeno
è abbastanza comune in grandi progetti, dove inizialmente
si scorgono grandi opportunità di riuso, ma dove fattori
fuori controllo tendono poi a minare le effettive opportunità:
investire due anni per creare una libreria di componenti riusabili
per l'interfaccia utente, solo per scoprire che le nuove versioni
dei sistemi operativi hanno introdotto una quantità di
modelli di interazione cui non avevamo pensato due anni prima,
non è un problema accademico ma un rischio che va gestito
e controllato.
Vediamo infine un ultimo fenomeno
piuttosto comune: talvolta una classe nasce riusabile e si trasforma
in codice non riusabile. In un mondo ideale ciò non avviene,
perché ogni decisione di codifica viene verificata usando
i requisiti di design come riscontro; nel mondo reale, tuttavia,
i problemi vengono spesso riscontrati "a valle" nel
processo di sviluppo (codifica, testing) e vi è una forte
pressione, reale o psicologica, che porta ad attuare soluzioni
locali, che mal si conciliano con il design originale. Per questo
è importante distinguere tra riuso del design e riuso
del codice: un buon design è spesso più facilmente
riusabile del codice che lo implementa, perché il codice
è stato "sporcato" durante lo sviluppo o la manutenzione;
i casi più emblematici di riuso del design sono rappresentati
dai design pattern [Coa95, GHJV94], ma anche a livello
di architettura delle applicazioni si possono avere ottimi risultati.
Ad esempio, una applicazione di gestione bonifici ed una di gestione
di ricevute bancarie possono condividere gran parte del design
architetturale: in un mondo ideale, con tempi di sviluppo adeguati,
personale esperto ed un buon architetto, probabilmente potrebbero
condividere anche buona parte del codice. Nel mondo reale, con
tempi di sviluppo sempre più ristretti, e programmatori
che non sempre hanno ricevuto training e formazione adeguati,
si può optare per un obiettivo meno ambizioso ma più
realistico: condividere gran parte del design, con i conseguenti
vantaggi sui tempi di progettazione ma anche di riassegnazione
degli sviluppatori, scegliere alcune classi fondamentali sulla
cui riusabilità vale la pena di investire, e lasciare le
altre alla discrezione dei programmatori.
Bibliografia:
[AB92] Mehmet Aksit, Lodewijk
Bergmans, "Obstacles in Object-Oriented Software Development",
Technical Report, University of Twente, 1992.
[BBM95] Victor Basili, Lionel
Briand, Walcélio Melo, "A Validation of Object Oriented
Design Metrics", Technical Report CS-TR-3443, University
of Maryland, 1995.
[Bro79] Frederick Brooks,
"The Mythical Man-Month", Addison-Wesley, 1979.
[BS96] Aaron Binkley, Stephen
Schach, "Impediments to the Effective Use of Metrics within
the Object-Oriented Paradigm", OOPSLA'96 Workshop on Software
Metrics, 1996.
[CEW93] Stephen Clyde, David
Embley, Scott Woodfield, "Object-Class Congruency: Improving
the Quality of Classification Abstractions in Object Oriented
Software Systems", Technical Report, Brigham Young University,
1993.
[CK94] S. Chidamber, C. Kemerer,
"A Metrics Suite for Object Oriented Design", IEEE Transactions
on Software Engineering, Vol 20, No 6, 1994.
[CN93] Peter Coad, Jill Nicola,
"Object Oriented Programming", Prentice-Hall, 1993.
[Coa95] Peter Coad, "Object
Models: Strategies, Patterns, & Applications", Prentice
Hall, 1995.
[DML87] Tom De Marco, Timoty
Lister, "Peopleware", Dorset House, 1987.
[EE94] Jürgen Ebert,
Gregor Engels, "Observable or Invocable Behaviour - You Have
to Choose!", Technical Report, Leiben University, 1994.
[EJW95] David Embley, Robert
Jackson, Scott Woodfield, "OO Systems Analysis: It Is or
Isn't It?", IEEE Software, Vol. 12 No. 4, July 1995.
[ES93] Robert Ebenau, Susan
Strauss, "Software Inspection Process", McGraw-Hill,
1993.
[Faf94] Danielle Fafchamps,
"Organizational Factors and Reuse", IEEE Software, Vol.
11 No. 5, September 1994.
[Fag76] M. E. Fagan, "Design
and code inspections to reduce errors in program development",
IBM Systems Journal, Volume 15, No 3, 1976.
[GHJV94] Gamma, Helm, Johnson,
Vlissides, "Design Patterns", Addison-Wesley, 1994.
[Hay96] Philip Haynes, "Detection
and Prevention of Software Cancer in OO Systems", OOPSLA'96
Workshop on Software Metrics, 1996.
[Höl93] Urs Hölzle,
"Integrating Independently-Developed Components in Object-Oriented
Languages", in ECOOP '93 Proceedings, Springer Verlag, 1993.
[JCJO92] Jacobson, Christerson,
Jonsson, Overgaard, "Object-Oriented Software Engineering,
a Use-Case Based Approach", Addison-Wesley, 1992.
[JF91] Ralph Johnson, Brian
Foote, "Designing Reusable Classes", Technical Report,
University of Illinois, Urbana-Champaign (una precedente versione
è apparsa su Journal of Object Oriented Programming, Vol.
12 No. 2, 1988).
[LK94] M. Lorenz, J. Kidd,
Object Oriented Software Metrics, Prentice-Hall, 1994.
[LH93] Karl Lieberherr, Ian
Holland, "Assuring Good Style for Object-Oriented Programs",
Technical Report, Northeastern University, 1993.
[McC95] Jim McCarthy, "Dynamics
of Software Development", Microsoft Press, 1995.
[Nor88] Donald Norman, "The
Psichology of Everyday Things", Basic Books, 1988.
[Par72] David Parnas, "On
the Criteria to be Used in Decomposing Systems into Modules",
Communications of ACM, December 1972.
[Par79] David Parnas, "Designing
Software for Easy of Extension and Contraction", IEEE Transactions
on Software Engineering, March 1979.
[Pes95a] Carlo Pescio, "Architettura:
Un Esempio", Computer Programming No. 40, Ottobre 1995.
[Pes95b] Carlo Pescio, "Il
Problema della Fragile Base Class in C++", Computer Programming
No. 41, Novembre 1995.
[Pes95c] Carlo Pescio, "C++
Manuale di Stile", Edizioni Infomedia, 1995.
[Pes96] Carlo Pescio, "Intervista
a Bjarne Stroustrup", Computer Programming No. 50, Settembre
1996.
[Sou94] Jiri Soukup, "Taming
C++, Pattern Classes and Persistence for Large Projects",
Addison-Wesley, 1994.
[TNG92] Dennis Tsichritzis,
Oscar Nierstrasz, Simon Gibbs, "Beyond Objects: Objects",
International Journal of Intelligent and Cooperative Information
Systems, Vol. 1 No. 1, 1992.
[Tog96] Bruce Tognazzini,
"Tog on Software Design", Addison-Wesley, 1996.
[Ude94] Jon Udell, "Componentware",
BYTE, Vol. 19 No. 5, May 1994.
[WCMH92] Norman Wilde, Allen
Chapman, Paul Matthews, Ross Huitt, "Describing Object Oriented
Software: What Maintainers Need to Know", Technical Report,
University of West Florida, 1992.
[Web95] Bruce Webster, "Pitfalls
of Object-Oriented Development", M&T Books, 1995.
[Wei71] Gerald Weinberg, "The
Psychology of Computer Programming", Dorset House, 1971.
[You96] Edward Yourdon, "Rise
& Resurrection of the American Programmer", Prentice-Hall
/ Yourdon Press, 1996.
Biografia
Carlo Pescio (pescio@acm.org)
svolge attività di consulenza in ambito internazionale
nel campo delle tecnologie Object Oriented. Ha svolto la funzione
di Software Architect in grandi progetti per importanti aziende
europee e statunitensi. Autore di numerosi articoli su tematiche
object oriented e di software engineering, nonché del libro
"C++ Manuale di Stile", è incaricato della valutazione
dei progetti dal Direttorato Generale della Comunità Europea
come Esperto nei settori di Telematica e Biomedicina. È
laureato in Scienze dell'Informazione ed è membro dell'ACM,
dell'IEEE e della New York Academy of Sciences.