MokaByte
Numero 25 - Dicembre 98
|
|||
|
|
||
Lorenzo Bettini |
|
||
I pattern, tanto di moda attualmente nel mondo dell'informatica, necessitano di un linguaggio ad oggetti, o almeno si possono studiare ed implementare meglio utilizzando un linguaggio che segue il paradigma ad oggetti, rispetto ad un linguaggio tradizionale. Nei testi classici sui pattern di solito vengono descritti esempi in Smalltalk ed in C++... ma anche Java non è da meno :-)
Si sente sempre
più spesso parlare di pattern negli ambienti informatici
(luoghi universitari, conferenze, e riviste di vario spessore e livello).
Ciò che sta dietro al concetto di pattern è molto semplice,
e più che altro serve a formalizzare tecniche di programmazione
già viste ed utilizzate.
Pattern ed OOP Si è visto quindi che i pattern si utilizzano meglio con linguaggi ad oggetti, in quanto si riesce ad implementare in modo più naturale e semplice classi e relazioni fra di esse. E' bene però precisare alcune definizioni della filosofia ad oggetti, in quanto potrebbero non risultare chiari, soprattutto se si è abituati ad utilizzare il C++. Queste almeno sono alcuni concetti che in un primo momento, a me, già amante del C++, sono risultati poco chiari a prima lettura. Tipi Prima di tutto
è bene introdurre il concetto di tipo. Un concetto fondamentale
nella programmazione ad oggetti è l'incapsulamento, tramite
il quale si nascondono certi dettagli implementativi a chi utilizzerà
effettivamente una certa classe; questo non è dovuto semplicemente
alla voglia di tenere nascosto l'implementazione di una procedura o la
struttura di un oggetto, ma è più orientato a favorire il
riutilizzo di quella classe: accedendo ai servizi o ai campi di un oggetto
tramite metodi, si evita di conoscere i suoi dettagli, e questo ci permette
di ignorarli: se la classe di tale oggetto verrà modificata noi
potremmo tranquillamente continuare ad utilizzare tale oggetto come eravamo
soliti fare (ho usato il condizionale, perché questo è vero
solo se la classe è realizzata effettivamente bene).
tipo: è un nome che viene utilizzato per denotare una particolare interfacciaQuindi quando si dice che un oggetto ha il tipo X, si intende che è in grado di accettare tutte le richieste che sono definite dall'interfaccia X. Chiaramente un oggetto può avere più tipi, e si possono avere oggetti diversi che però hanno lo stesso tipo. Un'interfaccia può contenere al proprio interno i metodi di un'altra interfaccia; si parla allora di: un tipo è un sottotipo di un altro tipo, se la sua interfaccia contiene l'interfaccia del secondo (detto supertype).Ovviamente, poiché l'interfaccia non dice niente dell'effettiva implementazione dei vari metodi, e poiché oggetti completamente differenti possono avere lo stesso tipo (cioè accettare le stesse richieste), è necessario un meccanismo che permetta di eseguire del codice in funzione all'oggetto a cui è stata effettuata una particolare richiesta, basandosi non tanto sul tipo (che si è visto essere uguale anche per oggetti morfologicamente diversi) ma sull'oggetto in questione. L'associazione di una richiesta ad un oggetto ad una delle sue operazioni viene detto dynamic binding, in quanto avviene a run-time. In questo modo si possono tranquillamente sostituire oggetti completamente differenti, ma che hanno la stessa interfaccia: questo concetto è conosciuto come polimorfismo. L'utente dell'oggetto utilizza solo la sua interfaccia: se l'oggetto cambia, ma è in grado comunque di ricevere le medesime richieste, per l'utente tutto rimane come prima: se siamo abituati ad utilizzare una certa macchina per effettuare certe operazioni (ad esempio per la visualizzazione di informazioni sui treni), se questa viene modificata, e magari sostituita con del nuovo software, mantenendo però la stessa interfaccia di quella precedente, per noi non cambierà niente, anzi potremmo addirittura non accorgerci che è stata cambiata. Classi E l'effettiva implementazione? Qualcuno (il programmatore) dovrà effettivamente dare l'implementazione delle varie operazioni contenute in una certa interfaccia. Diamo quindi la seguente definizione la classe di un oggetto definisce la sua implementazioneQuindi la classe di un oggetto non specifica solo l'implementazione dei vari metodi, ma definisce anche i vari campi, definisce cioè la struttura interna dell'oggetto. Quindi come si sente spesso dire: un oggetto è un'istanza di una classeOppure (detto in altri termini): gli oggetti si creano istanziando una classeIstanziando una classe, si crea effettivamente l'oggetto, cioè si alloca la memoria che conterrà i vari dati dell'oggetto (la sua struttura interna), e alle operazioni dell'oggetto (i metodi della sua interfaccia) saranno collegate le implementazioni (si è visto comunque che tale collegamento avviene spesso, se non sempre, a run-time). Si possono creare nuove classi basandosi su classi già definite, utilizzando il meccanismo dell'ereditarietà di classi. In questo modo, dalla classe da cui si eredita (detta classe base o parent class), si eredita tutte le definizioni di dati ed anche le sue operazioni. Ovviamente si possono aggiungere altri dati ed altre operazioni, oppure si può voler modificare il comportamento della classe base, dando implementazioni alternative dei vari metodi (effettuare cioè l'overriding di un metodo). E che differenza c'è? Un programmatore
C++ potrebbe a questo punto chiedersi che differenza c'è (almeno
questa è la domanda che mi posi io) fra tipi e classi in C++. In
C++ esiste solo la parola chiave class, e non esiste una parola
type. Questo vuole dire che in C++ non posso definire tipi, ma solo
classi?
un oggetto, pur appartenendo ad una classe può avere più tipi, e oggetti di classi differenti possono avere lo stesso tipo.Quindi che differenza c'è fra estendere (ereditare) una classe ed estendere un tipo? Poiché un tipo dichiara solo le operazioni che possono essere eseguite (l'interfaccia), ereditando un tipo si ereditano tutte queste operazioni, nel senso che si ereditano tutte le richieste a cui si deve essere in grado di rispondere; eventualmente ci è possibile anche definirne altre. Quindi posso ereditare il tipo X, ed arricchire la sua interfaccia con nuovi metodi. Del resto se sono in grado di effettuare più operazioni di un oggetto di tipo X, posso in particolare effettuare tutte le operazioni di un oggetto di tipo X (utilizzando un colorito e popolare modo di dire: "nel più ci sta il meno"), e cioè posso essere utilizzato al posto di un oggetto di tipo X. Del resto, sempre riprendendo l'esempio della macchina che distribuisce le informazioni, se questa macchina viene ampliata con nuove funzionalità (che possono non interessarci), noi possiamo comunque continuarla ad utilizzare come prima. Quindi l'ereditarietà di interfaccia (subtyping) descrive quando un oggetto può essere utilizzato al posto di un altro.Mentre ereditando da una classe, si eredita la sua implementazione, quindi si crea una nuova implementazione, basandosi su quella precedente. Quindi l'ereditarietà di classe (subclassing) è un meccanismo per condividere codice e dati.Ma in C++ quando si eredita si eredita sia l'interfaccia che l'implementazione, quindi sembra che non ci sia una forma di subtyping! Non è effettivamente così... introduciamo un altro concetto. Quando si creano delle gerarchie di classi, si intende definire cose e comportamenti che accomunano più classi, partendo da una classe che definisce le caratteristiche più generali e specializzando sempre di più derivando da questa, fino a formare una vera e propria gerarchia, dove all'interno, è molto facile trovare delle sottogerarchie ben evidenti (il solito esempietto presente in diversi testi di OOP, è quello della gerarchia degli animali: si parte da la classe più generale animale e poi si inizia a specializzare seguendo certi criteri: bipedi, quadrupedi, ecc...). Definendo le classi più comuni (quelle più in alto nella gerarchia) non si riuscirà sempre a dare le implementazioni delle interfacce in modo da avere comportamenti che ben si prestano ad essere riutilizzati dalle classi derivate: l'interfaccia può essere uguale nelle classi derivate, ma come le varie operazioni verranno implementate potrà differire molto a seconda del livello in cui ci troviamo nella gerarchia (del resto tutti gli animali si nutrono, ma è chiaro che lo faranno in modi completamente diversi, anche senza scendere troppo nella gerarchia delle classi). Questo discorso è stato fatto per giustificare la necessità che si può avere, quando si definisce una classe, di lasciare in sospeso l'implementazione di qualche metodo (i cosiddetti metodi astratti, quelli che in C++ vengono detti metodi virtuali puri). Una classe che ha almeno un metodo astratto viene detta classe astratta, e del resto non può essere istanziata direttamente (in quanto non si sarebbe in grado poi di richiedere certe operazioni a quell'oggetto). Se si definisce una classe astratta con soli metodi astratti, e senza definire nessun dato, ci si rende conto che non ci siamo discostati molto dal concetto di tipo: abbiamo definito solo l'interfaccia e non abbiamo definito nessun membro, quindi abbiamo in realtà definito un tipo. Quindi anche in C++ è possibile definire tipi (interfacce). Quindi se in C++ si eredita (in modo pubblico) da una classe che contiene solo metodi virtuali puri (in tal caso la classe viene detta classe astratta pura), si effettua in realtà un'eredità di tipo. Java e C++ In Java è
invece presente il concetto di interfaccia, in quanto si può utilizzare
la parola chiave interface; quindi un programmatore Java può
non avere difficoltà a distinguere il concetto di tipo da quello
di classe, in quanto anche nella programmazione può separare i due
concetti.
in realtà non funziona, in quanto main è un metodo statico, e quindi una sorta di funzione del C++, e non effettivamente un metodo della classe, che quindi può accedere tranquillamente ai campi ed ai metodi della classe: non è nemmeno stato istanziato un oggetto di tale classe! La versione giusta è la seguente:
Quindi un metodo
statico può essere visto come una funzione del C++, con la restrizione
che può essere utilizzata solo riferendoci alla classe in cui è
definito (se questo è pubblico), o altrimenti utilizzato solo all'interno
di tale classe (se questo è protetto o privato).
E allora ? (Conclusioni) Ed insomma chi
dei due vince questa sfida? Ovviamente nessuno :-) Il C++ mette a disposizione
più mezzi, e per questo permette di effettuare più operazioni
pericolose (uso dei puntatori) o realizzare progetti più difficilmente
gestibili (a causa delle dipendenze cicliche create con l'ereditarietà
multipla). Java invece, "restringendo il campo d'azione", permette di programmare
in modo più pulito, finché non ci si deve "sporcare" simulando
certi comportamenti invece immediati in C++. Senz'altro però non
ci si deve preoccupare della gestione della memoria :-)
abbastanza astratti da non dipendere dal linguaggio in cui verranno utilizzati; ovviamente il paradigma ad oggetti facilita moltissimo le cose, rappresentando effettivamente un must in questi casi Personalmente preferisco avere più libertà, ma questo è solo un giudizio personale, e ci sono dei casi in cui invece, è più semplice sfruttare cose già predisposte, e sicure! Nei prossimi numeri vorremmo proporvi una serie di articoli che parleranno di pattern e di come questi possono essere utilizzati ed implementati in Java, fornendo ovviamente del codice funzionante. Vedremo tra l'altro che Java utilizza già alcuni pattern. In effetti la reazione tipica di chi legge le descrizioni dei pattern è: "ah... ma io lo conoscevo già... guarda chi è..." ;-)
|
MokaByte Web 1998 - www.mokabyte.it MokaByte ricerca nuovi collaboratori. Chi volesse mettersi in contatto con noi può farlo scrivendo a mokainfo@mokabyte.it |