programmazione object-oriented per utenti di...
TRANSCRIPT
Programmazione Object-Oriented
per utenti di CMSSW
Mini-corso, Padova, 22/04/2010
Massimo Nespolo
1
Riassunto:
• Refactoring:
– Come far evolvere il codice.
– In che direzione andare.
– A che livello agire.
• Design pattern (GoF):
– Che cosa sono.
– Perché parlarne.
– Qualche esempio.
Refactoring e design pattern
2
I principi di design
3
Linee guida principali (SOLID) che permettono di
evitare un cattivo design (rigido, fragile, immobile)
Bisogna però bilanciare sempre vantaggi e svantaggi:
aggiungono complessità, e vanno usati dove serve flessibiltà
1. S - SRP: Single responsibility principle.
2. O - OCP: Open/closed principle.
3. L - LSP: Liskov substitution principle.
4. I - ISP: Interface segregation principle.
5. D - DIP: Dependency inversion principle.
Refactoring
4
Il codice “brutto” non va
spiegato mediante commenti:
va riscritto. I commenti sono
per le intenzioni!
Modifica della struttura interna
di un programma eseguita
senza cambiare il
comportamento funzionale
Se non riusciamo ad ottenere quanto descritto in precedenza
al primo colpo, conviene fermarsi e rimettere mano al codice
Le nostre idee devono sempre essere
espresse mediante il linguaggio
A volte il codice “puzza”
5
Si chiamano “puzze” (smells) i segnali
che un codice è di bassa qualità
1. Duplicate code:
Effetto del copia-incolla, ma bisogna fare una analisi “globale”.
2. Long method e long parameter list:
Codice troppo specializzato, difficile da leggere e riusare.
3. Primitive obsession:
Non lavoriamo al giusto livello di astrazione.
4. Large class, lazy class, data class, divergent change:
Classi troppo ricche, troppo povere, o troppo “mescolate”.
5. Switch statement, conditional complexity e shotgun surgery:
Diventano rapidamente ingestibili (indice di Mc Cabe).
Schema di massima
6
Refactoring può coinvolgere diversi livelli (dal piccolo al grande):
1. Impacchettare adeguatamente il codice in funzioni.
2. Rendere più semplici le chiamate ai metodi.
3. Spostare variabili e metodi da una classe all’altra.
4. Organizzare i dati (cont. diretto/indiretto, type code, …).
5. Semplificare le espressioni condizionali (decomposizione).
6. Gestire le generalizzazioni (gerarchie di derivazione, interfacce).
7. Cambiare l’organizzazione del programma.
Se succede (per imperizia,
cambiamento delle condizioni
esterne, …) si rifattorizza
Evoluzione/ripulitura del codice
fatta in modo disciplinato per
minimizzare il rischio di bug
Nomi comunicativi
7
I nomi di variabili e funzioni
vanno legati al problema
Effettuiamo un rename
(semplice ma efficace)
Usiamo sostantivi per le variabili (cose),
e verbi per le funzioni (azioni)
Spesso da qui partono
refactoring più complessi
Emergono relazioni tra variabili
e funzioni prima nascoste
Il nome di una funzione è
formato da 2 verbi. È giusto?
A livello di funzioni
8
Con pochi parametri (7±2),
allo stesso livello di astrazione
Non è banale scrivere funzioni “belle”,
ossia facili da leggere e da riutilizzare
Funzioni corte
(1, massimo 2 schermate)
Questa è la strategia fondamentale per
rimuovere il codice duplicato:
mettiamolo in una funzione separata.
Catene di operazioni booleane
tra variabili dentro un if
Estraggo tutto dentro una
funzione con nome adatto
Extract class/subclass
9
Spesso ci si accorge che le funzioni di un gruppo si
passano sempre gli stessi parametri: eliminiamoli!
Nuova classe con quelle
funzioni e quelle variabili
Se siamo già in una classe,
estraiamo una sotto-classe
Partiamo quindi con classi
semplici, ed aggiungiamo
funzionalità nuove
Se la classe diventa troppo
complessa, rifattorizziamo
estraendone una parte
Usiamo bene le gerarchie
10
Una sottoclasse “è-una” classe base,
ma in C++ eredita anche dati e funzioni
Se c’è del codice duplicato
nelle classi derivate
Sistemiamo i nomi, e spostiamo
tutto nella classe base
Se qualcosa nella classe base è
usato solo da poche derivate
Spostiamo quello che non è
comune verso il basso
EreditarietàContenimento
Qualche volta, si può fare lo scambio
Un caso dubbio…
12
Uno studente “è-una” persona Ereditarietà
E per chi studia e lavora? Eredito da
ambedue (ereditarietà multipla, ed
in C++ deve essere virtuale).
Il lavoratore “è-una” persona Ereditarietà
Ma queste relazioni non
possono più cambiare
(l’ereditarietà è statica)
virtual virtual
Soluzione
13
Stavamo confondendo essere ed avere!
Una persona ha un’attività, che
può cambiare nel tempo
Contenimento mediante
“classi ruolo” (astratte)
Il contenimento è una relazione
dinamica, che può cambiare
Persona non dipende da
Lavoro o Studio (DIP)
Contenimento
Ereditarietà
Extract interface
14
Ma attività è una classe astratta, che deve/può
avere varie realizzazioni concrete
Se abbiamo già le classi concrete, estraiamo
un’interfaccia unica come generalizzazione
Facilità di lettura
(compiti separati)
DIP
(dettagli isolati)
Hot-spot
(possiamo estendere)
Switch e condizioni sparpagliate
15
Gli switch (if-else) sono la
prima fonte di complessità
Nascono innocenti,
ma crescono male
Ancora peggio se la catena
si ripresenta in più punti
Devo fare modifiche in
parallelo (shotgun surgery)
Nuovi comportamenti
aggiungono nuove clausole
Da qui originano difficoltà
di lettura, bachi, rigidità
Polimorfismo (ancora)
16
Lavoratore lavoratore;
Studente studente;
if ( adessoSiamoInEstate ) {
lavoratore.lavora();
} else {
studente.studia();
}
Persona* p = new Persona;
p->setStatus(Persona::Studente);
p->FaiQuelCheDevi() ;
In un programma ad oggetti,
non dovrebbero esserci if-else
Utilizziamo interfacce e
polimorfismo (late binding)
Il polimorfismo è il sostituto ad oggetti dello switch
Dagli switch ai pattern
17
La logica condizionale (switch)
può avere diversi effetti sugli oggetti
Algoritmi diversi
per un problemaStrategy
Cambio di comportamento
a seconda dello statoState
Modifica di alcuni passi
in una proceduraTemplate method
Creazione di oggetti diversi
ma correlati Factory method
Design pattern (GoF, 1994)
18
L’idea viene dall’architettura
(Christopher Alexander)
Soluzione tipica, provata sul
campo, a problemi ricorrenti
L’idea viene portata dentro l’ingegneria del software dalla
banda dei quattro (Erich Gamma, Richard Helm, Ralph
Johnson and John Vlissides) con il libro Design Patterns:
Elements of Reusable Object-Oriented Software
Riflessione critica su
ereditarietà e contenimento
Catalogo dei pattern
divisi in tre categorie
Perché parlarne ora?
19
Noi discutiamo i design pattern
(non i pattern architetturali)
Risolvono problemi su scala
“medio-piccola” (di classi)
Strutture consolidate
dall’esperienza
Idee per il codice nuovo,
ma anche per il refactoring
Forniscono un linguaggio di
alto livello molto diffuso
Aiutano a capire la struttura e la
documentazione del software
Enorme valenza culturale per chiunque scriva software,
oppure debba interagire con grandi sistemi (CMSSW)
Struttura di un pattern
20
Ci sono vari modi per
implementare un certo pattern
È l’idea che lo definisce,
non il diagramma delle classi!
1. Nome (breve e comunicativo):
Alza il livello della discussione e della documentazione.
2. Il problema da risolvere:
Sintomi di un design rigido, condizioni da rispettare.
3. La soluzione:
Costituenti, relazioni, responsabilità e collaborazioni.
4. Le conseguenze:
Bilancio costi/benefici, impatto sul riuso, implementazioni.
Un pattern cattura la “saggezza” accumulata
L’introduzione vale il libro
21
2. Program to an interface, not an implementation
Non appoggiarsi al fatto che la classe
derivata eredita tutto il codice ed i dati
1. Separate things that change from things that stay the same
Posizionare interfacce tra la parte
stabile e quella che cambia (OCP)
3. Favor object composition over class inheritance
L’ereditarietà è statica, ed espone più
dettagli del semplice contenimento
Il catalogo (23 pattern)
22
Creazionali
Come posso creare gli oggetti
concreti se conosco solo le
interfacce astratte?
Strutturali
Come conviene combinare
classi (ereditando) ed
oggetti (componendo)?
Comportamentali
Come far interagire gli oggetti,
distribuendo bene le
responsabilità tra di essi?
Strategy (policy)
23
Lo stesso compito può essere
eseguito in modi diversi
Dobbiamo poter sostituire
gli algoritmi, anche a runtime
Context contiene un puntatore di
tipo Strategy (interfaccia)
Classi concrete con
i vari algoritmi
Ricostruzione delle tracce
24
const OrderedSeedingHits& triplets = theGenerator->run(region,ev,es);
unsigned int nTriplets = triplets.size();
for (unsigned int iTriplet = 0; iTriplet < nTriplets; ++iTriplet) {
const SeedingHitSet& triplet = triplets[iTriplet];
std::vector<const TrackingRecHit *> hits;
for (unsigned int iHit = 0, nHits = triplet.size(); iHit < nHits; ++iHit) {
hits.push_back( triplet[iHit]->hit() );
}
reco::Track* track = theFitter->run(es, hits, region);
if( theFilter && !(*theFilter)(track, hits) ) {
delete track;
continue;
}
tracks.push_back(TrackWithTTRHs(track, triplet));
}
theGenerator->clear();
theGenerator,
theFitter e theFilter
sono puntatori di
tipo classe astratta
Adapter (wrapper)
25
A volte un oggetto non ha
il “guscio giusto”
Ereditiamo dall’interfaccia
corretta, ed incapsuliamo
Interfaccia corretta
Ereditarietà privata
o contenimento
Adattatore
(classe o oggetto)
Classe vecchia che
vogliamo riciclare
Request gira
il messaggio
Decorator
26
Voglio fare le stesse cose,
ma con un passo in più
Eredito, e sposto le decorazioni
in una classe separata
Il decorator (astratto) eredita da
component, e contiene un
puntatore a component (astratto)
In Operation(), chiamo la
Operation() del decorator, e poi
aggiungo gli abbellimenti
Template method
27
Sequenza fissa di operazioni,
ma alcuni passi variano
Isoliamo lo scheletro, e
ridefiniamo i singoli passi
Classe base:
TemplateMethod
(concreto) definisce
la sequenza virtual or
pure virtual
Classe derivata:
sovrascrive i
singoli passi
Template method: un esempio
28
void VirtualJetProducer::produce(edm::Event& iEvent,
const edm::EventSetup& iSetup)
{
edm::Handle<reco::CandidateView> inputsHandle;
iEvent.getByLabel(src_, inputsHandle);
for(size_t i=0; i<inputsHandle->size(); ++i) {
inputs_.push_back( inputsHandle->ptrAt(i) );
}
…
fjInputs_.reserve( inputs_.size() );
inputTowers();
…
runAlgorithm( iEvent, iSetup );
…
output( iEvent, iSetup );
}
Virtuale pura: passo della
procedura che può (deve)
cambiare nelle classi derivate
Template
Method
Iterator
29
Devo accedere agli elementi di
un contenitore in sequenza
Voglio essere indipendente
dalla struttura del contenitore
Inizio, fine,
elemento corrente,
prossimo elemento
Nelle STL, disaccoppio
contenitori ed algoritmi
Dipendenze di creazione
30
Il nostro codice dipende solo
dalle interfacce (astratte)
Prima o poi, dobbiamo creare le
istanze di classi concrete
?
void BaseClass::run()
{
IOStrategy* s = new IOFromFile();
std::vector<double> ptVec;
s->read( ptVec );
…
s->write( ptVec );
}
Qui siamo isolati dal modo
in cui leggiamo/scriviamo
il vettore (estensibilità)
Anche se implemento nuove
strategie, e/o derivo da BaseClass,
viene creata sempre IOFromFile
Factory method
31
Elimino le chiamate dirette a
new (non ridefinibile)
Sposto il new dentro una
funzione che crei gli oggetti
Creo i sotto-oggetti
mediante una
funzione virtual
Posso ridefinire la
funzione, e creare
oggetti di tipo nuovo
Sintesi della quarta puntata
32
• Refactoring:
– Come far evolvere il codice.
– In che direzione andare.
– A che livello agire.
• Design pattern (GoF):
– Che cosa sono.
– Perché parlarne.
– Qualche esempio.
Pensieri finali
33
Non si diventa esperti di OOP
in 7-8 ore di lezione
Abbiamo toccato e discusso
gli aspetti fondamentali
Tuttavia…
Di strada insieme ne abbiamo fatta parecchia:
dal C alle classi, all’ereditarietà, al polimorfismo, ai pattern
Quanto detto dovrebbe servire
sia ad inquadrare la filosofia,
sia come linea guida pratica
Volutamente abbiamo mischiato
teoria e codice, concetti di base
e problemi sofisticati
Conclusione
34
Bjarne Stroustrup. The C++ Programming Language, pp. 692 :
Design and programming are human activities; forget that and all is lost.
Il codice deve comunicare
subito che cosa fa
Un codice ben strutturato abbrevia decisamente
il cammino verso la pubblicazione!
Il sistema deve poter essere
esteso facilmente