La programmazione ad oggetti



Cosa identifica la OOP ?  
Oggetti e classi :  identità, stato, classe, creazione,  metodi che osservano e che modificano,  notazione punto
Information hiding : ruolo e modalità di occultamento
Ereditarietà   : cosa si eredita ? ereditarietà semplice e multipla
Polimorfismo : quale?  perchè è così importante? classi astratte,  definizione e ridefinizione di metodi
Genericità : classi generiche, downcasting,   genericità vincolata
Aspetti avanzati : copia e uguaglianza,   asserzioni, eccezioni,  contenitori e iteratori,  distruttori,  persistenza, covarianza, concorrenza e reti

1. Cosa identifica la OOP ?
L'acronimo OOP in informatica sta per Object Oriented Programming (Programmazione Orientata agli Oggetti) e vuole denotare l'insieme delle caratteristiche a fondamento del paradigma della programmazione ad oggetti. Storicamente,  le prime idee  fondanti di questo paradigma sono sorte con il Simula '67, un linguaggio di simulazione discreta di scuola scandinava, e sono state poi riscoperte negli USA negli anni '70-'80 con il linguaggio Smalltalk. Buona parte del gergo della OOP si deve a questi due linguaggi.
Dalla metà degli anni '80 esiste un generale accordo sulle caratteristiche che devono essere presenti in un linguaggio per poterlo classificare come OOPL, cioè per affermare che mette a disposizione il paradigma della OOP:
1. insieme di operazioni astratte, associate ad un tipo
2. stato locale
3. ereditarietà
Le metafore più pertinenti per gli oggetti della programmazione sono probabilmente le apparecchiature di tipo elettrico/elettronico.  
esse riportano  un numero di serie (l'identità)
hanno degli indicatori di vario genere che descrivono (parte) dello stato interno
esibiscono dei comportamenti, in seguito al'azionamento di alcune leve, pulsanti, ecc.
La prima caratteristica, spesso associata  all'acronimo ADT (Abstract Data Type), stabilisce che l'unico modo per agire sugli oggetti di un certo tipo è quello di invocare delle operazioni (nel gergo OOP chiamate metodi) prestabilite e note agli utenti  in modo astratto, cioè indipendente dalla rappresentazione prescelta per la loro concreta implementazione (realizzazione).
La seconda caratteristica stabilisce che ogni oggetto ha un proprio stato locale che può cambiare per effetto dell'esecuzione di alcuni metodi. In pratica il valore dello stato locale ad un certo istante è stabilito dal contenuto di un insieme di celle di memoria associate all'oggetto, dette campi o attributi dell'oggetto.
L'ereditarietà  stabilisce infine che il linguaggio deve prevedere un meccanismo per stabilire delle relazioni IS-A ("è-un") tra classi di oggetti. Questo meccanismo consente in definitiva il riuso e il polimorfismo, due dei principali fattori alla base del successo della OOP.

2. Oggetti e classi
Un oggetto (software) è un'entità dotata di:
identità, che permette quindi sempre di distinguere un oggetto da un altro (un "numero di serie")
stato, quindi in grado di "ricordare" qualcosa
comportamento, che si traduce nella possibilità di osservare (in tutto o in parte) lo stato e/o di  modificare lo stato, tramite l'invocazione dei metodi sull'oggetto.
Un modo corretto di pensare ad un oggetto è rappresentato nella seguente figura:
  I blocchi P1, P2,  ...  ed  F1, F2, ...  rappresentano i "meccanismi interni" dell'oggetto: in pratica sono dei sottoprogrammi (i metodi) che realizzano le operazioni   previste dall'oggetto. Seguendo una metodologia di progetto ampiamente condivisa, i metodi vengono spesso divisi in  in due categorie:
comandi,   cioè metodi che modificano lo stato dell'oggetto. Sono procedure proprie, cioè che non restituiscono valori ma che producono degli effetti collaterali, generalmente limitati allo stato locale dell'oggetto.
osservatori ( queries),  cioè metodi che  si limitano ad osservare lo stato dell'oggetto. Sono procedure di tipo funzionale che restituiscono un valore ottenuto come elaborazione (solitamente molto semplice) dello stato corrente dell'oggetto.
Questa separazione, anche se può comportare qualche inefficienza, favorisce in genere la leggibilità e la trasparenza referenziale del codice.
Per creare gli oggetti la tecnica più seguita è quella di definire prima una classe, cioè uno schema di creazione che definisca i possibili stati e i comportamenti, e di invocare un'operazione di istanziazione della classe, detta costruttore o creatore, che determina univocamente l'identità di un oggetto e che ne stabilisce lo stato iniziale. Una possibile sintassi di creazione (per linguaggi tipati) potrebbe essere:
   variabile   nuovo [ nome_costruttore ( parametri_attuali_se_previsti ) ]
In pratica una classe sarà definita da un nome a cui viene associato un elenco di  costruttori,  attributi e metodi, che collettivamente chiameremo proprietà della classe. Nei linguaggi OOP tipati, la definizione di una classe comporta generalmente anche quella di un tipo con lo stesso nome.
Si ammette che dall'elenco possano mancare i costruttori: in tal caso viene usato implicitamente un costruttore di default (senza parametri).
Per accedere al valore di un attributo o per invocare  un metodo su un oggetto la maggior parte dei linguaggi fa uso della notazione punto:
espressione_oggetto.nome_attributo
espressione_oggetto.nome_metodo( eventuali_parametri_attuali_se_previsti )
Esempio di classe
classe CALCOLATRICE
    costruttori
crea()
esegui
    accumulatore:=0
fine
crea_inizializzando_a(valore_iniziale: REALE)
esegui
    accumulatore:=valore_iniziale
fine
    attributi  /* si intende che sono nascosti */
accumulatore: REALE
    metodi
somma(x: REALE)
esegui
   accumulatore := accumulatore+x
fine
moltiplica(x: REALE)
esegui
    accumulatore:=accumulatore*x
fine
risultato() : REALE
esegui
  restituisci accumulatore
fine
fine classe CALCOLATRICE

Esempio di creazioni:
var C1,C2: CALCOLATRICE
...
C1 nuovo crea();
C2 nuovo crea_inizializzando_a(3.0);
Esempi d'uso
C1.somma(5.0);
C2.somma(1.0);
C1.moltiplica(C2.risultato());
scrivi C1.risultato();  /* scrive 20.0 */
3. Information hiding
Il concetto di information hiding (che potremmo tradurre con "occultamento di informazione") nella OOP � implicito in quello di ADT  e consiste nell'idea che l'utente di un servizio è tenuto a conoscere solo le  informazioni strettamente necessarie per l'usufruizione del servizio.  Ogni altra informazione pu� confondere l'utente e/o mettere a rischio l'integrità dell'oggetto stesso.
Tecnicamente si afferma che l'utente deve conoscere solo l' interfaccia della classe, cioè il suo nome e l'interfaccia di ciascuna proprietà pubblica.
Alcuni sistemi sono in grado di estrarre/documentare automaticamente l'interfaccia di classe dalla definizione completa di un classe. 
In realtà esiste un altro importante vantaggio dell'information hiding: l'implementatore di una classe potrà ritenersi libero di effettuare delle migliorie senza che questo comporti delle modifiche ai programmi clienti, cioè che usino i servizi offerti da quella classe. L'information hiding è quindi determinante non solo per semplificare la vita ai programmatori, ma anche per la manutenzione e il "riuso del software".
interfaccia della classe CALCOLATRICE
    costruttori
crea()
crea_inizializzando_a(valore_iniziale: REALE)
    metodi
somma(x: REALE)
moltiplica(x: REALE)
risultato() : REALE
fine interfaccia
In particolare, nella OOP, l'informazione da  nascondere è la rappresentazione dello stato dell'oggetto, cioè  l'insieme degli attributi. A rigore, quindi, tutti gli attributi dovrebbero essere nascosti: l'utente deve potervi accedere solo indirettamente attraverso i metodi. Tuttavia,  per motivi di efficienza, alcuni linguaggi ad oggetti (tra cui  C++, Java e Delphi) permettono l'accesso agli attributi (sia in lettura che in scrittura).  Il problema dell'information hiding viene risolto in questi linguaggi permettendo l'accesso solo ad alcuni attributi classificati come pubblici (tipicamente saranno quelli presenti in tutte le possibili rappresentazioni). Oltre agli attributi si possono nascondere anche metodi. Tipico è il caso dei metodi ausiliari, cioè utilizzati per implementare i metodi pubblici, e più in generale, di tutti quei metodi la cui esistenza  è legata alla rappresentazione scelta.
In generale, ogni linguaggio propone proprie soluzioni al problema della visibilità, cioè di come stabilire a quali classi rendere visibili le propriet� (sia attributi che metodi) di una classe. A titolo puramente  indicativo possiamo citare la soluzione a tre livelli del C++:
proprietà pubbliche: tutte le classi possono accedervi
proprietà protette: solo le sottoclassi possono accedervi
proprietà private: nessuna classe pu� accedervi
Infatti, per questioni di flessibilità ed efficienza, si sente la necessità di poter definire diversi "livelli" di visibilità. L'ideale è poter decidere per ogni proprietà, quali sono le classi che possono accedervi. L'ereditarietà, in particolare, pone un problema che occorre risolvere di volta in volta: quali proprietà ereditate di una classe devono essere nascoste alle sue sottoclassi ? alcuni linguaggi (ad es. C++ e Java) danno questa risposta: quelle pubbliche e quelle dichiarate appunto come "protette". Altri (ad es. Eiffel) sostengono la tesi che tutto quello che è visibile all'implementatore di  una classe deve essere visibile anche  agli implementatori delle sue sottoclassi. Inoltre Eiffel non permette la modalità "private", sostenendo il punto di vista secondo cui l'implementatore di una classe non può decidere arbitrariamente che alcuni attributi non siano utili agli implementatori delle sottoclassi.
4. Ereditarietà
L'ereditarietà risulta  indispensabile per  processi di modellazione molto importanti nella moderna costruzione del software quali la specializzazione e la  generalizzazione.
Nella sua forma più semplice, l'ereditarietà (inheritance) è un meccanismo che consente ad una classe di considerarsi erede di un'altra, detta classe padre o genitore (parent class), con una dichiarazione che possiamo assumere del tipo:
classe <nome> eredita da <classe padre>
...
Cos� facendo la classe <nome>, detta classe figlia (o classe derivata), eredita tutte le propriet� della <classe padre> specificata : tutti gli attributi e i metodi (anche quelli nascosti).
classe CALCOLATRICE_DELUX eredita da CALCOLATRICE
/* ha  in più la sottrazione e la possibilità di verificare se il risultato dell'ultima operazione rientra in un certo intervallo definito in fase di creazione */
    costruttori
crea()
esegui
    CALCOLATRICE::crea()
    liminf:=MINREAL
    limsup:=MAXREAL
fine
crea_con(valore_iniziale,inf,sup: REALE)
esegui
    CALCOLATRICE::crea_inizializzando_a(valore_iniziale)
    liminf:=inf;
    limsup:=sup
fine
    attributi  /* si intende che sono nascosti-protetti */
liminf, limsup: REALE:
stato_supero: BOOLEANO
    metodi    /* si intende che sono pubblici*/
somma(x: REALE)    /* ridefinito */
esegui
   CALCOLATRICE::somma(x);
   controlla_risultato()
fine
moltiplica(x: REALE)   /* ridefinito */
esegui
    accumulatore:=accummulatore*x
    controlla_risultato()
fine
sottrai(x: REALE) 
esegui
    accumulatore:=accummulatore-x
    controlla_risultato()
fine
supero() : BOOLEANO
esegui
   restituisci stato_supero
fine
metodi nascosti
controlla_risultato()
esegui
   stato_supero := (accumulatore >= liminf ) and
                               (accumulatore <= limsup)
fine
fine classe CALCOLATRICE_DELUX
Sono state proposte varie notazioni grafiche per rappresentare una relazione di ereditarietà tra classi. Una delle più semplici è la seguente, in cui la relazione è  rappresentata da un arco diretto dalla classe figlia alla classe padre. Rappresentando l'insieme delle relazioni di questo genere si ottiene il grafo di ereditarietà
Da un punto di vista pratico si può ereditare da qualsiasi classe, a patto di non introdurre cicli nel  grafo di ereditarietà. Tuttavia, come suggerito dai nomi delle classi della figura, da un punto di vista metodologico è  opportuno e consigliato ereditare X da Y solo quando si è certi che tra Y e X  esiste una  relazione Y IS-A X ("Y è un X"), o in altri termini,  quando  la classe figlia è una specializzazione di quella padre.
Oltre a ereditare, la classe figlia può:
aggiungere propri attributi o metodi
ridefinire (overriding) metodi ereditati
In questo caso il metodo ereditato (metodo precursore) e quello ridefinito hanno lo stesso nome. I linguaggi devono predisporre meccanismi linguistici opportuni per consentire all'implementatore della classe di accedere ai precursori di un metodo di qualsiasi livello. Si intende che gli utenti della classe si riferiranno a quello ridefinito.
Si osservi che per ogni classe rimane definito l'insieme dei sui discendenti (supertipi) e dei sui discendenti (sottotipi).
Molti linguaggi ad oggetti consentono solo l'ereditarietà semplice: ogni classe ha al massimo un padre. La classificazione degli oggetti è quindi strettamente gerarchica. Se esiste un'unica radice questa ha nomi come Object o ANY.
I linguaggi ad oggetti più evoluti consentono l'ereditarietà multipla: cioè la possibilità per una classe di ereditare da più di una classe (cioè di avere più padri).  Sorgono però problemi non banali di ereditarietà ripetuta: esiste cioè la possibilità di ereditare più di un metodo con lo stesso nome e occorre risolvere in qualche modo le ambiguità che si vengono a creare.
5. Il polimorfismo
Tra i vari tipi di polimorfismo che possono essere presenti in un linguaggio, quello che si deve necessariamente   ritrovare nella programmazione ad oggetti è  noto come polimorfismo di inclusione universale, basato proprio sul concetto di ereditarietà. Questa forma di polimorfismo si basa sui seguenti presupposti:
a) la possibilità di usare variabili polimorfe, cioè che possono riferirsi ad oggetti di tipi diversi (generalmente "inclusi" in una certa gerarchia). In pratica basta permettere ad una variabile di tipo T di ricevere un oggetto di un qualsiasi sottotipo di T.
b) la possibilità di effettuare chiamate  polimorfe, cioè di indicare con lo stesso nome dei metodi che appartengono a classi diverse e che sono quindi generalmente diversi ("polimorfo" = "avente più forme").
  A differenza di altre forme di polimorfismo, come l'overloading (sovraccarico o sovrapposizione) degli operatori, il polimorfismo di inclusione universale prevede che la decisione su quale debba essere la routine da richiamare viene presa a tempo di esecuzione a seconda della classe effettiva (più stretta) di appartenenza dell'oggetto rispetto a cui viene fatta la chiamata. Questa tecnica è nota come collegamento dinamico (late o dynamic binding) dei nomi al codice che deve essere effettivamente eseguito. Esso si contrappone al tradizionale collegamento statico (early o static binding)  deciso dal compilatore, di norma, nel caso di chiamate non polimorfe.
