46 OOP7 classi3 ereditarietà
Un meccanismo interessante della programmazione ad oggetti è quello di derivare nuove classi prendendone a modello delle altre esistenti, il nome di questo meccanismo è ereditarietà.
Dato il codice della scorsa lezione che generava un punto sul primo quadrante oppure un errore se non rientra nel primo quadrante
class PuntoIQ { constructor(x,y) { if (x<0 || y<0) throw "Non è nel primo quadrante"; this._x =x; this._y=y;} get x() {return this._x;} set x(valore) { if ( typeof(valore) === "boolean" || isNaN(valore) ) throw "Non è un numero"; if(valore<0) throw "Non primo quadrante"; this._x = valore; } } try { let p = new PuntoIQ(10,30); //p.x = "paperino"; p.x = -12; } catch (eccezione) { alert(eccezione); }
andiamo ad estendere la la classe PuntoIQ con la parola dedicata extend
class PuntoIQColorato extends PuntoIQ { }
Il vantaggio di estendere la classe vuol dire che la nuova classe (PuntoIQColorato), anche senza inserire una riga di codice copiato, si ritroverà ogni proprietà e metodo della classe genitore, con un notevole risparmio di dati non ripetuti e un listato più pulito e meno ripetitivo.
Il codice che andrò ad aggiungere nella classe PuntoIQColorato sarà caratteristico solo di questa classe (o di eventuali discendenti) e servirà a distinguerla e caratterizzarla aggiungendo il codice che la contraddistingue o oscurando eventuali metodi che non si adattano della classe madre sostituendoli con altri più specifici.
Tale metodo si chiama anche programmazione per differenze , dove non si riparte da una copia del codice, ma aggiungo solo quello che lo distingue.
Potrebbe sembrare un meccanismo che presenta solo vantaggi, invece si possono riscontrare dei punti deboli:
- Il codice delle classi figlie è legato a filo doppio con quello delle classi da cui discendono, a volte, specialmente in classi con numerosi discendenti, se si va a modificare una caratteristica di un antenato si potrebbe ripercuotere ad effetto domino su molte classi eredi.
- il principio dell’ereditarietà mette a repentaglio un principio fondamentale che è quello dell’ information hiding o oscuramento delle informazioni. Avremmo un modulo, la classe figlia, che è a conoscenza di determinate informazioni o meccanismi interni che dovrebbero essere tenuti nascosti. Tanti programmatori preferiscono la composizione, costruire nuovi oggetti, incorporando al loro interno oggetti già esistenti, rispettando l’ information hiding, pertanto quando un oggetto è inserito come variabile nello stato interno di un altro potrò accedervi soltanto attraverso i metodi forniti dal progettista, senza dover conoscere i meccanismi interni, semplicemente lo uso. Ecco uno spezzone di codice per non lasciare troppo astratto il discorso
class PuntoIQColorato { constructor(x,y, colore) { this.punto = new PuntoIQ(x,y); this.colore = colore; } }
In questo esempio il costruttore crea un punto normale e aggiunge il colore, non fa riferimento ad una classe estendendola, si può creare un normale punto senza sapere come è fatto un punto normale e le caratteristiche interne della classe madre (PuntoIQColorato non sa nulla di PuntoIQ).
Tornando alla nostra classe PuntoIQ aggiungiamo un getter per estrarre la componente coordinata y
get y() {return this._y;}
aggiungo anche un metodo che calcola la distanza dal punto richiamato ad un altro passato come parametro
distanza(altro) { return Math.sqrt( Math.pow(this.x - altro.x, 2) + Math.pow(this.y - altro.y, 2)); else throw "Il parametro attuale `altro` non è un punto"; }
Un punto debole di linguaggi debolmente tipizzati come js è quello di non avere il controllo sul tipo di dato passato, l’unica tutela che possiamo avere è dato dal controllo fatto con instanceof che verifica se l’oggetto è un’istanza di una certa classe, pertanto aggiungiamo:
if (altro instanceof PuntoIQ)
se il controllo passa viene calcolata con la formula matematica di Pitagora per il calcolo dell’ipotenusa ( distanza tra 2 punti, funzione altro() ), secondo questo schema
differenza tra la x del punto sul quale è stato invocato il metodo distanza() e la x del punto passato come parametro
this.x - altro.x, 2
La differenza la si eleva al quadrato col metodo pow dell’oggetto Math
Math.pow(this.x - altro.x, 2)
stessa cosa per le y
Math.pow(this.y - altro.y, 2)
per ottenere l’ipotenusa si calcola la radice quadrata di queste 2 somme tramite il metodo sqrt
return Math.sqrt( Math.pow(this.x - altro.x, 2) + Math.pow(this.y - altro.y, 2));
Se non viene passato un punto si solleva l’eccezione con throw
else throw "Il parametro attuale `altro` non è un punto";
proviamo il metodo passando dei punti normali e chiedendo la distanza
let p1 = new PuntoIQ(5,5); let p2 = new PuntoIQ(10,10); writeln( p1.distanza(p2 ) );
il risultato è 7.0710678118654755 (che è l’ipotenusa).
Ereditarietà
Istanzio un oggetto pc richiamando l’operatore new sulla classe PuntoIQColorato
let pc = new PuntoIQColorato(100,100);
nell’esempio ho passato al costruttore le coordinate 100 e 100, Nel nostro esempio, però, PuntoIQColorato non ha un costruttore. Questo è un buon esempio di ereditarietà in quanto pc eredita dalla classe PuntoIQColorato, la quale eredita dalla classe PuntoIQ le proprietà.
Quindi nessuno problema a chiedere la stampa della x sull’oggetto pc o il metodo distanza() che effettuerà la verifica (un punto colorato è anche un puntoIQ)
let pc = new PuntoIQColorato(100,100); writeln("Punto Colorato: " + pc.x); writeln( pc.distanza(p2) );