43 0 386KB
Fondamenti di programmazione ad oggetti
Introduzione Programmi semplici e non orientati agli oggetti possono consistere di una lunga lista di ordini (statements). Programmi più complessi raggruppano spesso piccoli blocchi di questi ordini in funzioni o subroutines, ciascuna delle quali può realizzare un obiettivo particolare. Per programmi così strutturati è piuttosto comune che qualche dato sia “globale” e cioè accessibile, e modificabile, da qualsiasi sezione del programma. Col crescere della dimensioni dei programmi permettere ad ogni funzione di modificare qualsiasi dato rende inevitabile la propagazione di numerosi errori (bugs).
L’approccio della programmazione orientata agli oggetti, OOP, invece, fa sì che il programmatore localizzi i dati dove non siano direttamente accessibili dal resto del programma. Ai dati stessi si può ac-cedere chiamando (invocando) delle funzioni scritte in modo particolare, comunemente definite metodi, le quali, o sono “impacchettate” con i dati oppure “ereditate” da “oggetti classe." Tali funzioni agiscono come intermediari per ricuperare o modificare i dati che esse controllano. L’unità di programmazione, nella quale i dati sono associati ad una serie di metodi per gestirli, viene definita in OOP un oggetto.
L’architettura Software L’architettura software di un programma informatico è l’insieme di dati strutturali necessari per ragionare su di esso: tali dati comprendono elementi del software, le relazioni che intercorrono tra di essi e le proprietà degli uni e delle altre. Per definizione l’architettura software è l’insieme di regole pratiche, criteri e schemi di progettazione che regolano: • la suddivisione del problema, e del sistema informatico che si deve costruire per risolverlo, in moduli discreti; • le tecniche per creare “interfacce” fra tali moduli; • le tecniche per gestire la struttura generale ed il flusso del programma;
• le tecniche per interfacciare il sistema all’ambiente (cioè al linguaggio di programmazione); • l’uso corretto di approcci, tecniche e strumenti di sviluppo e distribuzione. Gli scopi primari dell’architettura sono la definizione dell’ambiente di un sistema (e cioè del linguaggio informatico relativo) e la definizione dei suoi requisiti non funzionali, di quelle qualità cioè che sono relative a ciò che esso è e non a ciò che fa (requisiti funzionali). Sono requisiti non funzionali quelli osservabili a run time, come per esempio la sicurezza e la facilità di utilizzo, e quelli incorporati nella struttura statica del sistema come, per esempio, la manutenibilità e l’espansibilità.
La definizione del progetto informatico viene seguita da quella della sua realizzazione funzionale in accordo con le regole dell’architettura. L’architettura è importante perché: • limita la complessità del programma consentendone la suddivisione in elementi più semplici (moduli o subroutines); • fa sì che vengano applicate le procedure migliori (best practice); • dà consistenza e uniformità; • consente il riutilizzo di moduli (subroutines) del programma.
Obiect Oriented Programming L’OOP è una filosofia di progettazione che utilizza linguaggi di programmazione diversi da quelli impiegati dai vecchi linguaggi procedurali (come il Pascal ed il C). È un tipo di programmazione in cui i programmatori non si limitano a definire il tipo di dati in una struttura dati (una struttura dati è un modo particolare di immagazzinare ed organizzare dati in un computer di modo che possano essere utilizzati in modo efficiente) ma definiscono anche i tipi di operazioni (funzioni) che possono essere applicate ad essa. Così la struttura dati diventa un “oggetto” che comprende sia dati che funzioni.
I programmatori, inoltre, possono mettere in relazione i vari oggetti (e.g, un oggetto può ereditare le caratteristiche di un altro). Uno dei principali vantaggi delle tecniche OOP, rispetto alle tecniche procedurali, consiste nel fatto che esse mettono in grado i program-matori di creare moduli autosostenibili che non devono essere modificati se si aggiunge un nuovo tipo di oggetto, rendendo il programma più facile da modificare (meno rigido). Un programma orientato agli oggetti conterrà di solito differenti tipi di oggetti, ed ogni tipo corrisponderà o ad un specie particolare di dati complessi da gestire, oppure ad un oggetto od un concetto del mon-do reale, come per esempio un conto corrente postale, un calciatore o una macchina per il movimento terra. Un programma potrebbe benissimo contenere più copie dello stesso
oggetto, una per ogni controparte del mondo reale con cui il programma ha a che fare. Per esempio ci potrebbe essere un oggetto calciatore per ciascun calciatore reale della serie A e ciascuno di essi sarebbe simile agli altri dal punto di vista dei metodi disponibili per manipolare o leggere i suoi dati, i quali sarebbero però diversi per ogni specifico atleta (nome, età, ruolo, …). Si può immaginare che gli oggetti siano una serie di dati “incartati” in una serie di funzioni progettate per far sì che tali dati siano usati in modo appropriato e sia reso più facile il loro impiego. Un oggetto può inoltre essere in grado di mettere a disposizione semplici metodi standardizzati per eseguire determinate operazioni sui suoi dati e ciò senza rendere accessibile al resto del programma il modo in cui tali operazioni vengono realizzate.
Ciò consente di modificare la struttura interna o i metodi di un oggetto senza la necessità di modificare il resto del programma. Tale caratteristica diventa particolarmente utile se il codice di un programma è scritto da più di un programmatore oppure se l’obiettivo è il riutilizzo di codice tra due programmi diversi. L’OOP è una tecnologia basata non tanto sulle funzioni quanto sui dati con programmi costituiti da moduli autosufficienti (classi), ciascuna istanza delle quali (oggetto classe) contiene tutte le informa zioni necessarie per manipolare la sua struttura dati (membri). Un programma orientato agli oggetti si può considerare una collezione di oggetti interattivi ciascuno dei quali è in grado di processare dati e di scambiare messaggi con gli altri. Ciascun oggetto è un’unità operativa autonoma, con un proprio ruolo
ed una propria responsabilità, le cui azioni (metodi) sono strettamente associate all’oggetto stesso. Tre dei concetti basilari, nella programmazione orientata agli oggetti, sono le Classi, gli Oggetti ed i Metodi ma sono notevolmente importanti anche concetti quali quelli di Astrazione, Incapsulazione, Ereditarietà e Polimorfismo.
Oggetti Un oggetto è un’ “entità” che può eseguire, o sulla quale si possono eseguire, un’insieme di attività ad essa correlate: tali attività definiscono il suo comportamento. Gli oggetti sono le entità fondamentali che compaiono in un sistema orientato agli oggetti al momento dell’esecuzione del programma (run time): in tale momento essi interagiscono tra di loro scambiandosi messaggi e ciò anche senza essere a conoscenza dei dettagli del loro codice o dei loro dati. Gli oggetti sono caratterizzati da tre proprietà: 1. identità: le caratteristiche di un oggetto che permettono di distinguerlo da altri;
2. stato: descrizione dei dati (proprietà) immagazzinati nell’oggetto; 3. comportamento: descrizione dei metodi associati all’oggetto che consentono il suo impiego. Così, ad esempio, l’oggetto motocicletta avrà le proprietà marca, modello, capacità del serbatoio, tipo di freni, modello di cambio… ed i metodi accelerare, frenare, cambiare marcia, rifornirsi di benzina.... Formalmente in OOP un oggetto è definito come una delle “istanze” di una “classe” la quale, prima di poter essere usata nel software, deve essere istanziata in un oggetto. In ogni momento possono esistere molteplici istanze di una data classe.
Classi Una classe è la rappresentazione generale di una certa tipologia di oggetti. In OOP una classe è un costrutto usato come stampo (progetto, prototipo), che definisce le variabili ed i metodi comuni a tutti gli oggetti di una certa specie, utilizzato per ottenere, al momento di esecuzione del programma, delle realizzazioni specifiche della classe stessa (istanze della classe, oggetti della classe o oggetti semplicemente). Una classe definisce dei membri costitutivi che consentono a tali istanze di avere uno stato (attributi) ed un comportamento (metodi). Come le sue istanze, di cui è una generalizzazione, una classe consta di un nome, di una serie di attributi e di una serie di metodi. Nel mondo reale si incontrano spesso parecchi oggetti individuali
dello stesso genere. Possono esistere, per esempio, migliaia di motociclette dello stesso tipo ciascuna delle quali, nel linguaggio OOP, è un’istanza della classe motocicletta. Una classe può essere rappresentata schematicamente come segue: Teacher - age: int - name : string + Teacher (): void + DoTeach (object): boolean
+ Name () : string + Age () : int
Il codice per creare un’istanza di una classe è il seguente:
public class Teacher { } Teacher objectTeacher = new Teacher(); L’oggetto della classe Teacher, denominato objectTeacher, è stato “istanziato” dalla classe Teacher (instanziazione della classe).
Identificazione e progettazione di una classe La progettazione di un software non è adeguata se, nonostante che il software stesso soddisfi alle richieste per cui è stato progettato, esso presenta una o più delle seguenti caratteristiche: 1. è difficile da modificare in quanto una modifica in una sua parte si riflette su troppe altre parti del sistema (Rigidità in contrapposizione a Flessibilità). 2. quando si modifica una parte si provoca inaspettatamente la rottura di altre parti (Fragilità in contrapposizione a Robustezza). 3. è difficile riutilizzarlo in un’altra applicazione perché non lo si può districare dall’applicazione corrente (Immobilità in contrapposizione a Riutilizzabilità).
Ciò che rende un progetto rigido, fragile ed immobile è l’interdipendenza dei moduli al suo interno. La rigidità dipende dal fatto che un singolo cambiamento in modulo di un software, i cui moduli siano fortemente dipendenti tra di loro, provoca una cascata di cambiamenti in moduli dipendenti. Un programma è fragile se un singolo e semplice cambiamento provoca l’insorgere di problemi in aree che non sembrerebbero correlate a quella in cui si è realizzato il cambiamento stesso. Nell’ovviare a questi problemi se ne provocano dei nuovi di modo che la manutenzione del software diventa praticamente impossibile. Un progetto è immobile quando i suoi moduli, che si vogliano eventualmente riutilizzare in un altro progetto, dipendono strettamente da altri, che non sono richiesti, di modo che risulta più conveniente ri-
scriverli ex novo piuttosto che separarli da quelli che non occorrono. Nel caso della progettazione di una classe le tecniche utilizzate variano da un progettatore all’altro e lo stato dell’arte è ancora, appunto, piuttosto artigianale. Esistono comunque alcuni principi che è opportuno seguire al fine di ottenere un software flessibile, robusto e riutilizzabile. Il principio di responsabilità singola: una classe dovrebbe avere una sola responsabilità, cioè una sola funzione da realizzare, per rendere minimo (uguale ad 1) il numero di possibilità di modificazioni. Il principio di apertura e chiusura: oggetti software come classi, moduli, funzioni devono essere espandibili ma non modificabili. Quando una singola modificazione in un programma si riflette in una cascata di modificazioni nei moduli correlati, esso diventa rigido, fragile im-
prevedibile e non riutilizzabile. Le classi quindi devono essere costruite in modo tale da non cambiare mai, e le eventuali nuove richieste, cui deve andare incontro un programma, devono essere soddisfatte non modificando il codice vecchio, già funzionante, ma aggiungendone del nuovo. Il principio dell’inversione della dipendenza: le classi di livello gerarchico superiore, che hanno caratteristiche più astratte e generali, non devono dipendere da quelle di livello inferiore, con carattere più concreto e specifico ma deve essere il contrario: ciò per impedire che una modifica a livello particolare si rifletta a livello generale. Per identificare correttamente una classe è necessario riconoscere ed elencare fino al livello più elementare tutte le funzioni (operazioni) che il sistema informatico dovrà essere in grado di eseguire.
Si passa quindi a raggruppare le varie funzioni in classi (con ciascuna classe che raggrupperà funzioni dello stesso tipo). In ogni caso una classe ben definita deve costituire un raggruppamento significativo di una serie di funzioni e deve consentire la riutilizzabilità incrementando nel contempo l’espandibilità e la manutenibilità del sistema complessivo. Nel mondo del software è sempre consigliabile l’applicazione del concetto del “dividi et impera”. L’analisi di un sistema nella sua globalità è sempre una cosa piuttosto difficile. L’approccio migliore consiste nell’identificare prima i moduli del sistema e quindi analizzarli separatamente per riconoscere le classi. Un sistema informatico può essere costituito da numerose classi la
cui gestione può comportare diversi problemi. Per risolverli i progettatori usano diverse tecniche che possono essere raggruppate in quattro gruppi concettuali fondamentali, che sono i quattro concetti principali della programmazione orientata agli oggetti, denominati Incapsulamento, Astrazione, Ereditarietà e Polimorfismo.
Incapsulamento (o segregazione delle informazioni). L’incapsulamento è una metodica che associa codice e dati in un oggetto e li mantiene entrambi al sicuro da interferenze esterne e/o da utilizzazioni scorrette: si isolano dati e codici particolari da altri dati e codici. Esso consiste nell’inclusione in un oggetto di un programma di tutte le risorse di cui esso ha bisogno per funzionare, fondamentalmente metodi e dati. In OOP l’incapsulamento è realizzato principalmente tramite la creazione di classi le quali rendono accessibili al resto del programma (altre classi) metodi e proprietà ma non i dati che risultano accessibili solo alle funzioni immagazzinate nella stessa classe.
La classe è una specie di contenitore, o capsula o cella, che incapsula la serie di metodi, attributi e proprietà da fornire, a richiesta, alle altre classi. In questo modo l’incapsulamento permette che una classe possa modificare la sua realizzazione interna senza interferire con il funzionamento globale del sistema. Il concetto base dell’incapsulamento è di tener segregate le modalità di funzionamento di una classe ma di permettere l’uso delle sue funzionalità. Una classe può utilizzare funzioni e/o proprietà rese disponibili da un’altra in molte maniere diverse. Nel contesto dell’OOP esistono varie tecniche che consentono a classi diverse di correlarsi: associazione, aggregazione e composizione.
Associazione, aggregazione e composizione. In OOP la composizione di oggetti è un modo per combinare oggetti semplici in oggetti più complessi. Un esempio di composizione, nel mondo reale, è la relazione tra un’automobile e le sue parti: l’automobile “has-a” o “è composta da” oggetti quali il volante, i sedili, la scatola del cambio ed il motore. Se, come nei liguaggi dell’OOP, gli oggetti sono tipizzati allora possono essere compositi o meno, e la composizione può essere considerata una relazione tra tipi: un oggetto di tipo composito (e.g. automobile) “has a” un oggetto di tipo più semplice (e..g. ruota). La composizione non va confusa con la sottotipizzazione che è un procedimento consistente nell’aggiungere dettagli ad un oggetto per ottenere oggetti più specifici. Per esempio le automobili sono un tipo
particolare di veicoli: car is a vehicle. La sottotipizzazione non descrive una relazione tra oggetti diversi ma afferma che oggetti di un certo tipo sono anche oggetti di un tipo più generale. Nella notazione UML (unifyed modelling language) la composizione è rappresentata da un rombo pieno e da una linea continua. L’aggregazione, una forma più generale della composizione, è rappresentata da un rombo vuoto e da una linea continua. composizione:
Car
aggregazione:
Pond
Carburetor
Duck
// Composition class Car { private: Carburetor* carb; public: Car() : carb(new Carburetor()) { } virtual ~Car() { delete carb; } };
// Aggregation class Pond { private: std::vector ducks; }; L’aggregazione differisce dalla composizione in quanto non implica il possesso. Nella composizione se l’oggetto “possessore” viene distrutto quelli posseduti subiscono la stessa sorte. Per esempio una università possiede diversi dipartimenti (e.g., chimica, fisica, …), e ogni dipartimento ha un certo numero di professori. Se l’università chiude i dipartimenti smettono di esistere ma i professori continuano la loro esistenza. Quindi un’università può essere
considerata una composizione di dipartimenti mentre i dipartimenti hanno aggregazioni di professori (un’altra diffrenza è che un professore potrebbe lavorare in più di un dipartimento ma un dipartimento non può far parte di più di un’università). University
Department
Professor
In OOP un’ associazione definisce una relazione tra classi di oggetti che permette ad un oggetto istanza di far sì che un altro compia un’azione in sua vece. Questo tipo di relazione è strutturale in quanto specifica che oggetti di un dato tipo sono connessi ad oggetti di un altro e non è collegata al comportamento.
Classe A
Classe B
Classe A
Classe B
A meno che non sia diversamente specificato l’associazione è bidirezionale anche se può essere limitata ad una sola direzione (in UML ciò viene rappresentato con una linea continua e, rispettivamente, con una freccia che punta in direzione della classe che viene utilizzata. Per qualche principiante il concetto di associazione può generare confusione dovuta non solo a questo concetto ma anche ai due concetti di aggregazione e composizione.
Un modo per comprendere analogie e differenze tra questi tre utili concetti è quello di esaminarli assieme. L’associazione è una (*a*) relazione tra due classi in cui una delle due utilizza l’altra. L’aggregazione invece è un tipo particolare di associazione, è la (*the*) relazione tra due classi. Se un oggetto di una classe possiede (*has*) un oggetto di un’altra, se il secondo è una parte del primo (relazione di inclusione) allora si dice che c’è un’aggregazione tra le due classi. A differenza dell’associazione, l’aggregazione specifica sempre la direzione. public class Academy { private Secretary academySecretary = new Secretary(); }
Academy - academySecretary : Secretary
Secretary - name : string
+ academySecretary () : Secretary
In questo caso si può dire che Academy aggrega Secretary o che ha un (*has-a*) Secretary. Una Academy, però, può esistere anche senza un Secretary. I Courses,invece, non possono esistere senza l’Academy. La “durata di vita” di uno, o più, Courses è dipendente dalla durata di vita dell’Academy. Qualora l’Academy venga chiusa i Courses ces-
sano di esistere e in questo è il caso si dice che l’Academy è composta di Courses: questa composizione può essere considerata come un tipo particolare di aggregazione. Un fattore molto importante, e che spesso viene dimenticato è quello relativo alla durata di vita. Le durate di vita di due classi, che siano legate da una relazione di composizione sono interdipendenti. Academy
Course - name : string
- academySecretary : Secretary
+ academySecretary () : Secretary
Secretary - name : string
Nel caso che non esista interdipendenza tra le durate di vita allora la relazione sarà di tipo aggregazione piuttosto che di tipo composizione Se se si vuole pertanto che due classi siano legate da una relazione di composizione, il modo migliore per realizzare ciò consiste nel definire l’una all’interno dell’altra. In questo modo la classe esterna può realizzare le sue funzionalità ed il ciclo di vita della classe interna è legato a quello della classe esterna. In sintesi si può affermare che la aggregazione è un tipo particolare di associazione e che la composizione è un tipo particolare di aggregazione.
Association → Aggregation → Composition
Astrazione e generalizzazione L’astrazione consiste nel mettere l’accento sulle caratteristiche essenziali tralasciando i dettagli specifici e le precisazioni. L’importanza dell’astrazione, essenziale nella costruzione di programmi, consiste nel fatto che essa consente di omettere i dettagli irrilevanti e di utilizzare nomi per gli oggetti di riferimento. Essa mette l’accento su ciò che un oggetto è o fa piuttosto di come è rappresentato o di come funziona, e questo approccio è fondamentale per controllare la complessità di grandi programmi. Mentre l’astrazione diminuisce la complessità eliminando i dettagli irrilevanti, la generalizzazione ottiene lo stesso risultato rimpiazzando una serie di entità che realizzano funzioni simili con un unico co-
strutto informatico. La generalizzazione amplia un’applicazione in modo che possa comprendere un insieme più esteso di oggetti dello stesso tipo o di tipo diverso. I linguaggi di programmazione realizzano la generalizzazione tramite variabili, parametrizzazione, tipi generici e polimorfismo. Si accentuano le somiglianze tra gli oggetti e ciò aiuta a tenere sotto controllo la complessità raccogliendo entità individuali in gruppi e fornendo un’entità rappresentativa che si può usare per specificare ogni componente individuale di un gruppo. L’astrazione e la generalizzazione sono spesso usate di concerto. Le entità astratte vengono generalizate tramite parametrizzazione per aumentarne l’utilità.
Con la parametrizzazione, una o più parti di un’entità sono rimpiazzate da un nome che è nuovo per essa e che è usato come un parametro. Si invoca l’entità astratta parametrizzata legando il parametro ad un argomento.
Classi astratte Le classi astratte, dichiarate con la parola chiave abstract, non possono essere istanziate. Possono essere utilizzate solamente come superclassi per altre classi che estendono la classe astratta. La classe astratta è un concetto la cui realizzazione si completa nel momento in cui esso viene realizzato da una sottoclasse. Oltre a ciò una classe può ereditare solo da una classe astratta (può però realizzare diverse interfacce), deve sovrascrivere tutti i suoi metodi e le sue proprietà astratti e può sovrascrivere i suoi metodi e le sue proprietà virtuali. Le classi astratte sono l’ideale nella realizzazione di strutture di base.
Interfacce Ogni classe realizza un’interfaccia fornendo una struttura (dati e stato) ed un codice che specifica il funzionamento dei metodi. C’è una differenza tra la definizione di un’interfaccia e la sua implementazione. nella maggior parte dei linguaggi questa differenza non è netta in quanto la dichiarazione di una classe definisce un’interfaccia e contemporaneamente la realizza. Tuttavia taluni linguaggi presentano caratteristiche che aiutano a separare la definizione di un’interfaccia dalla sua realizzazione. Per esempio una classe astratta può definire un’interfaccia senza implementarla. I linguaggi che supportano l’eredità tra classi consentono ad una classe di ereditare un’interfaccia da una classe da cui siano derivate.
La metodologia della programmazione orientata agli oggetti è disegnata in modo tale che le operazioni delle varie interfacce di una classe sono in genere indipendenti tra di loro. Il risultato è uno schema stratificato in cui i clienti di un interfaccia utilizzano i metodi dichiarati nell’interfaccia stessa. Per esempio i bottoni di un apparecchio televisivo sono l’interfaccia tra l’utente e i componenti elettrici ed elettronici contenuti nello chassis. Si spinge il bottone “power” per accendere o spegnere l’apparecchio. Nell’esempio il televisore è l’oggetto ed ogni metodo corrisponde ad uno dei bottoni che, tutti insieme, rappresentano l’interfaccia. Nella definizione più comune un’interfaccia è la specificazione di un gruppo di metodi correlati indipendentemente dalla realizzazione dei
metodi stessi. Un televisore ha tutta una serie di attributi, quali le dimensioni, lo schermo al plasma o a cristalli liquidi ..., che tutti insieme ne costituiscono la struttura. Una classe è la descrizione completa di un televisore, che comprende sia i suoi attributi (struttura), che i bottoni (interfaccia. In sintesi l’interfaccia definisce la struttura isolandone la realizzazione, concetto è molto utile qualora sia necessario che l’implementazione (realizzazione) sia intercambiabile. Oltre a ciò un’interfaccia è molto utile nel caso che l’implementazione cambi frequentemente. Un’interfaccia può essere impiegata per definire un modello generico e una o più classi astratte che definiscano le realizzazioni parziali del-
l’interfaccia. In questo caso le interfacce si limitano a specificare la dichiarazione del metodo che, implicitamente è “pubblico” ed “astratto” e può comprendere proprietà che sono, sempre implicitamente, pubbliche ed astratte. Metodi e proprietà astratti sono definiti ma non implementati e se sono pubblici sono accessibili (visibili) da qualsiasi blocco del programma. La definizione di un’interfaccia inizia con la parola chiave interfaccia. Un interfaccia di una classe astratta non può essere istanziata. Nel caso che una classe che realizza un’interfaccia non definisca tutti i metodi dell’interfaccia stessa allora la classe deve essere dichiarata astratta e le definizioni dei metodi devono essere fornite dalla sottoclasse che estende la classe astratta.
Oltre a ciò un interfaccia può ereditare altre interfacce. Come già ricordato in precedenza esistono linguaggi che supportano varie implementazioni. Il concetto di implementazione implicita ed esplicita fornisce una maniera sicura per realizzare i metodi di varie interfacce nascondendo, esponendo o preservando le identità di ciascuno dei metodi delle interfacce anche quando le “firme” dei metodi siano le stesse
Differenze tra una classe ed un’interfaccia In alcuni linguaggi si può definire una classe che implementi un’interfaccia e sia anche in grado di supportare più di un’implementazione. Quando una classe implementa un’interfaccia, un oggetto di tale classe può essere incapsulato all’interno di un’interfaccia. Se MyLogger è una classe, che implementa ILogger, allora si può scrivere: ILogger log = new MyLogger(); Concettualmente una classe ed un’interfaccia sono due tipi diversi: mentre una classe pone l’accento sul concetto di incapsulamento, un
interfaccia accentua quello di astrazione eliminando i dettagli della implementazione. Classe ed interfaccia sono nettamente distinte tra di loro e di conseguenza è molto difficile o addirittura impossibile confrontarle in modo significativo. È invece sia molto utile che significativo osservare analogie e differenze tra un’interfaccia ed una classe astratta tra le quali, anche se sembrano simili c’è una notevole differenza.
Differenze tra un’interfaccia ed una classe astratta Interfacce • la definizione di un’interfaccia inizia con la parola chiave inte-face che ne caratterizza il tipo (interfaccia); • non hanno implementazione ma devono essere implementate; • possono avere solo la dichiarazione, implicitamente pubblica ed astratta, dei metodi e dati, implicitamente pubblici e statici; • possono ereditare più interfacce; • possono essere utili quando l’implementazione varia; • rendono le implementazioni intercambiabili; • aumentano la sicurezza mantenendo nascosta l’implementazione.
Classi astratte • sono dichiarate per mezzo della parola chiave abstract che le caratterizza come classi; • i metodi delle classi astratte possono essere implementati e devono essere estesi; • i metodi non possono avere implementazione solo se sono dichiarati astratti; • possono implementare più di un’interfaccia ma ereditare da una sola classe; • devono sovrascrivere tutti i metodi astratti e possono sovrascrivere quelli virtuali; • possono essere utili nella realizzazione di un modello; • possono essere impiegate per fornire un comportamento per default per una classe base; • rappresentano un modo ideale per creare gerarchie di eredità pianificate ed inoltre sono utilizzabili come termini ultimi nelle gerarchie di classi.
Ereditarietà È il processo grazie al quale un oggetto acquista le proprietà di un altro oggetto, il che è alla base della classificazione gerarchica. Senza l’impiego delle gerarchie ogni oggetto richiederebbe una dichiarazione esplicita di tutte le sue proprietà. Grazie all’ereditarietà, invece, è sufficiente definire solo quelle caratteristiche dell’oggetto che lo rendono unico all’interno della sua classe. L’oggetto (sottoclasse) può ereditare tutti i suoi attributi generali dalle classi gerarchicamente superiori da cui deriva. La creazione di una classe nuova per estensione di una classe già esistente viene definita ereditarietà.
Exception
IOException
public class Exception { }
public class IOException extends Exception { } In accordo con l’esempio di cui sopra la nuova classe (IOException), che è una classe derivata o sottoclasse, “eredita” i “membri” di una classe già esistente (Exception), che è una superclasse o classe di base. La classe IOException può estendere la funzionalità della classe Exception aggiungendo tipi e metodi nuovi e sovrascrivendo quelli preesistenti. Nello stesso modo in cui l’astrazione è strettamente correlata alla generalizzazione l’ereditarietà è strettamente correlata alla specializzazione. Per una miglior comprensione e per ridurre la complessità è impor-
tante discutere i due concetti assieme a quello di generalizzazione. Nel mondo reale una delle più importanti relazioni tra oggetti è la specializzazione, che può essere definita come una relazione del tipo “is-a”. Quando si dice che un cane è un mammifero si intende che un cane è un tipo “specializzato” di mammifero. Esso presenta tutte le caratteristiche dei mammiferi (animali vertebrati che respirano aria, a sangue caldo, con peli, tre ossa nell’orecchio interno e ghiandole mammarie funzionali nelle femmine con prole giovane), ma tali caratteristiche sono specializzate in quelle ben note del canis domesticus. Anche un gatto è un mammifero e, in quanto tale ci si aspetta che esso condivida talune caratteristiche del cane, che sono generalizzate nei mammiferi, ma differisca per quelle caratteristiche che sono spe-
cifiche per i gatti. Le relazioni di specializzazione e di generalizzazione sono sia reciproche che gerarchiche. La specializzazione è l’altra faccia della generalizzazione: i Mammiferi generalizzano ciò che è comune tra cani e gatti mentre cani e gatti specializzano le caratteristiche dei mammiferi nei propri sottotipi. Analogamente, a titolo di esempio, si può dire che sia IOException che SecurityException sono di tipo Exception. Essi presentano tutte le caratteristiche ed i comportamenti di una Exception. Ciò significa che IOException è un genere specializzato di Exception. Anche una SecurityException è una Exception e, in quanto tale, ci si aspetta che essa condivida con IOException certe caratteristiche che
sono generalizzate in Exception, ma che differisca da essa per le caratteristiche che sono specializzate in SecurityExceptions. In altri termini, Exception generalizza le caratteristiche comuni a IOException ed a SecurityException, mentre IOException e SecurityException specializzano le proprie caratteristiche ed i propri comportamenti. In OOP, la relazione di specializzazione è implementata tramite il principio di ereditarietà e questo è il modo più diffuso, più naturale e più largamente accettato di realizzare questa relazione.
Polimorfismo Polimorfismo è un termine generico che significa “molte forme”. Più esattamente in OOP Polymorphism intende la capacità che le stesse operazioni siano realizzate da un’ampia gamma di differenti tipi di entità. Un operazione può mostrare comportamenti diversi in diverse istanze a seconda dei tipi di dati utilizzati nell’operazione. Questa caratteristica consente di usare la stessa interfaccia per una classe generale di azioni. L’azione specifica è determinata dalla natura esatta della situazione. Quindi in generale il polimorfismo implica che per un’interfaccia
si possano avere più metodi il che implica che si può progettare un’interfaccia generica per un gruppo di attività correlate. Ciò permette di ridurre la complessità utilizzando una sola interfaccia per specificare una classe generale di azioni. Sarà compito del compilatore selezionare l’azione specifica, vale a dire il metodo, per ogni data situazione. La comprensione dei concetti dell’OOP è resa meno semplice dal fatto che essi sono suddivisi in quattro concetti principali ognuno dei quali è strettamente correlato con gli altri. È quindi è necessario capire correttamente ogni concetto separatamente senza perdere di vista il modo in cui essi sono tra loro correlati. In OOP il polimorfismo si ottiene grazie a molte differenti tecniche
denominate overloading (sovraccarico) dei metodi, overloading degli operatori ed overriding (sovrascrittura) dei metodi.
Overloading dei metodi L’overloading dei metodi consiste nella possibilità di definire diversi metodi con lo stesso nome. Nei linguaggi fortemente tipizzati è a volte desiderabile avere un certo numero di metodi con lo stesso nome, ma che operino su dati di tipo diverso. Per esempio, una funzione radice quadrata potrebbe essere definita in modo da poter operare su numeri reali, numeri complessi o su matrici. L’algoritmo da usare in ciascun caso è diverso ed il risultato restituito può non essere lo stesso. Scrivendo tre funzioni separate con lo stesso nome il programmatore ha il vantaggio di non dover ricordare nomi diversi a seconda del tipo di dati. Inoltre se si può definire per i numeri reali un sottotipo che li
suddivida in positivi e negativi, per i reali stessi si possono scrivere due funzioni, una delle quali restituisce un numero reale quando il parametro è positivo, e l’altra che restituisce un numero complesso quando il parametro è negativo. Nella programmazione orientata agli oggetti qualora una serie di funzioni, metodi, con lo stesso nome possa accettare parametri di tipo diverso si dice che ciascuna delle funzioni è sovraccaricata, overloaded. Per esempio: public class MyLogger { public void LogError(Exception e) { // Implementazione }
public bool LogError(Exception e, string message) { // Implementazione } }
Overloading degli operatori L’overloading degli operatori (noto meno comunemente come polymorphism ad hoc) è un caso particolare di polymorphism in cui uno o più di una serie di operatori come +, - o == sono trattati come funzioni polimorfiche e, come tali, hanno comportamenti diversi a seconda del diverso tipo dei loro argomenti. public class Complex { private int real; public int Real { get { return real; } }
private int imaginary; public int Imaginary { get { return imaginary; } } public Complex(int real, int imaginary) { this.real = real; this.imaginary = imaginary; } public static Complex operator +(Complex c1, Complex c2) {
return new Complex (c1.Real + c2.Real, c1.Imaginary + c2.Imaginary); } } Nell’esempio di cui sopra è stato sovraccaricato l’operatore + per sommare due numeri complessi. Le due proprietà denominate Real ed Imaginary sono state dichiarate rendendo accessibile solo il metodo “get”, mentre il costruttore (constructor) dell’oggetto richiede valori obbligatori reali ed immaginari dal costruttore dela classe. Per esempio: Complex num1 = new Complex(5, 7); Complex num2 = new Complex(3, 8);
// sommiamo due numeri complessi usando l’overloading di + Complex sum = num1 + num2;
Overriding dei metodi Le classi possono essere derivate da una o più classi esistenti (anche se non tutti i linguaggi sono in grado di supportare l’ereditarietà multipla), originando così una relazione gerarchica tra le classi di origine (classi basi o classi genitrici o superclassi, superclasses) e le classi derivate (classi figlie o sottoclassi, subclasses). La relazione tra queste e quelle è nota comunemente come una relazione is - a. Per esempio la classe 'Button' potrebbe essere derivata dalla classe 'Control'. Quindi, un Button is a Control. I membri strutturali e comportamentali della superclasse sono “ereditati” dalla sottoclasse. Le classi derivate possono definire membri strutturali (data fields) e/o
membri comportamentali addittivi (metodi) oltre a quelli che esse ereditano e sono quindi specializzazioni delle relative superclassi. Ancora, qualora il linguaggio lo permetta, le classi derivate possono rimpiazzare (override) i metodi ereditati. L’overriding di un metodo, in OOP, è una caratteristica del linguaggio che permette ad una sottoclasse di fornire una realizzazione specifica di un metodo che è già fornito da una delle sue superclassi). La realizzazione, nella sottoclasse, rimpiazza quella della superclasse fornendo un metodo che ha lo stesso nome, gli stessi parametri e lo stesso tipo di risposta di quello della superclasse. La versione del metodo che sarà eseguita sarà determinata dall’oggetto che è usato per “invocarla”. Se viene usato un oggetto della superclasse verrà eseguita la versione del metodo della superclasse stessa
ma se viene usato un oggetto della sottoclasse allora verrà eseguita la versione della sottoclasse. Una sottoclasse può dare la sua propria definizione dei metodi che, devono però avere la stessa “firma” che presentano nella superclasse. Ciò significa che il metodo della sottoclasse, che viene scritto sopra quello della superclasse deve avere lo stesso nome e la stessa lista di parametri di quello della superclasse. Per esempio: using System; public class Complex { private int real; public int Real
{ get { return real; } } private int imaginary; public int Imaginary { get { return imaginary; } } public Complex(int real, int imaginary) { this.real = real; this.imaginary = imaginary; } public static Complex operator +(Complex c1, Complex c2)
{ return new Complex(c1.Real+ c2.Real, c1.Imaginary + c2.Imaginary); } public override string ToString() { return (String.Format("{0} + {1}i", real, imaginary)); } } Nell’esempio di cui sopra è stata estesa l’implementazione dell’operatore + nella classe Complex . Questa classe possiede un metodo
sovrascritto denominato “ToString”, che sovrascrive l’implementazione per default del metodo standard “ToString” per supportare la corretta conversione in stringa di un numero complesso. Complex num1 = new Complex(5, 7); Complex num2 = new Complex(3, 8); // Somma due numeri complessi utilizzando l’operatore + sovraccaricato Complex sum = num1 + num2;
// Stampa i numeri e la loro somma utilizzando il metodo ToString sovrascritto Console.WriteLine("({0}) + ({1}) = {2}", num1, num2, sum); Console.ReadLine();