Caricare documenti e articoli online 
INFtub.com è un sito progettato per cercare i documenti in vari tipi di file e il caricamento di articoli online.


Meneame
 
Non ricordi la password?  ››  Iscriviti gratis
 

Principi della programmazione orientata agli oggetti

informatica



Principi della programmazione orientata agli oggetti


Un problema che ha sempre assillato il mondo dell'informatica è quello di poter facilmente eseguire la manutenzione dei programmi, compito la cui difficoltà cresce al crescere della dimensione del codice.




Agli albori dell'informatica i programmi erano piccoli, non tanto perché i calcolatori di 30-40 anni fa erano assai meno potenti di un odierno PC, ma perché il linguaggio di programmazione di allora era l'assembler. Chiunque si sia cimentato a scrivere codice assembler avrà notato che il principale difetto di questo linguaggio è quello di produrre un codice sorgente non facilmente comprensibile e di grosse dimensioni anche per fare la cosa più semplice.


Da allora sono stati sviluppati nuovi linguaggi e paradigmi di programmazione che consentono un maggiore livello di astrazione e una più facile comprensione del codice prodotto: un esempio è dato dai linguaggi procedurali quali il Pascal; questi linguaggi implementano la metafora della scatola nera: un blocco di codice la cui complessità è racchiusa al proprio interno e che non produce alcun cambiamento nel mondo esterno ad esso (effetti collaterali). Essi tuttavia, pur avendo introdotto concetti ancora validi, hanno mancato l'obbiettivo principale, perché i loro progettisti fecero inconsciamente l'assunzione che una scatola nera una volta chiusa non va riaperta, cioè quando che un blocco di codice è stato realizzato ed è funzionante, non va più modificato.


Sia a causa di questa assunzione, sia perché i programmatori non rispettavano alla lettera il paradigma della programmazione procedurale, si ebbe tra la fine degli anni '60 e l'inizio degli anni '70 la "crisi del software": nessuno voleva più occuparsi di manutenzione.


Per migliorare il supporto che i linguaggi procedurali fornivano alla fase di manutenzione venne poi introdotto il concetto di sviluppo strutturato, che richiede una attenta pianificazione di ciò che si voleva realizzare e la produzione di una buona documentazione che descriva il funzionamento del sistema. Ma come ben si sa, quello che spesso accade è che si pensa a scrivere il codice, rimandando ad un successivo momento o ad altri la stesura della documentazione, con il risultato che nella migliore delle ipotesi la documentazione prodotta è inadeguata allo scopo per cui essa è stata realizzata. In sostanza, pur avendo prodotto molte utili tecniche, anche lo sviluppo strutturato si è rivelato poco efficace dal punto di vista della manutenzione del software; occorreva fare un ulteriore passo in avanti: la programmazione orientata agli oggetti.


Questo paradigma di programmazione ha introdotto molti nuovi concetti che hanno letteralmente rivoluzionato tutti (o quasi) i campi dell'informatica (e questi hanno influenzato la programmazione orientata agli oggetti). Prima di proseguire e di introdurre i concetti base della OOP (Object Oriented Programming) è bene fare una precisazione: pur essendo stata teorizzata per la prima volta parecchi anni fa, la OOP è lontana dal possedere uno standard di riferimento, tanto è vero che su molti concetti non esiste alcun accordo tra i teorici di questa disciplina, per cui non stupitevi se qualcun altro definirà un concetto in maniera più o meno diversa da quanto verrà fatto tra poco.


La caratteristica più rivoluzionaria dei linguaggi ad oggetti è quella di concepire i dati e le operazioni su di essi come una unica cosa (oggetto), e non entità separate (dopo tutto dati e operazioni su di essi sono in qualche modo correlati).


Una definizione formale direbbe che un oggetto è una entità software dotata di stato, comportamento e identità. Lo stato viene modellato con costanti e/o variabili dette attributi dell'oggetto; il comportamento è dato da procedure locali dette metodi (alcuni non fanno alcuna distinzione tra attributi e metodi, utilizzando unicamente il primo termine); infine l'identità è qualcosa di unico, immutabile e indipendente dal valore dello stato, che rende un oggetto diverso da ogni altro.


