Zoom Icon

Design pattern Observer

From UIC Archive

Eventi e comunicazione: il design pattern Observer

Contents


Design pattern Observer
Author: inedito
Email: [email protected]
Website: http://autistici.org/inedito
Date: 29/01/2008 (dd/mm/yyyy)
Level: Working brain required
Language: Italian Flag Italian.gif
Comments:


E come sempre musica riscalda il mio cuore.
Portando a passeggio i miei sogni.
Dovrei tornare ad ascoltare il vento.
Troppe parole spaventano segreti e desideri.


Introduzione

In questo articolo viene illustrato il comportamento e l'applicazione del design pattern Observer, uno dei piu' usati nella programmazione orientata agli oggetti. Vengono inoltre accennate le caratteristiche della OOP e le differenze fra i due linguaggi C++ e Smalltalk, con uno sguardo mirato nelle diverse implementazioni sull'ereditarieta' e il polimorfismo.

Obiettivo dell'articolo e' introdurre il lettore in questo particolare aspetto della programmazione, affrontando le tematiche (secondo me) piu' interessanti con la speranza di incuriosirlo.

Versione e note

Un esempio in codice sull'applicazione del design pattern in C++ e Smalltalk e' disponibile su: http://autistici.org/inedito.

Purtroppo la versione in Smalltalk non possiede un'immagine: ho raccolto i metodi e le classi in diversi file, cosi' da poterli ricostruire con il proprio compilatore (gst genera l'immagine ma durante il caricamento si blocca).

Note, errori e suggerimenti potete inviarli a: [email protected].
La versione attuale del documento e': 0.2 - 29 gennaio 2008.


Programmazione Orientata agli Oggetti

In questo capitolo viene illustrata brevemente la OOP e le caratteristiche principali che differenziano un linguaggio tradizionale da un linguaggio orientato agli oggetti.

Definizione

La programmazione orientata agli oggetti prende vita e seria considerazione negli anni Ottanta, presso i laboratori Bell: Bjarne Stroustrup sviluppo' il linguaggio C++ basando la struttura del codice e la sintassi su quella del C. Egli e' considerato il padre della OOP, sebbene gia' negli anni Quaranta, con l'avvento del FORTRAN II, le prime tracce di applicazione sugli oggetti e strutture dati simili prendevano forma: perche' allora questo ritardo? Molto probabilmente in quegli anni non c'erano supporti hardware e sintattici per interpretare il codice e sfruttarne al massimo le caratteristiche.

Ma cosa e' realmente la programmazione orientata agli oggetti? In cosa si differenzia rispetto agli altri linguaggi tradizionali? La parola chiave e' confezionamento: le strutture dati e le funzioni scritte dal programmatore, vengono assemblate in un'unica entita' che prende il nome di classe, formando quindi un oggetto pronto per essere utilizzato e applicato in qualsiasi codice. Potremmo quasi dire che, generalmente, non esiste nessuna differenza: non e' proprio cosi'.

La OOP non garantisce solo pulizia e ordine: le classi possono comunicare fra di loro e quindi il programmatore puo' separare il codice in vari moduli. Questo concetto prende il nome di modularizzazione.

Caratteristiche

Le caratteristiche che offre la programmazione orientata agli oggetti sono molte. In questa sezione vengono descritte quelle piu' interessanti, con un approfondimento verso l'ereditarieta' e il polimorfismo.

Incapsulamento

Per incapsulamento si intende l'insieme di dati e funzioni in un'unica struttura. E' una proprieta' importantissima, poiche' permette l'accesso ai dati interni dell'oggetto solo attraverso i metodi (funzioni) disponibili, realizzando quindi l'interfaccia. In questo modo il programmatore che utilizza l'oggetto conosce il suo funzionamento ma non come vengono gestite le informazioni.

Istanze

Gli oggetti che vengono creati sulla base di un'altra classe, vengono chiamati istanze. E' possibile dunque stilare una gerarchia delle classi, in cui vengono evidenziate la classe generale e tutte le sotto classi (istanze).

Ereditarieta'

La figura della classe e' ormai nota. E' possibile estendere il suo concetto introducendo un'altra figura importante: la sotto classe. Le sotto classi sono quegli oggetti che vengono creati basando la loro struttura su una classe generale.

Fondamentalmente, l'ereditarieta' e' quel meccanismo che permette a nuove classi di ereditare metodi e dati di altre classi: e' inoltre possibile aggiungere nuovi metodi e, cosa piu' importante, ridefinire il comportamento delle funzioni derivate.

Un esempio tipico sono i generi musicali: esistera' una classe Rock in cui vengono descritte le informazioni base, come gli strumenti tipicamente usati. Da questa, poi, si puo' creare una nuova classe Progressive Rock che, ereditati gli stessi strumenti della classe Rock, ne introdurra' dei nuovi oppure specifichera' un nuovo modo di utlizzare i primi.

Ovviamente, nuove istanze possono derivare da istanze di una classe generale. Una nota: una sotto classe puo' avere una o piu' classi genitrici. Nel primo caso avremo un'ereditarieta' semplice, nel caso di piu' classi generali si avra' un'ereditarieta' multipla.

Polimorfismo

Il polimorfismo e' la capacita' di modificare il comportamento di un metodo ereditato da una classe. Questa abilita', gia' accennata prima nell'ereditarieta', e' importante poiche' assicura al programmatore l'universalita' del codice: in istanze diverse, ereditando gli stessi metodi, possono essere eseguite azioni diverse.

Un esempio potrebbe essere la definizione di specie. Se nella super classe (classe generale) viene specificato il metodo Camminare, le istanze che derivano potrebbero essere Uomo, Serpente e Leone: seppure tutte e tre le specie camminino, lo fanno in modo diverso. e' necessario quindi modificare il comportamento del metodo a seconda dell'istanza.

E' anche possibile ridefinire il comportamento di operatori logici e aritmetici usati nella programmazione: l'esempio tipico e' quello dell'operatore +, che fra interi viene interpretato come sommatore mentre fra stringhe come concatenamento.

Da notare un'altra cosa: il binding dinamico. Attraverso questa proprieta' viene scelto il metodo giusto per l'esecuzione di una funzione in runtime in base all'oggetto contenuto nella variabile. Se ad esempio la classe di una variabile ha altre due sotto classi, quando viene invocato un metodo (ovviamente comune a tutte e tre le istanze), l'oggetto sapra' come comportarsi eseguendo la versione appropriata del metodo.

Astrazione e Information Hiding

E' possibile racchiudere in due termini la maggior parte delle proprieta' viste fino ad ora:

  • L'Astrazione e' la fase in cui viene descritto un oggetto o una struttura senza elencare le informazioni inutili. Si tratta quindi di descrivere le funzionalita' generali di un metodo senza scendere nei dettagli implementativi (questa proprieta' e' fondamentale nella scrittura delle classi).
  • L'Information Hiding e' la scrittura di interfacce con lo scopo di impedire l'accesso diretto ai dati degli oggetti. Assicura anche una facilita' notevole nella manutenzione e correzione del codice: apportare delle modifiche ad su una sola funzione e non ad un intero modulo e' certamente piu' semplice (questa proprieta' e' fondamentale per l'incapsulamento).

Vantaggi

La programmazione orientata agli oggetti offre tre importanti vantaggi:

  • Manutenzione del codice. Dividere e ordinare il codice in classi, ognuna delle quali svolge un compito preciso, rende piu' semplici la modifica e la leggibilita'.
  • Aggiungere nuovo codice. Inserire funzioni nuove al software e' semplice: bastera' inserire nuovi oggetti o ereditarli da un oggetto padre.
  • Riutilizzo degli oggetti. Se un oggetto e' scritto e testato correttamente, lo si puo' applicare ad altri codici sorgenti.

C++ e Smalltalk

In questo capitolo viene illustrata brevemente la storia dei due linguaggi, centrando le differenze d'implementazione sulle classi, ereditarieta' e polimorfismo.

Storia

C++

Bjarne Stroustrup inizio' il suo lavoro sul C with Classes nel 1979. Ispiratosi al C (per il diffuso utilizzo e la relativa semplicita' di implementazione) e al Simula (per l'originale introduzione alle classi), nel 1983 il nome del linguaggio cambio' in C++ (++ e' l'operatore C per aggiungere 1) per il semplice fatto che Stroustrup aggiunse il concetto di classi al C.

In pochi anni furono aggiunte molte caratteristiche al C++:

  • Funzioni virtuali
  • Overloading di operatori
  • Puntatori e costanti
  • Gestione dinamica della memoria e controllo sui tipi

Nel 1989 usci' la prima edizione di The C++ Programming Language, ma solo nelle ultime edizioni sono state aggiunte importanti caratteristiche come i templates e il tipo bool.

Smalltalk

Esistono oggi un gran numero di varianti dello Smalltalk, anche se la prima versione ufficiale fu lo Smalltalk-80. Nasce seguendo la nuova concezione per cui si sarebbe giunti ad un nuovo mondo della programmazione: simbiosi uomo-computer.

L'idea ed il progetto di base per lo Smalltalk furono sviluppati da un gruppo di ricercatori della Xerox PARC capitanati da Alan Kay. La prima versione di ricerca, lo Smalltalk-71, fu creata in poche mattinate e in a page of code. Allo *-71, seguirono lo *-72 e poi lo *-76 dove troviamo per la prima volta l'implementazione delle classi (prendendo spunto dal Simula).

Lo Smalltalk-80 fu la prima versione resa pubblica dalla Xerox PARC, promossa verso un piccolo numero di compagnie (HP, Apple Computer, Tektronix, ecc...) e universita' (tra cui la Barkeley) per testing e implementazione sulle loro piattaforme.

Solo nel 1983 usci' una versione applicabile su Virtual Machine, sfruttando l'immagine prodotta dal compilatore. La versione ufficiale e' attualmente l'ANSI Smalltalk.

Classi e metodi

C++

Il concetto di classe e' alla base della programmazione in C++. Ogni oggetto e' identificato da una nuova classe che contiene i metodi e le variabili che saranno proprie di quell'oggetto. Viste le profonde radici nel C, scriviamo una pseudo classe con l'aiuto del tipo struct:

struct data {

  int giorno, mese, anno;

}

void data_inizializza (&data);

Ora convertiamo la struttura in una classe C++:

class data {

  private: int giorno, mese, anno;
  public: data (int, int, int);

};

In queste pochissime righe di codice sono contenute varie informazioni riguardo l'oggetto data. Il primo metodo che troviamo e' detto costruttore e, sfruttando l'information hiding, riempie i campi (variabili) passati al metodo.

Le voci private e public descrivono le modalita' d'accesso all'oggetto: nella prima solo i metodi interni possono accedere ai campi; nella seconda, invece, chiunque puo' utilizzare i metodi (viene descritta quindi l'interfaccia all'oggetto).

La sintassi di un metodo in una classe e':

tipo nomeclasse::nomemetodo (...) {

  ...

}

Nell'esempio di prima, il codice del costruttore sara':

data::data (int tgiorno, int tmese, int tanno) {

  giorno = tgiorno;
  mese = tmese;
  anno = tanno;

}

Smalltalk

Nello Smalltalk, a differenza del C++, la classe e' la proprieta' fondamentale del linguaggio. Come e' scritto in moltissimi libri e articoli: qui tutto e' una classe.

La sintassi e': nomeclassegenerale subclass: #nomeclasse

  instanceVariableNames: '...'
  classVariableNames: '...'
  poolDictionaries: '...'
  category: '...'

Come gia' accennato nel capitolo precedente, durante la definizione occorre specificare il nome e la classe generale d'appartenenza: l'ereditarieta' e' spontanea. Occorre poi specificare le variabili della classe (instanceVariableNames) e le variabili comuni alla classe generale (classVariableNames).

La differenza e' sottile ma importante: nelle prime vengono definite le variabili utili alla classe attuale; nella seconda, invece, tutte quelle variabili che sono comuni alla classe da cui si deriva. Se ad esempio si eredita una classe che gestisce la data e si desidera sapere il mese corrispondente al numero, potremmo utilizzare un vettore di stringhe che interpretera' il numero della variabile mese: il vettore in questione sara' dichiarato proprio nel campo classVariableNames, evitando quindi di definirlo ogni volta.

Seguendo l'esempio delle ultime righe, scriviamo il codice: Object subclass: #data

  instanceVariableNames: 'giorno mese anno'
  classVariableNames: 
  poolDictionaries: 
  category: 'gestisce la data'

La classe data, quindi, e' una sotto classe di Object in cui sono definite tre variabili: giorno, mese ed anno (da notare che nello Smalltalk non esistono i tipi per le variabili).

A causa della mancanza di un main, lo Smalltalk mette a disposizione l'istruzione new per istanziare un nuovo oggetto: data new.

che si preoccupera' di ricercare la definizione della classe data e dichiarare sia l'oggetto sia le variabili interne.

Essendo lo Smalltalk un linguaggio che comunica attraverso messaggi (proprieta' comune a tutti i linguaggi di programmazione orientati agli oggetti), la scrittura di un metodo per le classi sara': metodo: parametro1 and: ... parametron

  ...
  ^self ...

L'analisi e' molto semplice, ma a volte molta semplicita' spaventa. La voce metodo corrisponde al nome del metodo, parametroX alle variabili in ingresso alla funzione e i punti di sospensione ... rappresentano il codice della funzione: l'istruzione ^self rappresenta il return del C++. Precisamente, il simbolo ^ rappresenta il return mentre, la voce self, ritorna l'esito del metodo all'oggetto chiamante.

Due proprieta' a confronto

Ereditarieta'

Questa proprieta', diffusa cosi' tanto nello Smalltalk al punto tale da poter scrivere solo classi, puo' causare alcuni problemi di efficienza. Linguaggi come il C++ limitano la creazione di classi solo agli oggetti e alle entita' che il programmatore dichiara e definisce.

Nello Smalltalk non occorre adottare una particolare sintassi: come visto precedentemente, durante la dichiarazione di classe va specificata sia la classe generale sia il nome di quella nuova. I metodi vengono ereditati automaticamente (l'ereditarieta' e' semplice).

E' comunque interessante il processo di ricerca del metodo. Infatti, tutti i metodi nello Smalltalk sono virtuali e quando viene richiesta una funzione si attiva un meccanismo di lookup: il metodo viene prima cercato nella classe dell'oggetto e, se non viene trovato, la ricerca prosegue in tutta la catena ereditaria.

Nel C++, invece, la sintassi e':

class nuovaclasse : public classegenerale {

  ...

};

La voce nuovaclasse e' il nome dell'istanza, mentre classegenerale e' la classe da cui si derivano i dati e i metodi.

Polimorfismo

Anche il polimorfismo, nello Smalltalk, e' di default: tutti gli oggetti che un metodo possiede sono polimorfi fra loro, tesi che viene confermata con l'intervento di prima riguardo la catena ereditaria nella derivazione di classi.

Nel C++ non bastera' riscrivere il metodo adottato per rispettare la proprieta' del polimorfismo: il programmatore si dovra' assicurare di aver definito tale metodo come virtuale nella classe generale.

Ecco un esempio di classe base (super classe):

class specie {

  protected: virtual void camminare ();
  ...

};

e di classe derivata:

class uomo : public specie {

  public: void camminare ();
  ...

};

Come si puo' notare, nella prima occorrera' definire il metodo come virtual nella sezione protected mentre, nella nuova istanza, sara' necessario combinare l'ereditarieta' e ridefinire la funzione.


Il design pattern Observer

Definizione

Il design pattern Observer e' legato al concetto di invocazione implicita, cioe' quell'architettura software che basa il suo funzionamento sulla generazione di eventi. Definisce uno schema uno a molti fra due oggetti chiamati osservatori e soggetti: i primi osservano un evento specifico del soggetto e, non appena quest'ultimo lo genera, gli osservatori vengono informati del cambiamento di stato e aggiornano automaticamente le informazioni.

Caratteristiche

Le classi principali del design pattern sono quattro:

  • Observer e' la classe che riceve le notifiche di aggiornamento e osserva un soggetto (ovvero l'interfaccia che si mette in ascolto dell'evento). Le funzioni che contiene sono:
  • update (): la funzione e' solo una dichiarazione, poiche' la sua definizione avviene nella classe che la eredita.
  • Subject e' la classe che invia le notifiche di aggiornamento ed e' anche l'interfaccia di collegamento e scollegamento verso gli osservatori. Le funzioni che contiene sono:
  • attach (): aggiunge un osservatore alla sua lista.
  • detach (): elimina un osservatore dalla sua lista.
  • notify (): scorre la lista e invia ad ogni osservatore la notifica di aggiornamento chiamando la funzione update () relativa.
  • Concrete Observer e' la classe che utilizza l'interfaccia Observer e mantiene il collegamento con il Concrete Subject per ricevere il segnale di aggiornamento. Implementa la funzione update ().
  • Concrete Subject e' la classe che utilizza l'interfaccia Subject e invia lo stato del soggetto alla lista dei suoi osservatori attraverso la funzione notify ().

Note

Alcune note e punti importanti:

  • Astrazione della classe Observer e Subject. Queste sviluppano il protocollo standard di comunicazione fra le classi associate al modello del design pattern.
  • Information hiding nell'applicazione del design pattern. Il solo scopo qui e' sincronizzare le informazioni, cioe' comunicare a classi simili o diverse un cambiamento di stato: e' nella funzione update () che il programmatore scrivera' il codice per registrare le modifiche ricevute.

Struttura

Per rendere piu' chiara la struttura del design pattern Observer, ho riportato l'immagine del libro Design Patterns: Elements of Reusable Object-Oriented Software:

Inedito observer.gif

Motivazioni

Perche' utilizzare il design pattern Observer? Si immagini un programma che gestice la vendita di cd e vinili musicali (multi utente). Questo software sara' costituito quindi da un database con l'elenco dell'inventario e da un form di ricerca. L'utente, durante la ricerca, dovra' essere avvisato in tempo reale della disponibilita' dell'articolo che desidera.

Applicando il design pattern Observer, e' possibile quindi costruire un modulo Magazzino come Concrete Subject e un modulo Ricerca come Concrete Observer: in questo modo, se un utente preleva un articolo ma nello stesso momento un altro utente sta cercando lo stesso, quest'ultimo vedra' modificare l'informazione riguardo la disponibilita'.

Interrogare periodicamente un numero indefinito di informazioni per verificare se ci sono state modifiche non e' una soluzione efficiente.

Esempio in codice

E' disponibile sul sito un programma che gestisce i post di un blog attraverso il design pattern Observer.

Ogni utente che si abbona ad un blog ricevera' la notifica di un nuovo post non appena verra' inviato un nuovo messaggio nel blog a cui e' iscritto.

Il codice e' scritto in C++ e in Smalltalk. Per la compilazione, ho utilizzato rispettivamente il compilatore g++ (GNU Compiler Collection) e gst (GNU Smalltalk).


Bibliografia

Oltre alla rete e all'enciclopedia libera Wikipedia (http://www.wikipedia.org), ho fatto riferimento ai seguenti testi:

  • Design Patterns: Elements of Reusable Object-Oriented Software di Erich Gamma, Richard Helm, Ralph Johnson e John Vlissides (Gang-Of-Four).
  • Visual C++ 6.0 di Chris H. Pappas e William H. Murray. Ho studiato solo il capitolo Fondamenti di programmazione orientata agli oggetti (trovato nella spazzatura :).
  • GNU Smalltalk User's Guide del progetto GNU (GNU's Not Unix).
  • Appunti di Lorenzo Bettini (disponibili sulla rete).


Ringraziamenti

Un abbraccio a Martina, Emiliano, Sigur Ros e Pink Floyd :)
Nota: un grazie a Quequero per avermi permesso di pubblicare l'appunto :).


Disclaimer

I documenti qui pubblicati sono da considerarsi pubblici e liberamente distribuibili, a patto che se ne citi la fonte di provenienza. Tutti i documenti presenti su queste pagine sono stati scritti esclusivamente a scopo di ricerca, nessuna di queste analisi è stata fatta per fini commerciali, o dietro alcun tipo di compenso. I documenti pubblicati presentano delle analisi puramente teoriche della struttura di un programma, in nessun caso il software è stato realmente disassemblato o modificato; ogni corrispondenza presente tra i documenti pubblicati e le istruzioni del software oggetto dell'analisi, è da ritenersi puramente casuale. Tutti i documenti vengono inviati in forma anonima ed automaticamente pubblicati, i diritti di tali opere appartengono esclusivamente al firmatario del documento (se presente), in nessun caso il gestore di questo sito, o del server su cui risiede, può essere ritenuto responsabile dei contenuti qui presenti, oltretutto il gestore del sito non è in grado di risalire all'identità del mittente dei documenti. Tutti i documenti ed i file di questo sito non presentano alcun tipo di garanzia, pertanto ne è sconsigliata a tutti la lettura o l'esecuzione, lo staff non si assume alcuna responsabilità per quanto riguarda l'uso improprio di tali documenti e/o file, è doveroso aggiungere che ogni riferimento a fatti cose o persone è da considerarsi PURAMENTE casuale. Tutti coloro che potrebbero ritenersi moralmente offesi dai contenuti di queste pagine, sono tenuti ad uscire immediatamente da questo sito.

Vogliamo inoltre ricordare che il Reverse Engineering è uno strumento tecnologico di grande potenza ed importanza, senza di esso non sarebbe possibile creare antivirus, scoprire funzioni malevole e non dichiarate all'interno di un programma di pubblico utilizzo. Non sarebbe possibile scoprire, in assenza di un sistema sicuro per il controllo dell'integrità, se il "tal" programma è realmente quello che l'utente ha scelto di installare ed eseguire, né sarebbe possibile continuare lo sviluppo di quei programmi (o l'utilizzo di quelle periferiche) ritenuti obsoleti e non più supportati dalle fonti ufficiali.