Programmazione orientata agli oggetti
La programmazione orientata agli oggetti (OOP, Object Oriented Programming) è un paradigma di programmazione, che prevede di raggruppare in un'unica entità (la classe) sia le strutture dati che le procedure (o funzioni) che operano su di esse, creando per l'appunto un "oggetto" software dotato di proprietà (dati) e metodi (procedure) che operano sui dati dell'oggetto stesso.
La modularizzazione di un programma viene realizzata progettando e realizzando il codice sotto forma di classi che interagiscono tra di loro. Un programma ideale, realizzato applicando i criteri dell'OOP, sarebbe completamente costituito da oggetti software (istanze di classi) che interagiscono gli uni con gli altri.
La programmazione orientata agli oggetti è particolarmente adatta a realizzare interfacce grafiche.
Storia
modificaIl concetto di classe può essere considerato l'erede del tipo di dato astratto, una tendenza che si è sviluppata all'interno del paradigma della programmazione procedurale, tipica di linguaggi come il C, secondo la quale un modulo dovrebbe implementare un tipo di dato definito dall'utente, con cui si possa interagire solo attraverso una interfaccia ben definita, che nasconda agli altri moduli i dettagli dell'implementazione, in modo che sia possibile modificarli contenendo gli effetti della modifica sul resto del programma. La classe può essere vista come il costrutto che permette di realizzare questa astrazione con un supporto strutturato da parte del linguaggio.
Il primo linguaggio di programmazione orientato agli oggetti fu il Simula (1967), seguito negli anni '70 da Smalltalk e da varie estensioni del Lisp. Negli anni '80 sono state create estensioni orientate ad oggetti del linguaggio C (C++, Objective C, e altri), e di altri linguaggi (Object Pascal). Negli anni '90 è diventato il paradigma dominante, per cui gran parte dei linguaggi di programmazione erano o nativamente orientati agli oggetti o avevano una estensione in tal senso.
Oggi i linguaggi più usati tra quelli che supportano solo il paradigma di programmazione orientata agli oggetti sono Smalltalk ed Eiffel. Tuttavia sono linguaggi in generale poco usati. I linguaggi più usati sono invece quelli che supportano anche il paradigma di programmazione orientata agli oggetti, come C++, Java, Delphi, Python, C#, Visual Basic .NET, Perl.
Le tre proprietà principali del paradigma di programmazione ad oggetti, spesso dette anche pilastri sono:
- Incapsulamento
- Ereditarietà
- Polimorfismo
(vedi sotto)
Classi
modificaLe classi sono uno strumento per costruire strutture dati che contengano non solo dati ma anche il codice per gestirli.
Come tutti i costrutti che permettono di definire le strutture dati, una classe definisce un nuovo tipo di dato.
I membri di una classe sono:
- dati (esattamente come i membri di un record), chiamati attributi;
- e metodi, ovvero procedure, che operano su un oggetto.
Dal punto di vista matematico, una classe definisce un insieme in modo intensivo, ovvero definendone le caratteristiche invece che elencandone gli elementi. Se l'accesso agli attributi è ristretto è inoltre possibile creare vincoli sui possibili valori che gli attributi possono o non puossono assumere, e anche sulle possibili transizioni tra questi stati. Un oggetto può quindi essere visto come una macchina a stati finiti.
Una classe può dichiarare riservate una parte delle sue proprietà e/o dei suoi metodi, e riservarne l'uso a sé stesso e/o a particolari tipi di oggetti a lui correlati.
Oggetti
modificaUn oggetto è una istanza di una classe. Un oggetto occupa memoria, la sua classe definisce come sono organizzati i dati (gli attributi) in questa memoria.
Ogni oggetto possiede tutti gli attributi definiti nella classe, ed essi hanno un valore, che può:
- mutare durante l'esecuzione del programma (in questo caso l'attributo è definito mutabile)
- o rimanere fissi dalla creazione alla distruzione dell'oggetto (in questo caso l'attributo è definito immutabile).
Un oggetto è immutabile se e solo se tutti i suoi attributi sono immutabili.
Uno dei pilastri del paradigma OOP è l'incapsulamento che impone che si debba accedere agli attributi dell'istanza solo tramite metodi invocati su quello stesso oggetto.
Sintatticamente, i metodi di una classe vengono invocati "su" un particolare oggetto, e ricevono come parametro implicito l'oggetto su cui sono stati invocati. Questo parametro normalmente può essere referenziato esplicitamente; per esempio, a tale scopo in C++, in Java, e in C# si usa la parola chiave this
, mentre in Smalltalk, in Objective-C, Python e in Ruby si usa la parola-chiave self
.
Gli oggetti effettivamente creati sono membri dell'insieme definito dalla loro classe.
Molti linguaggi forniscono un supporto per l'inizializzazione automatica di un oggetto, con uno o più speciali metodi detti costruttori. Analogamente, la fine della vita di un oggetto può essere gestita con un metodo detto distruttore.
Incapsulamento
modificaL'incapsulamento è la proprietà per cui un oggetto contiene ("incapsula") al suo interno gli attributi (dati) e i metodi (procedure) che accedono ai dati stessi. Lo scopo principale dell'incapsulamento è appunto dare accesso ai dati incapsulati solo attraverso i metodi definiti, nell'interfaccia, come accessibili dall'esterno. Gestito in maniera intelligente, l'incapsulamento permette di vedere l'oggetto come una black-box, cioè una scatola nera di cui, attraverso l'Interfaccia sappiamo cosa fa e come interagisce con l'esterno ma non come lo fa. I vantaggi principali portati dall'incapsulamento sono: robustezza, indipendenza e l'estrema riusabilità degli oggetti creati...
Ereditarietà
modificaL'OOP prevede un meccanismo molto importante, l'ereditarietà, che permette di derivare nuove classi a partire da classi già definite. L'ereditarietà permette di aggiungere membri ad una classe, e di modificare il comportamento dei metodi, in modo da adattarli alla nuova struttura della classe.
Da una stessa classe è possibile costruire diverse classi derivate. Da una classe derivata è possibile derivarne un'altra con lo stesso meccanismo.
Sintatticamente, una classe può essere definita come derivata da un'altra classe esistente. In molti linguaggi la classe derivata, o sottoclasse, eredita tutti i metodi e gli attributi della classe "genitrice", e può aggiungere membri alla classe, sia attributi che metodi, e/o ridefinire il codice di alcuni metodi.
L'ereditarietà può essere usata come meccanismo per gestire l'evoluzione ed il riuso del software: il codice disponibile definisce delle classi, se sono necessarie modifiche, vengono definite delle sottoclassi che adattano la classe esistente alle nuove esigenze.
Sottotipazione
modificaSe un oggetto di una sottoclasse può essere utilizzato al posto di un'istanza della superclasse, il tipo della classe derivata è detto sottotipo. Questo richiede che tutti i metodi della superclasse siano presenti nella sottoclasse, e che le signature siano compatibili. Di conseguenza, una sottoclasse che voglia definire un sottotipo può ridefinire i metodi della superclasse, ma non può eliminarli sintatticamente né modificare le loro signature.
In numerosi linguaggi, invece, una sottoclasse può decidere di eliminare o cambiare le proprietà di accesso ad un metodo, il che fa sì che l'operazione di subclassing non sia corrispondente a quella di subtyping. Alcuni linguaggi ad oggetti, in particolare Sather, dividono esplicitamente a livello sintattico subclassing e subtyping.
In linguaggi con tipizzazione statica esplicita, una variabile dichiarata di tipo puntatore o riferimento ad una certa classe può fare riferimento ad oggetti sia del tipo per cui è dichiarata che di tipi da esso derivati. Il tipo effettivo della variabile viene quindi in generale definito a runtime, e può essere modificato durante l'esecuzione del programma.
Esempio - Ereditarietà
modificaSe nel mio programma esiste già una classe "mezzoditrasporto" che ha come proprietà i dati di posizione, velocità, destinazione e carico utile, ed occorre una nuova classe "aereo", è possibile crearla direttamente dall'oggetto "mezzoditrasporto" dichiarando una classe di tipo "aereo" che eredita da "mezzoditrasporto" e aggiungendovi anche il dato "Quota di crociera", con il vantaggio che la nuova classe sarà sia un "aereo" che un "mezzoditrasporto", permettendo di gestire in modo omogeneo tutti i mezzi con una semplice lista di "mezziditrasporto".
Polimorfismo
modificaLa possibilità che le classi derivate implementino in modo differente i metodi e le proprietà dei propri antenati rende possibile che gli oggetti appartenenti a delle sottoclassi di una stessa classe rispondano diversamente alle stesse istruzioni. Ad esempio in una gerarchia in cui le classi Cane e Gatto discendono dalla superclasse Animale potremmo avere il metodo cosaMangia() che restituisce la stringa "carne" se eseguito sulla classe Cane e "pesce" se eseguito sulla classe Gatto. I metodi che vengono ridefiniti in una sottoclasse sono detti "polimorfi", in quanto lo stesso metodo si comporta diversamente a seconda del tipo di oggetto su cui è invocato.
In linguaggi in cui le variabili non hanno tipo, come Ruby, Python e Smalltalk è possibile richiamare un qualsiasi metodo su di un qualsiasi oggetto, sebbene ciò comporti la possibilità di errori a run-time, che in particolare sorgono quando l'oggetto non dispone del metodo che si cerca di invocare. Tali errori sono eliminabili da linguaggi puramente statici, in quanto essi vengono "scovati" già a compile-time.
Le buone regole di programmazione ad oggetti prevedono che quando una classe derivata ridefinisce un metodo, il nuovo metodo abbia la stessa semantica di quello ridefinito, dal punto di vista degli utenti della classe. Nell'esempio di cui sopra, se le specifiche della classe Animale asseriscono che il metodo cosaMangia() deve restituire una stringa di non più di 20 caratteri, allora le classi Cane e Gatto dovranno rispettare tale regola. In caso contrario, un client che utilizzi un oggetto Cane o Gatto tramite il tipo Animale (faccia, cioè, l'upcasting dell'oggetto) potrebbe ottenere un risultato non desiderato e ciò potrebbe generare a run-time errori difficili da scovare.
Binding dinamico
modificaIl polimorfismo è particolarmente utile quando la versione del metodo da eseguire viene scelta sulla base del tipo di oggetto effettivamente contenuto in una variabile a runtime (invece che al momento della compilazione). Questa funzionalità è detta binding dinamico (o late-binding), e richiede un grosso sforzo di supporto da parte della libreria runtime del linguaggio.
Se ho una variabile di tipo A, e il tipo A ha due sottotipi (sottoclassi) B e C, che ridefiniscono entrambe il metodo m(), l'oggetto contenuto nella variabile potrà essere di tipo A, B o C, e quando sulla variabile viene invocato il metodo m() viene eseguita la versione appropriata per il tipo di oggetto contenuto nella variabile in quel momento.
Per ritornare all'esempio di poco fa, supponiamo che un "aereo" debba affrontare procedure per l'arrivo e la partenza molto più complesse di un normale camion, come in effetti è: allora le procedure arrivo() e partenza() devono essere cambiate rispetto a quelle della classe base "mezzoditrasporto". Quindi provvediamo a ridefinirle nella classe "aereo" in modo che facciano quello che è necessario (polimorfismo): a questo punto, dalla nostra lista di mezzi possiamo prendere qualsiasi mezzo e chiamare arrivo() o partenza() senza doverci più preoccupare di che cos'è l'oggetto che stiamo maneggiando: che sia un mezzo normale o un aereo, si comporterà rispondendo alla stessa chiamata sempre nel modo giusto.
Il binding dinamico è supportato dai più diffusi linguaggi di programmazione ad oggetti come il Java o il C++. C'è però da sottolineare che in Java il binding dinamico è implicitamente usato come comportamento di default nelle classi polimorfe, mentre il C++ per default non usa il binding dinamico e se lo si vuole utilizzare bisogna inserire la keyword virtual nella signature del metodo interessato.
Il supporto runtime di una chiamata di metodo polimorfa richiede che ad una variabile polimorfa venga associato un metadato implicito che contiene il tipo del dato contenuto nella variabile in un dato momento, oppure la tabella delle funzioni polimorfe.
Diversi approcci all'OOP
modificaProblemi dei linguaggi OOP
modificaUn linguaggio OOP può soffrire di problemi di efficienza se l'approccio OOP viene applicato a tutto indiscriminatamente, come avviene nel linguaggio Smalltalk che utilizza gli oggetti anche per i tipi primitivi. Un approccio che per alcuni appare più pratico e realista è quello adottato da linguaggi come Java e C++ che limitano la creazione di oggetti alle sole entità che il programmatore decide di dichiarare come tali più eventualmente una serie di oggetti predefiniti, per lo più riguardanti il sistema. In questo modo tali linguaggi restano efficienti, ma l'uso degli oggetti creati richiede più attenzione e più disciplina. Non esistono al momento dati validi che permettano di dimostrare con certezza che uno dei due approcci sia intrinsecamente migliore dell'altro.
Inoltre il fatto di ridefinire i metodi ereditati dalle classi base può portare a introdurre errori nel programma se per caso questi sono usati all'interno della classe base stessa (il noto problema della classe base fragile).
Collegamenti esterni
modifica- bluej: Ambiente di sviluppo open source per l'insegnamento della programmazione ad oggetti (in Java)
- Python, un potente linguaggio ad oggetti.