Un oggetto comunica con il mondo esterno tramite messaggi; un messaggio altro non è che una operazione che può essere compiuta su quell'oggetto e l'insieme dei messaggi a cui un oggetto risponde ne costituisce l'interfaccia. Alcune precisazioni: le componenti di uno stato possono essere valori elementari o complessi quali record, oggetti, liste di tipi elementari o oggetti e così via; in generale la OOP prevede che gli attributi di un oggetto non siano mai accessibili dall'esterno, quando ciò accade si dice che l'oggetto incapsula lo stato. Altra caratteristica di un oggetto è quella di possedere una identità che permane nel tempo e che distingue sempre due oggetti: come due gemelli pur essendo fisicamente identici non sono la stessa persona, così due oggetti pur avendo uguali il valore dello stato, il comportamento e l'interfaccia non sono mai uguali; se due oggetti sono uguali allora sono lo stesso oggetto.


Il concetto di messaggio consente l'astrazione dai dati: il programma che usa un oggetto non ha alcuna necessità di conoscerne la struttura interna, per compiere una operazione su di esso tutto quello che deve fare è inviargli un messaggio a cui l'oggetto risponderà restituendo uno o più valori memorizzati nel suo stato o calcolati da esso mediante alcuni dei suoi metodi; ciò consente di cambiare la rappresentazione interna dell'oggetto senza dovere modificare anche i programmi che usano quell'oggetto, che invece continueranno a funzionare regolarmente.


