[ Suggerimenti
] [ Soluzioni
degli Esercizi] [ Volume
2 ] [ Newsletter
Gratuita ]
[ Seminari
] [ Seminari
su CD ROM ] [ Consulenza]
[ Capitolo Precedente ] [ Indice Generale ] [ Indice Analitico ] [ Prossimo Capitolo ]
traduzione italiana e adattamento a cura di Umberto Sorbo
La genesi della rivoluzione del computer fu
in una macchina. La genesi dei linguaggi di programmazione tende quindi ad
assomigliare a quella macchina.
Ma i computer non sono tanto macchine quanto strumenti
di ampliamento della mente ( biciclette per la mente, come ama dire Steve
Jobs) ed un diverso tipo di mezzo espressivo. Di conseguenza, gli strumenti
stanno cominciando ad assomigliare meno alle macchine e di più a parti
della nostra mente e agli altri mezzi espressivi come la scrittura, la pittura,
scultura, animazione e la produzione dei film. La programmazione orientata
agli oggetti è parte di questo movimento che va verso l'utilizzo del
computer come mezzo espressivo.
Questo capitolo introdurrà i concetti base della
programmazione orientata agli oggetti (object-oriented programming OOP), inclusa
una panoramica dei metodi di sviluppo della OOP. Questo capitolo, e questo
libro, presuppone che si abbia esperienza in un linguaggio di programmazione
procedurale, sebbene non necessariamente il C. Se si pensa di aver bisogno
di maggiore preparazione della programmazione e della sintassi del C prima
di affrontare questo libro, si dovrebbe cominciare da Thinking in C: Foundations
for C++ and Javatraining CD ROM, allegato a questo libro e disponibile
anche su www.BruceEckel.com.
Questo capitolo fornisce
materiali di inquadramento generale e materiali complementari. Molte persone
non si sentono a proprio agio nellaffrontare la programmazione orientata
agli oggetti senza prima essersi fatta unidea del quadro generale. Per
questa ragione, qui
si presentano molti concetti atti a dare una buona panoramica della OOP. Per
contro, molti non riescono ad afferrare i concetti di carattere generale se
prima non hanno visto qualche aspetto concreto dei meccanismi; questo genere
di persone potrebbe impantanarsi
e smarrirsi, se non gli si presenta un po di codice sul quale mettere
le mani. Se si appartiene al secondo gruppo e non si vede lora di passare
alle specifiche del linguaggio, si salti pure la lettura di questo capitolo:
ciò non impedirà di scrivere programmi né di
imparare il linguaggio. Tuttavia, si farà bene a tornare qui, alla
fine, per completare le proprie conoscenze e capire perché gli oggetti
sono importanti e come si fa a progettare con essi.
Tutti i linguaggi di programmazione forniscono astrazioni. Si può dire che la complessità dei problemi che si possono risolvere è direttamente correlata al tipo e qualità di astrazione. Per tipo intendiamo: "Cos'è che stiamo astraendo?". Il linguaggio assembler è una piccola astrazione della macchina che è alla base. I molti cosidetti linguaggi imperativi che seguirono ( come il Fortran, Basic ed il C) furono astrazioni del linguaggio assembler. Questi linguaggi sono un grosso miglioramento del linguaggio assembler, ma la loro astrazione primaria richiede ancora che si pensi in termini della struttura del computer piuttosto che la struttura del problema che si sta tentando di risolvere. Il programmatore deve stabilire l'associazione tra il modello della macchina ( nello spazio delle soluzioni, che è lo spazio dove si sta modellando il problema) ed il modello del problema che si sta risolvendo al momento ( nello spazio del problema, che è il posto dove esiste il problema). Lo sforzo richiesto per eseguire questa associazione ed il fatto che è estrinseco al linguaggio di programmazione, produce programmi che sono difficili da scrivere e da manuntenere e come effetto collaterale l'intera industria dei "metodi di programmazione".
L'alternativa alla modellazione della macchina è modellare il problema che si sta tendando di risolvere. I primi linguaggi come il LISP e l'ASP sceglievano particolari visioni del mondo( "Tutti i problemi sono alla fine liste" oppure "Tutti i problemi sono algoritmici"). Il PROLOG riconduce tutti i problemi a catene di decisioni. I linguaggi sono stati creati per la programmazione basata su vincoli e per programmare esclusivamente manipolando simboli grafici ( l'ultima si è dimostrata essere troppo restrittiva). Ognuno di questi approcci è una buona soluzione ad una particolare classe di problemi che essi risolvono, ma quando si esce fuori dal loro dominio essi diventano goffi.
L'approccio orientato agli oggetti fa un passo più
avanti per il programmatore nel rappresentare gli elementi nello spazio del
problema. Questa rappresentazione è sufficientemente generale da non
limitare il programmatore ad un particolare tipo di problema. Ci riferiferiamo
agli elementi nello spazio del problema e le loro rappresentazioni nello spazio
delle soluzioni come oggetti (naturalmente, si avrà bisogno di altri
oggetti che non hanno analoghi spazi di problema). L'idea è che un
programma può adattarsi al gergo del problema aggiungendo nuovi tipi
di oggetti, così quando si legge il codice che descrive la soluzione,
si leggono le parole che esprimono anche il problema. C'è un linguaggio
di astrazione più flessibile e potente di ciò che abbiamo avuto
prima. Così, la OOP permette di descrivere il problema in termini del
problema, piuttosto che in termini del computer dove verra attuata la soluzione.
C'è ancora un collegamento al computer, tuttavia. Ogni oggetto assomiglia
un pò ad un piccolo computer; esso ha uno stato e delle operazioni
che gli si possono chiedere di compiere. In fondo, non è impropria
lanalogia con gli oggetti del mondo reale: anchessi hanno caratteristiche
e comportamenti.
Qualche progettista di linguaggi ha deciso che la programmazione orientata agli oggetti di per se non è adeguata a risolvere facilemente tutti i problemi di programmazione e difende la combinazione di vari approcci nei linguaggi di programmazione multiparadigma.[4]
Alan Kay ha ricapitolato cinque caratteristiche base dello Smalltalk, il primo linguaggio orientato agli oggetti che ha avuto successo ed uno dei linguaggi sul quale il C++ è basato. Queste caratteristiche rappresentano un approccio puro alla programmazione orientata agli oggetti:
Aristotele fu probabilmente il primo a cominciare un
attento studio del concetto di tipo;egli parlò della "classe
dei pesci e la classe degli uccelli". L'idea che tutti gli oggetti,
pur essendo unici, sono anche parte di una classe di oggetti che hanno caratteristiche
e comportamenti in comune fu usata direttamente nel primo linguaggio orientato
agli oggetti, Simula-67, con la sua parola riservata fondamentale class
che introduce un nuovo tipo in un programma.
Simula, come implica il suo nome, fu creato per sviluppare simulazioni come il classico problema dello sportello bancario[5].” In questo si hanno un gruppo di sportelli, clienti, conti, transazioni ed unità di monetà: molti oggetti. Gli oggetti, che sono identici eccetto per il loro stato durante l'esecuzione di un programma, sono raggruppati insieme in classi di oggetti ed ecco da dove viene fuori la parole riservata class. Creare tipi di dato astratti (classi) è un concetto fondamentale nella programmazione orientata agli oggetti. I tipi di dato astratti funzionano quasi come i tipi predefiniti: si possono creare variabili di un tipo ( chiamati oggetti o istanze nel parlato orientati agli oggetti) e manipolare queste variabili ( chiamate invio di messaggi o richieste; si manda un messaggio e l'oggetto capisce cosa farne di esso). I membri (elementi) di ogni classe condividono alcuni aspetti comuni: ogni conto ha un saldo, ogni sportello accetta un deposito, ecc.. Allo stesso tempo, ogni membro ha il suo proprio stato, ogni conto ha un suo diverso saldo, ogni sportello ha un nome. Così, gli sportelli, i clienti, i conti , le transazioni, ecc..., possono essere rappresentati con un unica entità nel programma del computer. Questa entità è un oggetto ed ogni oggetto appartiene ad una particolare classe che definisce le sue caratteristiche e comportamenti.
Quindi, sebbene ciò che si fa realmente nella
programmazione orientata agli oggetti è creare nuovi tipi di dato,
virtualmente tutti i linguaggi di programmazione orientata gli oggetti usano
la parola riservata class. Quando si vede la parola "type"
si pensi a "class" e viceversa[6].
Poichè una classe descrive un insieme di oggetti che hanno caratteristiche identiche ( elementi dato) e comportamenti ( funzionalità), una classe è realmente un tipo di dato perchè un numero in virgola mobile, per esempio, ha anche un insieme di caratteristiche e comportamenti. La differenza è che un programmatore definisce una classe per adattarla ad un problema piuttosco che dover usare tipi di dato esistenti per i propri bisogni. Il sistema di programmazione dà il benvenuto a classi nuove e le dà tutte le attenzioni ed il controllo sul tipo che viene dato ai tipi predefiniti.
L'approccio orientato agli oggetti non è limitato
alla costruzione di simulazioni. Se si è d'accordo o meno che qualsiasi
programma è una simulazione del sistema che si sta progettando, l'uso
delle tecniche OOP può facilmente ridurre un grande insieme di problemi
ad una semplice soluzione.
Una volta che si è terminata una classe, si possono
fare quanti oggetti si vogliono e poi manipolarli come se essi fossero elementi
che esistono nel problema che si sta tentando di risolvere. Infatti, una delle
sfide della programmazione orientata agli oggetti è creare una corrispondenza
uno a uno tra gli elementi dello spazio del problema e gli oggetti dello spazio
della soluzione.
Ma come si ottiene un oggetto che sia utile per il nostro
lavoro? Ci deve essere un modo di fare una richiesta all'oggetto così
che esso faccia qualcosa, come completare una transazione, disegnare qualcosa
sullo schermo o azionare un interruttore. Ed ogni oggetto può soddisfare
solo alcune richieste. Le richieste che si possono fare ad un oggetto sono
definite dalla sua interfaccia ed il tipo è ciò che determina
la sua interfaccia. Un semplice esempio potrebbe essere una rappresentazione
di una lampadina:
Lampadina lt; lt.accendi();
L'interfaccia stabilisce quali richeste si possono
fare ad un particolare oggetto. Tuttavia, ci deve essere codice da qualche
parte per soddisfare quella richiesta. Questo, insieme con i dati nascosti,
include l'implementazione. Da un punto di vista della programmazione
procedurale non è poi tanto complicato. Un tipo ha una funzione associata
con ogni possibile richiesta e quando si fa una particolare richiesta ad un
oggetto quella funzione viene chiamata. Questo processo è di solito
riassunto dicendo che si "manda un messaggio" ( si fa un richiesta)
ad un oggetto e l'oggetto capisce cosa fare con quel messaggio ( esegue il
codice).
Qui, il nome del tipo/classe è Lampadina,
il nome di questa particolare oggetto Lampadina è lt
e le richieste che si possono fare ad una Lampadina sono di accenderla,
di spegnerla, è di aumentare o diminuire l'intensità della luce.
Si crea un oggetto Lampadina dichiarando un nome (lt) per quell'oggetto.
Per mandare un messaggio all'oggetto, si dichiara il nome dell'oggetto e lo
si connette alla richiesta del messaggio con un punto. Dal punto di vista
dell'utente di una classe predefinita, è tutto ciò che si deve
programmare con gli oggetti.
Il diagramma mostrato sopra segue il formato dell' Unified Modeling Language (UML). Ogni classe è rappresentata da una scatola, con il nome del tipo in cima, i membri dato nella porzione di mezzo e le funzioni membro ( le funzione che appartenfono a quest'oggetto, che ricevono tutti i messaggi che si mandano a quell'oggetto) nella parte bassa. Spesso, nei diagrammi di disegno UML sono mostrati solo il nome della classe e i membri funzione publici, mentre la parte centrale non compare. Se si è interessati solo al nome della classe, allora non c'è bisogno di indicare neanche la porzione inferiore.
È utile separare i campi di gioco in creatori
di classi ( quelli che creano nuovi tipi di dato) e programmatori client[7]
(gli utilizzatori di classi che usano i tipi di dato nelle loro applicazioni).
Lo scopo del programmatore client è di raccogliere un insieme completo
di classe da usare per un rapido sviluppo delle applicazioni. Lo scopo del
creatore di classi è di costruire una classe che espone solo ciò
che è necessario al programmatore client e mantiene tutto il resto
nascosto. Perchè? Perchè se è nascosta, il programmatore
client non può usarla, cioè il creatore della classe può
cambiare la parte nascosta a suo desiderio senza preoccuparsi dell'impatto
verso chiunque altro. La parte nascosta di solito rappresenta la parte più
fragile di un oggetto e può essere facilmente alterata da un programmatore
client sbadato o ignaro, perciò nascondere l'implementazione riduce
i bug nei programmi. Il concetto di implementazione nascosta non va preso
alla leggera.
In qualsiasi relazione è importante avere dei
limiti che siano rispettati da tutte le parti coinvolte. Quando si crea una
libreria, si stabilisce una relazione con il programmatore client, che è
un programmatore, ma è anche uno che sta realizzando un' applicazione
usando la nostra libreria oppure per costruire una libreria più grande.
Se i membri di una classe sono disponibili a tutti,
allora il progammatore client può fare qualsiasi cosa con quella classe
e non c'è modo di imporre regole. Anche se si preferirebbe che il programmatore
client non manipolasse direttamente alcuni membri della nostra classe , senza
il controllo del accesso non c'è modo di prevenirlo. È tutto
alla luce del sole.
Quindi la prima ragione per il controllo dell'accesso
è di fare in modo che i programmatori client non mettano le mani in
parti che sono necessarie per il funzionamento interno dei tipi di dato, ma
che non fanno parte dell'interfaccia di cui gli utenti hanno bisogno per risolvere
i loro particolari problemi. Questo è un servizio reso agli utenti
perchè essi possono facilmente vedere cosa è importante per
loro e cosa possono ignorare.
La seconda ragione per il controllo dell'accesso è
di permettere al progettista della libreria di cambiare il funzionamento interno
della classe senza preoccuparsi di come esso influirà sul programmatore
client. Per esempio, si può implementare una particolare classe in
una maniera semplice per facilitare lo sviluppo e scoprire più tardi
che c'è bisogno di riscriverla per renderla più veloce. Se l'interfaccia
e l'implementazione sono chiaramente separate e protette, si può realizzare
ciò facilmente e richiedere un nuovo linkaggio dall'utente.
Il C++ usa tre esplicite parole riservate per impostare
i limiti di una classe: public, private e protected.
Il loro uso e significato sono abbastanza semplici. Questi specificatori
di accesso determinano chi può usare le definizioni che seguono.
public significa che le definizioni seguenti sono disponibili a tutti.
La parola chiave private , dall'altro lato, significa che nessuno può
accedere a quelle definizioni tranne noi, il creatore del tipo, dentro le
funzioni membro di quel tipo. private è un muro tra noi ed il
programmatore client. Se qualcuno cerca di accedere un membro private, otterrà
un errore a tempo di compilazione. protected agisce proprio come private,
con l'eccezione che una classe che eredità ha accesso ai membri protetti,
ma non a quelli privati. L'ereditarietà sarà introdotta a breve.
Una volta che una classe è stata creata e testata,
dovrebbe idealmente rappresentare un' utile unità di codice. Ne consegue
che questa riusabilità non è così facile da realizzare
come molti spererebbero; ci vuole esperienza ed intuito per produrre un buon
progetto, ma una volta che si ottiene ciò, esso ci implora di essere
riusato. Il riuso del codice è una dei più grandi vantaggi che
la programmazione orientata agli oggetti fornisce.
Il modo più semplice di riusare una classe è
quello di usare un oggetto di quella classe direttamente, ma si può
anche piazzare un oggetto di quella classe dentro una nuova classe. Ciò
viene detto "creazione di un oggetto membro". La nuova classe
può essere composta da qualsiasi numero e tipo di oggetti, in qualsiasi
combinazione di cui si ha bisogno per raggiungere la funzionalità desiderata
nella nuova classe. Poichè si sta componendo una nuova classe da una
esistente, questo concetto è chiamato composizione (
o più generalemente, aggregazione). La composizione è
spesso indicata come una relazione "ha-un", per esempio "un'
auto ha un motore".
( Il diagramma UML di sopra indica la composizione con
il rombo, che specifica che c'è una macchina. Si userà tipicamente
una forma più semplice: solo una linea, senza il rombo, per indicare
una associazione.[8])
La composizione ha una grande flessibilità. Gli
oggetti membro della nuova classe sono di solito privati, in modo da essere
inaccessibili ai programmatori che useranno la classe. Ciò permette
di cambiare quei membri senza disturbare il codice esistente scritto da chi
ha utilizzato la classe. Si può anche cambiare gli oggetti membro a
runtime, per cambiare dinamicamente il comportamento del programma. L'ereditarietà,
che sarà descritta in seguito, non ha questa flessibilità poichè
il compilatore deve porre restrizioni del tempo di compilazione sulle classi
create con l'ereditarietà.
Poichè l'ereditarietà è molto importante
nella programmazione orientata agli oggetti essa è spesso enfatizzata
ed il programmatore novizio può avere l'impressione che essa debba
essere usata dovunque. Il risultato potrebbe essere un progetto goffo e troppo
complicato. Quando si creano nuove classi si dovrebbe, invece, prendere in
considerazione in primo luogo la composizione, perché è più
semplice e più flessibile. Se si segue questo approccio, si avranno
progetti più puliti. Quando si avrà un po di esperienza,
risulteranno evidenti i casi nei quali si avrà bisogno dellereditarietà.
Di per sè, l'idea di un oggetto è uno
strumento pratico. Esso ci permette di impacchettare dati e funzionalità
insieme con i concetti, così che si può descrivere un' appropriata
idea dello spazio del problema piuttosto che dover usare gli idiomi che sottendono
la macchina. Questi concetti sono espressi come unità fondamentali
nel linguaggio di programmazione usando la parola chiave class.
È un
peccato, tuttavia, andare incontro a tanti problemi per creare una classe
e poi essere forzati a crearne una nuova che può avere delle funzionalità
simili. È più carino se possiamo prendere una classe esistente,
clonarla e poi aggiungere le modifiche al clone. Ciò effettivamente
è quello che si ottiene con l'ereditarietà, con l'eccezione
che se la classe originale ( detta base o super o classe genitore ) viene
modificata, il clone modificato ( chiamato la classe derivata o ereditata
o sub o figlia) riflette anch' essa quei cambiamenti.
( La freccia del diagramma UML di sopra punta dalla
classe derivata alla classe base. Come si vedrà, ci possono essere
più di una classe derivata)
Un tipo fa più che descrivere i vincoli di un insieme di oggetti, esso ha anche una relazione con gli altri tipi. Due tipi possono avere caratteristiche e comportamenti in comune, ma un tipo può contenere più caratteristiche di un altro e può anche gestire più messaggi ( o gestirne differenti). L'ereditarietà esprime questa similarità tra i tipi utilizzando il concetto di tipo base e tipo derivato. Un tipo base contiene tutte le caratteristiche ed i comportamenti che sono condivisi tra i tipi derivati da esso. Si crea un tipo base per rappresentare il nucleo delle idee di alcuni oggetti nel nostro sistema. Dal tipo base, si derivano altri tipi per esprimere diversi modi in cui questo nucleo può essere realizzato.
Per esempio, una macchina per il riciclo dei rifiuti mette in ordine pezzi di rifiuti. Il tipo base è il "rifiuto" ed ogni pezzo di rifiuto ha un peso, un valore e così via, e può essere sminuzzato, fuso o decomposto. Da ciò vengono derivati tipi più specifici di rifiuti che possono avere caratteristiche addizionali ( una bottiglia ha un colore) o comportamenti ( l'alluminio può essere schiacciato, una lattina di acciaio è magnetica). In più, alcuni comportamenti possono essere differenti ( il valore della carta dipende dal suo tipo e condizione). Usando l'ereditarietà, si può costruire una gerarchia del tipo che esprime il problema che si sta cercando di risolvere in termini di tipi.
Un secondo esempio è il classico esempio della
figura, forse usato in un sistema di disegno assististo al computer o in un
gioco. Il tipo base è una figura ed ogni figura ha una misura, colore,
posizione e così via. Ogni figura può disegnata, cancellata,mossa,
colorata, ecc.. Da ciò, tipi specifici possono essere derivati (ereditati
): cerchio, quadrati, triangoli e così via ciascuno dei quali può
avere ulteriori caratteristiche e comportamenti. Alcuni comportamenti possono
essere differenti, come quando si vuole calcolare l'area di una figura. La
gerarchia del tipo incarna entrambe le similarità e differenze tra
le figure.
Proporre la soluzione negli stessi termini del problema
ha un enorme beneficio perchè non c'è bisogno di modelli intermedi
per passare da una descrizione del problema ad una descrizione della soluzione.
Con gli oggetti, la gerarchia del tipo è un modello primario, quindi
si va direttamente dalla descrizione del sistema nel mondo reale alla descrizione
del sistema nel codice. Infatti una delle difficoltà che si hanno nel
progetto orientato agli oggetti è che è troppo semplice andare
dall'inizio alla fine. Una mente allenata ad affrontare soluzioni complesse
rimane spesso perplesso da questa semplicità a principio.
Quando si eredita da un tipo esistente, si crea un nuovo
tipo. Il nuovo tipo contiene non solo tutti i membri del tipo esistente (
sebbene quelli privati sono nascosti ed inaccessibili), ma cosa più
importante esso duplica l'interfaccia delle classi base. Cioè, tutti
i messaggi che si mandano agli oggetti della classe base vengono mandati anche
alla classe derivata. Poichè conosciamo il tipo di una classe dai messaggi
che le mandiamo, ciò significa che la classe derivata è dello
stesso tipo della classe base. Nell'esempio precedente, un cerchio
è un figura, questa equivalenza di tipo tramite l'ereditarietà
è uno delle punti fondamentali per capire la programmazione orientata
agli oggetti.
Poichè sia la classe base che la derivata hanno la stessa interfaccia, ci deve essere una qualche implementazione che accompagna l'interfaccia. Cioè, ci deve essere del codice da eseguire quando un oggetto riceve un particolare messaggio. Si semplicemente si eredita una classe e non si fa nient'altro, i metodi dell'interfaccia della classe base sono presenti nella classe derivata. Ciò significa che gli oggetti della classe derivata non hanno solo lo stesso tipo, ma anche lo stesso comportamento, che non è molto interessante.
Si hanno due modi per differenziare la propria nuova classe derivata dalla classe base originale. Il primo è molto diretto: aggiungere semplicemente funzioni nuove di zecca alla classe derivata. Queste nuove funzioni non fanno parte dellinterfaccia della classe base. Ciò vuol dire che la classe base non faceva tutto quello che si voleva che facesse e quindi sono state aggiunte più funzioni. Questo utilizzo semplice e primitivo dellereditarietà è, a volte, la soluzione perfetta per il proprio problema. Tuttavia, di dovrebbe verificare con cura la possibilità che anche la classe base possa aver bisogno di queste funzioni addizionali. Questo modo di procedere nella progettazione per scoperte e iterazioni accade regolarmente nella programmazione orientata agli oggetti.
Sebbene a volte l'ereditarietà possa implicare
l'aggiunta di nuove funzioni all'interfaccia, ciò non è necessariamente
vero. Il secondo e più importante modo di differenziare le nuove classi
è cambiare il comportamento di una funzione di una classe base
esistente. Ciò è indicato come overriding di quella funzione
(ovvero forzare quella funzione).
Per forzare
una funzione, non si deve fare altro che creare una nuova definizione per
quella funzione nella classe derivata. Ciò che si dice è Qui
utilizzo la stessa funzione di interfaccia, però voglio fare qualcosa
di diverso per il mio nuovo tipo.
C'è un certo dibattito sull'ereditarietà:
dovrebbe l'ereditarietà ignorare solo le funzioni della classe base
( e non aggiungere nuove funzioni membro che non sono nella classe base) ?
Ciò significherebbe che il tipo derivato è esattamente lo stesso
tipo della classe base poichè esso ha esattamente la stessa interfaccia.
Come risultato si può sostituire esattamente un oggetto della classe
derivata con un oggetto della classe base. Si può pensare a ciò
come una sostituzione pura ed è spesso indicato come il principio
di sostituzione. In un senso, questo è un modo ideale di trattare
l'ereditarietà. Spesso ci si riferisce alla relazione tra la classe
base e le classi derivate in questo caso come una relazione è-un,
perchè si può dire un cerchio è una figura. Un test per
l'ereditarietà è determinare se si può specificare una
relazione è-un per le classi ed essa ha senso.
Ci sono volte in cui si devono aggiungere nuovi elementi
dell'interfaccia ad un tipo derivato, estendendo così l'interfaccia
e creando un nuovo tipo. Il nuovo tipo può essere ancora sostituito
con il tipo base, ma la sostituzione non è perfetta perchè le
nuove funzioni non sono accessibili dal tipo base. Ciò può essere
descritto come una relazione è-come-un; il nuovo tipo ha l'interfaccia
del tipo vecchio ma contiene anche funzioni, quindi non si può dire
veramente che è esattamente lo stesso. Per esempio, si consideri un
condizionatore d'aria. Si supponga che la nostra casa sia cablata con i controlli
per il raffreddamento, cioè ha un interfaccia che permette di controllare
il raffreddamento. Si immagini che si rompa un condizionatore d'aria e che
lo si rimpiazzi con la pompa di calore, che può sia riscaldare che
raffreddare. La pompa di calore è-come-un condizionatore d'aria,
ma può fare di più. Poichè il sistema di controlla della
casa è stato progettato per controllare solo il raffreddamento, esso
può solo comunicare con la parte del raffreddamento del nuovo oggetto.
L'interfaccia del nuovo oggetto è stata estesa e il sistema esistente
non conosce nient'altro che l'interfaccia originale.
Naturalmente, una volta che si nota questo progetto
diventa chiaro che la classe base "sistema di raffreddamento"non
è abbastanza generale e dovrebbe essere rinominata a "sistema
di controllo della temperatura"in modo che si possa includere il riscaldamento
al punto in cui valga il principio di sostituzione. Tuttavia, il diagramma
di sopra è un esempio di cosa può succedere nel disegno e nel
mondo reale.
Quando si capisce il principio di sostituzione è
facile sentire come questo approccio ( sostituzione pura) sia l'unico modo
di fare le cose ed infatti è carino se il nostro progetto è
fatto così. Ma si scoprirà che ci sono volte che è ugualmente
chiaro che si devono aggiungere nuove funzioni all'interfaccia della classe
derivata. Con l'inspezione entrambi i casi dovrebbero essere ragionevolmente
ovvi.
Quando si ha che fare con gerarchie di tipi, si vuole
spesso trattare un oggetto non come il tipo specifico che è ma come
il suo tipo base. Ciò permette di scrivere codice che non dipende da
tipi specifici. Nell'esempio delle figure, le funzioni manipolano figure generiche
senza preoccuparsi se esse sono cerchi, quadrati, triangoli e così
via. Tutte le figure possono essere disegnate e mosse, così queste
funzioni semplicemente mandano un messaggio all'oggetto della figura; esse
non si preoccupano come gli oggetti fronteggiano il messaggio.
Tale codice non è affetto dall'aggiunta di nuovi
tipi e l'aggiunta di nuovi tipi è il modo più comune di estendere
un programma orientato agli oggetti per gestire nuove situazioni. Per esempio,
si può derivare un nuovo sottotipo di figura chiamato pentagono senza
modificare le funzioni che si occupano solo con figure generiche. Questa capacità
di estendere facilmente un programma derivando nuovi sottotipi è importante
perchè migliora enormemente il disegno e allo stesso tempo riduce il
costo di manutenzione del software.
C'è un problema tuttavia, con il tentativo di
trattare oggetti di tipi derivati come i loro generici tipi base ( cerchi
come figure, biciclette come veicoli, cormorani come uccelli, ecc..). Se una
funzione dirà ad una generica funzione di disegnarsi o ad un generico
veicolo di sterzare o ad un generico uccello di muoversi, il compilatore non
può sapere a tempo di compilazione quale pezzo di codice sarà
eseguito. Questo è il punto, quando un messaggio viene mandato, il
programmatore non vuole sapere quale pezzo di codice sarà eseguito;
la funzione di disegno sarà applicata ugualmente ad un cerchio, un
quadrato o un triangolo e l'oggetto eseguirà l'esatto codice sul
specifico tipo. Se non si deve conoscere quale pezzo di codice sarà
eseguito, allora quando si aggiunge un nuovo sottotipo, il codice che esso
esegue può essere diverso senza richiedere cambiamenti della chiamata
della funzione. Perciò il compilatore non conosce precisamente quale
pezzo di codice viene eseguito, quindi cosa fa? Per esempio, nel diagramma
seguente l'oggetto ControllorePennuto funziona con l'oggetto generico
Pennuto e non sa quale tipo esattamente sono. Ciò è conveniente
dal punto di vista del ControllorePennuto, perchè non si deve
scrivere codice speciale per determinare il tipo esatto di Pennuto
con il quale si sta lavorando o il comportamento di quel Pennuto. Quindi
ciò che accade, quando sposta() viene chiamato mentre si ignora
il tipo specifico di Pennuto, si verificherà il giusto comportamento
( un Anitra corre, vola, o nuota e un Pinguino corre o nuota?)
La risposta è
il primo equivoco della programmazione orientata agli oggetti: il compilatore
non può fare una chiamata a funzione nel senso tradizionale. La chiamata
a funzione generata da un compilatore non OOP causa ciò che è
chiamato early binding(concatenamento anticipato o statico), un termine
che si può non aver sentito prima perchè non ci si è
pensato mai in altro modo. Esso significa che il compilatore genera una chiamata
ad un specifico nome di funzione ed il linker risolve questa chiamata ad un
indirizzo assoluto del codice che deve essere eseguito. Nella OOP, il programma
non può determinare l'indirizzo del codice fino al tempo di esecuzione,
quindi qualche altro schema è necessario quando un messaggio viene
mandato ad un generico oggetto.
Per risolvere il problema, i linguaggi orientati agli
oggetti usano il concetto di late binding (concatenamento ritardato
o dinamico). Quando si manda un messaggio ad un oggetto, il codice che viene
chiamato non è determinato fino al tempo di esecuzione. Il compilatore
non garantisce che la funzione esista ed esegue un controllo sul tipo sugli
argomenti ed il valore di ritorno ( un linguaggio in cui ciò non è
vero è detto weakly typed, debolmente tipizzato), ma non conosce
il codice esatto da eseguire.
Per eseguire il late binding, il compilatore C++ inserisce
uno speciale bit di codice invece della chiamata assoluta. Questo codice calcola
l'indirizzo del corpo della funzione, usando le informazioni memorizzate nell'oggetto
( questo processo è spiegato in dettaglio nel Capitolo 15). Quindi,
ogni oggetto può comportarsi diversamente a secondo del contenuto di
quel bit speciale di codice. Quando si manda un messaggio ad un oggetto, l'oggetto
non sa realmente cosa fare con quel messaggio.
Si dichiara che si vuole che una funzione abbia la flessibilità
del late binding usando la parola chiave virtual. Non c'è bisogno
di capire il meccanismo di virtual per usarlo, ma senza esso non si
può programmare ad oggetti in C++. In C++, si deve ricordare di aggiungere
virtual, perchè, per default, le funzioni membro non sono dinamicamente
legate. Le funzioni virtuali ci permettono di esprimere una differenza di
comportamento tra le classi di una stessa famiglia. Quelle differenze sono
ciò che causano un comportamento polimorfico.
Si consideri l'esempio figura. La famiglia delle classi
( tutte basate sulla stessa interfaccia uniforme) è stata diagrammata
precedentemente nel capitolo. Per dimostrare il polimorfismo, vogliamo scrivere
un singolo pezzo di codice che ignora i dettagli specifici del tipo e parla
solo con la classe base. Quel codice è disaccoppiato dall'informazione
specificata del tipo e quindi è più facile scrivere e più
semplice capire. E se un nuovo tipo, un Esagono, per esempio viene
aggiunto attraverso l'ereditarietà, il codice che si scrive funzionerà
anche per il nuovo tipo di figura come faceva per i tipi esistenti. In questo
modo il programma è estendibile.
Se si scrive una funzione in C++( come presto impareremo
a fare):
void faiQualcosa(Figura& f) { f.elimina(); // ... f.disegna(); }
Questa funzione parla ad ogni figura, quindi è
indipendente dal tipo specifico dell'oggetto che viene disegnato e cancellato
( la & significa "prendi l'indirizzo dell'oggetto che è passato
a faiQualcosa()", ma non è importante che si capiscano questi dettagli
ora). Se in qualche parte del programma usiamo la funzione faiQualcosa():
Cerchio c; Triangolo t; Linea l; faQualcosa(c); faQualcosa(t); faQualcosa(l);
La chiamata a faQualcosa() funziona bene automaticamente, a dispetto del tipo esatto di oggetto.
È veramente un trucco fantastico. Si consideri
la linea:
faiQualcosa(c);
Ciò che accade qui è che un Cerchio viene
passato ad una funzione che si aspetta una Figura. Poichè un Cerchio
è una Figura, esso può essere trattato come uno da faiQualcosa().
Cioè, qualsiasi messaggio che faQualcosa() può mandare ad una
Figura, Cerchio lo può accettare. Quindi è una cosa completamente
sicura e logica da fare.
Chiamiamo upcasting
questo modo di trattare un tipo derivato come se fosse il suo tipo base. Nel
nome cast è usato nel senso letterale inglese, fondere in uno stampo,
e up viene dal modo in cui normalmente viene disposto il diagramma dellereditarietà,
con il tipo
base in testa e le classi derivate disposte a ventaglio verso il basso. Quindi,
fare il casting su un tipo base significa risalire lungo diagramma dellereditarietà:
upcasting.
Un programma ad oggetti contiene qualche upcasting da
qualche parte, perchè questo è il modo in cui ci si disaccoppia
dal sapere qual è l'esatto tipo con cui si sta lavorando. Si guardi
nel codice a faiQualcosa():
f.elimina(); // ... f.disegna();
Si noti che esso non dice: "Se sei un Cerchio, fai questo, se sei un Quadrato, fai quello, ecc..". Se si scrive questo tipo di codice, che controlla per tutti i possibili tipi di una Figura che si possono avere, è una grande confusione e si ha bisogno di cambiare il codice ogni volta che si aggiunge un nuovo tipo di Figura. Qui, diciamo solo: "Tu sei una figura, so che puoi eseguire elimina() , disegna(), fallo e fai attenzione ai dettagli correttamente".
Ciò che colpisce del codice in faiQualcosa()
è che, in qualche modo, va tutto per il verso giusto. Chiamando
disegna() per Cerchio si ottiene l'esecuzione di un codice diverso
da quello eseguito quando si chiama disegna() per un Quadrato
o Triangolo, ma quando il messaggio disegna() viene mandato
ad una Figura anonima, il corretto comportamento avviene basandosi
sul tipo effettivo di Figura. Ciò è sbalorditivo perchè,
come menzionato prima, quando il compilatore C++ sta compilando il codice
per faiQualcosa(), non sa esattamente con che tipo sta trattando.
Perciò di solito, ci si aspetterebbe che esso chiamasse le versioni di
elimina() e disegna() per Figura e non in specifico per
Cerchio, Quadrato o Triangolo. E ancora tutto va come deve andare
grazie al polimorfismo. Il compilatore e il sistema a runtime gestiscono i
dettagli; tutto ciò che si deve sapere è che funziona e,
cosa più importante, come progettare. Se una funzione membro è
virtual, allora quando si manda un messaggio ad un oggetto, questo
farà la cosa giusta, anche quando è coinvolto l'upcasting.
Tecnicamente, il dominio della OOP è la creazione
di tipi di dato astratti, l'ereditarietà ed il polimorfismo, ma ci
sono altri argomenti altrettanto importanti. Questa sezione dà una
panoramica di questi argomenti.
Importante in particolare è il modo in cui vengono
creati e distrutti gli oggetti. Dov'è il dato per un oggetto e come
è controllato il tempo di vita di un oggetto? Linguaggi di programmazione
diversi usano diverse filosofie. Il C++ segue l'approccio secondo il quale
il controllo dell'efficienza è il problema più importante, perciò
dà la scelta al programmatore. Per ottenere la massima velocità
di esecuzione, la memorizzazione e il tempo di vita può essere determinato
mentre il programma viene scritto, piazzando gli oggetto nello stack o nella
memoria statica. Lo stack è un'area di memoria che è usata direttamente
dal microprocessore per memorizzare i data durante l'esecuzione del programma.
Le variabili nello stack sono a volte chiamate variabili automatiche
o delimitate. La memoria statica è semplicemente una parte fissa
della memoria che è allocata prima che il programma vada in esecuzione.
L'utilizzo dello stack o di aree di memoria statica piazza una priorità
sulla velocità di allocazione della memoria ed il rilascio, che può
essere di gran valore in alcune situazioni.Tuttavia, si sacrifica la flessibilità
perchè si deve conoscere esattamente la quantità, il tempo di
vita ed il tipo di oggetti mentre si sta scrivendo il programma. Se
si prova a risolvere un problema più generale, come il disegno assistito
al computer, warehouse management o controllo del traffico aereo, ciò
è troppo restrittivo.
Il secondo approccio è la creazione dinamica
di oggetti in un area di memoria detta heap. Con questo approccio non si conosce
quanti oggetti si ha bisogno fino a runtime, qual è il loro ciclo di
vita e qual è il loro tipo esatto. Queste decisione vengono prese quando
c'è la necessità durante l'esecuzione del programma. Se si ha
bisogno di un nuovo oggetto, lo si crea semplicemente nella heap usando la
parola chiave new. Quando non serve più, si deve rilasciare
la memoria con la parola chiave delete.
Poichè la memoria libera è gestita dinamicamente
a runtime, il tempo totale richiesto per allocare memoria nella heap è
significativamento più lungo del tempo di allocazione nello stack (
spesso serve una singola istruzione del microprocessore per spostare il puntatore
in basso ed un'altra per muoverlo in alto). L'approccio dinamico fa generalmente
l'assunzione logica che gli oggetti tendono ad essere complicati, quindi è
necessario un overhead extra per trovare spazio e rilasciare quello spazio
non avrà un impatto importante sulla creazione di un oggetto. In aggiunta,
la maggiore flessibilità è essenziale per risolvere i generali
problemi di programmazione.
C'è un altro problema, tuttavia , ed è
il tempo di vita di un oggetto. Se si crea un oggetto nello stack o nella
memoria statica, il compilatore determina dopo quanto tempo l'oggetto muore e può
automaticamente distruggerlo. Tuttavia, se lo si crea nella heap, il compilatore
non ha conoscenza del suo tempo di vita. Nel C++, il programmatore deve determinare
quando distruggere l'oggetto e poi eseguire la distruzione con la parola chiave
delete. In alternativa, l'ambiente può fornire una funzionalità
detta garbage collector che automaticamente scopre quando un oggetto
non è più usato e lo distrugge. Naturalmente, scrivere programmi
usando un garbage collector è molto più conveniente,
ma richiede che tutte le applicazione debbano tollerare l'esistenza del garbage
collector ed il suo overhead. Ciò non incontrava i requisiti di
design del C++ e quindi non fu incluso, sebbene sono stati sviluppati per
il C++ garbage collector di terze parti.
Fin dalla nascita dei linguaggi di programmazione, la
gestione degli errori è stato uno dei problemi più difficili.
Poichè è molto difficile progettare un buon meccanismo per la
gestione degli errori, molti linguaggi semplicemente ignorano il problema,
passandolo ai progettisti delle librerie che realizzano soluzioni a metà
strada funzionanti in molte situazioni, ma che possono essere facilmente aggirate,
generalmente soltanto ignorandole. Un problema principale con la maggior parte
dei sistemi di gestione degli errori è che essi si affidano sulla vigilanza
del programmatore nel seguire una convenzione convenuta che non è imposta
dal linguaggio. Se i programmatori non sono attenti, cosa che spesso accade
quando vanno di fretta, questi meccanismi possono venir facilmente dimenticati.
La gestione delle
eccezioni integra la gestione degli errori direttamente nel linguaggio
di programmazione e a volte persino nel sistema operativo. Un'eccezione è
un oggetto che è "lanciato" dal punto dell'errore e può
essere "preso" da un appropriato gestore delle eccezione progettato
per gestire quel particolare tipo di errore. È come se la gestione
delle eccezione sia un percorso diverso e parallelo di esecuzione che può
essere preso quando le cose vanno per il verso sbagliato. E poichè
usa un percorso di esecuzione diverso, non ha bisogno di interferire con il
codice normalmente in esecuzione. Ciò rende il codice più semplice
da scrivere poichè non si deve costantemente controllare se ci sono
errori. Inoltre, un' eccezione lanciata è diversa da un valore di errore
restituito da una funzione o da un flag impostato da una funzione per indicare
una condizione di errore: queste segnalazioni possono essere ignorate. Una
eccezione non può essere ignorata, quindi è garantito
che in qualche punto verrà affrontata. Infine, le eccezioni forniscono
un modo per riprendersi in modo affidabile da una cattiva situazione. Invece
di uscire e basta, spesso si può rimettere le cose a posto e ripristinare
lesecuzione di un programma, ottenendo per questa via programmi più
robusti.
Vale la pena notare che la gestione delle eccezioni
non è una caratteristica object-oriented, sebbene nei linguaggi orientati
agli oggetti l'eccezione è normalmente rappresentata con un oggetto.
La gestione delle eccezioni esisteva prima dei linguaggi orientati agli oggetti.
La gestione delle eccezioni è introdotta soltanto parzialmente in questo Volume; il Volume 2 ( disponibile su www.BruceEckel.com) tratta esaustivamente questo argomento.
Il paradigma orientato agli oggetti è un nuovo e diverso modo di pensare alla programmazione e molte persone hanno problemi a come approcciare un progetto OOP. Una volta che si sà che tutto è supposto essere un oggetto e che si è imparato a pensare più in un modo orientato agli oggetti, si può cominciare a creare buoni progetti ed approfittare di tutti i benefici che la OOP ha da offrire.
Un metodo ( spesso detto una metodologia) è un
insieme di processi e ausilii per dividere la complessità di un problema
di programmazione. Molti metodi
OOP sono stati formulati dall'alba della programmazione orientata agli oggetti.
Questa sezione darà un
assaggio di cosa si sta tentando di ottenere quando si usa un metodo.
Specialmente nella OOP, la metodologia è un campo
in cui si fanno molti esperimenti, quindi è importante capire quale
problema il metodo sta cercando di risolvere prima di adottarne uno. Ciò
è particolarmente vero nel C++, in cui il linguaggio di programmazione
è inteso a ridurre la complessità ( a paragone con il C) riguardante
l'espressione di un programma. Ciò può alleviare il bisogno
di metodologie ancora più complesse. Invece, metodologie più
semplici possono bastare in C++ per una classe di problemi più vasta
che si può gestire usando metodologie semplici con linguaggi procedurali.
È importante capire che il termine "metodogia"
è spesso esagerato e promette molto. Qualsiasi cosa si faccia quando
si progetta e si scrive un programma è un metodo. Può essere
il proprio metodo e si può non essere consci di farlo, ma è
un processo che si segue mentre si crea. Se un processo è efficace,
può aver bisogno solo di una piccola messa a punto per funzionare con
il C++. Se non si è soddisfatti della propria produttività e
del modo in cui riescono i propri programmi, si dovrebbe considerare di utilizzare
un metodo formale o di sceglierne una parte dai molti disponibili.
Mentre si attraversa il processo di sviluppo, la cosa più importante è questa: non perdersi. È facile farlo. La maggior parte dei metodi di analisi e di design sono pensati per risolvere la maggior parte dei problemi. Si ricordi che la maggior parte dei progetti non appartiene a questa categoria, quindi di solito si può aver un' analisi e design vincente con un sottoinsieme relativamente piccolo di ciò che un metodo raccomanda[9]. Tuttavia un processo di un qualche tipo, non importa quanto limitato, in generale indicherà il giusto cammino molto meglio di quanto non si farebbe cominciando subito a codificare.
È anche facile
restare impantanati, cadere nella paralisi da analisi, nella quale
si ha la sensazione di non poter proseguire perché non si è
ancora sistemato ogni più piccolo particolare dello stadio corrente.
Si ricordi, per quanto approfondita possa essere la propria analisi, vi sono
sempre cose di un sistema che non si lasciano rivelare fino al momento della
progettazione e altre ancora, in maggior quantità, che non si manifestano
fino a quando non passerà alla codificare o addirittura fino a quando
il programma non è finito ed è in esecuzione. Per queste ragioni,
è essenziale percorrere velocemente le fasi di analisi e progettazione
ed implementare un collaudo del sistema in sviluppo.
Cè un punto
che merita di essere sottolineato. Per via della storia che abbiamo vissuto
con i linguaggi procedurali, è encomiabile una squadra che intenda
procedere con cautela e capire ogni minimo particolare prima di passare alla
progettazione e allimplementazione. Certo, quando si crea un DBMS cè
tutto da guadagnare a capire in modo esauriente i fabbisogni di un cliente.
Ma un DBMS è una classe di problemi che sono ben formulati e ben capiti;
in molti programmi di questo genere la struttura del database è il
problema da affrontare. La classe di problemi di programmazione di cui ci
si occupa in questo capitolo appartiene alla famiglia dei jolly
(termine mio), nella quale la soluzione non può essere trovata semplicemente
ricreando una soluzione ben conosciuta, ma coinvolge invece uno o più
fattori jolly: elementi per i quali non esiste una soluzione precedente
ben capita e per i quali è necessario effettuare delle ricerche[10].
Tentare di analizzare in modo
esauriente un problema jolly prima di passare alla progettazione e allimplementazione
porta alla paralisi da analisi, perché non si hanno sufficienti informazioni
per risolvere questo genere di problemi durante la fase di analisi. Per risolvere
questo tipo di problemi occorre ripercorrere più volte lintero
ciclo e questo esige un comportamento incline allassunzione di rischi
(cosa che ha un suo senso, perché si cerca di fare qualcosa di nuovo
e il compenso potenziale è più elevato). Potrebbe sembrare che
il rischio aumenti affrettando una implementazione preliminare,
ma ciò potrebbe invece ridurre il rischio in un progetto jolly, perché
si scopre con anticipo se un determinato approccio al problema è plausibile.
Sviluppare un prodotto significa gestire il rischio.
Spesso si propone di costruirne
uno da buttare. Con la OOP, ci si troverà certo a buttarne via
una parte, ma siccome il codice è incapsulato nelle classi, durante
la prima passata si produrrà inevitabilmente qualche schema di classe
utile e si svilupperanno valide idee in merito al progetto del sistema che
non saranno da buttare. Di conseguenza, una prima rapida passata sul problema
non soltanto produce informazioni importanti per la successiva passata di
analisi, progettazione e implementazione, ma crea anche una base di codice.
Detto questo, se si va in cerca di una metodologia che contenga enormi volumi
di dettagli e imponga molti passi e documenti, è difficile stabilire
dove fermarsi. Si tenga ben chiaro in mente quel che si sta cercando di scoprire:
1. Quali sono gli oggetti?
(Come si scompone il proprio progetto in parti?)
2. Quali sono le loro interfacce? (Quali messaggi si devono mandare a ciascun
oggetto?)
Se si viene fuori con nientaltro che gli oggetti e le loro interfacce,
si può cominciare a scrivere un programma. Per varie ragioni si potrebbe
aver bisogno di una maggior quantità di informazioni e di documenti,
ma si può fare meno di questo. Il processo può essere articolato
in cinque fasi, più una Fase 0 che è semplicemente limpegno
iniziale ad utilizzare un qualche tipo di struttura.
Come prima cosa si deve decidere in quali passi si articolerà il proprio processo. Sembra semplice (e in effetti tutto questo sembra semplice) eppure la gente spesso non prende questa decisione prima di mettersi a codificare. Se il proprio piano è diamoci sotto e cominciamo a scrivere il codice, bene (a volte è quello giusto, quando il problema è ben chiaro). Almeno si accetti lidea che il piano è questo.
Si potrebbe anche decidere in questa fase che il processo va strutturato ancora
un po, ma senza impegnarsi eccessivamente. Si può capire che
a certi programmatori piaccia lavorare con spirito vacanziero,
senza una struttura che ingabbi il processo di sviluppo del loro lavoro:Sarà
fatto quando sarà fatto. La cosa può anche essere divertente,
per un po, ma mi sono accorto che avere qualche obiettivo intermedio,
le pietre miliari (o milestone come vengono chiamate nel gergo dei pianificatori),
aiuta a focalizzare e a stimolare gli impegni riferendoli a quelle pietre
miliari invece di ritrovarsi con lunico obiettivo di finire il
progetto. Inoltre, così si suddivide il progetto in segmenti
più agevoli da afferrare, facendolo diventare meno minaccioso (e poi
le tappe intermedie sono ottime occasioni per festeggiamenti).
Quando cominciai a studiare la struttura narrativa (così una volta
o laltra scriverò un romanzo) provavo una certa riluttanza nei
confronti del concetto di struttura, perché mi sembrava di scrivere
meglio quando buttavo giù le pagine direttamente. In seguito, però,
mi resi conto che, quando scrivo di computer, la struttura mi è chiara
al punto che non ci devo pensare più di tanto. Ma strutturo comunque
mentalmente il mio lavoro, seppure in modo non del tutto consapevole. Anche
se si è convinti che il proprio piano sia di mettersi subito a codificare,
si finirà comunque col percorrere le fasi che seguono mentre ci si
farà certe domande e ci i daranno le risposte.
Qualunque sistema si andrà a costruire, per quanto complicato, ha uno scopo fondamentale; il contesto nel quale si trova, il fabbisogno base che deve soddisfare. Se si riesce a guardare al di là dell'interfaccia utente, dei particolari specifici dell'hardware o del software, degli algoritmi di codifica e dei problemi di efficienza, si finirà per scoprire il nucleo essenziale del sistema: semplice e diretto. Come la cosiddetta idea base di un film di Hollywood, potrete descriverlo con una o due frasi. Questa descrizione pura è il punto di partenza.
L'idea base è molto
importante perché dà il tono a tutto al proprio progetto; è
la dichiarazione della missione. Non si riuscirà a coglierla con esattezza
fin dalla prima volta (potrebbe essere trovata in una fase successiva del
progetto prima che diventi del tutto chiara), però bisogno insistere
finché non sembra quella giusta. Per esempio, in un sistema per il
controllo del traffico aereo si potrebbe cominciare con un'idea base focalizzata
sul sistema che si sta costruendo: Il programma della torre tiene traccia
degli aerei. Si pensi, però, a quel che accade se si riduce il
sistema alla dimensione di un aeroporto molto piccolo; magari c'è un
solo controllore del traffico o addirittura nessuno. Un modello più
utile, invece di riferirsi alla soluzione che si sta creando, descrive il
problema: arrivano aeroplani, scaricano, si riforniscono, ricaricano e
ripartono.
Qualsiasi sistema si costruisca, non importa quanto complicato, ha uno scopo fondamentale,il contesto nel quale si trova, le basi che esso soddisfa. Se si guarda oltre l'interfaccia utente, il dettaglio dell'hardware o del sistema specifico, gli algoritmi di codica e i problemi di efficienza, alla fine si trovera il nocciolo della sua essenza: semplice e lineare. Come la cosidetta idea base di un film di Hollywood, lo si può descrivere in una o due frasi. Questa pura descrizione è il punto di partenza.
Nella progettazione
procedurale, come veniva chiamato il metodo di progettazione dei programmi
della generazione precedente, questa fase era dedicata a creare l'analisi
dei requisiti e la specifica del sistema. Si trattava di attività
nelle quali era facile smarrirsi; certi documenti, i cui soli nomi già
mettevano soggezione, finivano per diventare a loro volta grossi progetti.
Le intenzioni erano buone, però. L'analisi dei requisiti dice: "Si
faccia un elenco delle linee guida che si utilizzeranno per sapere quando
il lavoro è concluso ed il cliente è soddisfatto".
La specifica del sistema dice: "Questa è la descrizione di
ciò che il programma farà (non come) per soddisfare i requisiti".
L'analisi dei requisiti è in realtà un contratto fra il programmatore
ed il cliente (che può anche essere qualcuno che lavora nella vostra
stessa azienda oppure qualche altro oggetto o sistema). La specifica del sistema
è una panoramica generale del problema ed in un certo senso la verifica
che si possa risolvere e quanto ci vorrà per risolverlo. Siccome richiedono
entrambi un accordo fra persone (e siccome queste di solito cambiano col passare
del tempo), sarebbe meglio produrre documenti brevi ed essenziali, idealmente
puri elenchi e diagrammi schematici, per risparmiare tempo. Potrebbero
esservi altri vincoli che costringono ad ampliarli in documenti più
voluminosi, però se ci si impegna a dare al documento iniziale un contenuto
breve e conciso, è possibile crearlo in poche sedute di discussione
in gruppo, con un leader che crea dinamicamente la descrizione. In questo
modo non soltanto si stimola il contributo di tutti, ma si favorisce l'adesione
iniziale e l'accordo di tutti i componenti della squadra. Quel che è
più importante, forse, è il fatto che così si può
avviare un progetto con molto entusiasmo.
È necessario mantenersi concentrati sul cuore di ciò che si sta tentando di realizzare in questa fase: determinare cosa si suppone che il sistema faccia. Lo strumento più prezioso per ciò è una raccolta do ciò che sono detti "Use case" (casi d'utilizzo) . Gli Use case identificano le caratteristiche chiave del sistema che riveleranno alcune delle classi fondamentali che si useranno. Ci sono essenzialmente risposte descrittive alle domande tipo[11]:
Se, per esempio, si sta progettando uno sportello bancario automatico, il caso di utilizzo di un aspetto particolare della funzionalità è in grado di descrivere quello che lo sportello automatico fa in ogni possibile situazione. Ciascuna di queste situazioni prende il nome di scenario e un caso di utilizzo può essere considerato una raccolta di scenari. Si può pensare a uno scenario come una domanda che comincia con: "Che cosa fa il sistema se. . .?". Per esempio: "Che cosa fa lo sportello automatico se un cliente ha versato un assegno nel corso delle ultime 24 ore e non c'è sufficiente disponibilità nel conto per soddisfare una richiesta di prelievo se l'assegno non è stato ancora accettato?"
I diagrammi dei casi di
utilizzo sono deliberatamente semplici, per evitarvi di restare impantanati
prematuramente nei particolari dell'implementazione del sistema.
Ciascun simbolo di persona rappresenta un "attore", che di solito è un essere umano o qualche altro tipo di agente libero (questi possono essere anche altri sistemi computerizzati, come nel caso del Terminale). Il riquadro rappresenta i confini del proprio sistema. Gli ovali rappresentano i casi di utilizzo, che sono descrizioni di lavori utili che si possono eseguire con il sistema. Le linee che collegano attori e casi di utilizzo rappresentano le interazioni.
Non importa come il sistema sia effettivamente implementato, basta che si presenti in questo modo all'utente.
Un caso di utilizzo non deve essere terribilmente complesso, anche se il sistema sottostante è complesso. Serve soltanto per far vedere il sistema nel modo in cui appare all'utente. Per esempio:
I casi di utilizzo producono
le specifiche dei requisiti stabilendo le interazioni che l'utente può
avere col sistema. Si tenti di scoprire un insieme completo di casi di utilizzo
per il proprio sistema e, quando si riuscirà, si avrà il nucleo
di quello che il sistema dovrebbe fare. Il bello dei casi di utilizzo sta
nel fatto che riconducono sempre all'essenziale ed impediscono di sconfinare
su questioni che non sono essenziali ai fini del risultato del lavoro. In
altri termini, se si ha un insieme completo di casi di utilizzo, si è
in grado di descrivere il proprio sistema e di passare alla fase successiva.
Probabilmente non si avrà il quadro completo fin dal primo tentativo,
ma va bene così. Tutto salterà fuori, col passare del tempo
e se si pretende di avere in questa fase una specifica perfetta del sistema,
si finirà per impantanarsi.
Se ci si impantana per
davvero, si può rilanciare questa fase ricorrendo a un grossolano strumento
di approssimazione: si descriva il sistema con poche frasi e si cerchino nomi
e verbi. I nomi possono suggerire attori, contesto del caso di utilizzo (per
esempio "magazzino") oppure manufatti manipolati nel caso
di utilizzo. I verbi possono suggerire interazioni fra attori e casi di utilizzo
e specificare passi entro il caso di utilizzo. Si scopriranno anche che nomi
e verbi producono oggetti e messaggi durante la fase di progettazione (e si
tenga presente che i casi di utilizzo descrivono interazioni fra sottosistemi,
quindi la tecnica "nomi e verbi" può essere utilizzata soltanto
come strumento per una seduta di brainstorming, perché non produce
casi di utilizzo) [12].
La linea di demarcazione fra un caso di utilizzo ed un attore può segnalare l'esistenza di un'interfaccia utente, ma non la definisce. Per approfondire il processo col quale si definiscono e si creano interfacce utente, si consulti Software for Use di Larry Constantine e Lucy Lockwood (Addison-Wesley Longman, 1999) oppure si visti il sito www.ForUse.com.
Per quanto sia un pò
un'arte magica, a questo punto è importante tracciare un piano di massima.
Si ha a disposizione una panoramica di quello che si intende costruire, quindi
si dovrebbe riuscire a farsi un'idea del tempo che ci vorrà. Qui entrano
in gioco parecchi fattori.
Se dalla propria stima emerge un piano molto lungo, la società potrebbe
decidere di non realizzarlo (e quindi utilizzare le risorse su qualcosa di
più ragionevole: e questa è un'ottima cosa). Oppure un dirigente
potrebbe aver già deciso per conto suo quanto tempo ci vorrà
per realizzare il progetto e cercherà di influenzare in questo senso
le stime. La cosa migliore, però, è definire onestamente un
piano fin dall'inizio e affrontare quanto prima le decisioni più ardue.
Sono stati fatti un sacco di tentativi per individuare tecniche di pianificazione
accurate (non molto diverse da quelle impiegate per fare previsioni sul mercato
azionario), però la cosa migliore è fidarsi della propria esperienza
e del
proprio intuito. Si faccia una stima a spanne di quanto ci si metterà
per davvero, poi la si raddoppi e si sommi un dieci per cento. La propria
stima a spanne sarà probabilmente corretta; si riuscirà ad ottenere
qualcosa che funziona entro quel periodo di tempo. Raddoppiandola,
si trasformerà la stima in qualcosa di plausibile ed il dieci per cento
servirà per i ritocchi finali ed i dettagli[13]
. Comunque si voglia spiegarlo e indipendentemente dalle lamentele e dalle
manovre che salteranno fuori quando si rivela un piano del genere, sembra
che le cose vadano comunque in questo modo.
In questa fase bisogna produrre un progetto che descriva l'aspetto delle classi e il modo in cui interagiscono. Una tecnica eccellente per determinare classi e interazioni è la scheda Classe-Responsabilità-Collaborazione (CRC). La validità di questo strumento deriva in parte dal fatto che si basa su un tecnologia molto povera: si comincia con un pacchetto di schede di cartoncino bianche in formato 7 × 12 e vi si scrive sopra. Ciascuna scheda rappresenta una sola classe e sulla scheda si scrive:
Si potrebbe
pensare che le schede dovrebbero essere più grandi per contenere tutte
le informazioni che piacerebbe scrivervi sopra, ma sono deliberatamente piccole,
non soltanto per far sì che le classi siano piccole, ma anche per impedire
di impegnarsi troppo presto
in troppi particolari. Se non si riesce a far stare in una piccola scheda tutto
quello che si deve sapere su una classe, la classe è troppo complessa
(o si sta scendendo troppo nei particolari, o si dovrebbe creare più
di una sola classe). La classe ideale deve essere capita a prima vista. Le schede
CRC sono concepite per aiutare a produrre una prima stesura del progetto, in
modo da avere il quadro di massima per poi raffinarlo.
Le schede CRC si dimostrano
particolarmente vantaggiose ai fini della comunicazione. È qualcosa
che è meglio fare in tempo reale, in un gruppo, senza computer. Ogni
persona si prende la responsabilità di svariate classi (che dapprima
non hanno nomi né altre informazioni). Si esegue una simulazione dal
vivo risolvendo un dato scenario per volta, stabilendo quali messaggi vengono
inviati ai vari oggetti per soddisfare ciascun scenario. Mentre si percorre
questo processo si scoprono le classi di cui si ha bisogno insieme con le
loro responsabilità e collaborazioni, si riempiono le schede a mano
a mano che si procede. Quando sono stati percorsi tutti i casi di utilizzo,
dovrebbe essere pronto un primo schema generale del progetto, ragionevolmente
completo.
Prima di cominciare a
usare le schede CRC, la mia esperienza di consulente di maggior successo è
stata quando mi sono trovato a dover lavorare sulla fase iniziale di progettazione
disegnando oggetti su una lavagna con una squadra che non aveva mai costruito
prima un
progetto OOP. Si parlava di come gli oggetti avrebbero dovuto comunicare fra
loro, ne cancellavamo alcuni e li sostituivamo con altri oggetti. In pratica,
gestivo tutte le "schede CRC" sulla lavagna. Era la squadra (che
sapeva che cosa il progetto avrebbe dovuto fare)
che creava materialmente il disegno generale; erano loro i "proprietari"
del disegno, non era qualcosa che gli veniva dato da altri. Io non facevo
altro che orientare il processo ponendo le domande giuste, mettendo alla prova
le ipotesi e ricevendo le reazioni della squadra
per modificare quelle ipotesi. Il bello del processo era nel fatto che la
squadra imparava a fare progettazione orientata agli oggetti non studiando
esempi astratti, ma lavorando sull'unico disegno che per loro era il più
interessante in quel momento: il loro.
Quando si sarà
messo insieme un pacchetto di schede CRC, potrebbe essere utile creare una
descrizione più formale del progetto utilizzando l'UML [14]
. Non è obbligatorio servirsene, però può essere d'aiuto,
specialmente se si vuole appendere un diagramma sul muro, in modo che tutti
possano pensarci sopra, che è un'ottima idea. Un'alternativa all'UML
è una descrizione testuale degli oggetti e delle loro interfacce, oppure,
a seconda del linguaggio di programmazione utilizzato, il codice stesso [15].
Con l'UML si dispone anche
di una ulteriore notazione grafica per descrivere il modello dinamico di un
sistema. Questo aiuta in situazioni nelle quali le transizioni di stato di
un sistema o di un sottosistema sono dominanti al punto da aver bisogno di
propri diagrammi (come nel caso di un sistema di controllo). Può anche
essere necessario descrivere la struttura dei dati, per sistemi o sottosistemi
nei quali i dati sono un fattore dominante (come nel caso di un database).
Si capirà
di aver concluso la Fase 2 quando saranno descritti gli oggetti e le loro
interfacce. Beh, non proprio tutti, la maggior parte: ce ne sono sempre alcuni
che sfuggono e non si manifestano fino alla Fase 3. Ma non è un problema.
L'unica cosa che interessa davvero è arrivare a scoprire tutti i propri
oggetti, alla fine. Fa certo piacere scoprirli all'inizio del processo, ma
la struttura dell'OOP fa sì che non sia grave scoprirli in tempi successivi.
In effetti, la progettazione di un oggetto tende ad articolarsi in cinque
stadi, nell'arco dello sviluppo del programma.
Il ciclo di vita del design di un oggetto non è
limitato al tempo in cui si scrive il programma. Invece, il progetto di un oggetto
appare in una sequenza di fasi. È d'aiuto avere questa prospettiva perchè
non ci illude di essere perfetti da subito; invece, si capisce che la comprensione
di ciò che fa un oggetto e come deve apparire accade sempre.
Questo punto di vista si applica anche al design
di vari tipi di programma; il pattern di un particolare tipo affiora quando
si lotta con quel problema ( i Design Patterns sono trattati nel Volume 2).
Gli oggetti, anche, hanno i loro pattern che emergono attraverso la comprensione,
l'uso ed il riuso.
Queste fasi suggeriscono alcune linee guida quando si pensa allo sviluppo delle classi:
Questa è la conversione
iniziale dalla prima bozza in un insieme di codice che si compila e si esegue
e può essere collaudato, ma soprattutto che dimostra la validità
o la non validità della propria architettura. Non si tratta di un processo
da svolgere in una sola passata, ma è piuttosto l'inizio di una serie
di passi che costruiranno iterativamente il sistema, come si vedrà nella
Fase 4.
L'obiettivo è trovare il nucleo dell'architettura da implementare per
generare un sistema funzionante, non importa quanto quel sistema sia incompleto
in questa passata iniziale. Si sta creando uno schema di riferimento sul quale
continuare a costruire con ulteriori
iterazioni. Si sta anche eseguendo la prima di molte operazioni di integrazione
di sistema e di collaudo, facendo sapere a tutti gli interessati come si presenterà
il loro sistema e come sta progredendo. Idealmente, ci si sta anche esponendo
a qualche rischio critico.
Si scoprirà anche, molto probabilmente, modifiche e miglioramenti che
si potrebbero apportare all'architettura originale: cose che non si sarebbero
potuto capire senza implementare il sistema.
Fa parte del processo di costruzione di sistema il confronto con la realtà
che si ottengono collaudandolo a fronte dell'analisi dei requisiti e delle specifiche
del sistema (in qualunque forma queste esistano). Ci si assicuri che i propri
test verifichino i requisiti e i casi di utilizzo.
Quando il nucleo del sistema è stabile, si è pronti a procedere
oltre e ad aggiungere nuove funzionalità.
Una volta che il nucleo
base gira, ciascuna funzionalità che si aggiunge è di per sé
un piccolo progetto. Un insieme di funzionalità si aggiunge durante
un'iterazione, un periodo di sviluppo relativamente breve.
Quanto dura un'iterazione?
Idealmente, da una a tre settimane (può variare in funzione del linguaggio
di implementazione). Alla fine di quel periodo, si avrà un sistema
integrato e collaudato, con più funzionalità di quelle che si
avevano prima. Quello, però, che è particolarmente interessante
è la base dell'iterazione: un solo caso di utilizzo. Ciascun caso di
utilizzo è un pacchetto di funzionalità correlate che vengono
integrate nel sistema in un colpo solo, durante una sola iterazione. Non soltanto
questo modo di operare vi dà un'idea migliore di
quel che dovrebbe essere la portata di un caso di utilizzo, ma conferisce
maggior validità all'idea di ricorrere a un caso di utilizzo, dal momento
che il concetto non viene scartato dopo l'analisi e la progettazione, ma è
invece un'unità di sviluppo fondamentale nel corso dell'intero processo
di costruzione del software.
Le iterazioni finiscono
quando si arriva alla funzionalità prefissata oppure matura una scadenza
esterna ed il cliente può venir soddisfatto dalla versione corrente.
(Ricordate, quello del software è un mercato per abbonamenti). Siccome
il processo è iterativo, si avranno molte opportunità per consegnare
un prodotto invece che un solo punto terminale; i progetti di tipo open source
funzionano esclusivamente in un ambiente iterativo, ad elevato feedback, ed
è esattamente per questo che hanno tanto successo.
Sono molte le ragioni
che giustificano un processo iterativo. Si può rilevare e risolvere
con molto anticipo situazioni rischiose, i clienti hanno ampie possibilità
di cambiare idea, la soddisfazione dei programmatori è più elevata
ed il progetto può essere guidato con maggior precisione. Un ulteriore,
importante vantaggio viene dal feedback verso gli interessati,che possono
rilevare dallo stato attuale del prodotto a che punto si trovano tutti gli
elementi. Ciò può ridurre o addirittura eliminare la necessità
di condurre estenuanti riunioni di avanzamento, aumentando la fiducia ed il
sostegno da parte degli interessati.
Questo è quel punto nel ciclo dello sviluppo che si chiamava tradizionalmente "manutenzione", un termine ombrello che può significare qualunque cosa, da "farlo funzionare nel modo in cui doveva davvero funzionare fin dall'inizio" a "aggiungere funzionalità che il cliente si era dimenticato di indicare"ai più tradizionali "correggere gli errori che sono saltati fuori" e "aggiungere nuove funzionalità quando ce n'è bisogno". Al termine "manutenzione"sono stati attribuiti un tal numero di significati distorti che ha finito coll'assumere una qualità leggermente ingannevole, in parte perché suggerisce che si è in realtà costruito un programma immacolato, che basta lubrificare e tenere al riparo dalla ruggine. Forse esiste un termine migliore per descrivere come stanno le cose.
Verrà utilizzato il termine evoluzione[16].
Vale a dire: "Non vi verrà giusto la prima volta, quindi ci si
conceda il respiro sufficiente per imparare, tornare sui propri passi e modificare".
Si potrebbe aver bisogno di fare un mucchio di modifiche a mano a mano che
si impara e si comprende più a fondo il problema. L'eleganza che si
raggiunge evolvendo fino ad ottenere il risultato giusto ripagherà
sia nel breve sia nel lungo periodo. Evoluzione è quando il proprio
programma passa da buono a eccellente e quando diventano chiari certi aspetti
che non erano stati capiti bene nella prima passata. E si manifesta anche
quando le proprie classi da oggetti utilizzati per un solo progetto evolvono
in risorse riutilizzabili.
Quando si dice "farlo giusto" non si intende soltanto che il programma
funziona secondo i requisiti ed i casi di utilizzo. Significa anche che la
struttura interna del codice ha per ognuno che la scritto un senso e dà
la sensazione di essere ben integrata, senza contorcimenti sintattici, oggetti
sovradimensionati o frammenti di codice esposti goffamente. Inoltre, si deve
anche avere la sensazione che la struttura del programma sopravviverà
alle modifiche alle quali sarà inevitabilmente soggetto durante la
sua vita e che tali modifiche potranno essere effettuate agevolmente e in
modo pulito. Non è cosa da poco. Non si deve soltanto capire quello
che si sta costruendo, ma anche come il programma evolverà (quello
che io chiamo il vettore del cambiamento[17]).
Fortunatamente i linguaggi di programmazione orientati agli oggetti sono particolarmente
adatti a supportare questo tipo di continue modifiche: i confini creati dagli
oggetti sono quel che impedisce alla struttura di collassare. Questi linguaggi
vi consentono anche di effettuare modifiche "che sembrerebbero drastiche
in un programma procedurale" senza provocare terremoti in tutto il resto
del codice. In effetti, il supporto dell'evoluzione potrebbe essere il vantaggio
più importante dell'OOP.
Tramite l'evoluzione si arriva a creare qualcosa che almeno si avvicina a
quello che si pensa di star costruendo, si può toccare con mano quel
che si è ottenuto, paragonarlo con quel che si voleva ottenere e vedere
se manca qualcosa. A questo punto si può tornare indietro
e rimediare, riprogettando e implementando di nuovo le parti del programma
che non funzionavano correttamente [18] .
Si potrebbe davvero aver bisogno di risolvere il problema, o un suo aspetto,
più di una volta prima di azzeccare la soluzione giusta. (Di solito
in questi casi è utile studiare i Design Patterns. Si possono
trovare informazioni in Thinking in Patterns with Java, scaricabile
da www.BruceEckel.com)
Si ha evoluzione anche quando si costruisce un sistema, si vede che corrisponde
con i propri requisiti, e poi si scopre che in realtà non era quello
che si voleva. Quando si osserva il sistema in funzione, si scopre che in
realtà si voleva risolvere un problema diverso. Se si ritiene che questo
tipo di evoluzione possa manifestarsi, si ha il dovere verso di se stessi
di costruire la vostra prima versione il più rapidamente possibile,
in modo da poter scoprire se è proprio quella che si voleva.
Forse la cosa più importante da ricordare è che, per default
, in realtà se si modifica una classe le sue super e sottoclassi continueranno
a funzionare. Non si deve aver paura delle modifiche (specialmente se si possiede
un insieme integrato di test unitari per verificare la correttezza delle proprie
modifiche). Le modifiche non devastano necessariamente il programma e qualunque
cambio nel risultato sarà circoscritto alle sottoclassi e/o agli specifici
collaboratori della classe che sarà stata modificata.
Naturalmente non ci si metterà mai a costruire una casa senza un bel pò di piani di costruzione accuratamente disegnati. Se si deve costruire una tettoia o un canile non serviranno disegni molto elaborati, ma anche in questi casi probabilmente si inizierà con qualche schizzo che servirà per orientarsi. Lo sviluppo del software è passato da un estremo all'altro. Per molto tempo, la gente non si curava affatto della struttura quando faceva sviluppo, ma poi i progetti di maggiori dimensioni hanno cominciato a fallire. Per reazione, ci siamo ritrovati a ricorrere a metodologie che avevano una quantità terrificante di struttura e di dettagli, concepite soprattutto per quei progetti di grandi dimensioni. Erano metodologie troppo spaventose da utilizzare: sembrava che uno dovesse passare tutto il suo tempo a scrivere documentazione, senza mai dedicare tempo alla programmazione (e spesso era proprio quello che accadeva). Spero che quello che vi ho presentato fin qui suggerisca una via di mezzo: una scala mobile. Si scelga l'approccio che meglio si adatta alle proprie necessità (e alla propria personalità). Anche se si deciderà di ridurlo a dimensioni minime, qualche tipo di piano rappresenterà un notevole miglioramento per il proprio progetto rispetto alla mancanza assoluta di un piano. Si ricordi che, secondo le stime più diffuse, più del 50 per cento dei progetti fallisce (alcune stime arrivano fino al 70 per cento!).
Seguendo un piano, meglio se è uno semplice e breve, arrivando a una
struttura del progetto prima di iniziare la codifica, si scoprirà che
le cose si mettono insieme molto più agevolmente di quando vi tuffate
nella mischia e cominciate a menare fendenti. Otterrete anche parecchia soddisfazione.
In base alla mia esperienza, arrivare ad una soluzione elegante è qualcosa
che soddisfa profondamente un livello interamente diverso; ci si sente più
vicini all'arte che alla tecnologia. E l'eleganza rende sempre; non è
un obiettivo frivolo da perseguire. Non soltanto dà un programma più
facile da costruire e da correggere, ma sarà anche più facile
da capire e da gestire ed è qui che si annida il suo valore economico.
Ho studiato tecniche di analisi e progettazione, a più riprese, fin dai tempi delle superiori. Il concetto di Extreme Programming (XP) è la più radicale e piacevole che abbia mai visto. La si può trovare in Extreme Programming Explained di Kent Beck (Addison-Wesley 2000) e sul web su www.xprogramming.com.
XP è sia una filosofia
sul lavoro della programmazione e un insieme di linea guida per farlo. Alcune
di queste linee guida sono riflesse in altre recenti metodologie, ma i due
contributi più importanti e notevoli, secondo me, sono "scrivere
prima il test" e "programmazione in coppia". Sebbene parli
energicamente dell'intero processo, Beck mette in evidenza che se si adottano
solo queste due prassi si migliorerà enormemente la propria produttività
e affidabilità.
Il test del software è stato relegato tradizionalmente alla fine di un progetto, dopo che "tutto funziona, ma proprio per essere sicuri". Implicitamente ha una bassa priorità e alle persone che si specializzano in materia non viene riconosciuta grande importanza e sono state spesso collocate negli scantinati, lontano dai "veri programmatori". I team di test hanno reagito vestendo abiti neri e sghignazzando ogni volta che rompono qualcosa (per essere onesti, ho avuto questa sensazione quando ho messo in crisi i compilatori C++).
XP rivoluziona completamente il concetto di test dandogli
la stessa ( o maggiore) priorità del codice. Infatti, si scrivono i
test prima di scrivere il codice che deve essere testato e i test stanno con
il codice per sempre. I test devono essere eseguiti con successo ogni volta
che si fa un integrazione del progetto ( il che avviene spesso e più
di una volta al giorno).
Scrivere prima i test ha due effetti estremamente importanti.
Per prima cosa, forza una chiara definizione dell'interfaccia di una classe. Ho spesso suggerito alle persone: " si immagini la classe perfetta per risolvere un particolare problema" come un strumento quando si cerca di progettare un sistema. Il test con la strategia di XP va oltre ciò che esso specifica esattamente come la classe deve sembrare al consumatore di quella classe e esattamente come la classe si deve comportare. E questo senza ambiguità. Si può scrivere tutta la prosa, creare tutti i diagrammi che si vogliono descrivendo come una classe si dovrebbe comportare e sembrare, ma niente è reale come un insieme di test. La prima è una lista dei desideri, ma i test sono un contratto che è rafforzato dal compilatore e dal programma in esecuzione. È difficile immaginare una descrizione più concreta di una classe dei test.
Durante la creazione dei test, si deve avere in mente la classe e spesso si scopriranno le funzionalità di cui si ha bisogno che potrebbero mancare durante gli esperimenti con i diagrammi UML, le CRC card, gli use case, ecc...
Il secondo effetto importante
che si ottiene nello scrivere prima i test deriva dalla loro esecuzione ogni
volta che viene fatta una compilazione del proprio software. Questa attività
fornisce l'altra metà del collaudo che viene eseguita dal compilatore.
Se si osserva l'evoluzione dei linguaggi di programmazione da questa prospettiva,
si noterà che gli autentici miglioramenti nella tecnologia sono avvenuti
in realtà intorno ai collaudi. Il linguaggio assembler controllava
soltanto la sintassi, ma il C ha imposto alcuni vincoli semantici, che impediscono
di fare determinati tipi di errori. I linguaggi OOP impongono ulteriori vincoli
semantici che, se ci si pensa, sono in realtà forme di collaudo. "Questo
tipo di dato viene utilizzato in modo appropriato?"e "Questa
funzione viene chiamata in modo corretto?" sono i tipi di test che
vengono eseguiti dal compilatore o dal sistema a run time. Si è visto
che cosa succede quando meccanismi di collaudo di questi tipo vengono incorporati
nel linguaggio: la gente è in grado di scrivere programmi più
complessi e di metterli in funzione in meno tempo e con minor fatica. Mi sono
spesso domandato come mai le cose stiano in questo modo, ma ora mi rendo conto
che sono i test: si fa qualcosa di sbagliato e la rete di sicurezza costituita
dai test incorporati dice che c'è un problema ed indica dove si trova.
Tuttavia, le forme di
collaudo intrinseco fornite dall'impostazione del linguaggio possono arrivare
solo fino ad un certo punto. Arriva un momento in cui bisogna farsi avanti
ed aggiungere i test rimanenti che producono un insieme completo (in cooperazione
con il compilatore ed il sistema a run time) che verifica l'intero programma.
E, così come si ha un compilatore che guarda le spalle del programmatore,
non si vorrebbe forse che questi test aiutassero fin dall'inizio? Ecco perché
si deve scrivere prima i test ed eseguirli automaticamente ad ogni nuova compilazione
del proprio sistema. I propri test diventano un'estensione della rete di sicurezza
fornita dal linguaggio.
Una delle cose che ho scoperto in merito all'utilizzo di linguaggi di programmazione sempre più potenti è il fatto che mi sento incoraggiato a tentare esperimenti sempre più azzardati, perché so che il linguaggio mi impedirà di sprecare tempo andando a caccia di bachi. La logica dei test di XP fa la stessa cosa per l'intero proprio progetto. Siccome si sa che i propri test intercetteranno sempre eventuali problemi che si creeranno (e che si aggiungeranno sistematicamente nuovi test quando si penserà a quei problemi), si possono fare modifiche importanti, quando serve, senza preoccuparsi di mandare in crisi l'intero progetto. Tutto questo è davvero molto potente.
La programmazione in
coppia è contraria al rude individualismo al quale si è indottrinati
fin dai primi passi tramite la scuola (dove raggiungiamo gli obiettivi o li
manchiamo da soli e lavorare insieme con i compagni è considerato “copiare”)
e tramite i mezzi di comunicazione, specialmente i film di Hollywood, nei
quali l'eroe di solito combatte contro il bieco conformismo [19].
Anche i programmatori sono considerati modelli di individualismo – “cowboy
della codifica”, come ama dire Larry Constantine. Eppure, XP, che
pure si batte contro il modo di pensare tradizionale, sostiene che il codice
andrebbe scritto da due persone per stazioni di lavoro. E che si dovrebbe
farlo in un'area
che contenga un gruppo di stazioni di lavoro, senza le barriere che piacciono
tanto agli arredatori
degli uffici. In effetti Beck sostiene che il primo passo verso la conversione
a XP consiste nell'arrivare al lavoro muniti di cacciaviti e di chiavi a tubo
e smontare tutto ciò
che ingombra [20]
(per far questo ci vuole un dirigente capace di sviare le ire del reparto
che gestisce gli ambienti di lavoro).
Il valore della programmazione in coppia sta nel fatto che una sola persona scrive materialmente il codice mentre l'altra ci pensa sopra. Il pensatore tiene presente il quadro generale, non soltanto il quadro del problema sul quale si lavora, ma le linee guida di XP. Se sono in due a lavorare, è meno probabile che uno di loro possa cavarsela dicendo: "Non voglio scrivere prima i test", per esempio. E se quello che codifica resta impantanato, il collega può dargli il cambio. Se restano impantanati entrambi, le loro riflessioni possono essere udite da qualcun altro nella stessa area di lavoro, che può dare una mano. Lavorare in coppia mantiene le cose in movimento e in riga. Cosa probabilmente più importante, rende la programmazione molto più sociale e divertente.
Ho cominciato a utilizzare la programmazione in coppia durante le esercitazioni in alcuni miei seminari e sembra che la cosa migliori in modo significativo l'esperienza di ognuno.
Uno dei motivi per cui il C++ ha avuto
così tanto successo è che lo scopo non era solo trasformare
il C in un linguaggio OOP ( sebbene si era partiti in quel modo), ma anche
di risolvere molti altri problemi che oggi gli sviluppatori fronteggiano,
specialmente quelli che hanno grossi investimenti nel C. Tradizionalmente,
per i linguaggi OOP si è detto che si dovrebbe abbandonare tutto ciò
che si conosce e partire da zero con un nuovo insieme di concetti e nuove
sintassi, motivando che alla lunga è meglio perdere tutto il bagaglio
di vecchie conoscenze dei linguaggi procedurali. Ciò potrebbe essere
vero, alla lunga. Ma nei primi tempi tale bagaglio è prezioso. Gli
elementi più utili possono non essere la base di codice esistente (
che, dati adeguati strumenti, possono essere tradotti), ma invece la base
mentale esistente. Se si è un buon programmatore C e si deve buttar
via tutto ciò che si conosce del C per adottare il nuovo linguaggio,
si diventa immediatamente molto meno produttivi per molti mesi, fino a che
la propria mente non si adatta al nuovo paradigma. Mentre se si può
far leva sulle conoscenze acquisite del C ed espanderle, si può continuare
ad essere produttivi con ciò che si conosce già mentre si passa
nel mondo della OOP. Poichè ognuno ha un proprio modello mentale di
programmazione, questo passaggio è abbastanza disordinato poichè
esso è privo dello sforzo aggiunto di una partenza con un nuovo linguaggio
da uno noto. Quindi le ragioni del successo del C++ sono economiche: costa
ancora passare alla OOP, ma il C++ può costare meno[21].
Lo scopo del C++ è migliorare
la produttività. Questa produttività si realizza in diversi
modi, ma il linguaggio è progettato per aiutare il programmatore quanto
più è possibile, impedendo allo stesso tempo per quanto sia
possibile, con regole arbitrarie che si usi un particolare insieme di caratteristiche.
Il C++ è progettato per essere pratici; le decisioni prese per il linguaggio
C++ furono dettate per fornire massimi benefici per il programmatore (almeno,
dal punto di vista del C).
Si può avere un
beneficio istantaneo persino se si continua a scrivere in C perchè
il C++ ha chiuso molti buchi del C e fornisce un controllo del tipo migliore
ed analisi a tempo di compilazione. Si è forzati a dichiarare le funzioni
in modo che il compilatore può controllare il loro uso. La necessità
del preprocessore è stata virtualmente eliminata per la sostituzione
di valore e le macro, che rimuovono un un insieme di bachi difficili da trovare.
Il C++ ha una caratteristica chiamata riferimento che permette una
gestione degli indirizzi più conveniente per gli argomenti delle funzioni
e i valori di ritorno. Una caratteristica detta namespace che migliora
anche il controllo dei nomi. La gestione dei nomi viene migliorata attraverso
una caratteristica detta function overloading
( sovraccaricamento
della funzione), che permette di usare lo stesso nome per funzioni diverse.
Ci sono numerose funzionalità più piccole che migliorano la
sicurezza del C.
Quando si impara un nuovo linguaggio
ci sono problemi di produttività. Nessuna azienda può permettersi
di perdere improvvisamente un software engineer produttivo perchè egli
o ella sta imparando un nuovo linguaggio. Il C++ è un estensione del
C, non una sintassi e un modello di programmazione completamente nuovo. Esso
permette di continuare a scrivere codice, applicando le funzionalità
gradualmente mentre le si imparano e capiscono. Questa può essere una
delle ragioni più importanti del successo del C++.
In aggiunta, tutto il codice C esistente
è ancora vitale in C++, ma poichè il compilatore C++ è
più esigente, si troveranno spesso errori
del C nascosti quando si ricompila il codice in C++.
A volte bisogna trovare un compromesso tra la velocita
di esecuzione e la produttività del programmatore. Un modello finanziario,
per esempio, può essere utile solo per un breve periodo di tempo, quindi
è più importante creare il modello rapidamente che eseguirlo
rapidamente. Tuttavia, la maggior parte delle applicazioni richiede gradi
di efficienza, quindi il C++ pecca sempre sul lato di un''efficienza maggiore.
Poichè i programmatori C tendono ad essere molto consci dell'efficienza,
questo è anche un modo di assicurare che essi non potranno dire che
il linguaggio è troppo grasso e lento. Molte caratteristiche in C++
sono intese per permettere di migliorare le prestazione quando il codice generato
non è abbastanza efficiente.
Non solo non bisogna fare gli stessi controlli a basso livello del C ( e scrivere direttamente linguaggio assembler in un programma C++), ma l'evidenza suggerisce che la velocità del programma di un programma C++ ad oggetti tende ad essere tra il ±10% di un programma scritto in C e spesso molto di più[22]. Il progetto prodotto per un programma OOP può essere molto più efficiente della controparte in C.
La classi concepite per
adattarsi al problema tendono a esprimerlo meglio. Questo vuol dire che, quando
si scrive il codice, si descrive la propria soluzione nei termini dello spazio
del problema (Metti il manico al secchio) invece che nei termini
del computer, che è lo
spazio della soluzione (Imposta nel circuito il bit significa che il relè
verrà chiuso). Si lavori con concetti di livello superiore e si
può fare molto di più con una sola riga di codice. Laltro
vantaggio che deriva da questa facilità di espressione sta nella manutenzione
che (se possiamo credere alle statistiche) assorbe unenorme quota dei
costi durante il ciclo di vita di un programma. Se un programma è più
facile da capire, è anche più facile farne la manutenzione. Il
che può anche ridurre i costi della creazione e della manutenzione della
documentazione.
Il modo più veloce di creare un programma è di usare il codice che è già stato scritto: una libreria. Uno dei maggiori scopi del C++ è di rendere più facile la realizzazione delle librerie. Questo viene realizzato fondendo le librerie in nuovi tipi di dato ( classi), in modo che utilizzare una libreria significa aggiungere nuovi tipi al linguaggio. Poichè il compilatore C++ fa attenzione a come viene usata la libreria, garantendo una corretta inizializzazione e pulizia ed assicurando che le funzioni siano chiamate correttamente, ci si può focalizzare su ciò che si vuole la libreria faccia, non su come utilizzarla.
Poichè i nomi possono essere isolati dalle parti del nostro programma usando i namespace del C++, si possono usare quante librerie si vogliono senza gli scontri tra i tipi di nome che avvengono nel C.
C'è una significativa classe di tipi che richiede
modifiche ai sorgenti se si vuole riutilizzarli efficacemente. La funzionalità
template in C++ esegue le modifiche ai sorgenti automaticamente, fornendo
un potente strumento per il riuso del codice di libreria. Un tipo che si progetta
usando i template funzionerà con molti altri tipi. I template sono
molto utili perchè nascondono le complessità di questo genere
per il riuso del codice dal programmatore client.
Molti linguaggi
tradizionali hanno limitazioni intrinseche per quanto riguarda dimensioni e
complessità dei programmi. Il BASIC, per esempio, può essere ottimo
per mettere assieme rapide soluzioni per determinate classi di problemi, però
se il programma si allunga
su troppe pagine o si avventura al di fuori dal dominio normale dei problemi
di quel linguaggio, ci si ritrova a tentare di nuotare in un liquido che diventa
sempre più vischioso. Anche il C ha queste limitazioni. Per esempio,
quando un programma va oltre le 50000 linee di codice, iniziano problemi di
collisione di nomi, effettivamente si finiscono i nomi di funzioni e di variabili.
Un altro problema sono i piccoli bachi nel linguaggio C, errori sepolti in un
grosso programma che possono essere estremamente difficili da trovare.
Non cè
una chiara linea di demarcazione che segnali quando il linguaggio che si sta
utilizzando non va più bene e anche se ci fosse la si ignorerebbe. Non
si dice: Questo programma in BASIC è diventato davvero troppo
lungo; dovrò riscriverlo in C!. Invece, si tenta di ficcarci
dentro ancora un po di righe per aggiungere giusto una sola nuova funzionalità.
E così i costi supplementari cominciano a prendere il sopravvento.
Il C++ è
concepito per aiutare a programmare senza limitazioni: vale a dire, per
cancellare quelle linee di demarcazione derivate dalla complessità che
si collocano fra un programma piccolo e uno grande. Non servirà certo
lOOP per scrivere un programma di servizio
nello stile hello world, però le funzionalità sono
a disposizione per quando servono. Ed il compilatore è molto aggressivo
quando si tratta di stanare errori che generano bachi tanto nei programmi di
piccole dimensioni quanto in quelli grandi.
Se si fa proprio il concetto di OOP, probabilmente la domanda che ci si pone subito dopo sarà: " Come posso convincere il mio capo/i colleghi/il reparto/gli amici ad utilizzare gli oggetti?". Si rifletta su come si farebbe in proprio, da "programmatori indipendenti" a imparare a usare un nuovo linguaggio e un nuovo paradigma di programmazione. Lo si è già fatto in passato. Prima vengono la formazione e gli esempi, poi viene un progetto di prova per dare il senso dei concetti essenziali senza fare nulla che possa creare confusione. Infine viene un progetto del "mondo reale", che fa davvero qualcosa di utile. Nel corso dei primi progetti si continuerà nella propria formazione leggendo, facendo domande agli esperti e scambiando suggerimenti con gli amici. È questo l'approccio che molti programmatori esperti suggeriscono per passare dal C al C++. Il passaggio di un'intera azienda produrrà, naturalmente, alcune dinamiche di gruppo, ma ad ogni punto di svolta verrà utile ricordare come se l'è cavata una singola persona.
Queste che seguono sono
alcune linee guida da considerare quando si passa all'OOP e a al C++.
Il primo passo da compiere
è una qualche forma di addestramento. Si tengano presente gli investimenti
che la propria società ha già fatto nel codice e si cerchi di
non scardinare tutto per sei o nove mesi mentre tutti cercano di capire come
funzionano le interfacce. Si scelga
un piccolo gruppo da indottrinare, preferibilmente formato da persone dotate
di curiosità, che lavorano bene assieme e che possono assistersi a
vicenda mentre imparano il C++.
Talvolta si suggerisce
un approccio alternativo, che consiste nel formare tutti i livelli della società
in un colpo solo, tenendo corsi di orientamento generale per i dirigenti e
corsi di progettazione/programmazione per i capi progetto. Questo è
ottimo per imprese di piccole
dimensioni che si accingono a dare una svolta radicale al loro modo di lavorare
o per il livello divisionale di società di maggiori dimensioni. Siccome,
però, i costi sono più elevati, si preferisce di solito iniziare
con un addestramento a livello di progetto, eseguire un progetto pilota (magari
con l'aiuto di un mentore esterno) e lasciare che le persone che hanno partecipato
al progetto diventino i docenti per il resto della società.
Si cominci
con un progetto a basso rischio e si preveda che si sbaglierà qualcosa.
Quando si avrà un pò di esperienza, si potranno avviare altri
progetti affidandoli ai componenti del primo gruppo di lavoro oppure assegnare
a queste persone il compito di dare assistenza
tecnica per l'OOP. Il primo progetto potrebbe non funzionare bene la prima
volta, quindi non dovrà essere un progetto di importanza critica per
la società. Dovrebbe essere qualcosa di semplice, a sè stante
ed istruttivo; questo vuol dire che dovrebbe portare a creare classi che saranno
significative per gli altri programmatori della società quando verrà
il loro turno per imparare il C++.
Si cerchino esempi
di buona progettazione orientata agli oggetti prima di cominciare da zero.
È molto probabile che qualcuno abbia già risolto il problema
e, se proprio non lo hanno risolto esattamente come si presenta, si può
probabilmente applicare quello che si è imparato sull'astrazione per
modificare uno schema esistente e adattarlo alle proprie necessità.
Questo è il principio ispiratore dei design patterns
trattato nel Volume 2.
La principale motivazione economica per passare all'OOP è la comodità con la quale si può utilizzare codice esistente sotto forma di librerie di classi (in particolare le librerie Standard C++, che sono illustrate in dettaglio nel Volume due di questo libro). Il più breve ciclo di sviluppo di un'applicazione lo si otterrà quando si dovrà scrivere solo main() e si potranno creare ed utilizzare oggetti ricavati da librerie belle e pronte. Tuttavia, certi programmatori alle prime armi non capiscono ciò, non sono a conoscenza di librerie di classi in circolazione oppure, affascinati dal linguaggio, hanno voglia di scrivere classi che potrebbero già esistere. Si otterranno migliori risultati con l'OOP ed il C++ se si fa lo sforzo di cercare e riusare il codice degli altri all'inizio del processo di transizione.
Sebbene compilare il codice C con un compilatore C++ produce di solito ( a volte tremendi) benefici per trovare i problemi del vecchio codice, di solito non conviene prendere codice esistente e funzionale per riscriverlo in C++ ( bisogna trasformarlo in oggetti, si può inglobare il codice C in classi C++). Ci sono dei benefici, specialmente se il codice deve essere riusato. Ma ci sono possibilità che non si vedano i grossi incrementi di produttività che si speravano, fino a che il progetto non parta da capo. il C++ e la OOP brillano di più quando si porta un progetto dall'idea alla realtà.
Se si è un capo, il proprio lavoro è quello di acquisire risorse per la propria squadra, superare le barriere che impediscono alla squadra di avere successo ed in generale creare l'ambiente più produttivo e gradevole che sia possibile affinché la propria squadra possa fare quei miracoli che di solito vengono chiesti. Passare al C++ ha riflessi in tutte e tre queste categorie e sarebbe davvero meraviglioso se non costasse qualcosa. Sebbene il passaggio al C++ possa essere più economico,in dipendenza da vincoli[23] che si hanno con altre alternative OOP per una squadra di programmatori in C (e probabilmente per programmatori in altri linguaggi procedurali), non è del tutto gratuito e vi sono ostacoli che si farebbe bene a conoscere prima di promuovere il passaggio al C++ all'interno della propria società imbarcandosi nel passaggio vero e proprio.
Il costo del passaggio
al C++ è molto più che la semplice acquisizione di compilatori
C++ (il compilatore GNU C++ è gratuito, quindi non è certo un
ostacolo). I costi di medio e lungo periodo si ridurranno al minimo se si
investirà in addestramento (e magari
in una consulenza per il primo progetto) e se inoltre si individuerà
e si acquisterà librerie di classi che risolvono il proprio problema,
piuttosto di tentare di costruire quelle librerie. Questi sono costi in denaro
contante, che vanno conteggiati in una proposta realistica. Inoltre, vi sono
costi nascosti che derivano dalla perdita di produttività mentre si
impara un nuovo linguaggio ed eventualmente un nuovo ambiente di programmazione.
Addestramento e consulenza possono certamente ridurre al minimo questi costi,
ma i componenti della squadra devono superare le loro difficoltà nel
capire la nuova tecnologia. Durante questo processo faranno un maggior numero
di sbagli (e questo è un vantaggio, perché gli sbagli riconosciuti
sono la via più rapida per l'apprendimento) e saranno meno produttivi.
Anche in questi casi, almeno per determinati tipi di problemi di programmazione,
disponendo delle classi giuste e con un ambiente di sviluppo adeguato, è
possibile essere più produttivi mentre si impara il C++ (pur tenendo
conto che si fanno più sbagli e si scrivono meno righe di codice al
giorno) di quanto non sarebbe se si restasse col C.
Una domanda molto diffusa è: "Non è che l'OOP renda automaticamente i miei programmi molto più voluminosi e più lenti?"La risposta è: "Dipende". Le maggior parte dei linguaggi OOP tradizionali sono stati progettati per scopi di sperimentazione e prototipazione rapida. Quindi essi virtualmente garantiscono un significativo incremento in dimensioni ed una diminuzione di velocità. Il C++, tuttavia, è progettato per la produzione. Quando lo scopo è la prototipazione rapida, si possono mettere insieme componenti il più velocemente possibile ignorando i problemi di efficienza. Se si utilizzano librerie di terzi, di solito esse saranno già state ottimizzate dai loro fornitori; in tutti i casi, questo non è un problema quando si lavora in una prospettiva di sviluppo rapido. Quando si ha un sistema che piace, se è sufficientemente piccolo e veloce, non serve altro. Altrimenti, si comincia a ritoccarlo con uno strumento di profilatura, cercando in primo luogo possibili accelerazioni che si potrebbero ottenere riscrivendo piccole parti del codice. Se questo non basta, si cercano le modifiche che si possono apportare all'implementazione sottostante, in modo che non si debba cambiare il codice che utilizza una classe particolare. Soltanto se nient'altro risolve il problema si ha bisogno di modificare la progettazione. Il fatto che le prestazioni siano così critiche in quella parte della progettazione fa capire che devono entrare a far parte dei criteri primari della progettazione. Con lo sviluppo rapido si ha il vantaggio di accorgersi molto presto di questi problemi.
Come menzionato prima,
il numero che è più spesso dato per la differenza in dimensioni
e velocità tra C e C++ è ±10% e spesso molto prossimo alla
parità. Si può persino ottenere un significativo miglioramento
in dimensioni e velocità quando si usa il C++ piuttosto che il C perchè
il progetto che si fa in C++ potrebbe essere molto diverso da quello che si
farebbe in C.
L'evidenza nelle comparazioni
di dimensioni e velocità tra C e C++ tende ad essere aneddotica e a
rimanere probabilmente così. Malgrado il numero di persone che suggerisce
ad un'azienda di provare a sviluppare lo stesso progetto usando C e C++, nessuna
di esse spreca denaro così, a meno che non è molto grande ed
interessata in tali progetti di ricerca. Persino in questo caso, sembra che
ci sia un modo migliore di spendere il denaro. Quasi universalmente, i programmatori
che sono passati dal C ( o qualche altro linguaggio procedurale) al C++ (
o qualche altro linguaggio OOP) hanno avuto l'esperienza personale di una
grande accelerazione nella loro produttività e questo è il miglior
argomento che si possa trovare.
Quando un team comincia a lavorare con l'OOP ed il C++, i programmatori di solito commettono una serie di comuni errori di progettazione. Ciò accade spesso a causa del limitato contributo degli esperti durante il progetto e l'implementazione dei primi progetti, poichè nessun esperto ha sviluppato nell'azienda e ci può essere resistenza ad acquisire consulenti. È facile avere troppo presto la sensazione di aver capito l'OOP e di andar via per la tangente. Qualche cosa che è ovvio per qualcuno che ha esperienza con il linguaggio può essere un soggetto di un grande dibattito interno per un novizio. Questo trauma può essere evitato usando l'esperienza di un esperto esterno per l'addestramento ed il mentoring.
Dall'altro lato, il fatto che è facile commettere questi errori di disegno indica i principali svantaggi del C++: la sua compatibilità all'indietro con il C ( naturalmente, questa è anche la sua forza principale). Per realizzare l'impresa di compilare un codice C, il linguaggio ha dovuto fare qualche compromesso, che consiste in qualche punto oscuro: essi sono una realtà e constituiscono le maggiori difficoltà per l'apprendimento del linguaggio. In questo libro e nel volume seguente ( ed gli altri libri, si veda l'Appendice C), si cercherà di rivelare la maggior parte delle insidie che si incontrano quando si lavora con il C++. Bisognerebbe essere sempre consapevoli che ci sono dei buchi nella rete di sicurezza.
Questo capitolo cerca di dare un'idea dei vari argomenti
della programmazione orientata agli oggetti, incluso il perchè la OOP
è qualcosa di diverso e perchè il C++ in particolare è
diverso, presentando concetti delle metodologie OOP ed infine i tipi di problematiche
che si incontreranno quando la propria azienda comincerà ad usare la
OOP ed il C++.
La OOP ed il C++ possono
non essere per tutti. È importante valutare i propri bisogni e decidere
se il C++ soddisferà in maniera ottimale quelle necessità o
se è migliore un altro sistema di programmazione ( incluso quello che
si sta utilizzando correntemente). Se si sa che le proprie necessità
saranno molto specializzate per il fututo e si hanno dei vincoli specifici
che potrebbero non essere soddisfatti dal C++, allora bisogno investigare
su possibili alternative[24].
Anche se alla fine si sceglie il C++ come linguaggio, si capiranno almeno
quali sono le opzioni e si avrà una chiara visione del perchè
si è presa quella direzione.
Si sa come appare un programma procedurale: definizioni
di dati e chiamate a funzioni. Per cercare il significato di tale programma
si deve lavorare un pò, guardando tra chiamate a funzioni e concetti
a basso livello per creare un modello nella propria mente. Questa è
la ragione per cui abbiamo bisogno di una rappresentazione intermedia quando
si progettano programmi procedurali. Di per se, questi programmi tendono a
confondere perchè i termini delle espressioni sono orientati più
verso il computer che verso il problema che si sta risolvendo.
Poiche il C++ aggiunge molti concetti nuovi al linguaggio C, l'assunzione naturale può essere che il main() in un programma C++ sarà molto più complicato di un equivalente programma C. Qui si rimarrà piacevolmente sorpresi: un programma C++ ben scritto è generalmente molto più semplice e più facile da capire di un equivalente programma C. Ciò che si vedrà sono le definizioni degli oggetti che rappresentano i concetti nel nostro spazio del problema ( invece che concetti relativi ad aspetti del computer) e i messaggi mandati a quegli oggetti per rappresentare le attività in quello spazio. Una delle delizie della programmazione orientata agli oggetti è che, con un programma ben progettato, è facile capire il codice leggendolo. Di solito c'è molto meno codice, poichè molti dei problemi saranno risolti riutilizzando il codice delle librerie esistenti.
[4] Si veda Multiparadigm
Programming in Leda di Timothy Budd (Addison-Wesley 1995).
[5] Si può
trovare un'implementazione interessante di questo problema nel Volume 2 di
questo libro, disponibile su www.BruceEckel.com.
[6] qualcuno fa
distinzione, indicando che tipo determina
l'interfaccia mentre classe è una particolare implementazione di quella
interfaccia.
[7] Sono in debito
con il mio amico Scott Meyers per questo termine.
[8] Di solito
la maggior parte dei diagrammi ha già abbastanza dettagli, quindi non
si avrà bisogno di specificare se si utilizza laggregazione o
la composizione.
[9] Un eccellente
esempio di ciò è UML Distilled, by Martin Fowler (Addison-Wesley
2000), che riduce il processo UML, spesso opprimente, ad un maneggevole sottoinsieme.
[10] La mia
regola del pollice per stimare tali progetti è: se c'è più
di un fattore jolly, non provo neache a cercare di pianificare quanto tempo
ci vorrà o quanto costerà fino a che non si ha un prototipo
funzionante. Ci sono troppi gradi di libertà.
[11] Grazie
per l'aiuto di James H Jarrett.
[12] Maggiori
informazioni sugli use case possono essere trovate in Applying Use Cases
di Schneider & Winters (Addison-Wesley 1998) e Use Case Driven Object
Modeling with UML di Rosenberg (Addison-Wesley 1999).
[13] La mia
personale opinione su ciò è cambiata in seguito. Duplicare ed
aggiungere il 10 percento darà una accurata stima ( assumento che non
ci sono molti fattori jolly), ma si deve lavorare ancora abbastanza diligentemente
per finire in quel tempo. Se si vuole tempo per renderlo elegante ed apprezzarsi
del processo, il corretto moltipicatore è tre o quattro, io credo.
[14] Per chi
inizia, raccomando il predetto UML Distilled.
[15] Python
(www.Python.org) viene spesso usato come un "pseudo codice eseguibile
".
[16] Almeno
un aspetto dell'evoluzione è discusso nel libro di Martin Fowler Refactoring:
improving the design of existing code (Addison-Wesley 1999). Questi libro
utilizza esclusivamente esempi in Java.
[17] Questo
termine è esplorato nel capitolo Design Patterns del Volume
2.
[18] Ciò
è tipo la "prototipazione rapida" dove si suppone di costruire
una versione rapida e sporca in modo da imparare qualcosa del sistema e poi
gettare via il prototipo e costruirlo esatto. Il problema della prototipazione
rapida è che la gente non getta via il prototipo, ma ci costruisce
sopra. Combinata con la mancanza di struttura della programmazione procedurale,
ciò spesso produce sistemi disordinati che sono costosi da manuntenere.
[19] Sebbene
ciò possa essere una prospettiva più Americana, le storie di
Hollywood arrivano ovunque.
[20] Incluso
(specialemente) il sistema PA. Una volta ho lavorato in una azienda che insisteva
nel diffondere ogni telefonata che arrivava ad ogni executive ed essa interrompeva
costantemente la nostra produttività ( ma i manager non concepivano
di soffocare una cosa così importante come il PA). Alla fine, quando
nessuno guardava ho cominciato a tagliare i cavi delle casse.
[21] Dico posso
perchè, a causa della complessità del C++, potrebbe convenire
passare a Java. Ma la decisione di quale linguaggio scegliere ha molti fattori
e si assume che si è scelto il C++.
[22] Tuttavia,
si vedano gli articoli di Dan Saks nel C/C++ User's Journal per importanti
inestigazioni sulle performance della libreria C++ .
[23]A causa di miglioramenti della produttività, Java dovrebbe essere preso in considerazione a questo punto.
[24] In particolare,
si raccomanda di vedere Java (http://java.sun.com) e Python (http://www.Python.org).
[ Capitolo Precedente ] [ Indice generale ] [ Indice analitico ] [ Prossimo Capitolo ]