Richiami di programmazione e di ingegneria del software: l’orientamento ad oggetti
Nella progettazione di un sistema software è opportuno basarsi sul principio della modularità, affinché esso risulti più semplice da progettare, realizzare e manutenere.
I moduli sono componenti (parti) del sistema che realizzano un’astrazione.
I meccanismi di astrazione più diffusi sono:
Astrazione sul controllo
Astrazione di una funzionalità dai dettagli dell’implementazione.
E’ ben supportata dai linguaggi di programmazione tradizionali tramite il concetto di sottoprogramma.
Astrazione sui dati
Consiste nell’astrarre le entità (oggetti) costituenti il sistema, descritte in termini di una struttura dati e delle operazioni possibili su di essa.
Può essere realizzata con un uso opportuno delle tecniche di programmazione modulare nei linguaggi tradizionali.
E’ alla base della modellazione a oggetti.
E’ supportata da appositi costrutti nei linguaggi di programmazione ad oggetti.
Oggetti astratti
Un oggetto (astratto) è costituito da una struttura dati e dalle operazioni possibili su di essa.
Il valore corrente che assume la struttura dati costituisce lo stato dell’oggetto.
Tipi di dati astratti (TDA)
Un TDA è la specifica di un insieme di oggetti astratti.
Un TDA è un tipo d’utente, cioè non predefinito dal linguaggio.
Come tipo di dati, è una specifica, va istanziato per creare specifici oggetti.
Gli esemplari del tipo sono oggetti astratti.
Il concetto di tipo di dato in un linguaggio di programmazione tradizionale è quello di insieme dei valori che può assumere un dato (una variabile).
Il tipo di dati astratto (TDA) estende questa definizione, includendo anche l’insieme di tutte e sole le operazioni possibili su dati di quel tipo. La struttura dati “concreta” è incapsulata nelle operazioni su di essa definite.
Non è possibile accedere alla struttura dati incapsulata (né in lettura né in scrittura) se non attraverso le operazioni definite su di essa.
Esempio: TDA Auto
Un vantaggio: la struttura dati interna non può essere alterata da operazioni scorrette da parte dell’utente, in quanto ad essa si accede solo tramite le operazioni previste e realizzate dal produttore.
Interfaccia: specifica del TDA, descrive la parte direttamente accessibile dall’utilizzatore.
Realizzazione: implementazione del TDA.
Il cliente o utilizzatore fa uso del TDA per realizzare procedure di un’applicazione, o per costruire TDA più complessi.
Il produttore realizza le astrazioni e le funzionalità previste per il dato.
Un produttore di un TDA può essere utilizzatore di un altro TDA
Una modifica nella sola realizzazione del TDA non influenza i moduli che ne fanno uso (in quanto non cambia l’interfaccia).
Il conducente interagisce con una interfaccia per effettuare le operazioni consentite sull’automobile:
Metodologia discendente o “top-down“‘: basata sulla decomposizione funzionale nella definizione del sistema software => individuazione delle funzionalità e raffinamenti successivi.
Metodologia ascendente o “bottom-up“‘: basata su individuazione di entità del sistema (classi e/o oggetti), delle loro proprietà e delle interrelazioni tra di esse.
Il sistema viene costruito assemblando componenti con un approccio “dal basso verso l’alto2.
La modellazione a oggetti è di tipo bottom-up.
Si individuano le classi di oggetti (entità del mondo reale o concettuale) che caratterizzano il dominio applicativo:
L’applicazione si costruisce assemblando oggetti e individuando le modalità con cui questi devono collaborare per realizzare le diverse funzionalità dell’applicazione.
I principi della modellazione ad oggetti non sono vincolati ad una fase, ma riguarda l’analisi, la progettazione e l’implementazione.
In fase di analisi, la classe modella, a livello concettuale, una entità del dominio del problema, indipendentemente da come essa sarà rappresentata e gestita nel sistema software.
L’attenzione dovrà essere rivolta alla comprensione di “cosa” il sistema software dovrà fare e non “come” lo fa.
In fase di progetto, una classe rappresenta un modulo del sistema software (ancora indipendentemente dalla successiva implementazione in termini di codice, con strutture dati e algoritmi).
In fase di implementazione (codifica o programmazione), le classi e le loro relazioni costituiscono la reale struttura del software.
La classe è un modulo software con le seguenti caratteristiche:
Al livello dell’implementazione, distinguiamo tra:
Programmazione con oggetti con riferimento a tecniche di programmazione basate sul concetto di oggetto (dati+operazioni).
Programmazione basata sugli oggetti (object-based programming) con riferimento alle tecniche di programmazione basate sui concetti di:
Programmazione orientata agli oggetti (object-oriented programming, OOP) con riferimento alle tecniche basate sui concetti di:
È possibile adottare tecniche di programmazione con oggetti o basate sugli oggetti anche in linguaggi tradizionali (ad es., in C o Pascal), adoperando opportune discipline di programmazione, aderendo cioè ad un insieme di regole, il cui uso però non può essere forzato né verificato dal compilatore.
Un linguaggio di programmazione ad oggetti offre costrutti espliciti per la definizione di entità (oggetti) che incapsulano una struttura dati nelle operazioni possibili su di essa.
Alcuni linguaggi, in particolare il C++, consentono di definire tipi astratti, e quindi istanze (cioè, variabili) di un dato tipo astratto. In tal caso il linguaggio basato sugli oggetti presenta costrutti per la definizione di classi e di oggetti.
Esistono dunque linguaggi ad oggetti.
Non tipizzati
Tipizzati
C++ è un linguaggio di programmazione general-purpose, tipizzato, di tipo compilato, che supporta:
C++ è quindi un linguaggio ibrido, nel senso che supporta più paradigmi di programmazione.
Java è un linguaggio general-purpose, tipizzato, per la programmazione orientata gli oggetti.
È un linguaggio compilato, ma il compilatore non genera codice macchina dipendente dallo specifico processore, ma un codice intermedio (bytecode), interpretato dalla cosiddetta macchina virtuale Java (JVM).
L’uso delle classi è orientato alla riusabilità del software.
Occorre dunque fare riferimento ad una situazione di produzione del software nella quale operano:
Utente.cpp Mostra codice
Contatore.h Mostra codice
Contatore.cpp Mostra codice
Nei linguaggi a oggetti tipizzati, il costrutto classe consente di definire nuovi tipi di dato (astratti) e le relative operazioni.
Sotto forma di operatori o di funzioni (dette metodi o funzioni membro), i nuovi tipi di dato possono essere gestiti quasi allo stesso modo dei tipi predefiniti del linguaggio:
Un oggetto è una variabile istanza di una classe.
Lo stato di un oggetto è rappresentato dai valori correnti delle variabili che costituiscono la struttura dati concreta sottostante il tipo astratto.
Il linguaggio C++ supporta esplicitamente la dichiarazione e la definizione di tipi astratti da parte dell’utente mediante il costrutto class.
Le istanze di una classe sono dette oggetti.
In una dichiarazione class occorre specificare sia la struttura dati che le operazioni consentite su di essa.
Una classe possiede, in generale, una sezione pubblica ed una privata.
La sezione pubblica contiene tipicamente le operazioni (dette anche metodi) consentite ad un utilizzatore della classe. Esse sono tutte e sole le operazioni che un utente può eseguire, in maniera esplicita od implicita, sugli oggetti.
La sezione privata comprende le strutture dati e le operazioni che si vogliono rendere inaccessibili dall’esterno.
Esempio di interfaccia di un tipo astratto “Contatore” in C++
Contatore.cpp Mostra codice
L’ereditarietà consente di definire nuove classi per specializzazione o estensione di classi preesistenti, in modo incrementale.
Il polimorfismo consente di invocare operazioni su un oggetto, pur non essendo noto a tempo di compilazione il tipo (classe) corrente cui fa riferimento l’oggetto stesso.
L’ereditarietà è di fondamentale importanza nella modellazione ad oggetti, in quanto induce una strutturazione gerarchica nel sistema software in costruzione.
Essa consente di realizzare relazioni tra classi di tipo generalizzazione-specializzazione, in cui una classe, detta base, realizza un comportamento generale comune ad un insieme di entità, mentre le classi derivate (sottoclassi) realizzano comportamenti specializzati rispetto a quelli della classe base.
Esempio
In una gerarchia gen-spec, le classi derivate sono specializzazioni (cioè casi particolari) della classe base.
Generalizzazione: dal particolare al generale.
Specializzazione o particolarizzazione: dal particolare al generale.
Oltre a poter descrivere un sistema secondo un modello gerarchico, esiste un altro motivo, di ordine pratico, per cui conviene usare l’ereditarietà; esso è legato al concetto di riuso del software.
In alcuni casi si ha a disposizione una classe che non corrisponde esattamente alle proprie esigenze
Anziché scartare del tutto il codice esistente e riscriverlo, si può seguire con l’ereditarietà un approccio diverso, costruendo una nuova classe che eredita il comportamento di quella esistente, salvo che per i cambiamenti che si ritiene necessario apportare.
Tali cambiamenti possono riguardare sia l’aggiunta di nuove funzionalità che la modifica di quelle esistenti.
In definitiva, l’ereditarietà offre il vantaggio di ridurre i tempi di sviluppo, in quanto minimizza la quantità di codice da scrivere quando occorre:
Non è necessario conoscere in dettaglio il funzionamento del codice da riutilizzare, ma è sufficiente modificare (mediante aggiunta o specializzazione) la parte di interesse.
Favorisce riuso, manutenibilità, incrementalità.
Per polimorfismo si intende la proprietà di una entità di assumere forme diverse nel tempo.
Una entità è polimorfa se può fare riferimento, nel tempo, a classi diverse.
Siano ad esempio a, b due oggetti appartenenti rispettivamente alle classi A, B, che prevedono entrambe una operazione m, con diverse implementazioni. Si consideri l’assegnazione:
a := b
L’esecuzione della operazione m sull’oggetto a dopo l’assegnazione, per la quale è spesso adoperata la sintassi:
a.m()
produce l’esecuzione della implementazione di m specificata per la classe B.
Esempio:
Si consideri una gerarchia di classi di figure geometriche, come mostrato in figura.
Sia ad es. A un vettore di N oggetti della classe Figura, composto di oggetti delle sottoclassi Triangolo, Rettangolo, Quadrato:
(ad es.: A[0] è un quadrato, A[1] un triangolo, A[2] un rettangolo, etc.)
Si consideri una funzione Disegna_Figure(), che contiene il seguente ciclo di istruzioni:
for i = 1 to N do
A[i].disegna()
L’esecuzione del ciclo richiede che sia possibile determinare dinamicamente (a tempo di esecuzione) l’implementazione della operazione disegna() da eseguire, in funzione del tipo corrente dell’oggetto A[i].
L’istruzione A[i].disegna() non ha bisogno di essere modificato in conseguenza dell’aggiunta di una nuova sottoclasse di Figura (ad es.: Cerchio), anche se tale sottoclasse non era stata neppure prevista all’atto della stesura della funzione Disegna_Figure().
Il polimorfismo supporta dunque la proprietà di estensibilità di un sistema, nel senso che minimizza la quantità di codice da modificare quando si estende il sistema, cioè si introducono nuove classi e nuove funzionalità.
Un meccanismo con cui viene realizzato il polimorfismo è quello del binding dinamico.
Il binding dinamico (o late binding) consiste nel determinare a tempo d’esecuzione, anziché a tempo di compilazione, il corpo del metodo da invocare su un dato oggetto.
Modularità: le classi sono i moduli del sistema software.
Coesione dei moduli: una classe è un componente software ben coeso in quanto rappresentazione di una unica entità.
Disaccoppiamento dei moduli: gli oggetti hanno un alto grado di disaccoppiamento in quanto i metodi operano sulla struttura dati interna ad un oggetto.
Information hiding: sia le strutture dati che gli algoritmi possono essere nascosti alla visibilità dall’esterno di un oggetto.
Riuso: l’ereditarietà consente di riusare la definizione di una classe nel definire nuove (sotto)classi; inoltre è possibile costruire librerie di classi raggruppate per tipologia di applicazioni.
Estensibilità: il polimorfismo agevola l’aggiunta di nuove funzionalità, minimizzando le modifiche necessarie al sistema esistente quando si vuole estenderlo.
2. La modellazione a oggetti e il linguaggio UML (richiami)
3. Generalità su Java e la programmazione ad oggetti
6. Regole di traduzione da UML a Java/C++
7. Programmazione multi-thread
8. Sincronizzazione tra thread
9. Programmazione client-server con socket TCP/IP (Java networkin...
10. Programmazione di applicazioni client-server: il Pattern Proxy...
12. Design Patterns
13. Pattern architetturali - Esempi
14. Design pattern creazionali. Esempi
15. Design pattern strutturali. Esempi
16. Introduzione alle tecnologie middleware
17. Modelli di middleware: RPC, MOM, TP, TS
C. Savy: Da C++ a UML: guida alla progettazione, McGraw-Hill
Cap. 15 (escluso § 15.5), Cap. 21 §§ 21.1, Cap. 22 § 22.2, Cap. 32 §§ 32.1, 32.2