Inviare un messaggio ad un oggetto è possibile grazie ad un costrutto del tipo SEND oggetto arg1 arg2 ... argN; questo in pratica equivale ad un nuovo meccanismo di chiamata di procedura (si chiama una procedura dell'oggetto: quella relativa al messaggio inviato). Ciò benché sia semanticamente diverso dal chiamare una procedura qualsiasi, tende secondo alcuni a rendere le cose più difficili e di fatto molti linguaggi implementano il meccanismo dei messaggi nello stesso modo in cui sono implementati i metodi; l'unica differenza tra un metodo ed un messaggio è che il primo è locale (privato) all'oggetto, mentre il secondo non lo è (pubblico). Nel seguito, al fine di eliminare possibili incomprensioni, faremo distinzione tra attributo, metodo e messaggio.


Per poter creare un oggetto è necessario definire ciò che lo caratterizza, è cioè necessario dare una definizione di tipo oggetto. Un tipo oggetto definisce come minimo l'interfaccia di un insieme di possibili oggetti, ma per molti linguaggi dichiarare un tipo oggetto vuol dire definire la struttura dello stato, l'insieme dei metodi, l'interfaccia e implicitamente (almeno) un costruttore e (almeno) un distruttore.


Costruttori e distruttori sono procedure che il linguaggio usa per creare e rimuovere gli oggetti di cui il programma necessita; in generale costruttori e distruttori vengono realizzati dal linguaggio stesso sulla base della definizione data, ma viene sempre consentito al programmatore di specificare altre operazioni (per ciascun costruttore e distruttore) che questi devono intraprendere quando vengono invocati.


A questo punto può sorgere una domanda: distruggere un oggetto che al proprio interno racchiude un altro oggetto non pone alcun problema (non è necessario distruggere anche quest'ultimo, a meno che il programmatore non specifichi altrimenti), ma cosa succede nel caso contrario?


La risposta a questa domanda è dipende: se quella componente dello stato può assumere il valore indefinito, non ci sono problemi, altrimenti è necessario distruggere anche l'oggetto più grosso, iterando automaticamente il procedimento (in quei linguaggi che trattano gli oggetti indipendentemente dalla loro complessità), oppure lasciando al programmatore il compito di eseguire esplicitamente la distruzione di tutti gli oggetti dal più piccolo al più grosso (tutto dipende dal fatto che il linguaggio tratti gli oggetti come un tutt'uno, indipendentemente dalla loro complessità, o meno). Il motivo di tutto ciò è semplice, dato che un oggetto possiede una identità unica, assegnarlo ad un campo di un altro oggetto non vuol dire copiarlo in quel campo, ma attivare un riferimento all'oggetto assegnato; ciò implica che in seguito alla distruzione dell'oggetto tutti i riferimenti ad esso divengono non validi e un tentativo di accedere all'oggetto distrutto genera un errore a tempo di esecuzione. Molti linguaggi object oriented non offrono alcun modo di verificare ciò, lasciando al programmatore il compito di assicurarsi della corretta creazione e distruzione degli oggetti.


Un altro importante concetto della OOP è quello di ereditarietà. Supponiamo di aver definito un tipo Persona e di aver bisogno anche di un tipo Studente, in fondo uno studente è sempre una persona, anche se con delle caratteristiche in più: oltre agli attributi ed ai metodi di una persona, possiede una matricola, si iscrive... ecc. Tutto questo un linguaggio non orientato agli oggetti non consente di esprimerlo con facilità, ne tanto meno di farlo senza dover riscrivere parte del codice del tipo Persona nel tipo Studente;

in un linguaggio ad oggetti invece ciò si fa dichiarando il tipo Studente discendente del tipo Persona, in tal modo il tipo Studente (detto tipo figlio e in generale sottotipo) eredita tutti gli attributi e i metodi del tipo Persona (tipo padre e in generale supertipo), bisognerà poi dichiarare solo ciò che è proprio del tipo Studente aggiungendo nuove definizioni e/o sovrascrivendone alcune del tipo padre.

Naturalmente il tipo figlio eredita dal padre anche l'interfaccia. L'ereditarietà è quindi un meccanismo innovativo che consente il reimpiego di codice precedentemente scritto; si noti inoltre che tale concetto stabilisce una gerarchia tra i tipi: in cima sta quello definito per primo, poi quelli definiti a partire da questo e via via iterando ai livelli successivi al primo (naturalmente si tratta di una relazione di ordinamento parziale).


Se la ridefinizione di un attributo è consentita solo per specializzazione si dice che l'ereditarietà è stretta, altrimenti si parla di ereditarietà debole; se un linguaggio implementa l'ereditarietà stretta, là dove è richiesto un supertipo può essere utilizzato un sottotipo (in quanto il sottotipo "riempie" completamente il supertipo), si ha cioè anche l'ereditarietà del contesto, ciò invece non è vero in caso di ereditarietà debole, in questo caso vale solo che il sottotipo risponde anche ai messaggi del supertipo.


La ridefinizione di un metodo può riguardare tanto gli argomenti e il risultato, quanto l'implementazione stessa del metodo; se si ridefinisce l'implementazione del metodo, in quella nuova è possibile eseguire una chiamata allo stesso metodo come implementato prima (in questo modo si può essere certi che i campi ereditati, se non ridefiniti, vengano manipolati correttamente, limitando così la possibilità di errore).


L'ereditarietà stretta pone un problema: supponiamo di avere una procedura F(x:Persona) che chiami il metodo Presentati ridefinito nel sottotipo Studente, e siano Caio di tipo Persona e Tizio di tipo Studente; la chiamata ad F(Caio) è del tutto lecita e non comporta problemi, ma la chiamata ad F(Tizio), lecita perché in presenza di ereditarietà del contesto, quale metodo Presentati chiamerà? Quello relativo al tipo Persona o al tipo Studente? Questo problema viene risolto chiamando sempre il metodo più specializzato tramite la tecnica di collegamento ritardato (late binding) che rinvia a tempo di esecuzione la scelta del metodo da applicare e quindi F(Tizio) chiamerà il metodo Presentati relativo al tipo Studente.

Ovviamente ciò può non essere gradito, ad esempio perché il metodo restituisce qualcosa in più che non vogliamo; per risolvere tale problema tutti i linguaggi ad oggetti consentono di forzare la chiamata del metodo relativo al tipo dell'argomento.

Chiaramente se un sottotipo non ridefinisce un metodo, una chiamata ad esso viene risolta chiamando l'implementazione che per prima si incontra risalendo la gerarchia dei tipi (relativa a quel tipo), generando un errore se tale metodo non e stato definito.


Il concetto di ereditarietà può essere esteso in modo tale da consentire ad un tipo oggetto di ereditare da più supertipi, distinguendo così tra ereditarietà semplice ed ereditarietà multipla. Non tutti sono favorevoli all'ereditarietà multipla, molti sostengono che ciò che essa consente è ottenibile anche con l'ereditarietà semplice; entrambe le fazioni hanno validi motivi a sostegno delle proprie idee: molte situazioni che in passato richiedevano l'ereditarietà multipla oggi sono risolvibili utilizzando opportune tecniche, ma in alcune situazioni l'ereditarietà multipla è ancora la situazione migliore: tipicamente quando si vogliono combinare le caratteristiche di più tipi per ottenere un sottotipo più flessibile: ad esempio un tipo FileDiInput ed un tipo FileDiOutput combinati insieme per ottenere un tipo FileDiInputOutput. A fronte di questi vantaggi l'ereditarietà multipla pone tuttavia un problema di ambiguità quando due o più tipi utilizzano lo stesso identificatore, in questi casi bisogna disporre di uno strumento per risolvere il problema: ad esempio una soluzione (troppo restrittiva) potrebbe semplicemente impedire che la cosa accada; inoltre è molto più difficile gestire bene una gerarchia di tipi in presenza di ereditarietà multipla.


Il meccanismo dell'ereditarietà nasconde un'altra importante possibilità che va oltre la riusabilità del codice: il polimorfismo. La possibilità di avere oggetti diversi con la stessa interfaccia (o parte di essa) consente di realizzare applicazioni capaci di utilizzare correttamente oggetti di diverso tipo senza alcuna distinzione: supponiamo di voler realizzare una applicazione che tracci e cancelli poligoni di varie forme, quello che bisogna fare è dichiarare un tipo Poligono che risponda ai messaggi TRACCIA e CANCELLA e poi derivare da esso i tipi Triangolo, Quadrato ecc. i quali risponderanno ancora ai messaggi del supertipo, ma in maniera diversa. Il nostro programma non dovrà fare altro che gestire il solo tipo Poligono limitandosi a inviare ad esso solo i messaggi a cui risponde, sarà poi l'oggetto in questione che penserà a tracciarsi o cancellarsi.


Programmi così fatti possono facilmente essere estesi senza dover letteralmente toccarne l'implementazione, basta aggiungere nuovi tipi derivandoli dal supertipo che sta in cima alla gerarchia. La possibilità di avere oggetti polimorfi, che quindi si comportano in maniera differente (secondo il loro tipo effettivo) ha non a caso dato ai linguaggi ad oggetti il soprannome di "linguaggi attori", proprio perché come un attore un oggetto può "recitare" ruoli diversi a seconda della situazione.


Una utile estensione al paradigma ad oggetti visto finora (molto utile nel campo delle basi di dati) è il concetto di classe; una classe è un insieme di oggetti dello stesso tipo con associati operatori per estrarre, inserire e cercare elementi di quel tipo. Anche per le classi come per i tipi oggetto si parla di gerarchia: una classe C2 si dice sottoclasse di C1 se il tipo T2 degli elementi di C2 è sottotipo di T1, tipo di C1 (vincolo intensionale); ciò comporta che gli elementi di C2 sono anche elementi di C1 (vincolo estensionale), cioè C2 è un sottoinsieme di C1 (ricordate? un oggetto di un tipo è anche un oggetto di ogni suo supertipo).

Spesso si parla anche di metaclassi, intendendo con questo termine una classe i cui elementi sono altre classi.


Fate attenzione al fatto che in molti linguaggi (compreso il C++) il termine di classe è usato come sinonimo di tipo e quindi non ha niente a che vedere con la definizione di prima; in tali linguaggi può divenire difficile gestire gli oggetti di un tipo, specie se ce ne sono molti; in questi casi purtroppo il programmatore non dispone di alcun aiuto e deve programmarsi una opportuna struttura e gli operatori su di essa.


Un'altra estensione prevede che gli oggetti possano assumere e perdere dinamicamente ruoli, rispettivamente trasformandoli in un oggetto del sottotipo o riportandoli ad un supertipo. Tutto questo nasce dalla constatazione che nella realtà le entità assumono e perdono delle caratteristiche: ad esempio una persona salendo su un'auto diviene passeggero o automobilista, scendendo ritorna ad essere un pedone. Ancora una volta non tutti i linguaggi offrono tale possibilità.


Benché la OOP abbia risolto diversi problemi, non ha centrato tutti gli obbiettivi: la riusabilità del codice è una realtà locale alla singola applicazione, è assai difficile che il codice di una applicazione possa essere facilmente utilizzato in un'altra: la cosa è fattibile solo relativamente a oggetti e tools di applicazione generale, in quanto spesso è necessario apportare grosse modifiche al codice già disponibile, al punto che è più conveniente ricominciare da capo.


Tuttavia è innegabile che la manutenzione del software sia oggi molto più semplice di quanto lo fosse una volta, ma è necessario uno studio approfondito del sistema da realizzare, scrivere del codice il più possibile autoesplicativo e naturalmente PENSARE AD OGGETTI.






Privacy




Articolo informazione


Hits: 2003
Apprezzato: scheda appunto

Commentare questo articolo:

Non sei registrato
Devi essere registrato per commentare

ISCRIVITI



Copiare il codice

nella pagina web del tuo sito.


Copyright InfTub.com 2024