Spesso il polimorfismo viene introdotto con le classe astratte, vale a dire  classi in cui compaiono uno o più metodi la cui definizione viene rinviata alle sottoclassi (metodi astratti). Tipico è l'esempio rappresentato nella figura qui sotto, in cui la classe ANIMALE ha un metodo astratto   fai_verso() che trova una concreta realizzazione (definizione)  solo nei discendenti "concreti" (ad es. CANE, GATTO, ecc. ).  
Il polimorfismo trova applicazione soprattutto in schemi simili a quello esemplificato dal seguente frammento:
per ogni a : ANIMALE in S esegui
    a.fai_verso()
fineperogni
Esempio:
classe astratta ANIMALE
    metodi   
fai_verso()    astratto
fine classe ANIMALE
----------------------------------------------------------------------------------------------------------------
classe CANE eredita da ANIMALE
   metodi
fai_verso()  esegui print("bau-bau") fine
fine classe CANE
------------------------------------------------------------------------------------------------------------------
classe GATTO eredita da ANIMALE
   metodi
fai_verso()  esegui print("miao-miao") fine
fai_fusa()  esegui print("purr-purr") fine
fine classe GATTO
------------------------------------------------------------------------------------------------------------------
var a: ANIMALE; c: CANE;  g: GATTO
....
      c nuovo CANE;
      g nuovo GATTO;
      se random() =< 0.5 /*probabilità che sia un cane*/
         allora a:=c
         altrimenti a:=g
      finese;
      a.fai_verso();  /* chiamata polimorfa */
