Metodo per ottenere concetti universali da oggetti particolari, mettendo da parte le loro caratteristiche specifiche.
Applicazione del metodo logico di astrazione nella strutturazione della descrizione dei sistemi informatici complessi, per facilitarne la progettazione e manutenzione o la stessa comprensione.
Ogni linguaggio di programmazione introduce un livello di astrazione per rappresentare il problema reale.
C, Pascal: Programmazione imperativa e procedurale
Computing function/procedure over data structures
Lisp, Haskell: Programmazione funzionale
Everything is a function
Java, C++, C#: Programmazione orientata agli oggetti
Everything is an object (OO Programming)
JavaScript supporta tutti e tre i paradigmi, ma oggi ci concentriamo sull’OOP.
Nella lezione 2 abbiamo visto un tipo di dato fondamentale: l’oggetto.
Abbiamo visto l’oggetto come una struttura dati con proprietà (chiave-valore) e metodi (funzioni).
Problema: se vogliamo creare 10 persone diverse, dobbiamo riscrivere tutto 10 volte!
La soluzione: creare una funzione costruttore che produce oggetti simili:
function Persona(nome, eta) {
this.nome = nome;
this.eta = eta;
this.saluta = function() {
console.log("Ciao, sono " + this.nome);
};
}
// Creare istanze con "new"
const mario = new Persona("Mario", 30);
const luigi = new Persona("Luigi", 28);
mario.saluta(); // "Ciao, sono Mario"
luigi.saluta(); // "Ciao, sono Luigi"Per convenzione, i costruttori iniziano con la maiuscola (Persona, non persona).
new: cosa fa esattamente?Quando scriviamo new Persona("Mario", 30), JavaScript:
{}this nel costruttore = nuovo oggettothis)this automaticamentethisthis è una parola chiave speciale che si riferisce all’oggetto corrente (quello su cui stiamo operando).
Il valore di this viene valutato al momento dell’esecuzione. Ad esempio, la stessa funzione potrebbe avere diversi this quando viene chiamata da oggetti diversi:
Consiglio
Crea una funzione costruttore Libro con:
titolo, autore, paginedescrivi() che stampa “Titolo di Autore (X pagine)”Crea 3 istanze diverse e chiamane i metodi.
A partire da ES6 (2015), JavaScript introduce la sintassi class:
Nonostante si tratti principalmente di zucchero sintattico, ci sono vantaggi rispetto al costruttore classico:
constructor: il metodo specialeIl metodo constructor viene chiamato automaticamente quando creiamo un’istanza con new:
Regole:
constructor per classeI metodi definiti nella classe sono condivisi da tutte le istanze:
class Contatore {
constructor() {
this.valore = 0;
}
incrementa() {
this.valore++;
}
mostra() {
console.log("Valore: " + this.valore);
}
}
const c1 = new Contatore();
const c2 = new Contatore();
c1.incrementa();
c1.incrementa();
c1.mostra(); // "Valore: 2"
c2.mostra(); // "Valore: 0" (indipendente!)Ogni istanza ha il proprio stato (valore), ma condivide i metodi.
I metodi statici appartengono alla classe, non alle istanze:
class Matematica {
static raddoppia(x) {
return x * 2;
}
static somma(a, b) {
return a + b;
}
}
// Chiamata sulla classe (NON su istanze)
console.log(Matematica.raddoppia(5)); // 10
console.log(Matematica.somma(3, 7)); // 10
// const m = new Matematica();
// m.raddoppia(5); // Errore! raddoppia non esiste sull'istanzaQuando usare static:
Math.random(), Array.isArray())Consiglio
Crea una classe Prodotto con:
constructor(nome, prezzo)descrivi()confrontaPrezzo(prod1, prod2) che ritorna il più economicoCrea due prodotti e confrontali.
L’incapsulamento nella programmazione orientata agli oggetti consiste nel nascondere i dettagli interni di un oggetto e fornire un’interfaccia pubblica per accedere ai suoi metodi e attributi. Due ingredienti:
Ogni classe dovrebbe esporre solo quei (pochi) metodi necessari a interagire con le sue istanze in modo completo. Il resto dovrebbe essere mantenuto privato.
Per fare un’analogia con il mondo reale, un bancomat nasconde il meccanismo interno. L’utente usa i pulsanti (interfaccia pubblica), non accede direttamente ai soldi.
L’information hiding è ciò che sta alla base dell’incapsulamento.
Il client (chi usa l’oggetto) conosce cosa fare (l’interfaccia), non come funziona (i dettagli interni).
function Conto(saldoIniziale) {
this.saldo = saldoIniziale;
// Metodo pubblico che dovrebbe gestire i controlli
this.deposita = function(importo) {
if (typeof importo !== "number" || importo <= 0) return;
this.saldo += importo;
};
}
const conto = new Conto(1000);
conto.deposita(200);
// nulla impedisce di bypassare l'API pubblica e modificare `saldo` direttamente
conto.saldo = -500;La soluzione è rendere privati i campi e controllare con getter/setter
#In JavaScript moderno (ES2022), usiamo # per dichiarare proprietà private:
class ContoCorrente {
#saldo; // Campo privato
constructor(saldoIniziale) {
this.#saldo = saldoIniziale;
}
deposita(importo) {
if (importo > 0) {
this.#saldo += importo;
}
}
getSaldo() {
return this.#saldo; // Accesso controllato
}
}
const conto = new ContoCorrente(1000);
conto.deposita(500);
console.log(conto.getSaldo()); // 1500
console.log(conto.#saldo); // SyntaxError: Private field '#saldo' must be declared in an enclosing classVantaggi: impossibile modificare #saldo dall’esterno. Il controllo è centralizzato nei metodi.
Con getter e setter, creiamo proprietà “virtuali” con logica di controllo:
class Persona {
constructor(nome, annoNascita) {
this.nome = nome;
this.annoNascita = annoNascita;
}
get eta() {
return new Date().getFullYear() - this.annoNascita;
}
set eta(valore) {
if (valore < 0 || valore > 150) return;
this.annoNascita = new Date().getFullYear() - valore;
}
}
const mario = new Persona("Mario", 1994);
console.log(mario.eta); // 31 (calcolato)
mario.eta = 25; // Modifica via setter
console.log(mario.annoNascita); // 2001 (aggiornato)Nota: dall’esterno sembrano proprietà normali, ma sono metodi con logica.
Consiglio
Crea una classe Temperatura con:
#celsiusfahrenheit (converte da Celsius)fahrenheit (converte e salva in Celsius)F = C × 9/5 + 32L’ereditarietà è un concetto chiave dell’OOP, insieme all’incapsulamento.
Si tratta di un meccanismo che consente di definire una nuova classe specializzandone una esistente, ossia “ereditando” i suoi campi e metodi, eventualmente modificandoli o aggiungendone di nuovi.
Si tratta dunque di una strategia di riuso di codice già scritto e testato. Inoltre, influenza anche il polimorfismo che ne consegue.
extendsUna classe può specializzarsi da un’altra usando extends:
class Animale {
constructor(nome) {
this.nome = nome;
}
verso() {
console.log("Verso generico");
}
}
class Cane extends Animale {
verso() {
console.log("Bau bau!");
}
}
const fido = new Cane("Fido");
console.log(fido.nome); // "Fido" (ereditato da Animale)
fido.verso(); // "Bau bau!" (ridefinito in Cane)Concetto: Cane eredita constructor e metodi di Animale, ma può ridefinire (override) il comportamento.
super: accedere alla classe genitoreCon super() nel constructor e super.metodo() nei metodi, accediamo alla classe padre:
class Animale {
constructor(nome) {
this.nome = nome;
}
presentati() {
console.log("Sono " + this.nome);
}
}
class Cane extends Animale {
constructor(nome, razza) {
super(nome); // Chiama constructor genitore
this.razza = razza;
}
presentati() {
super.presentati(); // Chiama metodo del genitore
console.log("Sono un " + this.razza);
}
}Avviso
Se la classe figlia ha un constructor, deve chiamare super() prima di usare this!
L’ereditarietà crea catene di specializzazione:
class AutoElettrica extends Auto {
constructor(marca, numPorte, autonomia) {
super(marca, numPorte);
this.autonomia = autonomia;
}
info() {
return super.info() + ", autonomia " + this.autonomia + " km";
}
}
const tesla = new AutoElettrica("Tesla", 4, 500);
console.log(tesla.info());
// "Veicolo Tesla con 4 porte, autonomia 500 km"Relazione “è un”: AutoElettrica è una Auto, che è un Veicolo.
Spesso composizione è preferibile a ereditarietà:
Ereditarietà (“è un”)
Consiglio
Crea una gerarchia:
Forma (base): constructor(colore), metodo descrivi()Rettangolo extends Forma: aggiunge larghezza, altezza, metodo area()Quadrato extends Rettangolo: solo un parametro latoCrea istanze e testa i metodi ereditati.
Il polimorfismo permette a oggetti diversi di rispondere allo stesso metodo in modo specifico:
class Strumento {
suona() {
return "Suono generico";
}
}
class Chitarra extends Strumento {
suona() {
return "Ding ding!";
}
}
class Piano extends Strumento {
suona() {
return "Pam pam!";
}
}
const strumenti = [new Chitarra(), new Piano(), new Strumento()];
strumenti.forEach(s => console.log(s.suona()));
// Ding ding!
// Pam pam!
// Suono genericoLate binding: il metodo corretto viene scelto a runtime in base al tipo reale dell’oggetto.
Se cammina come un’anatra e starnazza come un’anatra, è un’anatra.
In JavaScript, puoi usare un oggetto dove serve un comportamento, senza ereditarietà:
// Nessuna classe comune, ma tutti hanno speak()
const persona = { speak() { return "Ciao!"; } };
const robot = { speak() { return "Beep boop!"; } };
const animale = { speak() { return "Verso!"; } };
function chiediDiParlare(essere) {
console.log(essere.speak()); // Non controlla il tipo!
}
chiediDiParlare(persona); // "Ciao!"
chiediDiParlare(robot); // "Beep boop!"
chiediDiParlare(animale); // "Verso!"instanceof: verificare il tipoPer controllare se un oggetto appartiene a una classe, usiamo instanceof:
class Animale {}
class Cane extends Animale {}
const fido = new Cane();
console.log(fido instanceof Cane); // true
console.log(fido instanceof Animale); // true (ereditarietà)
console.log(fido instanceof Object); // true (tutto eredita da Object)
const obj = { nome: "Test" };
console.log(obj instanceof Cane); // falseUtile quando:
JavaScript usa prototipi, non classi classiche. Le classi ES6 sono syntax sugar:
Catena dei prototipi: oggetto → Persona.prototype → Object.prototype → null
__proto__ e la catena di prototipiOgni oggetto JavaScript ha una proprietà nascosta __proto__ che punta al suo prototipo:
const mario = new Persona("Mario");
// mario ha accesso a:
// - proprietà proprie: nome
// - metodi di Persona.prototype: saluta()
// - metodi di Object.prototype: toString(), hasOwnProperty()
console.log(mario.__proto__); // Persona.prototype
console.log(mario.__proto__.__proto__); // Object.prototype
console.log(mario.__proto__.__proto__.__proto__); // null (fine della catena)Quando cerchiamo una proprietà o metodo, JavaScript cerca lungo la catena finché non lo trova:
Se ridefinisci un metodo nella classe figlia, oscuri (shadow) il metodo del genitore:
Nota: con super.verso() puoi comunque accedere al metodo del genitore.
Per accedere al prototipo, il modo corretto sarebbe usando Object.getPrototypeOf() anziché __proto__:
class Persona {
saluta() {
console.log("Ciao");
}
}
const mario = new Persona();
console.log(Object.getPrototypeOf(mario)); // Persona.prototype
console.log(Object.getPrototypeOf(mario) === Persona.prototype); // true
// Verifica se un metodo è proprio dell'oggetto o ereditato
console.log(mario.hasOwnProperty("nome")); // false (se non assegnato)
console.log(mario.hasOwnProperty("saluta")); // false (è ereditato)
console.log("saluta" in mario); // true (proprio o ereditato)Può essere utile per:
Ricapitolando, le classi ES6 sono una sintassi più pulita e accessibile sopra i prototipi:
Internamente, JavaScript:
{}__proto__ = Persona.prototypeconstructor per inizializzare thisQuando cerchiamo mario.saluta(), JS segue la catena prototipale e trova il metodo in Persona.prototype.
Ricettario interattivo con classi
Riprendiamo il ricettario della lezione precedente e lo trasformiamo in un’applicazione OOP con classi e navigazione!
Obiettivi:
Ricetta con proprietà (nome, ingredienti, preparazione)Ricettario che gestisce un array di ricetteFunzionalità richieste:
Suggerimenti:
Ricettario usando #ricetteaggiungi(), getRicetta(), getTotale()constructor di Ricetta (nome e ingredienti non vuoti)mostraRicetta(indice) è il cuore: aggiorna DOM e gestisce i pulsantiNiccolò Maltoni