EuraStudy
Appunti/Informatica/Programmazione orientata agli oggetti (OOP)
Appunti · InformaticaIT · Maturità

Programmazione orientata agli oggetti (OOP)

La programmazione orientata agli oggetti è un paradigma che organizza il software intorno a «oggetti», entità che racchiudono insieme i dati (gli attributi che ne descrivono lo stato) e i comportamenti (i metodi che operano su quei dati). Studiando i quattro pilastri del paradigma — incapsulamento, ereditarietà, polimorfismo e astrazione — si impara a modellizzare problemi reali individuando classi, responsabilità e relazioni, e a scrivere codice modulare, riusabile ed estensibile. L'argomento rientra pienamente nel programma del triennio del Liceo Scientifico, opzione Scienze Applicate, ed è valutabile nell'Esame di Stato (seconda prova e colloquio).

5 sezioni·~24 min di lettura·3 competenze·Livello Base 1 · Standard 2 · Approfondimento 2·Verificato · 07/2026

T·0444 / 14
Profilo d’esame
OSA-OOP-1 · Modellizzare un problema individuando oggetti, responsabilità e relazioni tra classiOSA-OOP-2 · Utilizzare i meccanismi dell'OOP (incapsulamento, ereditarietà, polimorfismo) per realizzare software modulare, estensibile e riusabileOSA-OOP-3 · Cogliere il valore astrattivo del paradigma a oggetti nella rappresentazione della realtà
Operatori:spiegadescriviprogettaimplementaconfrontaclassificaanalizzagiustificaillustra

livello base

È richiesto saper definire una classe con attributi, costruttore e metodi, comprendere l'incapsulamento e leggere/scrivere una semplice gerarchia di ereditarietà.

livello avanzato

L'indirizzo Scienze Applicate approfondisce il polimorfismo con overriding, le classi astratte/interfacce e la progettazione orientata agli oggetti di un problema reale, anche con cenni di notazione UML.

Profondità

Profondità di lettura: Approfondimento

Testo

Dimensione del testo: Standard

Contenuti · 5 sezioni▾
  1. Programmazione orientata agli oggetti (OOP)
    • 01Oggetti e classi: attributi, metodi, costruttori○
    • 02Incapsulamento e information hiding◐
    • 03Ereditarietà e gerarchie di classi◐
    • 04Polimorfismo, overriding e classi astratte●
    • 05Progettazione OOP di un problema reale e cenni UML●
§ 01

Oggetti e classi: attributi, metodi, costruttori#

●○○BaseLPOSA-informatica-fondamenti-oop

Dalla classe agli oggetti: lo stampo e le sue istanze

Dalla classe agli oggettiGrafo, Classe Studente → m · Maria, 8.5, Classe Studente → l · Luca, 6.0, Classe Studente → s · Sara, 7.2Classe Studentem · Maria, 8.5l · Luca, 6.0s · Sara, 7.2istanziaistanziaistanzia
Fig. 1La classe Studente (attributi: nome, media, classe; metodi: studia(), promosso()) è lo stampo; ogni oggetto è un’istanza con valori propri.

Punti chiave