Si immagina che  la variabile a assuma di volta in volta i vari oggetti (di tipo ANIMALE)  che si trovano in un insieme o in un array S. Alla variabile a verrà quindi di volta in volta associato un animale potenzialmente diverso e a priori sconosciuto.  Quindi, per  il compilatore, la chiamata a.fai_verso() è  polimorfa e, come tale, soggetta al collegamento dinamico.
6. Genericità
Un'altra delle caratteristiche desiderabili del linguaggi OOP tipati  è la genericità, cioè la possibilità di definire delle classi con uno o più  parametri di tipo "tipo". Questa possibilità è utile soprattutto quando si vogliono definire delle classi di tipo contenitore di dati: array, pile, code, alberi, ecc.
L'idea è quella di estendere la possibilità di dichiarare il tipo degli elementi presente per il tipo predefinito  array (ad es. array of integer, array of string, ecc. ) ai contenitori definiti dall'utente (pila of integer, pila of string, ecc. ).
Esempio:
classe PILA di T
    creatori
crea_con_max(n: INTERO)
esegui
    a nuovo  crea(1,n);
    sp:=0;
fine
    attributi  /* si intende che  sono nascosti-protetti */ 
a: ARRAY di T
sp: INTERO
    metodi
top(): T esegui restituisci a[sp]   fine
pop() esegui sp:=sp-1 fine
push(x: T) esegui sp:=sp+1; a[sp] := x  fine
empty(): BOOLEANO esegui restituisci (sp=0) fine
fine classe PILA
----------------------------------------------------------------------------------------------------------------
Esempi di creazione:
var pilaint: PILA di INTERO; pilastr: PILA di STRINGA;
..
      pilaint nuovo crea_con_max(100);
      pilastr nuovo crea_con_max(50);
      pilaint.push(4);
      pilaint.push(pilaint.top()+10);
      pilastr.push("salve");
      pilastr.push(5);  /* il  compilatore segnala incompatilità sul  tipo dell'argomento di push */
