ToolBox Logo  home  up


Piccolo Cane

posta


 
 
 
Oggetti
 
 

    Se esiste un termine inflazionato nell'ambito dell'informatica attuale questo è certamente quello di "programmazione ad oggetti", od Object Oriented Programming, in sigla OOP. Sembra che tutto quello che è bello, potente, moderno non possa essere che "orientato agli oggetti".
 
    Eppure, se si cerca di capire di cosa si tratti, senza aver prima provato effettivamente a programmare nel vecchio e nel nuovo modo, si rischierà di restare spiazzati. In genere infatti le introduzioni all'argomento che si trovano sono di due tipi, opposti quanto a difficoltà ma di solito entrambi praticamente inutili per farsi un'idea dell'argomento, perché troppo semplicistiche o troppo tecniche.
 
    Le prime, sostanzialmente spiegano che un oggetto "è un oggetto", e gli esempi che portano sono sempre zoologici (programmiamo l'oggetto "equino" e poi gli aggiungiamo l'ulteriore proprietà di avere le strisce, ottenendo così il nuovo oggetto "zebra"), al punto che ci si può chiedere se la cosa sia adatta solo a dei biologi, o se possa essere usata per farci davvero qualcosa di utile.
 
    In questo tipo di spiegazioni si mostra come nella OOP ci sia un meccanismo, l'ereditarietà, che permette di costruire oggetti più complessi a partire da uno semplice. L'oggetto così spiegato è però sostanzialmente solo il "modulo", un concetto presente in linguaggi precedenti l'OOP, come Modula 2 ad esempio. L'ereditarietà come viene qui presentata è semplicemente una "estensione di moduli", un servizio che il compilatore ci fa, evitandoci, per esempio, di riscrivere tutte le caratteristiche di "equino" quando programmiamo "zebra", in modo che noi dobbiamo solo "aggiungere le strisce".
 
    A volte addirittura ci si ferma prima, e quello che si presenta è in sostanza l'oggetto come "componente". La programmazione attraverso componenti ha avuto un largo successo con la diffusione di Visual Basic, ma i componenti mancano di una caratteristica fondamentale degli oggetti della OOP, l'ereditarietà, pur possedendone un'altra, l'incapsulamento.
 
    Il secondo tipo di presentazioni invece entra pesantemente nell'argomento, introducendo una marea di concetti (riusabiltà, ereditarietà, incapsulamento, polimorfismo, ADT, classi, istanze, dynamic binding, ecc.), al punto che uno che non debba per necessità imparare a programmare ad oggetti può pensare che la cosa sia troppo difficile.
 
    E spesso la cosa è resa più difficile dal linguaggio ad oggetti usato, cioè il C++ o, più di recente, Java. Il problema di entrambi è quello di essere derivati dal C, ottimo linguaggio per la programmazione a livello di sistema, ma con il difetto di avere una sintassi criptica. Inoltre, cosa che complica ancora di più la situazione, entrambi sono linguaggi "misti", avendo in sé sia costrutti OOP che imperativi. Java da questo punto di vista è molto migliore del C++, pur tuttavia il fatto di avere alcuni dati di tipo "oggetto" e altri di tipo "primitivo" porta la sua dose di inutili complicazioni.
 
    Per imparare e capire i concetti della OOP è invece importantissimo utilizzare un linguaggio ad oggetti "puro". In questi le infinite difficoltà causate dalla mescolanza dei due tipi di programmazione spariscono del tutto, e i concetti fondamentali della OOP sono le uniche cose da affrontare. Per la cronaca, gli unici linguaggi "Object Oriented" puri di una certa diffusione si chiamano Smalltalk ed Eiffel.
 
    Dopo tal premessa si capirà che chi scrive queste poche righe, dopo aver speso una notevole quantità di tempo per cercare di comprendere cosa si intendesse per programmazione ad oggetti, cercherà di stare nel mezzo (assieme alla virtù ;^). Più precisamente, proverò a spiegarvi approfonditamente in che cosa, e perché, la programmazione ad oggetti sia diversa da quella ordinaria, e così tanto superiore a questa, senza tuttavia complicare tale spiegazione con dettagli sintattici del codice reale di qualche, pur bello, linguaggio OOP.
 
    Innanzi tutto, come è noto, programmare è mettere assieme dati ed operazioni che su questi agiscono (algoritmi). Nella programmazione normale (imperativa) il dato è qualcosa di semplice e "passivo", e la sua caratteristica più importante è il numero dei bit di cui è costituito: un numero intero "corto" (integer) fatto con un byte, od un numero reale fatto con due, ad esempio. Il dato ha sì un suo tipo, e quindi per esempio sui bit di un numero reale si opera in modo diverso che su quelli di un intero, ma rimane pur tuttavia un insieme di bit, "passivo". La componente attiva, dominante, è la funzione, o procedura, che agisce sui bit passivi. Per questo la normale programmazione, imperativa, viene anche detta procedurale: procedure e funzioni sono al centro dell'attenzione.
 
    Nella programmazione ad oggetti il rapporto dati-procedure si inverte, ed i primi prendono il centro della scena e vengono riferiti con il nome, forse l'avrete intuito, di "oggetti". Le procedure diventano dei semplici "comportamenti" degli oggetti. Sembra che un oggetto divenga una cosa intelligente, strutturata, complicata, come un oggetto reale.
 
    Quando si spiega la OOP partendo dagli oggetti, è inevitabile di finire per pensare che l'oggetto non possa che essere molto "grosso", cioè essere costruito con molti bit, e fornito di molto codice che descriva i suoi complicati comportamenti. Per esempio, rifacendoci agli esempi zoologici di sopra, si è portati a pensare ad oggetti come "zebra", con un sacco di caratteristiche e proprietà, e un gran numero di comportamenti particolari. In sostanza, si pensa alla "simulazione" di oggetti reali complessi. Questo è in realtà proprio l'ambito in cui è nata la programmazione ad oggetti: il primo linguaggio OO si chiamava Simula e serviva proprio allo scopo che il nome suggerisce.
 
    Così, non si pensa minimamente che dati semplici, come un misero numero intero, possano essere oggetti con una loro forma rudimentale di "intelligenza comportamentale". Sembra un controsenso anche perché, per fornirgli questa intelligenza e sofisticazione, bisognerebbe contornare i pochi bit di cui è costituito il numero intero di chissà quant'altro, che però sarebbe fatto di dati quasi altrettanto semplici di esso, che a loro volta sembrerebbero richiederne altri, in un ciclo senza fine. Una zebra può avere un sacco di numeri che descrivono la sua grandezza, il suo peso, l'età ecc, ma un semplice intero?
 
    Questa divisione è incoraggiata anche dai linguaggi di programmazione "misti", cioè non OO puri, come il C++, l'Object Pascal e, più recentemente, Java. Qui ci sono effettivamente due tipi di dati diversi, quelli "primitivi", come caratteri, numeri interi e reali, valori booleani ecc. (cioè diverse combinazioni di bit), con i quali si "costruiscono" effettivamente i veri dati di tipo "oggetto", che sono quindi qualcosa di complesso.
 
    Il centrare l'attenzione sull'oggetto, purtroppo, manca del tutto l'obiettivo di spiegare l'essenza della programmazione ad oggetti. L'oggetto infatti è solo il "come", non il "perché". La vera cosa che fa la differenza tra OOP e programmazione ordinaria è l'astrazione. Se mi permettete un gioco di parole, è dannoso credere che l'astrazione sia un concetto "astratto". Certo, è pane quotidiano dei matematici (anzi, si potrebbe dire che la matematica, specie nella sua forma moderna, è semplicemente pensiero astratto), ma questo non vuol dire che il normale ragionare umano ne sia privo. Si può al contrario dire che tutto il pensiero umano è astrazione. Ed è esattamente questa l'enorme differenza con il "pensiero" dei computer, ed è da qui che nasce l'impressione che i computer siano molto "pignoli", ma poco "intelligenti".
 
    La programmazione ad oggetti è solo la modalità tecnica (tanto per spaventare il coraggioso lettore: l'associazione dinamica di procedura in dipendenza dallo stato dell'oggetto al momento dell'esecuzione o, detto all'inglese, il " procedure dynamic binding, at runtime") che i progettisti di linguaggi di programmazione hanno trovato, per introdurre la potenza del pensiero astratto nel mondo dei computer, "umanizzando il loro modo di pensare", se così si può dire.
 
    Ma non restiamo ancora nel vago (diremmo astratto!). Come primo esempio, che resta parzialmente solo sul versante dell'oggetto come componente, pensiamo ad un ingegnere cui sia affidato il compito di progettare un intero aeroplano (potete pensare ad un piccolo aereo, visto che è troppo irrealistico pensare che una sola persona ne progetti uno grande). Questo significa forse che si sarà autorizzati a pensare che il brav'uomo conosca tutti i dettagli di funzionamento, per esempio, del motore ad elica o della radio di bordo? Ovviamente no. E questo perché quando lui progetta l'aereo lo fa pensando a componenti già disponibili, come appunto un motore o una radio, di cui lui deve solo conoscere le caratteristiche esterne, cioè i servizi che questi componenti mettono a disposizione. Il suo lavoro consiste nel metterli assieme secondo un progetto coerente che trasformi un mucchio di ferraglia in un'opera tecnologica funzionante.
 
    Questa metafora coglie una prima parte delle caratteristiche degli oggetti della OOP, l'incapsulamento, quello che fa sì che si possa parlare di una parte (il motore) all'interno del tutto (l'aereo), in modo che per il funzionamento di quest'ultimo sia necessario conoscere solo le caratteristiche "esterne" del componente (ad esempio, la potenza del motore). Come detto sopra però, si ha uso di componenti anche in linguaggi come Visual Basic, che sono molto distanti dalla potenza espressiva dei linguaggi orientati agli oggetti.
 
    Per capire l'essenza della OOP bisogna arrivare all'idea di "componente intelligente", a cui è dedicato il secondo esempio.
 
    Immaginate di essere un allenatore di pallacanestro, che come tutti gli allenatori di pallacanestro, è molto prolifico nel produrre schemi di gioco che la squadra dovrà poi attuare con precisione. Per descrivere il gioco userete termini noti ad un cestista (stoppa, fai un blocco, tira da tre, schiaccia, se non va' subito prendi il rimbalzo, fai fallo, aspetta lo scadere del tempo, ecc.), termini che ad uno che non sa giocare possono anche non dir nulla. Usando questi termini, la vostra descrizione risulterà semplice e lineare, descriverà l'azione nella sua essenza senza perdersi in dettagli inutili . E tuttavia la descrizione non esaurirà tutto, perché vi possono capitare giocatori che la eseguono in modi che non avevate ancora immaginato. E' sempre il vostro schema, ma se ad eseguirlo avete Michael Jordan, risulterà molto più soddisfacente, andrà al di là di quello che credevate possibile mentre lo pensavate.
 
    Un programma Object Oriented è come lo schema dell'allenatore. Descrive un'azione che degli oggetti (i giocatori) saranno chiamati ad eseguire. Il punto importante è che i giocatori sanno cosa vogliono dire le azioni contenute nello schema, hanno una loro intelligenza, esperienza e capacità, sono dei soggetti attivi, non passivi.
 
    Un programma ordinario si può invece paragonare a tutto ciò che un appassionato di giochi elettronici, che avesse a disposizione una squadra di robot (totalmente passivi), sarebbe costretto a comandare dalla console per far sviluppare la stessa azione. Nella descrizione di queste manovre mancherà proprio l'astrazione, perché i dettagli dei movimenti meccanici (di braccia, gambe ecc.), oscureranno il "senso" dello schema, e per di più, dovrà essere adattata a quel tipo di robot, alle loro particolari capacità "fisiche".
 
    La programmazione normale prevede che si descrivano le azioni che i dati "subiscono", avendo il pieno controllo di tutto. Se il sistema diventa molto grande, come è già una squadra di robot, è ovvio che un singolo programmatore ad un certo punto non riuscirà più a gestirlo. La cosa non cambia nemmeno con un team di programmatori, perché ogni pur piccolo pezzo di codice che ciascuno dovrà esaminare, dovrà essere capito in tutta la sua estensione, dal più alto livello, giù giù fino al bit.
 
    Questo preclude la tipica costruzione per gerarchie e livelli: con componenti semplici se ne costruiscono di più complessi, e quelli che montano l'oggetto completo non devono affatto essere più intelligenti di quelli che costruiscono il componente più minuscolo. Ma soprattutto, chi deve ideare il montaggio finale può benissimo ignorare il funzionamento interno dei componenti che usa: l'unica cosa che gli deve interessare, è il fatto se facciano o meno quello per cui vengono utilizzati. Così ciascun partecipante al progetto si occuperà del lavoro "al suo livello", astraendo dai dettagli dei componenti.
 
    Quando si programma nello stile della OOP, si rimane sempre ad un livello astratto, ordinando delle azioni ad oggetti che si suppone le sapranno eseguire. Come può capitare ad un allenatore che metta in campo una persona che ignora il gioco, può essere possibile che nella realtà ci capiti un oggetto che non sa cosa fare, nel qual caso si avrà un errore di funzionamento. Ma il principio di dividere il compito di approntare schemi di gioco (all'allenatore) da quello di conoscere le giocate (al giocatore) è assolutamente fondamentale per creare una grande squadra. Allo stesso modo, le civiltà industriali non sono basate sull'opera di inventori geniali che hanno costruito un aereo dal nulla, ma sull'opera cooperativa di tante intelligenze, ognuna delle quali inventa o migliora un piccolo componente, che qualcun altro utilizzerà per costruire qualcosa di più complesso, e nuovo.
 
    I due esempi di sopra si possono trasportare immediatamente nel mondo dei computer. Si può per esempio pensare, rispettivamente, ai programmi di controllo e navigazione di un aereo, e ad un videogioco di una partita di basket (per semplicità pensate ad una partita completa che vedete giocare, non ad un programma interattivo come al solito).
 
    Nel caso dell'aereo, chi programma un componente, dovrà conoscere le caratteristiche ed i comportamenti di un certo numero di altri componenti, ma solo quelli "ufficiali", non quelli "interni". Per esempio, il pilota automatico avrà bisogno di sapere come agire sul motore per aumentare la potenza di spinta, ma altri particolari, come ad esempio se è un jet o un motore ad elica, potranno rimanere nascosti perché ininfluenti. Così chi programma il componente "pilota automatico", potrà pensare che il suo pezzo di software continui ad essere usato anche quando si cambiasse il tipo di propulsione, proprio perché descrive l'azione da eseguire in termini sufficientemente astratti da includere anche quella con un motore non ancora inventato. La programmazione ordinaria invece, non riesce a fare nulla di tutto questo, proprio perché per comandare le azioni ha bisogno di nominarle in concreto, e quindi conoscere tutti i particolari di chi dovrà eseguirle, cioè la precisa procedura da attivare.
 
    Nell'esempio del videogioco, ci sarà un primo programmatore che crea il programma della partita, magari copiando pari pari una partita reale che ha appena visto in TV. E ci potrà essere un secondo programmatore che dieci anni dopo riutilizza il programma del collega senza modificarne una virgola, ma facendolo eseguire con altri oggetti (giocatori, palla, campo di gioco) con caratteristiche molto migliorate, per esempio nella grafica, nella fluidità dei movimenti dei giocatori ecc. Nel secondo caso si giocherà una partita astrattamente uguale a quella che il primo programmatore aveva pensato, anche se, vedendola, questo non potrà certo dire di aver previsto che sarebbe stata realizzata così. Allo stesso modo, utilizzando tecniche non OO, un programmatore avrebbe potuto scrivere un videogioco per la sua epoca, per esempio una versione di Tetris, cominciando a prescrivere dettagli grafici come la risoluzione dello schermo a 300x400, in modo che dieci anni dopo, un suo collega che volesse aggiornarlo alla molto miglior grafica disponibile, non potrebbe far altro che cercare di capirne la logica interna, e tradurre una per una le azioni comandate. Naturalmente ci sarebbe una versione astratta del gioco, ma questa sarebbe rimasta nella testa del primo programmatore, perché nei bit del programma sarebbe stata incisa solo la sua versione concreta, con i mezzi di cui al momento disponeva. Il secondo programmatore dovrà quindi faticare per invertire il processo, dal concreto all'astratto, per poi riconcretizzare tutto.
 
    Da quanto detto, si può capire che programmare in stile OO risulta molto più diretto e generale, e che il lavoro così fatto può essere riutilizzato più e più volte, sconfiggendo il principale problema della programmazione ordinaria, quell'impressione che chi programma ha, di fare spesso le stesse cose in modi tra loro poco diversi, senza tuttavia essere capaci di riutilizzare parti di programma già scritte.
 
    Dopo questa introduzione, forse lunga ma certamente necessaria per dare un'idea di dove si intenda arrivare, veniamo finalmente ai mezzi concreti che la OOP utilizza per raggiungere i suoi scopi.
 
    Vedremo adesso il punto cruciale che differenzia programmazione ordinaria ed OOP, e cioè il diverso modo di impartire un comando (=funzione, f) ad un oggetto (=dato, x).
 
    Nella programmazione ordinaria, procedurale, ai dati si applicano "funzioni" che li trasformano, producendo i risultati. Per la cronaca, le procedure sono funzioni in cui i dati che subiscono trasformazione non sono espliciti, e questo tipo di programmazione non è detta funzionale solo perché il nome è già usato per un altro tipo di concetti, quelli che riguardano il Lisp ed i linguaggi ad esso affini. L'applicazione della funzione f al dato x viene indicata di solito con la stessa simbologia d'uso in matematica:
 
            f(x)
 
    Anche nell'ordine di apparizione dei termini, si vede che viene prima la funzione, e poi il dato. Il codice fa quindi riferimento ad una ben determinata funzione (già scritta da qualcuno) che accetta solo certi tipi di dato, quelli previsti dal suo autore. Quindi scrivere:
 
            f( )
 
    implica già il conoscere l'esatto tipo del dato x. Per esempio, noi possiamo scrivere una funzione Raddoppia (x) che agisce su numeri nel seguente modo:
 
            Raddoppia (x) = x + x
 
    Se pensiamo però di aver fatto un lavoro una volta per tutte, dobbiamo presto ricrederci. Quando si definisce una funzione in un normale linguaggio di programmazione infatti, bisogna esplicitamente informare il compilatore che produce il programma eseguibile dell'esatto tipo del dato x, se sia esso un intero corto, lungo, oppure ancora un numero reale. E questo non è un problema di insufficiente capacità di chi ha creato il linguaggio di programmazione, è una necessità inderogabile.
 
    In Pascal per esempio dovremo scrivere:
 
            function Raddoppia (x : integer) : integer
                    begin
                            Raddoppia := x + x
                    end
 
    Questa funzione "funzionerà" però solo con i numeri interi "corti" (integer), e bisognerà riscriverla per i lunghi, i reali ecc. Noi avremo in testa il semplicissimo concetto di "raddoppio", ma non avremo alcun modo di tradurlo in codice una volta per tutte. E c'è da notare che questo concetto non si limita ai tipi primitivi di numeri che il linguaggio di programmazione ci fornisce, perché potremmo applicarlo anche, ad esempio, a vettori e matrici di numeri. Ritorneremo tra un attimo su questo.
 
    Nella OOP, non si avrà più l'applicazione della funzione f al dato x, bensì "l'invio del comando f all'oggetto x", scritto (notazione puntata):
 
            x.f
 
    Chi scrive la funzione f, la strutturerà come una sequenza di comandi cui l'oggetto x obbedirà. Cosa avrà necessità di conoscere sul tipo effettivo di oggetto che sarà x? Importante è solamente che x conosca i comandi di cui f è costituita, e sappia reagire ad essi. Solo alcuni tipi di oggetti conosceranno quei comandi, per cui il programmatore che scrive f dovrà sapere a che tipi di oggetti rivolgersi. Sembra che siamo da capo, di nuovo a scrivere la funzione "Raddoppia" per gli interi, i reali ecc. In realtà, disponendo di un linguaggio OOP si può lo stesso ricadere nel problema, ma si hanno, se si vuole, i mezzi per evitarlo. Se si osserva la definizione "astratta" di "Raddoppia":
 
            Raddoppia (x) = x + x
 
    si vede che l'unica cosa indispensabile è che l'oggetto x sappia cosa vuol dire l'addizione. Nella OOP (almeno in quella vera, di Eiffel e Smalltalk per esempio) noi potremmo scrivere la funzione Raddoppia prevedendo di applicarla, come si dice, "alla classe degli oggetti addizionabili". Potremmo scrivere, usando una sintassi quasi alla Eiffel:
 
            class Addizionabili
            ...
                     feature Raddoppia is
                             Result := x + x
                     end
             ...
 
    in cui, alla classe di oggetti Addizionabili, cioè quelli che conoscono come rispondere al comando + , rendiamo noto cosa voglia dire il comando (o caratteristica: feature) Raddoppia, spiegandoglielo in termini di addizioni, che loro comprendono.
 
    A questo punto, la funzione Raddoppia si estenderà a tutti gli oggetti Addizionabili, tra cui ci potranno essere numeri interi e reali, che sono già sottotipi del tipo addizionabile per il linguaggio di programmazione, ma anche tipi di dato costruiti da noi, per esempio matrici di numeri. Basterà che i nuovi oggetti siano dichiarati "eredi" del tipo addizionabile:
 
            class Matrici inherit Addizionabili
 
    e si spieghi loro come reagire al comando caratterizzante la classe di oggetti Addizionabili, cioè il + . Nel caso delle matrici questo significherà addizionare tra loro tutti gli elementi di identica posizione.
 
    A questo punto, i comandi:
 
            intero_tal_dei_tali.Raddoppia
 
     oppure:
 
             matrice_tal_altra.Raddoppia
 
    saranno interpretati ed eseguiti, perché Raddoppia è scritto usando solo l'addizione, e sia l'intero che la matrice sanno cosa vuol dire sommarsi, perché sono "eredi", potremmo dire sottotipi, della classe di oggetti Addizionabili.
 
    Come si vede, la programmazione OOP è "nominale", cioè il suo codice è una sequenza di comandi di cui si conosce il nome ma non l'esatta esecuzione: chi scrive Raddoppia non conosce affatto cosa bisogna fare per raddoppiare una matrice. E' infatti la matrice, come oggetto intelligente che sa rispondere a dei comandi, che sa cosa fare quando le viene inviato il comando Raddoppia. E' del tutto ovvio che ci vorrà qualche programmatore (quello che implementa la classe Matrici) che scriva nei dettagli cosa vuol dire fare l'addizione di matrici, ma il punto importante, centrale, fondamentale, è che qualcuno avrà potuto scrivere una volta per tutte la funzione Raddoppia, cogliendola nella sua più totale astrazione, e tutti gli altri potranno riutilizzare il suo lavoro, senza ripartire ogni volta dall'invenzione della ruota.
 
    Questo modo di procedere non produce programmi "che si scrivono da soli", semplicemente ripartisce il lavoro nel modo più opportuno, separando il compito di definire comandi come Raddoppia, da quello di programmare l'addizione di matrici, in modo che combinandoli, non sia più necessario per nessuno scrivere direttamente il caso (concreto) di "raddoppio di matrice".
 
    Ovviamente la semplicità dell'esempio portato non implica che in OOP ci si occupi di cose così stupide, anzi. Ci sono per esempio delle costruzioni ricorrenti con frequenza quotidiana nel lavoro di programmazione, come liste, tabelle, stack ecc., e tutte possono essere colte nella loro generalità programmandole una volta per tutte, e creando i casi concreti che di volta in volta servono quali eredi di quelli generali.
 
    Possiamo ora passare ad una spiegazione dettagliata dei termini tipici della OOP, che potranno essere comprensibili avendo presente quanto detto finora.
 
    Innanzi tutto, in OOP il tipo di dato viene sostituito dal concetto di "classe di oggetti". Come nella programmazione ordinaria, quando si specifica una classe di oggetti, si elencano le parti di cui questi oggetti sono composti (per esempio, un vettore tridimensionale è composto di tre numeri reali), ma in più si definisce anche il "comportamento" di questi tipi di oggetti, cioè i comandi ("messaggi") a cui questi tipi di oggetti sapranno reagire. Le azioni che porranno in essere quando riceveranno un messaggio, saranno delle procedure interne, i cosiddetti "metodi" (di risposta, si intende).
 
    Importante è capire che sia i sottocomponenti dell'oggetto che queste procedure interne, che costituiscono "l'implementazione" dell'oggetto e che dal punto di vista di un programmatore ordinario sarebbero tutto, dal punto di vista della OOP non sono nemmeno (direttamente) necessarie.
 
    Quello che è fondamentale è l'elenco dei nomi dei messaggi a cui quella classe di oggetti sa rispondere, quello che in breve potremmo definire il "vocabolario" dell'oggetto. Si possono definire infatti quelle che vengono dette classi "astratte", che non hanno alcuna implementazione, cioè sono senza una definizione esplicita di cosa siano i loro componenti e le loro procedure interne, ed hanno solo un elenco di messaggi a cui rispondere. Un tipico esempio che rende bene l'idea è, nell'ambito di un programma di grafica, la definizione della classe astratta di "Figura_geometrica". Questa corrisponde ad un'astrazione della nostra mente, e ad essa possiamo associare delle operazioni anch'esse astratte nel senso di applicabili a qualunque figura, come ad esempio sposta in alto di x, ruota di y, dilata di z, riempi con il colore xyz, ecc. Ogni volta che introduciamo un nuovo tipo di figura geometrica, per esempio un cerchio, la definiamo come erede della classe Figura_geometrica:
 
            class Cerchio inherit Figura_geometrica
 
    e implementiamo i suoi sottocomponenti (nel caso del cerchio si può definirlo usando le coordinate del centro e la misura del raggio) e i metodi di risposta a tutti i messaggi della classe Figura_geometrica (per esempio, sposta in alto di x verrà tradotta nell'aggiungere x al valore della coordinata verticale del centro del cerchio). L'implementazione della classe astratta viene quindi eseguita non subito, ma rimandata alle classi sue eredi, cioè alle sue sottoclassi. In Eiffel non si parla di classe astratta, ma di "deferred class", classe la cui implementazione è solo rimandata. Qual é il vantaggio, se poi dobbiamo sempre di volta in volta definire il significato di ciascuna delle operazioni astratte che si possono eseguire su una figura geometrica? Il vantaggio è che si possono concepire e realizzare pezzi di codice OO che utilizzano solo il concetto di figura geometrica e di operazioni generali su di essa, e questi funzioneranno con ogni tipo concreto di figura, magari ancora da realizzare, non appena questa venga dichiarata "erede di Figura_geometrica".
 
    Nelle introduzioni alla OOP tipicamente si riassumono le caratteristiche più importanti di questa con i termini incapsulamento, ereditarietà e polimorfismo. Vediamone il significato.
 
    La stretta unione di dati e procedure di una classe, viene detta "incapsulamento". Questo concetto è legato a quello di segretazione dell'informazione ("information hiding"), ma non coincide del tutto con essa. Nella OOP è necessario legare insieme (incapsulare) dati e procedure che su essi operano perché solo l'oggetto (il suo programmatore cioè) sa come reagire ad un determinato messaggio: lo spostamento di questo compito dalla funzione all'oggetto è il punto cruciale di tutta la OOP. Si può inoltre intendere la parola "capsula" nel senso che l'implementazione concreta dell'oggetto non deve importare a nessuno dei "programmi clienti", cioè quelli che usano quel tipo di oggetti: essi si devono basare solo sul comportamento pubblico dell'oggetto, cioè su quello che esso promette di fare in risposta ai messaggi che gli si mandano, non sul modo concreto con cui internamente lo fa. In questo senso l'interno della capsula deve rimanere nascosto agli utenti dell'oggetto. A questo proposito, può sorgere un dubbio: se i programmi me li faccio tutti da solo, cosa significa che non posso vedere l'interno di un oggetto, forse che devo farmi colpire da amnesia non appena lo utilizzo? In realtà no, ovviamente, significa solo che quando uso un oggetto il compilatore non potrà che permettermi di mandargli messaggi ufficialmente non riconosciuti, anche se so che al suo interno l'oggetto ha altri dati e procedure che non fanno parte della "superficiale visibile" della capsula. E, cosa più sottile ma anche più importante, non dovrò presupporre nell'oggetto certi tipi di comportamenti che so avere (perché l'ho programmato io), ma che ufficialmente non ha, perché non fanno parte delle sue specifiche pubbliche.
 
    Il concetto di "ereditarietà" è quello che permette di applicare i comportamenti astratti definiti da una classe alle sue classi eredi. Questo significa che con:
 
            class Classe_figlia inherit Classe_madre
                     ...
                     ...
 
    si metterà Classe_figlia in grado di rispondere a tutti i messaggi cui sa rispondere Classe_madre, e quindi di utilizzare tutti i programmi scritti per essa. Così Classe_figlia avrà tutte le caratteristiche e capacità di Classe_madre, e qualcosa in più di particolare e solo suo.
 
    Il concetto di polimorfismo è strettamente collegato a quello di ereditarietà, e consiste in questo: uno stesso codice scritto per Classe_madre sarà applicabile anche alla sua erede Classe_figlia, ma in questo caso avrà effetti diversi perché si applica ad un tipo diverso di oggetto, magari molto più complesso del tipo originale. Lo stesso comando avrà quindi due forme diverse di effetti, a seconda dell'oggetto su cui si trova ad agire, sarà cioè "polimorfo". Per capire quale esatto tipo di azione applicare (binding), bisognerà attendere il momento dell'esecuzione (runtime), avendo un collegamento tra comando ed azione che è quindi dinamico, per cui si parla di "runtime dynamic binding".
 
    Questo fatto che dei comandi da noi scritti possano venire interpretati in modo notevolmente diverso da quello che abbiamo in mente mentre scriviamo il nostro programma, mette in luce una possibile debolezza del codice OOP, cioè il fatto che questo codice è "nominale", nel senso che si basa tutto sui nomi dei tipi di oggetti e delle loro caratteristiche. Questo vuol dire che non basta che io dia ordini precisi e che questi vengano interpretati fedelmente da bravi esecutori, se poi per loro non hanno il significato che intendevo io: non basta avere molte parole in comune, se poi queste non hanno lo stesso significato.
 
    Questo è il problema nell'affidare ad altri parte di un lavoro: si può rischiare che non facciano quello che intendevamo. Non per questo però si può pensare di far tutto da sé, rinunciando agli enormi vantaggi della cooperazione: questa debolezza deriva quindi dallo stesso punto di forza del metodo.
 
    Ci sono due linee di comportamento che si possono adottare riguardo al problema "della fiducia": uno è quello di chi non si fida, che controlla che quanto promesso dai fornitori nei contratti sia effettivamente mantenuto, l'altro è quello che si fida del prossimo e si accorge delle eventuali fregature solo all'ultimo momento. E' anche interessante notare che questa distinzione corrisponde, per quanto riguarda l'uso diverso che si fa della lingua, alla distanza tra il linguaggio tecnico-scientifico e quello letterario.
 
    Queste due tendenze sono incarnate dai due linguaggi OOP citati all'inizio, Eiffel e Smalltalk. Il primo incarna lo spirito matematico ed ingegneristico che pretende specifiche di funzionamento, e garantisce risultati sottoponendo tutto a rigidi controlli. Il secondo invece, nell'ambito del quale è stato introdotta non a caso la parola "messaggio" per indicare un comando inviato ad un oggetto, l'associazione è molto più liberale, tanto che può capitare che una esecuzione si interrompa con l'informazione (per l'utente umano stavolta!) "messaggio non compreso", che significa che qualche parte del programma ha avuto un "fraintendimento" con l'oggetto con cui comunicava.
 
    Entrambi sono modi di procedere legittimi, perché adatti ad ambiti e scopi diversi.
 
    Per terminare, è utile un brevissimo riassunto che sprema tutto il contenuto di quanto scritto sopra:
 
    - la programmazione ordinaria ha un difetto fondamentale, quello che bisogna sempre specificare su che preciso tipo di dati una funzione, come Raddoppia, lavora (ad esempio gli interi);
 
    - il ragionare umano non procede però in questo modo, ma per astrazioni: l'unica cosa importate per esprimere Raddoppia è che l'oggetto a cui si applica possa essere addizionato, non se sia un intero, espresso da un byte, o un reale, rappresentato con due;
 
    - i "tipi di oggetti" che quindi ci servono sono quelli per cui hanno significato alcune operazioni astratte: per esempio la classe degli oggetti Addizionabili è quella per cui ha significato l'operatore +;
 
    - un programma che contenga comandi ad oggetti Addizionabili, non è tenuto a sapere cosa esattamente siano questi oggetti, proprio per l'astrazione di cui si diceva. Non potrà essere quindi lui ad eseguire l'operazione di addizione;
 
    - il programma che conosce il modo di addizionare due oggetti Addizionabili di un certo specifico tipo, ad esempio due matrici, deve stare quindi da qualche altra parte;
 
    - nasce così una spontanea e perfetta unione tra il dato, ad esempio una matrice, e il codice che gestisce le operazioni su di essa. L'oggetto della OOP è proprio l'unione di queste due cose: l'operazione diventa una caratteristica (feature) dell'oggetto, che può essere attivata "mandando un messaggio" all'oggetto, in questo caso "sommati a ...";
 
    - un oggetto complesso come una matrice può quindi essere utilizzato anche da un programma scritto da qualcuno che ignorava perfino l'esistenza di tale tipo di oggetti. Basta infatti che questo programma usi solo messaggi di una classe più ampia di oggetti (gli Addizionabili) della quale la classe Matrici abbia ereditato le caratteristiche;
 
    - si vede quindi che la richiesta di astrazione porta necessariamente all'incapsulamento (dati e procedure che su questi operano, strettamente unite in una capsula, la classe), alla ereditarietà (se si dichiara un oggetto erede di una classe generale, l'oggetto saprà rispondere a tutti i comandi della classe progenitrice) e al polimorfismo (l'oggetto derivato interpreterà il messaggio a modo suo, un modo possibilmente diverso da quello della classe originale). Le tre caratteristiche tipicamente invocate per descrivere l'OOP derivano quindi in modo naturale dalla sola necessità di astrazione;
 
    - la programmazione OOP è sostanzialmente nominale, e questo modo di procedere può essere lasciato alla libera anarchia e creatività, come in Smalltalk, o essere interpretato alla stringente maniera dei matematici, come in Eiffel, ottenendo due diversi, entrambi utili, modi di realizzare la programmazione ad oggetti.
 
    Questo è quanto, riguardo ai "perché" della programmazione ad oggetti. Non diremo nulla dell'altra importantissima caratteristica della OOP, quella organizzativa: la visione di un mondo di oggetti in interazione che sostituisce quella di un computer che esegue un "programma principale", il quale opera richiamando delle sottoprocedure. Dal punto di vista di chi ci lavora, è proprio questa diversa organizzazione che costituisce il salto più grande nello stile di lavoro di programmazione, rispetto a quella ordinaria. Ma, come detto, questo è tutto un altro argomento.