Estensioni
Estensioni pure
• Un’estensione C' di una classe C è un erede di C
• Un’estensione è detta pura se non modifica la specifica dei
metodi ereditati
• Un’estensione pura può quindi solo estendere la specifica,
aggiungendo nuove operazioni...
• ...tuttavia può modificare (a parità di specifica)
l’implementazione dei metodi ereditati
2
Estensione pura MaxIntSet: Specifica
• Come IntSet, ma ha metodo per estrarre il massimo elemento.
Quindi specifica di tutti gli altri metodi è inalterata.
public class MaxIntSet extends IntSet {
// OVERVIEW: MaxIntSet è sottotipo di IntSet con metodo max per
//estrarre il massimo elemento
//costruttori:
public MaxIntSet(){
//@ensures (*inizializza this come il MaxIntSet vuoto*)
}
//metodi:
public int /* pure */ max() throws EmptyException {
//@ ensures \result>= (\max int i; this.isIn(i);i);
//@signals(EmptyException) this.size() == 0;
}
La specifica di MaxIntSet assume come
data la specifica di IntSet
Aggiunge solo ciò che è nuovo (max() e costruttore)
3
MaxIntSet: Implementazione
• Per migliorare efficienza, si memorizza il max elemento => occorre reimplentare
tutti i metodi modificatori, anche se specifica non è cambiata! (insert e remove)—di
fatto, cambia RI.
public class MaxIntSet extends IntSet {
private int biggest;//memorizza il max. Non definito per this=vuoto.
public MaxIntSet(){ super(); }
public void insert(int x){
overriding
if (size() == 0 || x>biggest) biggest = x;
super.insert(x); //chiama metodo insert di IntSet
}
public void remove(int x){
super.remove(x);
if(size()==0||x<biggest) return;
Iterator g=elements();
biggest=((Integer) g.next()).intValue();
while(g.hasNext() {
int z=((Integer) g.next()).intValue();
if(z>biggest) biggest=z;}
public int max() throws EmptyException {
if (size() == 0) throw new EmptyException(“MaxIntSet.max);
return biggest;}
}
ridondante
(ridefinizione)
4
AF e RI?
• AF di sottoclasse di solito è uguale a quella della sovraclasse
– AF di MaxIntSet è identica a AF di IntSet: MaxIntSet utilizza ancora IntSet per memorizzare gli
elementi.
// la funzione di astrazione è:
//AF_MaxIntSet(c) = AF_IntSet(c)
• Il rep invariant della sottoclasse non ha bisogno di riportare il rep invariant della sovraclasse
(perché questo è comunque ereditato senza possibilità di modifca. In JML si scrive “also” per
dire che il nuovo RI viene “aggiunto” a quello ereditato.
// il rep invariant RI_MaxIntSet(c) è:
//@ also
//@ private invariant this.size>0 ==>
//@
(this.isIn(this.biggest) &&
//@
(\forall int x; this.isIn(x); x <= this.biggest));
RI è quindi sempre vero quando this è l’insieme è vuoto. In tal caso, biggest può assumere un
valore qualunque.
NB: Se serve, si possono usare parti pubbliche della sovraclasse per definire RI. Infatti le parti
private della superclasse non si possono utilizzare nell’invariante. Quindi RI di una
sottoclasse può usare: gli attributi privati della classe, gli attributi protected ereditati, i metodi
puri pubblici ereditati
Es. solo se els fosse protected si potrebbe scrivere al posto dell’ultima riga:
(\forall int i; 0<= i && i<this.size(); els.get(i) <= this.biggest)
5
Un caso particolare di estensioni pure:
implementazioni multiple
Quando usare classi astratte e interfacce?
Attributi protected
Poly densi e sparsi
• DensePoly: una implementazione di Poly adatta al caso in cui ci sono pochi coefficienti nulli
(ad es. quella vista per i Poly con un array per tutti i coefficienti);
• SparsePoly: un’altra implementazione che è efficiente quando molti coefficienti sono nulli
(es. lista a puntatori, in cui ogni nodo memorizza il coeff. e il grado di ogni termine!=0).
• Se usiamo questa soluzione:
DensePoly
SparsePoly
• In DensePoly il rep include un array! Questo è ereditato da SparsePoly! Potremmo ignorarlo e
dichiarare comunque una lista, ma l’instance variable terms sarebbe comunque presente:
– Occupa memoria inutile.
– potremmo dimenticare di ridefinire qualche operazione che usa il rep dei DensePoly (non ce ne
possiamo accorgere a compile time!)
7
Implementazioni multiple
• Soluzione: definire una classe Poly in modo che non memorizzi elementi:
saranno sottoclassi a stabilire modalità di memorizzazione
• Poly deve diventare astratta: non è possibile fare add, ecc. senza il rep
Gerarchia di tipi può essere utilizzata per fornire più implementazioni dello
stesso tipo
– DensePoly e SparsePoly possono essere definiti come sottotipi di Poly.
Poly
DensePoly
SparsePoly
• Il tipo da implementare è di solito descritto con interfaccia (se nessuna
operazione è implementabile) o classe astratta (se alcune operazioni sono
implementabili)
8
Il tipo Poly
public abstract class Poly {
//@ ensures (* \result == grado del polinomio *);
public abstract int degree(){}
//@ ensures (* \result == coeff. del termine di grado d *);
public abstract int coeff(int d){}
//@ ensures (* \result == this + q *);
//@ signals (NullPointerException e) q == null;
public abstract Poly add(Poly q) throws NullPointerException {}
//@ ensures (* \result == this - q *);
//@ signals (NullPointerException e) q == null;
public abstract Poly sub(Poly q) throws NullPointerException {}
//@ ensures (* \result == this * q *);
//@ signals (NullPointerException e) q == null;
public abstract Poly mul(Poly q) throws NullPointerException {}
//@ ensures (* \result == -this *);
//@ signals (NullPointerException e) q == null;
public abstract Poly minus() {}
//@ ensures (* \result == iteratore su tutti gli elementi di this in ordine dal coeff di grado 0 al coeff di
grado degree() *);
public Iterator terms() {}
Utilizzatore di Poly “vede” solo i metodi definiti in Poly.
//@ ensures (* \result == derivata di p *);
public static Poly derivata(Poly p)
Non importa se p sarà un DensePoly o uno SparsePoly.
9
Protected
• La Poly potrebbe definire almeno una parte del rep, che dia il
grado del polinomio. Questa è un’informazione che deve
essere presente in tutte le implementazioni di Poly.
– In questo modo si può implementare anche la degree().
• Se è private, non è usabile nelle sottoclassi, se è public espone
il rep. Esiste un altro modificatore:
protected = visibile alle sottoclassi (e a classi nello stesso
package)
public abstract class Poly {
protected int deg;
//@ ensures (* \result == grado del polinomio *);
public int degree(){return deg;}
10
Riassunto Accesso ed ereditarietà
•
Attributi e metodi di una classe possono essere:
– public
• sono visibili a tutti
• vengono ereditati
– private
• sono visibili solo all’interno della stessa classe
• non sono visibili nelle sottoclassi, ma sono ereditati
– protected
• sono visibili solo alle sottoclassi e alle classi nello stesso package
• vengono ereditati
– “friendly”:
• sono visibili a tutte le classi nello stesso package
• vengono ereditati, ma sono visibili alle sottoclassi che stanno nella
stesso package della classe
11
Quando usare protected?
• Membri protected dovrebbero essere evitati se non necessari:
– senza di loro classe può essere reimplementata senza disturbare l’
implem. delle sottoclassi
– sono visibili nello stesso package, e quindi altro codice potrebbe
interferire con la loro implementazione
• Si introducono per consentire implementazioni efficienti per le
sottoclassi
– Primo modo: protected instance variables
– Secondo modo (da preferire): private instance variables ma accesso
tramite metodi protected. Utile per
• consentire a sovraclasse di mantenere invariante
• mantenersi la possibilità di cambiare il rep, ridefinendo
opportunamente i metodi protected
12
Implementazione parziale di Poly
• Il campo deg è presente in tutti i Poly: perche’ non inizializzarlo in Poly?
Inizializzazione va fatta dai costruttori e la classe Poly è abstract!
• Ma si può comunque definire un costruttore!
public abstract class Poly {
protected int deg;
//costruttori
//@ ensures (* inizializza this a grado n *);
protected Poly(int n){}
//è protected perché non ha senso definire il Poly di grado n: serve solo per
//inizializzare un pezzo del rep. Se fosse public, le sottoclassi di Poly dovrebbero
//ridefinirlo. Cosi’ lo chiamano con super(n);
//@ ensures (* \result == grado del polinomio *);
public int degree(){return deg;}
…//qui tutti gli altri metodi di Poly, astratti
}
13
Implementazione con DensePoly
public class DensePoly extends Poly{
private int[] trms; //array per memorizzare i coeff.
//costruttori
//@ ensures this.degree() == 0 && this.coeff.(0)==0 ;
public DensePoly(){
super (0); trms=new int[1];}
public DensePoly(int c, int n) throws NegativeExponentException {...}
public poly add(Poly q) { //implementa la abstract add di Poly
if (q instanceof SparsePoly) {...}
//la def. di add tra un DensePoly e uno SparsePoly è ad hoc
else {...} //codifica di add di due DensePoly
...
}
• La SparsePoly è analoga ma usa una lista...
14
Combinare Densi e Sparsi
• polinomi sono dichiarati tutti di tipo Poly: poi si chiama il costruttore di
DensePoly o di SparsePoly a seconda dellla definizione preferita.
Poly p1 = new DensePoly();
Poly p2 = new SparsePoly();
….//qui inseriamo monomi in p1 e p2
Poly p3 = p1.add(p2);
La add è definita come Poly add(Poly)!
• La add di due Poly può essere un DensePoly o uno SparsePoly
– ad es. sommando due SparsePoly si potrebbe ottenere un polinomio con pochi
zeri intermedi: sarebbe più efficiente rappresentarlo con un DensePoly; somma
di un DensePoly e di uno SparsePoly potrebbe a volte essere sparsa, ecc..
• Conviene che add, mult ecc (Produttori) convertano “automaticamente” da
una impl. all’altra, per sfruttare rappresentazione più efficiente!
15
Conversione fra DensePoly e SparsePoly
• Usare un costruttore (“di conversione”):
– Es. in classe SparsePoly mettere costruttore:
//@ ensures (* this come il DensePoly implem. da trms *);
SparsePoly(int []trms)
– il costruttore può essere chiamato da DensePoly per costruire uno SparsePoly
(es. quando la add si accorge che in realtà ci sono molti coeff. nulli...)
– public o protected? public non va bene (utenti potrebbero accedere a rep!),
protected nemmeno (DensePoly non è sottoclasse di SparsePoly)
– usare “friendly”: visibile all’interno dello stesso package
– Però DensePoly e SparsePoly devono essere in stesso package (e
l’implementazione di ciascuna dipende dall’implementazione dell’altra)
– anche un costruttore SparsePoly(DensePoly p) può essere definito per realizzare la
conversione, ma poi può essere poco efficiente (SparsePoly non può accedere a
implementazione di DensePoly...). E’ soluzione migliore dal punto di vista della
manutenzione...
16
Progettare a oggetti
Estensioni con ridefinizione di metodi
Open-Closed Principle
• (dovuto a B. Meyer)
• “Software entities should be open for extension, but closed for
modification”
• "open for extension“ = comportamento dei moduli può essere
modificato per soddisfare cambiamenti nei requisiti
• “closed for modification” = i moduli non possono essere
modificati
• Sembra una contraddizione. Come e’ possibile?
• Il vantaggio in manutenzione è ovvio: se non modifico, non
faccio errori
• Esaminiamo un esempio di progetto tradizionale e progetto a
oggetti per capire che cosa significa...
18
Esempio: Progetto tradizionale in C
typedef struct …Figura;
Figura
Cerchio
Rettangolo
Figura figure[100];
Triangolo
void disegnaTutto(Figura *figure) {
for (i= 0; i<100;i++) {
if (figure[i] è “rettangolo”)
“disegna rettangolo”
if (figure[i] è “triangolo”)
“disegna triangolo”
if (figure[i] è “cerchio”)
“disegna cerchio”
}
}
figure[1] = “rettangolo”;
figure[2] = “triangolo”;
figure[3] = “cerchio”;
Estendere progetto tradizionale: arriva il
Trapezio
Figura
Cerchio
Rettangolo
Triangolo
typedef struct …Figura;
Figura figure[100];
Trapezio
void disegnaTutto(Figura *figure) {
for (i= 0; i<100;i++) {
if (figure[i] è “rettangolo”)
“disegna rettangolo”
if (figure[i] è “triangolo”)
“disegna triangolo”
if (figure[i] è “cerchio”)
“disegna cerchio”
if (figure[i] è “trapezio”)
“disegna trapezio
}}
figure[1] = “rettangolo”;
figure[2] = “triangolo”;
figure[3] = “cerchio”;
figure[4] = “trapezio”
Codice già definito per
disegnaTutto deve cambiare
per potere supportare il
nuovo tipo!!!
Open/closed?
• Quindi progettazione tradizionale non verifica open-closed:
• Il modulo disegnaTutto è aperto a estensioni, ma solo tramite
modifica: non è chiuso rispetto alle modifiche.
• Come si puo’ fare a rispettare open/closed?
21
Es. in versione OO
//si definisce classe astratta Figura, con
metodo astratto disegna(), e sue eredi
Rettangolo, Cerchio, Triangolo che
implementano disegna()
Figura
Figura figure = new Figura[100];
Cerchio
Rettangolo
Triangolo
figure[1] = new Rettangolo();
figure[2] = new Triangolo();
figure[3] = new Cerchio();
…
….disegnaTutto(figure);
public static void disegnaTutto(Figura [] figure) {
for (i= 0; i<100;i++)
figure[i].disegna();
}
Quantità di codice è più o meno la stessa di prima ...
Estendibilità: arriva il Trapezio
Figura figure = new Figura[100];
Figura
Cerchio
Rettangolo
Triangolo
Trapezio
figure[1] = new Rettangolo();
figure[2] = new Triangolo();
figure[3] = new Cerchio();
figure[4] = new Trapezio();
…disegnaTutto(figure);
public static void disegnaTutto(Figura [] figure) {
for (i= 0; i<100;i++)
figure[i].disegna();
}
Codice definito
per Figura non
cambia
Open-closed
• Nei linguaggi a oggetti, è possibile definire astrazioni, tramite
classi (tipicamente classi astratte o interfacce), da richiamare
poi in altri moduli.
• Un modulo è chiuso rispetto alla modifica perché la
definizione delle astrazioni non cambia
– Ad esempio disegnaTutto() è definita in base alla classe astratta Figura.
disegnaTutto non deve più essere modificata.
• Le classi però possono essere estese, tramite sottoclassi: ciò
rende un modulo aperto per l'estensione:
– definire una nuova classe dalle astrazioni può modificare il
comportamento di un modulo.
– Ad esempio disegnaTutto() diventa in grado di disegnare anche trapezi,
ecc.
24
Estendibile se….
• Estendibilità tramite ereditarietà è garantita solo se possiamo
usare oggetti di una classe che sono “sostituibili” a quelli della
sopraclasse.
• Ad esempio, la disegna() deve contenere codice che disegna
l’oggetto this sullo schermo. Ogni sottoclasse di Figura deve
implementare una disegna() che abbia lo stesso effetto!
• Come fare a garantirlo?
25
Specifica di estensioni
Modificano specifica di sovraclassi
Principio di sostituzione di Liskov
• “gli oggetti di una sottoclasse devono potere essere
sostituiti ovunque siano usati oggetti della sopraclasse,
senza influenzare il comportamento del codice che li usa”
• In altre parole:
• “moduli che usano oggetti di un tipo devono potere usare
oggetti di un tipo derivato senza accorgersi della
differenza”
• Significa che il comportamento della sottoclasse deve
essere “compatibile” con la specifica della sopraclasse
27
Esempio 1
public static int traccia(IntSet x) {
int somma;
for (Iterator i = x.elements(); i.hasNext();)
somma+=((Integer) x.next()).intValue();
return somma;
}
• In una sottoclasse SortedIntSet di IntSet ridefiniamo l’iteratore elements():
public class SortedIntSet extends IntSet {
//@ ensures (* \result == un generatore che produce tutti gli elementi di
//@ this come Integers, ciascuno una sola volta, in ordine crescente *);
public Iterator elements ()
}
• traccia(s ); non sarà “sorpreso” se s è un SortedIntSet: l’iteratore restituisce
comunque tutti gli elementi anche se in un ordine particolare
28
Esempio 2
• Se invece elements() fosse:
//@ ensures (* \result == un generatore che produce tutti gli elementi
//@ dispari di this, come Integers, ciascuno una sola volta *);
public Iterator elements ()
• La traccia(s) non calcola più la traccia dell’insieme! l’iteratore
non è compatibile con quello di IntSet()—non restituisce gli
elementi di valore pari.
• Comportamento scorretto
29
Sottoclassi e sottotipi
• Un tipo S è un sottotipo di un tipo T se S è usabile in ogni contesto
in cui è usabile T.
• Una classe S è una sottoclasse di una classe T se S è dichiarata come
estensione di T: extends T
– La sottoclasse S eredita tutta l’implementazione (attributi e metodi) della
sopraclasse T
• Quindi principio di sostituzione equivale a dire che relazione di
eredtarietà deve essere relazione di sottotipo
• In generale (sotto)classe e (sotto)tipo sono nozioni distinte
– coincidono se viene adottato approccio disciplinato a ereditarieta`
– altrimenti sottoclassi semanticamente svincolate da superclassi; non
rappresentano sottotipi ma tipi diversi costruiti incrementalmente
– Su [Liskov] (sotto)classe e (sotto)tipo usati come sinonimi perche` si
promuove/impone approccio disciplinato
– su [Ghezzi&Jazayeri] (sotto)classe e (sotto)tipo nozioni distinte e si
analizzano condizioni perché coincidano
30
Regole per la specifica di sottotipi
• Principio di sostituzione impone vincolo sulla specifica del
sottotipo: deve essere “compatibile” con specifica del
sopratipo (equivalenza comportamentale (behavioral) tra
esemplari della sovraclasse e della sottoclasse).
– Poi basta che implementazione sia corretta rispetto a specifica
• Tre proprietà della specifica dei tipi per compatibilità:
– Regola della segnatura: un sottotipo deve avere tutti i metodi del
sopratipo, e segnature dei metodi del sottotipo devono essere
compatibili.
– Regola dei metodi: le chiamate ai metodi del sottotipo devono
comportarsi come le chiamate ai metodi corrispondenti del sopratipo.
– Regola delle proprietà: sottotipo deve preservare tutti i public invariant
degli oggetti del sovratipo.
31
Regola della segnatura
• Garantisce che ogni chiamata corretta (senza errori di tipo) per il sovratipo
sia corretta anche per il sottotipo, e ciò deve essere verificabile
staticamente
• In Java, la regola delle segnatura diventa:
– un sottotipo deve avere tutti i metodi del sovratipo, e le segnature dei metodi
del sottotipo devono essere identiche alle corrispondenti del sovratipo
• però un metodo del sottotipo può avere meno eccezioni nella segnatura (il
codice che fa uso dell’astrazione, scritto con riferimento al metodo del
sovratipo, funziona anche se alcune delle eccezioni non vengono lanciate
dai metodi del sottotipo)
• In realtà la regola della segnatura in Java è inutilmente restrittiva...
• (in alcuni linguaggi OO si può violare addirittura regola delle segnature: un
metodo della sovraclasse può essere eliminato nella sottoclasse).
32
Controvarianza e Covarianza (1)
A
f: B → C
Esempio (stilizzato)
Classe A' derivata da classe A
A’
f : B’ → C’
Classe A ha metodo f: B → C
In A' ridefinizione di metodo f : B'
→C'
33
Controvarianza dei parametri
• Perché A’ sia sostituibile ad A (dal punto di vista della tipizzazione delle
chiamate del metodo f) basterebbe che
• Gli argomenti in ingresso di f siano tutti accettabili per f ’, cioè B’sia un
sovratipo di B (controvarianza dei parametri in input)
⇒ una chiamata della f ’ di un esemplare di A’ accetta in ingresso qualsiasi valore
accettato da una qualsiasi chiamata della f su un esemplare di A
Esempio:
public static void usaA(A a0) {
B b0;
...
C c0 = a0.f(b0); ...
}
• Chiamata di f con argomento di tipo B è valida se f prevede un argomento di
tipo B o di un suo sopratipo. Quindi se parametro attuale per a0 fosse di tipo A’,
B’ deve essere di tipo B o di un sopratipo di B (se fosse sottotipo non sarebbe
accettabile)
• TUTTO QUESTO NON SI APPLICA A JAVA!
34
Covarianza dei risultati
C’ sia un sottotipo di C (covarianza sui risultati)
C c0 = a0.f(b0);
⇒qualsiasi valore restituito da una chiamata di f per un esemplare di A’
è accettabile come risultato nel contesto di una chiamata della f su un
esemplare di A, perchè non viene prodotto nulla di “inaspettato”
⇒VALIDO in Java solo da 1.5
B
A.f
B’
C
C
A’.f
B
C’
35
Regola dei metodi
• “le chiamate ai metodi del sottotipo devono comportarsi come le chiamate
ai metodi corrispondenti del sopratipo”
• Non può essere verificata dal compilatore
• Perché la chiamata a un metodo del sottotipo abbia lo stesso effetto, basta
che la specifica sia la stessa (e quindi ok con estensioni pure):
– se y ha tipo statico IntSet, dopo y.insert(x), sappiamo che x è nell’insieme y
quando la chiamata termina (indipendentemente dal tipo effettivo di y, se un
IntSet o un MaxIntSet).
– se p ha tipo statico Poly e p.coeff(3) restituisce un valore >0, sappiamo che il
grado di p è almeno 3 (indipendentemente dal tipo effettivo di p).
• Ma spesso è necessario cambiare la specifica!
– Es. elements() in SortedIntSet() non restituisce gli elementi in un ordine
qualsiasi ma in ordine crescente
– In questo caso, è corretto farlo, perche’ la ensures di Set.elements() era
nondeterministica: qualunque ordine in cui si ritornano gli elementi andava
bene. Allora ritornarli in ordine crescente è accettabile (“rafforziamo la
postcondizione”).
• Valgono regole della precondizione più debole e della postcondizione più
forte
36
Sulla Forza e Debolezza delle Condizioni (1)
• Che significano?
– FORTE: più restrittivo, meno facile da rendere vero, verificato “in meno casi”
– DEBOLE: meno restrittivo, più facile da rendere vero, verificato “in più casi”
– Es: x > 15 è più forte di x > 7, detto altrimenti {x | x > 15} ⊆ {x | x > 7})
• Come si formalizza in Logica Matematica la “forza” di una condizione?
– le condizioni (formule logiche) sono ordinate (anche se non totalmente): dalle più
forti (vere in meno casi) alle più deboli (vere in più casi)
• a un estremo FALSE, la condizione più forte di tutte
• all’altro estremo TRUE, la più debole che ci sia
– l’operatore logico per indicare che α è più forte di β è l’implicazione α ==> β
– l’implicazione logica è una specie di operatore di relazione ‘≤’ che confronta il
valore di verità delle formule
– l’implicazione corrisponde all’inclusione insiemistica dei “valori che rendono
vere le formule” (dei “casi” in cui la formula è vera, o modelli)
• Es: se α è x>15, β è x>7, vale l’implicazione α ==> β
in termini insiemistici {x | x > 15} ⊆ {x | x > 7}
37
Sulla Forza e Debolezza delle Condizioni (2)
• Effetto degli operatori logici sulla forza delle condizioni
– Disgiunzione (OR, ||) indebolisce
• rispetto alla formula α, la formula α || β è vera in qualche caso in più:
quelli in cui è vera β
– Es. (1<x<10) è più forte di (1<x<10) || (20<x<30)
– Congiunzione (AND, &&) rafforza
• rispetto alla formula α, la formula α && β è vera in qualche caso in meno:
quelli in cui è vera α ma non è vera β
– Es. (1<x<10) è più debole di (1<x<10) && dispari(x)
– soddisfatte rispettivamente da [2 .. 9] e da {3, 5, 7, 9}
– Implicazione (passare da β ad α ==> β) indebolisce
• rispetto alla formula β, la formula α ==> β è vera in qualche caso in più:
quelli in cui α è falsa (infatti α ==> β equivale a !α || β)
– Es. (1<x<10) è più forte di dispari(x) ==> (1<x<10)
– soddisfatte rispettivamente da [2 .. 9] e da [2 .. 9] ∪ {x | pari(x)}
• inoltre un’implicazione si indebolisce se viene rafforzata la premessa
38
JML ed estensioni
• Come si ridefinisce in JML la specifica di un metodo?
– Una sottoclasse eredita pre e postcondizioni dei metodi pubblici e
protetti della superclasse e i sui invarianti pubblici
• Sintatticamente: tutto come al solito, basta aggiungere //@also
//@also
//@ensures ………
//@requires ………
Ci dice che la specifica non è completa,
ma serve quanto definito nella superclasse
Vanno considerate insieme a
quanto definito nella superclasse
• Semanticamente: clausole aggiuntive interpretate in mdo da
rispettare la regola dei metodi
– la parte //@requires della classe erede va in OR (||, disgiunzione) con
quella della classe antenato Î requires risulta indebolita
– la parte //@ensures della classe erede va in AND (&&, congiunzione)
con quella della classe antenato Î ensures risulta rafforzata
39
Quindi
• In JML è impossibile specificare sottoclassi che non siano
sottotipi perchè non seguono la regola dei metodi
– e perciò tutti i nostri (contro)esempi di violazione della regola dei metodi
non sarebbero accettati da JML
• Superclasse
– Requires Presup
– Ensures Postsup
Sottoclasse
Requires Presotto
Ensures Postsotto
– Requires Presup || Presotto
– Ensures Postsup && Postsotto
• Usando spesso i commenti (sempre veri in JML), la verifica a
runtime è spesso eccessivamente permissiva e quindi inadeguata
40
Precondizioni più deboli
• Precondizione: ciò che il chiamante deve garantire al chiamato perché un
metodo operi correttamente
• se specifichiamo un metodo indebolendo la precondizione, il chiamante la
verifica a fortiori
– Es. in IntSet ci sia metodo addZero che aggiunge 0 solo se l’insieme non è
vuoto :
//@ requires this.size()>0;
//@ ensures this.isIn(0);
public void addZero()
– e in un sottotipo di IntSet ridefiniamo il metodo:
//@also
//@requires this.size()=0;
public void addZero()
– precondizione è più debole (this.size()>0 || this.size()=0)
– il codice che usava il metodo addZero() non è “sorpreso” dalla nuova
precondizione: non chiama addZero quando this è vuoto. Ma se ora addZero()
funziona con this = vuoto, per il chiamante non cambia nulla
– più debole = “implicato logicamente”
• Regola della precondizione: presuper => presub
41
Se la precondizione fosse più forte?
public class Stack {
//OVERVIEW: una pila di elementi di tipo Object
//@ ensures (* inizializza this alla pila vuota *);
public Stack( )
//@ ensures (* inserisce v in cima a this *);
public void push(Object v)
//@ requires this.size()>0;
//@ ensures (* restituisce ed elimina la cima di this *);
public Object topPop( ) throws EmptyStackException
//@ ensures (* restituisce il numero di elementi di this *);
public int size( )
}
public class BoundedStack {
//OVERVIEW: una pila di elementi di tipo Object di dim. max pari a cento
//@ ensures (* inizializza this alla pila vuota *);
public BoundedStack()
//@ requires this.size() <=100;
//@ ensures (* inserisce v in cima alla pila *);
public void push(Object v)
}
Classe BoundedStack non può essere sottoclasse di Stack. Codice chiamante:
public static void riempiStack (Stack s) { “inserisce 200 elementi in s”}
riempiStack funziona con uno Stack, ma non con un BoundedStack!
42
Postcondizioni più forti
• Postcondizione:
– ciò che il chiamato deve garantire al chiamante al termine
dell’esecuzione (se vale precondizione)
• Se rafforziamo postcondizione, allora la postcondizione attesa
dal chiamante sarà comunque verificata
– “più forte” nel senso di “implica logicamente”:
postsub ==> postsuper
(se postsub è vera, allora anche postsuper è vera)
– poiché al termine dell’esecuzione del metodo, postsub deve valere, anche
postsuper vale, e l’utilizzatore del sopratipo non è “sorpreso”
43
Esempio dal calcolo numerico
public class Num {
…
//@requires r > 0
//@ensures abs(\result * \result - r) < 0.1
public float sqrt(float r) {…}
…
}
…
public class NumPreciso {
…
//@also
//@requires r = 0
//@ensures abs(\result * \result - r) < 0.01
…
public float sqrt(float r) {…}
…
}
Nella sottoclasse il radicando può essere anche nullo (presuper => presub)
il risultato deve essere più preciso (postsub => postsuper)
44
Esempio di violazione della regola dei metodi
public class CharBuffer {
//OVERVIEW: Un buffer di caratteri.
…
//@ensures this.isIn(x)
public void insert(char x) {…}
...
}
public class LowerCaseCharBuffer {
//OVERVIEW: Un buffer di caratteri minuscoli.
…
//@ensures minuscolo(x) ==> this.isIn(x)
// se x e’ minuscolo inserisce x in this, se non e’ minuscolo non assicura nulla
public void insert(char x) {…}
…
}
NB: LowerCaseCharBuffer non è sottotipo di CharBuffer, Se venisse definito come
sottoclasse il principio di sostituzione verrebbe violato. La post di insert non inserisce
caratteri maiuscoli e quindi e’ piu’ debole. La regola della segnatura è invece verificata
45
Regola completa della postcondizione
• Come deve essere la postcondizione se la precondizione è indebolita?
– NB: La postcondizione più forte deve valere solo per i casi in cui vale la precondizione
originale, perchè il codice che usa il metodo della superclasse non lo chiama nei casi
aggiunti dalla precondizione più debole nella versione della sottoclasse
– Esempio: metodo addZero() di IntSet aggiunge 0 solo negli insiemi non vuoti
//@requires this.size() > 0
//@ensures this.isIn(0)
public void addZero()
– definiamo sottotipo di IntSet in cui AddZero() ridefinito in modo che se l’insieme è
vuoto viene aggiunto 1
//@also
//@requires this.size() = 0
//@ensures \old(this.size()>0) ==> this.isIn(0) &&
\old(this.size()=0) ==> this.isIn(1)
public void addZero()
Il metodo ha diffferente ensures solo per quei valori (this = vuoto) che non erano
legali per addZero() della superclasse: il codice che usa addZero non è sorpreso
dalla modifica (non chiamera’mai addZero con this == vuoto)
– Regola della postcondizione: (presuper && postsub) => postsuper
– La regola della postcondizione. è verificata
– La regola sarebbe violata se invece postcondizione fosse:
//@ensures this.isIn(1);
46
Riassumendo
• Regola dei metodi può essere enunciata come:
require no more, promise no less:
• NON RICHIEDERE DI PIU' del metodo della classe
base
• NON PROMETTERE DI MENO del metodo della
classe base
• Entrambe le richieste devono essere soddisfatte
47
attenzione quando si eliminano eccezioni!
• Supponiamo di avere definito in IntSet una insert che non ignora i duplicati, ma lancia
un’eccezione:
public class IntSet {...
//@ ensures !\old(this.isIn(x)) && this.isIn(x);
//@ signals (DuplicateException e) this.isIn(x);
public void insert (int x) throws DuplicateException
}
• Poi sottoclasse (per es. MaxIntSet) elimina eccezione:
public class MaxIntSet extends IntSet {...
//@ also
//@ ensures !\old(this.isIn(x)) ==> this.isIn(x);
public void insert (int x)
}
• Regola della segnatura: OK
• Regola dei metodi: KO! Utilizzatore di IntSet potrebbe usare l’eccezione Duplicate anche
per stabilire se l’elemento era gia’ presente e sara’ sorpreso...
– Dim: Sia x in this. Postsub è vera. Ma PostSuper è falsa (eccezione non viene lanciata).
• Quindi si può eliminare un’eccezione dalla segnatura solo se non è effettivamente usata o se il
suo lancio e’ opzionale…ad esempio se era prevista non per trattare violazioni di
precondizioni sui dati, ma per il verificarsi di altre condizioni che non possono più verificarsi
(es. SportelloApertoException usata per metodo read() di una classe che descrive un floppy e
poi in una sottoclasse che descrive una memoria SD rimovibile). Notare la differenza con le
precondizioni, che possono invece essere indebolite.
48
Esempio con tipi primitivi
• int (Integer) è sottotipo di long (Long)? O viceversa
• Osservazione:
– la somma di due int a 32 bit in caso di overflow lancia eccezione,
mentre la somma degli stessi numeri rappresentati come long a 64 bit
calcola il risultato!
• Non possono essere uno sottotipo dell’altro perche’ la
rispettiva postcondizioni della somma non si implicano.
49
Regola delle proprietà
• “sottotipo deve conservare le proprietà degli oggetti del sovratipo”
– proprietà definite tipicamente come public invariant del sopratipo
• Occorre mostrare che tutti metodi nuovi o ridefiniti, inclusi i costruttori, del
sottotipo conservano le proprietà invarianti e le proprietà evolutive del
sovratipo, osservabili con i metodi pubblici observer della sopraclasse
– Si usa solo specifica dei metodi, non implementazione
• proprietà invariante: proprietà degli stati astratti osservabili dalla
sopraclasse ().
– es: size() di un IntSet è sempre >= 0. Grado di un Poly non è negativo.
• proprietà evolutiva: relazione fra uno stato astratto osservabile e lo stato
astratto osservabile successivo
– Es. Mutabilità; grado di un Poly non cambia
50
Esempio violazione proprietà invariante
• tipo FatSet: come IntSet ma non è mai vuoto:
public class FatSet {
//OVERVIEW Un FatSet è insieme mutabile di interi con
// almeno un elemento
//@ public invariant this.size()>=1; //sempre almeno un elemento
//@ ensures (*costruisce insieme {i}*)
public FatSet(int i)
//@ ensures \old(this.size()>=2) ==> !this.isIn(x); //elimina x da this
public void removeNonEmpty(int x)
}
public class IntSet {
...
//@ensures !this.isIn(x);
public void remove (int x) {…}
}
• IntSet non può essere definita come sottoclasse di FatSet (IntSet extends FatSet)
perché ne viola la proprietà (anche solo aggiungendo costruttore IntSet() per
l’insieme vuoto).
51
Esempio violazione proprietà evolutiva
• tipo SimpleSet come IntSet ma senza remove:
public class SimpleSet
//OVERVIEW Un SimpleSet è insieme mutabile di interi che
// non può ridursi nel tempo
• IntSet non può essere definito come sottoclasse di SimpleSet
aggiungendo remove, perché ne viola proprietà
– Utilizzatori di SimpleSet sarebbero sorpresi non ritrovando più gli
elementi che avevano inserito in precedenza in un SimpleSet!
52
Riassumendo: il principio di sostituzione
• Tre proprietà della specifica dei tipi per compatibilità:
– Regola della segnatura: un sottotipo deve avere tutti i metodi del
sopratipo, e segnature dei metodi del sottotipo devono essere
compatibili.
• Garantito dal compilatore Java
– Regola dei metodi: le chiamate ai metodi del sottotipo devono
comportarsi come le chiamate ai metodi corrispondenti del sopratipo.
• Garantito da JML ma non da Java
– Regola delle proprietà: sottotipo deve preservare tutti i public invariant
e le proprieta’ evolutive degli oggetti del sovratipo.
• Garantito in parte da JML
• L’implementazione Java deve essere aderente alla propria
specifica.
53
Chiarimento su immutabilità e osservabilità
• Se S estende T immutabile, S potrebbe essere mutabile.
• Immutabilità è proprietà evolutiva
– Un sottotipo di un tipo immutabile è pure immutabile, o perlomeno le
sue eventuali mutazioni non sono visibili utilizzando i metodi
osservatori dichiarati nel sopratipo immutabile (in questo modo, il
codice che usava il sopratipo continua a ritenerlo immutabile)
• Quindi l’importante è che, usando solo le operazioni note nella
segnatura di T, il tipo “sembri” immutabile.
• Ad esempio, un erede di Poly potrebbe aggiungere una nuova
componente mutabile dello stato astratto. Questa nuova
componente non è comunque visibile agli utilizzatori di Poly,
che quindi continuano a ritenere Poly immutabile.
54
Consigli per usare ereditarietà
• Un approccio errato
– Ereditarietà = riuso diretto codice.
– Es: classe Persona eredita da classe Data perché ha una data di
nascita
• Approccio corretto: principio di sostituzione
– Un oggetto della classe derivata deve potere essere sostituito
ovunque ci sia un oggetto della classe base.
– Persona non può essere usato dove c'è una Data! Vi siete iscritti in
data "Mario Rossi"?
– Ereditarietà quindi non è un meccanismo di condivisione/riuso del
codice: la sottoclasse deve essere simile alla sopraclasse
55
Esempio: Quadrato vs Rettangolo
• Un Quadrato può essere erede di Rettangolo?
(es: costruttore public Quadrato(float l){ lato1 = lato2 = l})
• Dipende (di solito no): Se Rettangolo ha metodo stretch() per allungare o
accorciare una figura…
• public void stretch() //@ensures (*cambia rapporto tra i lati*)
• Quadrato non si può allungare…
56
Esempio: Pinguino vs Uccello
• Un Pinguino può essere erede di Uccello?
– Forse sì, forse no. Però se Uccello ha metodo:
– public void vola(){...} come si fa?
– Se lo togliessimo (impossibile comunque in Java), chi usa
Pinguino pensando che sia un Uccello e quindi abbia vola()
sarà sorpreso spiacevolmente…
– se ridefiniamo metodo che non fa nulla, si sorprende
utilizzatore che si aspetta che post-condizione di "vola" sia
che "quota" sia positiva...
57
Ereditarietà e contenitori
class Automobile {...}
class AutomobileElettrica extends Automobile {...}
class Parcheggio {
public void aggiungi(Automobile a) {
//@ensures (*inserisce auto a in parcheggio*)
....
•
La classe ParcheggioElettriche rappresenta parcheggi speciali dedicati
esclusivamente alle AutomobiliElettriche. ParcheggioElettriche può
essere sottotipo di Parcheggio?
public class ParcheggioElettriche extends Parcheggio {...}
Automobile a=new Automobile();
Parcheggio p=new ParcheggioElettriche(100);
p.aggiungi(a);
/*
Sorpresa: inserisce Automobile non elettrica in ParcheggioElettriche! Se per
impedirlo si ridefinisce ParcheggioElettriche.aggiungi(Automobile) in modo da
inserire solo Auto elettriche, si viola invece la postcondizione di aggiungi!*/
58
Scarica

Ereditarietà - Home page docenti