Il paradigma orientato agli oggetti nasce per descrivere il software come un insieme di entità che imitano gli oggetti del mondo reale: ciascun oggetto possiede uno «stato» (i dati che lo caratterizzano) e un «comportamento» (le operazioni che sa eseguire). A differenza del paradigma imperativo, in cui dati e funzioni sono separati, l'OOP li tiene uniti nella stessa unità, rendendo il programma più vicino al dominio del problema.
La «classe» è il modello, lo stampo che descrive una categoria di oggetti: definisce quali «attributi» (variabili d'istanza) e quali «metodi» (funzioni associate) avranno tutti gli oggetti di quel tipo. L'«oggetto» (o «istanza») è invece l'entità concreta creata a partire dalla classe: la classe «Studente» è il progetto, mentre l'oggetto che rappresenta Maria Rossi, con il suo nome e la sua media, è una specifica istanza di quel progetto.
Il «costruttore» è un metodo speciale, eseguito automaticamente al momento della creazione di un oggetto, che ha il compito di inizializzarne lo stato assegnando valori coerenti agli attributi. In Java porta lo stesso nome della classe e non ha tipo di ritorno; in Python coincide con il metodo «__init__». Senza un costruttore appropriato un oggetto potrebbe nascere in uno stato non valido — per questo l'inizializzazione è un punto cruciale della progettazione.
Ogni metodo, quando opera su un oggetto, accede ai suoi attributi tramite un riferimento all'istanza corrente: la parola chiave «this» in Java e C++, l'argomento esplicito «self» in Python. Questo distingue i dati di un'istanza da quelli di un'altra: due studenti diversi hanno lo stesso insieme di attributi, ma con valori indipendenti. La distinzione tra attributi d'istanza (uno per oggetto) e attributi di classe (condivisi da tutte le istanze) è un punto da padroneggiare con precisione.
Esempio svolto

Definire una classe con costruttore e metodi

Progetta la classe «ContoCorrente» con gli attributi «intestatario» e «saldo». Scrivi il costruttore e i metodi «versa(importo)» e «preleva(importo)», individuando attributi, stato e comportamento.

  1. 01Individuare attributi (stato)

    Lo stato dell'oggetto è descritto da due attributi d'istanza: «intestatario» (testo) e «saldo» (numero reale). Ogni conto avrà i propri valori indipendenti.

  2. 02Scrivere il costruttore

    Il costruttore riceve l'intestatario e un saldo iniziale e li assegna agli attributi tramite self/this: «def __init__(self, intestatario, saldo): self.intestatario = intestatario; self.saldo = saldo». Così l'oggetto nasce già in uno stato valido.

  3. 03Definire i metodi (comportamento)

    «versa(importo)» aumenta il saldo: «self.saldo = self.saldo + importo». «preleva(importo)» lo diminuisce solo se i fondi bastano: «if importo <= self.saldo: self.saldo = self.saldo - importo». I metodi modificano lo stato dell'istanza su cui sono invocati.

  4. 04Istanziare e usare l'oggetto

    «c = ContoCorrente("Rossi", 100)» crea un'istanza; «c.versa(50)» porta il saldo a 150; «c.preleva(30)» lo porta a 120. Un secondo oggetto avrebbe il proprio saldo indipendente.

Risultato: La classe incapsula stato (intestatario, saldo) e comportamento (versa, preleva); ogni istanza mantiene valori propri. Dopo le operazioni dell'esempio il saldo finale è 120.

Obiettivo Maturità

  • Saper distinguere con esattezza, in un colloquio o in una traccia, i concetti di «classe» (il modello astratto) e «oggetto/istanza» (l'entità concreta) è una richiesta ricorrente: usa un esempio concreto per mostrare la differenza.
  • Nella seconda prova è frequente la richiesta di «definire una classe» dato un dominio: ci si aspetta attributi pertinenti, un costruttore che inizializzi lo stato e metodi coerenti con il comportamento dell'oggetto.

Errori frequenti

  • Confondere la classe con l'oggetto: scrivere «la classe Studente ha media 8.5» è scorretto — la media è un valore di una specifica istanza, non della classe.
  • Dimenticare di inizializzare gli attributi nel costruttore, lasciando l'oggetto in uno stato indefinito (valori nulli o casuali) che provoca errori difficili da individuare in fase di esecuzione.

Approfondimento

Un oggetto vive nella memoria dinamica (heap) e la variabile che lo «contiene» custodisce in realtà un riferimento (un indirizzo) ad esso: per questo assegnare «b = a» fa sì che a e b puntino allo stesso identico oggetto, non a una copia. Da qui la distinzione capitale fra identità e uguaglianza: due riferimenti sono identici se puntano allo stesso oggetto in memoria (in Python «a is b», in Java «a == b» sui riferimenti), mentre due oggetti sono uguali se hanno lo stesso stato secondo un criterio definito dalla classe (il metodo «equals» in Java, «__eq__» in Python, da ridefinire coerentemente con «hashCode»). Il ciclo di vita di un oggetto va dalla costruzione, quando il costruttore ne fissa uno stato valido, all'uso, fino alla distruzione: nei linguaggi con garbage collector (Java, Python) la memoria di un oggetto non più raggiungibile viene recuperata automaticamente, mentre in C++ va rilasciata esplicitamente da un distruttore. Accanto ai membri d'istanza esistono i membri di classe («static»), condivisi da tutte le istanze — utili per un contatore di oggetti creati o per costanti — a cui si accede tramite la classe e non tramite un singolo oggetto (per questo un metodo statico non dispone di «this»/«self»). Molti linguaggi permettono infine il sovraccarico del costruttore (più costruttori con parametri diversi), per offrire modi alternativi di creare un oggetto sempre ben inizializzato.

Ripasso attivo

Progetta la classe «ContoCorrente» con gli attributi «intestatario» e «saldo». Scrivi il costruttore che inizializza l'intestatario e pone il saldo iniziale, e i metodi «versa(importo)» e «preleva(importo)». Indica chiaramente quali sono gli attributi d'istanza e quale parte del codice rappresenta lo stato e quale il comportamento dell'oggetto.

Richiamo attivo

Ricorda i punti chiave — poi rivela.

Fonti: Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento (Ministero dell'Istruzione e del Merito (MIM))

§ 02

Incapsulamento e information hiding#

●●○StandardLPOSA-informatica-fondamenti-oop

Information hiding: interfaccia pubblica e stato privato

Information hidingGrafo, Codice esterno → Metodi public, Metodi public → Stato privateCodice esternoMetodi publicStato privatevedeaccede
Fig. 2Il codice esterno vede solo i metodi public (getSaldo(), preleva()); lo stato private (saldo, intestatario) è raggiungibile solo attraverso di essi.

Punti chiave

L'«incapsulamento» è il principio per cui dati e operazioni che li manipolano sono raccolti in un'unica unità — la classe — e l'accesso diretto allo stato interno viene controllato. È il primo dei quattro pilastri dell'OOP e ha lo scopo di proteggere l'integrità dei dati: l'oggetto è responsabile del proprio stato e nessuno può alterarlo se non attraverso l'interfaccia che esso stesso espone.
Strettamente legato all'incapsulamento è l'«information hiding» (occultamento dell'informazione): i dettagli interni di una classe vengono nascosti al mondo esterno, che ne conosce soltanto l'interfaccia pubblica. Si distinguono così i membri «privati» (accessibili solo dall'interno della classe) dai membri «pubblici» (l'interfaccia visibile). In Java si usano i modificatori «private» e «public»; in Python esiste una convenzione (un trattino basso iniziale segnala un membro «protetto» per convenzione).
Per leggere e modificare in modo controllato gli attributi privati si usano metodi appositi: i «getter» (metodi di lettura, p. es. «getSaldo()») e i «setter» (metodi di scrittura, p. es. «setSaldo()»). Il setter può imporre vincoli — rifiutare valori non validi — garantendo che lo stato resti sempre coerente. Questo separa il «cosa» (l'interfaccia) dal «come» (l'implementazione interna).
Il vantaggio decisivo dell'incapsulamento è il disaccoppiamento: finché l'interfaccia pubblica resta stabile, l'implementazione interna può cambiare liberamente senza rompere il codice che usa la classe. Una classe ben incapsulata è come un elettrodomestico: si usa tramite i suoi comandi (l'interfaccia) senza conoscerne i circuiti interni (l'implementazione).
Esempio svolto

Proteggere lo stato con un attributo privato e un setter validante

Rendi privato l'attributo «saldo» di «ContoCorrente» e aggiungi i metodi che impediscano un saldo negativo. Spiega il problema di integrità risolto.

  1. 01Dichiarare l'attributo privato

    In Java: «private double saldo;». In Python si usa la convenzione «self._saldo». In questo modo nessun codice esterno può scrivere direttamente «conto.saldo = -5000».

  2. 02Esporre un getter

    «public double getSaldo() { return saldo; }» offre la sola lettura controllata del valore, senza esporre l'attributo.

  3. 03Validare nel metodo di scrittura

    Nel metodo «preleva(importo)» si controlla la condizione di validità: si esegue il prelievo solo se «importo > 0 && importo <= saldo», altrimenti si segnala un errore e lo stato resta invariato.

  4. 04Verificare l'integrità

    Provando a prelevare un importo superiore al saldo, l'operazione viene rifiutata: il saldo non può mai diventare negativo, l'invariante della classe è garantita dall'oggetto stesso.

Risultato: L'attributo privato + il metodo validante garantiscono l'invariante «saldo >= 0»: nessun codice esterno può violare l'integrità dei dati, perché lo stato è modificabile solo tramite l'interfaccia controllata.

Obiettivo Maturità

  • Saper spiegare la differenza tra incapsulamento (raccogliere dati e metodi nell'unità classe) e information hiding (nascondere i dettagli dietro un'interfaccia pubblica): all'esame i due concetti vanno distinti, non confusi.
  • Ci si aspetta che, dato uno scenario, si motivi perché un attributo va reso «private» e si fornisca un setter che valida l'input (p. es. impedire un saldo negativo).

Errori frequenti

  • Rendere pubblici tutti gli attributi «per comodità»: così facendo si annulla l'incapsulamento e qualunque codice esterno può portare l'oggetto in uno stato incoerente.
  • Scrivere setter «vuoti» che si limitano ad assegnare il valore senza alcun controllo: in tal caso il setter non aggiunge protezione rispetto all'accesso diretto e l'information hiding perde di significato.

Approfondimento

L'incapsulamento non serve solo a «nascondere»: il suo scopo profondo è custodire un'invariante di classe, una proprietà che deve valere per tutta la vita dell'oggetto (per esempio «saldo ≥ 0» in un conto, «0 ≤ ora < 24» in un orario). Rendendo privati gli attributi e incanalando ogni modifica attraverso metodi che verificano l'invariante, si garantisce che l'oggetto non possa mai trovarsi in uno stato incoerente: è l'esatto analogo, per i dati, dell'invariante di ciclo per gli algoritmi. L'idea è formalizzata da D. Parnas (1972): un modulo deve nascondere le decisioni progettuali soggette a cambiamento, esponendo solo un'interfaccia stabile. Una forma forte di incapsulamento è l'immutabilità: un oggetto i cui campi, una volta costruiti, non cambiano più (attributi «final» in Java, oggetti «read-only») non può violare le proprie invarianti né essere corrotto da accessi concorrenti, e per questo è più sicuro da condividere fra più parti del programma. Sul piano progettuale, l'incapsulamento riduce l'accoppiamento (coupling) fra le classi e ne aumenta la coesione (cohesion): meno una classe conosce dei dettagli interni delle altre — principio riassunto dalla «legge di Demeter», «parla solo con i tuoi amici diretti» — più il sistema si può modificare senza propagare effetti a catena. È questo disaccoppiamento, non il semplice divieto di accesso, il vero guadagno dell'information hiding.

Ripasso attivo

Riprendi la classe «ContoCorrente». Rendi privato l'attributo «saldo» e aggiungi un getter «getSaldo()» e un setter (o un metodo «preleva») che impedisca al saldo di diventare negativo. Spiega quale problema di integrità dei dati hai risolto rendendo privato l'attributo.

Richiamo attivo

Ricorda i punti chiave — poi rivela.

Fonti: Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento (Ministero dell'Istruzione e del Merito (MIM))

§ 03

Ereditarietà e gerarchie di classi#

●●○StandardLPOSA-informatica-fondamenti-oop

Gerarchia di ereditarietà: dalla classe base alle derivate

Gerarchia di ereditarietàDiagramma ad albero, 2 percorsi, Dati: Studente; DocentePersonaStudenteDocente
Fig. 3Persona (nome, età) è la superclasse; Studente (media, studia()) e Docente (materia, insegna()) ereditano da essa: relazione «è-un».

Punti chiave

L'«ereditarietà» è il meccanismo che permette di definire una nuova classe (detta «sottoclasse» o «classe derivata») a partire da una classe esistente (detta «superclasse» o «classe base»), ereditandone attributi e metodi. È il secondo pilastro dell'OOP e il principale strumento di riuso del codice: ciò che è comune si scrive una sola volta nella superclasse e si condivide con tutte le derivate.
L'ereditarietà esprime una relazione «è-un» (is-a): se «Studente» eredita da «Persona», allora ogni studente «è una» persona. Questa relazione organizza le classi in «gerarchie» ad albero, dal generale (in alto) al particolare (in basso). Riconoscere quando una relazione è davvero «è-un» — e non semplicemente «ha-un» (has-a, che si modella invece con la composizione) — è essenziale per progettare gerarchie corrette.
La sottoclasse può fare tre cose: ereditare i membri della superclasse, aggiungere nuovi attributi e metodi propri, e ridefinire metodi ereditati (overriding, trattato nella sezione successiva). Nel costruttore della derivata si richiama tipicamente il costruttore della base — «super(...)» in Java e Python — per inizializzare la parte ereditata dello stato prima di inizializzare quella specifica.
Occorre distinguere l'ereditarietà «singola» (una classe deriva da una sola superclasse, come in Java) dall'ereditarietà «multipla» (da più superclassi, ammessa in C++ e Python). L'ereditarietà multipla è potente ma può generare ambiguità (il celebre «problema del diamante»), per cui molti linguaggi la sostituiscono con le interfacce. Va inoltre evitato l'abuso dell'ereditarietà quando la relazione corretta sarebbe la composizione.
Esempio svolto

Costruire una gerarchia con richiamo al costruttore base

Modellizza «Persona» (nome, età) con le sottoclassi «Studente» (media) e «Docente» (materia). Mostra come il costruttore di «Studente» richiama quello di «Persona» e indica i membri ereditati e aggiunti.

  1. 01Definire la superclasse

    «Persona» ha gli attributi comuni «nome» ed «età» e il costruttore «__init__(self, nome, eta)» che li inizializza, più eventuali metodi comuni (p. es. «presentati())».

  2. 02Derivare le sottoclassi

    «class Studente(Persona):» e «class Docente(Persona):» dichiarano l'ereditarietà. Entrambe ricevono per ereditarietà nome, età e i metodi comuni di Persona.

  3. 03Richiamare il costruttore base

    Nel costruttore di «Studente»: «def __init__(self, nome, eta, media): super().__init__(nome, eta); self.media = media». La chiamata a super() inizializza la parte ereditata; poi si inizializza l'attributo aggiunto «media».

  4. 04Distinguere ereditati e aggiunti

    Studente: ereditati nome, età (e i metodi di Persona); aggiunto «media» e i metodi specifici. Docente: ereditati nome, età; aggiunto «materia». Il codice comune vive una sola volta nella base.

Risultato: La gerarchia riusa nome/età definiti una sola volta in «Persona»; ogni sottoclasse estende lo stato comune con i propri attributi specifici, inizializzati dopo la chiamata a super(), che garantisce la corretta inizializzazione della parte ereditata.

Obiettivo Maturità

  • Saper riconoscere e giustificare quando usare l'ereditarietà (relazione «è-un») rispetto alla composizione (relazione «ha-un») è una distinzione frequentemente richiesta sia nella prova scritta sia al colloquio.
  • Ci si aspetta di saper progettare una piccola gerarchia (superclasse + sottoclassi) indicando quali membri sono ereditati, quali aggiunti, e come il costruttore della derivata richiama quello della base.

Errori frequenti

  • Usare l'ereditarietà per riusare codice anche dove la relazione non è «è-un» (p. es. far ereditare «Pila» da «Array» solo per riusare i metodi): questo crea gerarchie fragili; in questi casi si usa la composizione.
  • Dimenticare di chiamare il costruttore della superclasse nella sottoclasse, lasciando non inizializzata la parte ereditata dello stato.

Approfondimento

L'ereditarietà multipla, ammessa in C++ e Python, apre il celebre problema del diamante: se «D» eredita da «B» e «C», che a loro volta ereditano entrambe da «A», e sia «B» sia «C» ridefiniscono uno stesso metodo, quale versione eredita «D»? Per sciogliere l'ambiguità Python fissa un ordine di risoluzione dei metodi (MRO, linearizzazione C3) che stabilisce univocamente la catena di ricerca; Java elude il problema alla radice, ammettendo l'ereditarietà singola fra classi e concedendo solo l'implementazione di più interfacce. Un difetto insidioso delle gerarchie profonde è la classe base fragile (fragile base class): una modifica apparentemente innocua alla superclasse può rompere silenziosamente le sottoclassi che ne dipendevano. È il motivo tecnico della linea guida «preferire la composizione all'ereditarietà»: invece di far ereditare «Automobile» da «Motore» (che non è affatto una relazione «è-un»), si dà ad «Automobile» un attributo di tipo «Motore» (relazione «ha-un»), ottenendo lo stesso riuso con minore accoppiamento e maggiore flessibilità — si può perfino sostituire il motore a tempo di esecuzione. Il criterio ultimo di una gerarchia corretta è che ogni sottoclasse sia realmente sostituibile alla superclasse senza sorprese per chi la usa, principio che il polimorfismo formalizza nella sezione seguente e che distingue una gerarchia «è-un» solo di nome da una davvero ben progettata.

Ripasso attivo

Modellizza una gerarchia per una scuola: una superclasse «Persona» (attributi «nome», «età») e due sottoclassi «Studente» (aggiunge «media») e «Docente» (aggiunge «materia»). Scrivi i costruttori, mostrando come quello di «Studente» richiama quello di «Persona», e indica quali membri sono ereditati e quali aggiunti.

Richiamo attivo

Ricorda i punti chiave — poi rivela.

Fonti: Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento (Ministero dell'Istruzione e del Merito (MIM))

§ 04

Polimorfismo, overriding e classi astratte#

●●●ApprofondimentoLPOSA-informatica-fondamenti-oop

Polimorfismo: una superclasse astratta, molte implementazioni di area()

Polimorfismo e overridingDiagramma ad albero, 3 percorsi, Dati: Cerchio; Quadrato; TriangoloFiguraCerchioQuadratoTriangolo
Fig. 4Figura è astratta con il metodo astratto area(); ogni sottoclasse ne fa l’override: Cerchio πr², Quadrato l², Triangolo b·h/2. Stessa chiamata area(), comportamenti diversi.

Punti chiave

Il «polimorfismo» (dal greco «molte forme») è il terzo pilastro dell'OOP: permette di trattare in modo uniforme oggetti di classi diverse purché legati da una stessa superclasse o interfaccia comune. Un riferimento di tipo «Figura» può puntare a un «Cerchio», a un «Quadrato» o a un «Triangolo»: chiamando «area()» su quel riferimento, viene eseguita automaticamente la versione propria dell'oggetto effettivamente puntato.
Il meccanismo che lo rende possibile è l'«overriding» (ridefinizione): una sottoclasse ridefinisce un metodo ereditato dalla superclasse, fornendone una propria implementazione. La scelta di quale versione eseguire avviene «a tempo di esecuzione» in base al tipo reale dell'oggetto (dynamic dispatch / late binding), non al tipo dichiarato della variabile. Va distinto dall'«overloading» (sovraccarico), che è invece la presenza di più metodi con lo stesso nome ma firme diverse, risolto a tempo di compilazione.
Una «classe astratta» è una classe che non può essere istanziata direttamente perché rappresenta un concetto generale incompleto (p. es. «Figura»): può contenere «metodi astratti», dichiarati ma privi di corpo, che le sottoclassi concrete sono obbligate a implementare. Un'«interfaccia» è il caso limite di pura astrazione: un contratto di soli metodi, senza implementazione, che una classe «implementa» impegnandosi a fornire quei comportamenti. Sono lo strumento principe per definire un'astrazione comune polimorfica.
Il polimorfismo è ciò che rende il software realmente estensibile: si può aggiungere una nuova sottoclasse (p. es. una nuova «Figura») senza modificare il codice che la usa attraverso la superclasse comune. Questo realizza il principio «aperto-chiuso» (aperto all'estensione, chiuso alla modifica) e fa dei quattro pilastri — incapsulamento, ereditarietà, polimorfismo, astrazione — un sistema coerente per gestire la complessità.
Acerchio=πr2A_{\text{cerchio}} = \pi r^2Acerchio​=πr2

Cerchio

Area del cerchio in funzione del raggio r; è l'overriding di area() nella sottoclasse Cerchio.

Aquadrato=l2A_{\text{quadrato}} = l^2Aquadrato​=l2

Quadrato

Area del quadrato in funzione del lato l; è l'overriding di area() nella sottoclasse Quadrato.

Esempio svolto

Polimorfismo con classe astratta e overriding di area()

Definisci «Figura» astratta con «area()» astratto e le sottoclassi «Cerchio» (raggio) e «Quadrato» (lato). Scorri una lista di figure miste e stampane l'area; calcola l'area per un cerchio di raggio 2 e un quadrato di lato 3.

  1. 01Classe astratta

    «Figura» dichiara il metodo astratto «area()» senza corpo: rappresenta il concetto comune ma non può essere istanziata. Impone un contratto a tutte le sottoclassi.

  2. 02Overriding nelle sottoclassi

    «Cerchio» ridefinisce «area()» restituendo «pi raggio2»; «Quadrato» restituendo «lato*2». Stesso nome e firma del metodo base: è overriding, non overloading.

  3. 03Uso polimorfico

    Data «figure = [Cerchio(2), Quadrato(3)]», il ciclo «for f in figure: print(f.area())» chiama su ogni elemento la versione corretta in base al tipo reale: il dispatch è dinamico.

  4. 04Calcolare i valori

    Cerchio di raggio 2: area = pi 2^2 = 4pi ≈ 12.566. Quadrato di lato 3: area = 3^2 = 9.

Risultato: La stessa istruzione f.area() produce risultati diversi (≈ 12.566 per il cerchio di raggio 2, 9 per il quadrato di lato 3) secondo il tipo reale dell'oggetto: è il polimorfismo per overriding. Aggiungere una nuova figura non richiede di modificare il ciclo che le elabora.

Obiettivo Maturità

  • Distinguere con precisione overriding (ridefinizione, stessa firma, scelta a runtime) da overloading (stesso nome, firme diverse, scelta a compile-time) è una domanda classica del colloquio: tieni pronti due esempi distinti.
  • Ci si aspetta di saper spiegare il polimorfismo con un esempio (p. es. la gerarchia «Figura» con «area()») e di motivare perché esso rende il software estensibile senza modificare il codice esistente.

Errori frequenti

  • Confondere overriding e overloading: il primo richiede stessa firma e relazione di ereditarietà ed è risolto a runtime; il secondo è solo lo stesso nome con parametri diversi, risolto a compile-time.
  • Pensare che la versione eseguita di un metodo polimorfico dipenda dal tipo dichiarato della variabile: dipende invece dal tipo reale dell'oggetto a cui essa si riferisce in quel momento.

Approfondimento

Il polimorfismo dinamico ha un'implementazione concreta ed elegante: a ogni classe con metodi ridefinibili il compilatore associa una tabella dei metodi virtuali (vtable), un vettore di puntatori alle versioni effettive dei metodi; ogni oggetto porta con sé un riferimento alla vtable della propria classe, così la chiamata «figura.area()» diventa una consultazione della tabella a tempo di esecuzione (dynamic dispatch) e finisce nella versione giusta. Perché il polimorfismo sia corretto, però, non basta la meccanica: deve valere il principio di sostituzione di Liskov (LSP), secondo cui un oggetto di una sottoclasse deve poter sostituire uno della superclasse senza alterare la correttezza del programma — la sottoclasse non deve rafforzare le precondizioni né indebolire le postcondizioni dei metodi ereditati. Il controesempio classico è «Quadrato» che eredita da «Rettangolo»: se un rettangolo permette di fissare larghezza e altezza in modo indipendente, un quadrato che le tiene forzatamente uguali viola le aspettative di chi lo tratta come rettangolo, e la gerarchia — pur «è-un» in geometria — risulta scorretta nel software. Sul piano progettuale il polimorfismo è il motore di molti schemi ricorrenti (design pattern): nel pattern Strategy, per esempio, un algoritmo intercambiabile viene incapsulato in oggetti che implementano una stessa interfaccia e vengono scelti a run-time — la stessa idea che realizza il principio «aperto all'estensione, chiuso alla modifica».

Ripasso attivo

Definisci una classe astratta «Figura» con un metodo astratto «area()» e le sottoclassi «Cerchio» (attributo «raggio») e «Quadrato» (attributo «lato»), ciascuna con il proprio overriding di «area()». Scrivi poi un frammento che, scorrendo una lista di figure miste, ne stampa l'area, mostrando il polimorfismo all'opera. Calcola l'area per un cerchio di raggio 2 e un quadrato di lato 3.

Richiamo attivo

Ricorda i punti chiave — poi rivela.

Fonti: Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento (Ministero dell'Istruzione e del Merito (MIM))

§ 05

Progettazione OOP di un problema reale e cenni UML#

●●●ApprofondimentoLPOSA-informatica-fondamenti-oop

Diagramma delle classi UML (semplificato) di una biblioteca

Diagramma delle classi UMLGrafo, Utente → Prestito, Prestito → LibroUtentePrestitoLibro1 : 0..·· : 1
Fig. 5Un Utente (nome, tessera) ha molti Prestiti (dataInizio, scadenza); ogni Prestito riguarda un Libro (titolo, isbn). Ogni classe ha tre scomparti (nome / attributi / metodi); visibilità: + pubblico, − privato, # protetto.

Punti chiave

La «progettazione orientata agli oggetti» è il processo che, partendo dalla descrizione di un problema reale, individua le classi, i loro attributi e metodi, e le relazioni tra esse. Una tecnica didattica classica consiste nell'analizzare il testo del problema: i «sostantivi» suggeriscono candidate classi e attributi, i «verbi» suggeriscono i metodi. Si parte dalle responsabilità — «di cosa è responsabile ciascun oggetto?» — per ottenere un modello coeso e con un basso accoppiamento.
Oltre all'ereditarietà («è-un») le classi si legano per «associazione» (un oggetto usa o conosce un altro), «aggregazione» e «composizione» («ha-un», con la composizione che implica un legame di vita: la parte non esiste senza il tutto). Scegliere la relazione giusta è il cuore della progettazione: preferire la composizione all'ereditarietà quando non c'è una vera relazione «è-un» è una linea guida fondamentale per ottenere software flessibile.
Lo standard per documentare il progetto è l'«UML» (Unified Modeling Language). Il diagramma più usato è il «diagramma delle classi» (class diagram): ogni classe è un rettangolo a tre scomparti (nome; attributi; metodi); la visibilità si annota con «+» (pubblico), «-» (privato), «#» (protetto); le relazioni si rappresentano con linee tipizzate (triangolo vuoto per l'ereditarietà, rombo per aggregazione/composizione, freccia per l'associazione) corredate da «cardinalità» (p. es. «1», «0..*»).
Il valore astrattivo del paradigma sta proprio qui: l'OOP fornisce un linguaggio per rappresentare la realtà di interesse come un sistema di oggetti che collaborano. Un buon progetto traduce un dominio (una biblioteca, una scuola, un negozio) in classi ben incapsulate, organizzate in gerarchie sensate e collegate da relazioni corrette; i quattro pilastri sono gli strumenti per gestire la complessità e ottenere codice modulare, riusabile ed estensibile.
Esempio svolto

Modellare a oggetti una piccola biblioteca

Progetta il modello a oggetti di una biblioteca con le classi «Libro», «Utente» e «Prestito», definendone attributi e metodi e descrivendo le relazioni con le cardinalità.

  1. 01Individuare le classi (sostantivi)

    Dal dominio emergono tre classi: «Libro» (titolo, isbn, copieDisponibili), «Utente» (nome, tessera) e «Prestito» (dataInizio, scadenza), che collega un utente a un libro.

  2. 02Assegnare i metodi (verbi)

    Libro: «disponibile()». Utente: «prendi(libro)», «restituisci(prestito)». Prestito: «rinnova()», «inRitardo()». Ogni metodo è una responsabilità dell'oggetto cui appartiene.

  3. 03Definire le relazioni e le cardinalità

    Un Utente può avere molti Prestiti (1 a 0..); ogni Prestito riguarda esattamente un Libro (0.. a 1). Sono associazioni «ha-un», non ereditarietà: nessuna classe «è-un'altra».

  4. 04Tradurre in UML

    Si disegnano tre rettangoli a tre scomparti (nome/attributi/metodi), con visibilità +/-, collegati da linee di associazione annotate con le cardinalità (vedi Fig. 5).

Risultato: Il modello distribuisce le responsabilità su tre classi coese collegate da associazioni «ha-un» con cardinalità esplicite: un progetto modulare ed estensibile (p. es. una futura classe «Bibliotecario» si aggiunge senza riscrivere le esistenti), correttamente documentato da un diagramma delle classi UML.

Obiettivo Maturità

  • La seconda prova può richiedere di progettare un piccolo sistema (p. es. la gestione di una biblioteca): ci si aspetta l'individuazione delle classi, dei loro attributi/metodi e delle relazioni, idealmente con un diagramma delle classi UML semplificato.
  • Al colloquio è valutata la capacità di leggere/interpretare un diagramma delle classi UML, riconoscendo visibilità (+/-/#), ereditarietà (triangolo) e relazioni «ha-un» (rombo/freccia) con le rispettive cardinalità.

Errori frequenti

  • Creare una sola classe «contenitore» che fa tutto (basso grado di astrazione, scarsa coesione) invece di distribuire le responsabilità su più classi collaboranti.
  • Confondere i simboli UML delle relazioni: usare il triangolo dell'ereditarietà per una relazione «ha-un» (che richiede invece il rombo dell'aggregazione/composizione o la freccia dell'associazione).

Approfondimento

Il diagramma delle classi coglie la struttura statica del sistema, ma un progetto completo descrive anche il comportamento dinamico con altri diagrammi UML: il diagramma dei casi d'uso (use case) fissa che cosa gli attori possono fare con il sistema; il diagramma di sequenza mostra lo scambio di messaggi fra oggetti nel tempo per realizzare un'operazione (chi chiama chi, e in quale ordine); il diagramma di stato descrive come un oggetto cambia stato in risposta agli eventi (un «Prestito» passa da «attivo» a «scaduto» a «restituito»). Struttura e comportamento sono le due facce complementari del modello. Per giudicare la qualità di un progetto si usano due criteri: alta coesione (ogni classe ha una responsabilità ben definita) e basso accoppiamento (le classi dipendono le une dalle altre il meno possibile). Questi criteri si sistematizzano nei principi SOLID — fra cui la singola responsabilità (una classe, un solo motivo per cambiare) e l'inversione delle dipendenze (dipendere da astrazioni, non da implementazioni concrete). Le soluzioni ricorrenti a problemi di progettazione noti sono infine catalogate come design pattern (schemi della «Gang of Four»), classificati in creazionali (come si creano gli oggetti, p. es. Factory), strutturali (come si compongono, p. es. Adapter) e comportamentali (come collaborano, p. es. Observer, Strategy): conoscerli significa riusare non solo codice, ma esperienza di progettazione consolidata.

Ripasso attivo

Progetta il modello a oggetti di una piccola biblioteca. Individua almeno le classi «Libro», «Utente» e «Prestito», definendone attributi e metodi principali, e descrivi le relazioni tra esse (chi «ha» chi, con quali cardinalità). Rappresenta il risultato con un diagramma delle classi UML semplificato.

Richiamo attivo

Ricorda i punti chiave — poi rivela.

Fonti: Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento (Ministero dell'Istruzione e del Merito (MIM)) · Esame di Stato del secondo ciclo — quadri di riferimento e griglie di valutazione (Ministero dell'Istruzione e del Merito (MIM))

Contenuti

Sezione -- / 05

    • 01Oggetti e classi: attributi, metodi, costruttori○
    • 02Incapsulamento e information hiding◐
    • 03Ereditarietà e gerarchie di classi◐
    • 04Polimorfismo, overriding e classi astratte●
    • 05Progettazione OOP di un problema reale e cenni UML●

0/5 Letti

Dagli appunti all'allenamento

Programmazione orientata agli oggetti (OOP)

Consolida questo argomento con domande dalla banca dati.

~24
min
3
Competenze
Esercitati

Riferimenti e fonti

Fonti

Ministero dell'Istruzione e del Merito (MIM)

  • Indicazioni Nazionali per i Licei (DPR 89/2010, DM 211/2010) — Obiettivi Specifici di Apprendimento
  • Esame di Stato del secondo ciclo — quadri di riferimento e griglie di valutazione

Argomento precedente

Strutture dati e loro implementazione

Argomento successivo

Architettura dei computer (AC)

EuraStudy·Appunti T·04·MMXXVI

Continua con l'argomento successivo: il percorso viene conservato.