In assenza di questo strumento (ad es. Java ne è sprovvisto) si è costretti a definire una classe di oggetti "qualsiasi" (ANY o Object) e fare quindi un downcasting, cioè una conversione forzata al sottotipo che ci interessa di volta in volta.
Il ricorso al downcasting si rivela comunque necessario in altre occasioni, come quelle che si presentano quando dobbiamo elaborare una collezione di oggetti di varie classi utilizzando le loro operazioni specifiche (non polimorfe). 
Esempio
Supponiamo di voler far fare il verso  a tutti gli animali  che si trovano in S e di far  fare inoltre le fusa a tutti i gatti:
var g: GATTO
per ogni a : ANIMALE  in S esegui
    a.fai_verso()
    g:= a come GATTO; /* tentativo di downcasting */
    se g <> nullo allora    
          g.fai_fusa()
    finese
fineperogni
Nel caso in cui il casting non abbia successo in g viene restituito l'oggetto nullo, cioè l'oggetto su cui non è permessa alcuna operazione, a parte quella di confronto per = e <>.
A volte esiste la necessità di  restringere i possibili parametri attuali di tipo "tipo" a quelli che possiedono determinate operazioni (tipico è il caso dell'operazione di confronto : <, >, ecc.). Alcuni linguaggi (ad es. Eiffel e Sather)  mettono a disposizione a tale proposito  la cosiddetta genericità vincolata: solo le classi  discendenti di una certa classe specificata possono essere sostituite al posto del parametro.
7. Aspetti avanzati
7.1 Copia e uguaglianza
La semantica di default per le operazioni di assegnamento e di confronto per = fra oggetti prevede che vengano coinvolte solo le identità degli oggetti (in pratica, spesso, dei puntatori di memoria). Nel caso dell'assegnamento questo può creare degli spiacevoli effetti collaterali  dovuti alla condivisione di strutture dinamiche che si viene a creare. Per evitarli occorre effettuare l'assegnamento con operazioni del tipo a:=copia(b) che ha l'effetto di creare in a  un puntatore ad una copia dell'oggetto indicato in b.
Nel confronto x = y tra oggetti, se si vuole che il confronto non venga fatto sulle identità (questo è quasi sempre il caso) ma bensì sullo stato dei due oggetti,  è necessario usare operatori diversi come, ad es.,  uguale(x,y) .
In genere i linguaggi mettono a disposizione del programmatore  la possibilità di ridefinire copia e uguale   classe per classe.
7.2 Asserzioni
Le asserzioni in un linguaggio di programmazione sono delle espressioni booleane valutabili a tempo di esecuzione che  possono assolvere a diversi scopi:
forma primitiva di specifica e di documentazione prevenzione contro possibili malfunzionamenti del software; in particolare nella OOP come precondizioni e postcondizioni dei metodi strumento di verifica durante la fase di testing
possibili clausole di uno stile di programmazione noto come progetto per contratto
I linguaggi OOP si differenziano notevolmente rispetto al supporto e alla varietà di tipi di asserzioni che mettono a disposizione del programmatore.
7.3 Eccezioni
Per aumentare la robustezza di un'applicazione, tutti i più recenti linguaggi OOP mettono a disposizione la possibilità definire, sollevare e trattare eccezioni, cioè eventi causati da malfunzionamenti o imprevisti hardware o software che si possono sempre verificare. In particolare il programmatore di ogni metodo ha la possibilità di catturare un'eccezione, riconoscerla e prendere le necessarie azioni di recupero almeno per mantenere l'oggetto in uno stato interno consistente. Il trattamento standard di default è quello di propagare l'eccezione al chiamante, fino eventualmente a raggiungere il metodo avviato per primo, facendolo abortire. 
7.4 Contenitori e iteratori
Una volta definito come classe un contenitore astratto di dati (ad es. una pila, una lista, una tabella, ecc.) si può presentare il problema di attraversare uno di questi contenitori,  cioè di costruire cicli del tipo
portati sul primo elemento
finchè non sei giunto all'ultimo e  non hai trovato una condizione di arresto
   elabora l'elemento corrente
   passa all'elemento successivo
finefinchè
La soluzione più generale, flessibile e astratta  a questo problema prevede il ricorso a degli oggetti chiamati iteratori la cui classe di appartenenza viene definita in "simbiosi" a quella dei contenitori che li attraversano. In pratica essi assolvono ad un ruolo molto simile a quello rivestito dagli indici dei "for" per l'attraversamento degli array. La differenza è che viene completamente nascosta la rappresentazione dei cursori. Il cursore  è l'oggetto   che all'interno di un iteratore "punta" all'elemento del contenitore in corso di attraversamento. Questo permette un domani di cambiare sia la rappresentazione dei contenitori che quella degli iteratori senza cambiare il codice delle applicazioni "clienti".
7.5 Distruttori
Oltre ai costruttori può essere talvolta necessario che il programmatore definisca dei metodi, chiamati spesso distruttori, che assolvono al compito di svolgere delle azioni finali prima che l'oggetto venga completamente distrutto e le sue aree di memoria rese disponibili per operazioni di allocazione di nuovi oggetti.  In particolare, in C++, i distruttori hanno spesso l'onere di recuperare parte della memoria occupata dall'oggetto. Da questo onere sono liberi tutti i linguaggi che prevedono il garbage collector, cioè quel processo della macchina virtuale del linguaggio che assolve appunto al compito di individuare e recuperare le aree occupate da oggetti che si rendono inaccessibili ("garbage", appunto).
7.6 Persistenza
  La persistenza nella OOP è la proprietà di un oggetto di sopravvivere al processo che l'ha creato. Un modo per implementare la persistenza è tipicamente quello di "salvarlo" su un supporto di memoria persistente, come il disco. I linguaggi OOP adottano varie tecniche per fornire questo tipo di servizio all'utenza. Il più semplice e primitivo  è quello di mettere a disposizione delle primitive di salvataggio e recupero di un oggetti su un file con un certo nome. Il più sofisticato (ma anche il più semplice da usarsi) è quello di prevedere la possibilità di dichiarare come persistenti alcune classi: gli oggetti creati saranno automaticamente resi persistenti .
7.7 Covarianza
Una questione che si può porre nel progetto delle classi è:  si può, al momento di ridefinire un metodo (overriding), cambiare il tipo di un parametro formale ?   in particolare: si può specificare un sottotipo ?
I linguaggi si dividono sostanzialmente in due categorie: quelli che rispondono negativamente (linguaggi non-varianti, come C++ e Java) e quelli che rispondono affermativamente (linguaggi covarianti, come Eiffel). Purtroppo la covarianza del sistema di tipi,  pur venendo incontro a utili esigenze pratiche di flessibilità, espone il linguaggio ad alcuni rischi.
7.8 Concorrenza e reti
Fin dal suo concepimento, nel paradigma ad oggetti è stato preso in considerazione l'ipotesi di considerare ogni oggetto come avente un proprio "thread of control". In particolare, Smalltalk vedeva la chiamata di un metodo come la spedizione di un messaggio da parte di un oggetto mittente (sender)   ad uno ricevente (receiver) contenente la richiesta di un particolare servizio il cui termine veniva comunicato al mittente assieme all'eventuale  restituzione di un valore. Con una visione più moderna, si può considerare la chiamata di un metodo come la richiesta di un servizio ad un oggetto servente in grado di  accettare e ordinare opportunamente le richieste provenienti dai vari oggetti clienti. Alcuni linguaggi ad oggetti mettono a disposizione  strumenti od  estensioni a supporto della concorrenza e/o della distribuzione degli oggetti. Ad esempio Java mette a disposizione la libreria RMI (Remote Method Invocation) per la chiamate di metodi ad oggetti remoti.  

Commenti

Post popolari in questo blog

Simulazioni di reti (con Cisco Packet Tracer)

Esercizi sulla rappresentazione della virgola mobile IEEE 754 (Floating Point)