Introduzione alla Programmazione Orientata agli Oggetti in Java
Programmazione Orientata agli Oggetti
Un programma scritto in un linguaggio orientato agli oggetti è costituito, appunto, da oggetti.
A differenza del caso procedurale, dove un programma era composto da funzioni che invocavano altre funzioni, nella programmazione orientata agli oggetti (OOP, Object Oriented Programming) un programma è composto da oggetti che interagiscono tra loro.
Ciascun oggetto ha una funzionalità specifica che viene esposta all'utente, inteso come utilizzatore dell'oggetto stesso, e un'implementazione nascosta e non visibile dall'esterno.
Molti degli oggetti adoperati in un programma Java sono oggetti predefiniti, chiamati spesso off-the-shelf. Tali oggetti sono presi da una libreria, che è un insieme di oggetti già scritti e pronti all'uso. Un esempio è la libreria standard di Java, che contiene oggetti per la gestione delle stringhe, delle date, delle collezioni, ecc.
Altri oggetti, invece, sono progettati e definiti dall'utente. Questo è il caso quando la funzionalità richiesta non è disponibile in nessuna libreria o quando si vuole creare un oggetto che rappresenti un concetto specifico del dominio applicativo.
Il fatto di adoperare un oggetto già esistente oppure di crearne uno nuovo dipende dalla complessità del problema da risolvere, dalla disponibilità di oggetti già pronti oppure, semplicemente, da una questione di tempo e di risorse.
In ogni caso, fintanto che un oggetto soddisfi una serie di requisiti, non importa (fino ad un certo punto) come esso sia stato implementato.
La programmazione strutturata tradizionale consisteva nel progettare una serie di procedure e funzioni che, combinate tra di loro, permettevano di implementare un algoritmo per la risoluzione di un problema. Una volta che tali procedure fossero state determinate, il passo successivo era quello di trovare il modo appropriato di memorizzare i dati su cui esse andavano ad operare.
Risulta emblematico che Niklaus Wirth, uno dei padri della programmazione strutturata e creatore del linguaggio Pascal, diede come titolo al proprio libro di testo "Algoritmi + Strutture Dati = Programmi" nel 1975. Da notare che nel titolo gli algoritmi vengono per primi e le strutture dati per seconde. Questo riflette il modo in cui i programmatori pensavano all'epoca: prima si progettavano le procedure per la manipolazione dei dati, poi si pensava a quali strutture dati adoperare per semplificare tali manipolazioni.
L'approccio OOP inverte l'ordine. Prima si pensa ai dati poi si guarda agli algoritmi per operare su di essi.
Per piccoli problemi, la suddivisione in procedure funziona bene. Ma per problemi più complessi, la suddivisione in oggetti è più adatta. Questo perché la programmazione orientata agli oggetti permette di raggruppare i dati e le procedure che operano su di essi in un'unica entità, l'oggetto, che può essere trattata come un'unità.
Se consideriamo un programma complesso come un browser web, possiamo immaginare che esso sia composto da una serie di oggetti che interagiscono tra di loro. Un oggetto potrebbe essere la finestra del browser, un altro l'oggetto che rappresenta la pagina web, un altro ancora l'oggetto che rappresenta il pulsante di navigazione, ecc.
Questa suddivisione è più naturale e intuitiva rispetto alla programmazione procedurale, dove si sarebbe dovuto creare una serie di procedure che operano su di un insieme di dati globali. In quest'ultimo caso la risoluzione di un bug o l'aggiunta di una nuova funzionalità sarebbe stata più complessa e rischiosa, in quanto un cambiamento in una parte del programma avrebbe potuto avere effetti inaspettati in altre parti.
Classi
Una classe specifica il modo in cui un oggetto è costruito.
Una classe è un modello che definisce le caratteristiche e il comportamento di un oggetto. Si potrebbe pensare ad essa come una sorta di stampo o schema da cui vengono creati gli oggetti.
Quando si costruisce un oggetto a partire da una classe, si dice che si crea un'istanza della classe. L'istanza è un oggetto concreto, che esiste in memoria e che può essere manipolato.
Come abbiamo visto finora, tutto il codice che scriviamo in Java deve essere contenuto all'interno di una classe. Java fornisce un'ampia libreria di classi predefinite usate per gli scopi più disparati: dalla creazione di interfacce utente, alla manipolazione delle date, alla comunicazione su rete e molto altro.
Nonostante ciò, in Java bisogna creare nuove classi per modellare oggetti specifici del dominio applicativo. Questo perché le classi predefinite non possono coprire tutti i possibili casi d'uso.
L'Incapsulamento è un concetto fondamentale della OOP. Formalmente, l'incapsulamento consiste nel combinare i dati e il comportamento in un unico contenitore, nascondendo i dettagli dell'implementazione a chi utilizza l'oggetto.
I dati presenti all'interno di un oggetto prendono il nome di campi dell'istanza mentre invece le procedure che operano su di essi sono chiamate metodi.
Uno specifico oggetto che è un'istanza di una classe avrà valori specifici all'interno dei propri campi. L'insieme di tali valori prende il nome di stato dell'oggetto.
Quando si invoca un metodo su di un oggetto il suo stato potrebbe mutare.
Affinché l'incapsulamento sia efficace, bisogna fare in modo che i metodi non accedano mai direttamente ai campi di un'istanza che non sia la propria. I programmi, infatti, dovrebbero interagire con gli oggetti solo attraverso i metodi che essi espongono.
Proprio attraverso l'incapsulamento un oggetto diventa una black-box, cioè un'entità che può essere usata senza conoscere i dettagli della sua implementazione. In tal modo, una classe può cambiare se non stravolgere del tutto come essa memorizza i dati al suo interno, ma se continua ad esporre gli stessi metodi, il resto del programma non ne risente.
Oggetti
Abbiamo detto che un oggetto è un'istanza di una classe.
Quando si lavora con un oggetto bisogna identificare tre caratteristiche principali:
-
Il comportamento dell'oggetto:
Cosa si può fare con un oggetto? Quali metodi si possono applicare su di esso?
-
Lo stato dell'oggetto:
Come reagisce l'oggetto quando si invoca un metodo su di esso?
-
L'identità dell'oggetto:
Come si distingue un oggetto da un altro? Come distinguiamo due oggetti che appartengono alla stessa classe, quindi hanno lo stesso comportamento e hanno lo stesso stato?
Tutti gli oggetti che sono istanze della stessa classe condividono lo stesso comportamento. Quest'ultimo è definito dai metodi che si possono applicare su di esso.
Ogni oggetto memorizza al proprio interno delle informazioni. Ciò rappresenta lo stato dell'oggetto. Lo stato può variare nel tempo ma non può variare in maniera spontanea. Un cambio di stato è sempre una conseguenza dell'invocazione di un metodo.
Infatti, se lo stato di un oggetto cambia senza che qualcuno abbia invocato un suo metodo, ciò significa che l'incapsulamento è stato violato. In altre parole, significa che da qualche parte un metodo, che non fa parte della classe e quindi dell'oggetto, ha avuto accesso ai campi interni dell'oggetto stesso e li ha modificati.
Lo stato, tuttavia, non descrive del tutto un oggetto, in quanto ognuno ha una propria identità.
Ad esempio, se stiamo realizzando un sistema di e-commerce che processa ordini effettuati da clienti, due ordini potrebbero avere lo stesso stato (potrebbero contenere gli stessi prodotti acquistati) ma avranno due identità distinte. Questo perché ogni ordine è un oggetto a sé stante, con un proprio identificativo univoco.
Quindi, due oggetti della stessa classe differiscono sempre per la loro identità e, in generale, potrebbero differire per il proprio stato.
Queste tre caratteristiche possono influenzarsi a vicenda. Ritornando all'esempio dell'ordine su un sistema di e-commerce, se un ordine è in stato pagato o spedito, invocare un metodo di aggiunta prodotti potrebbe essere rigettato dall'oggetto stesso perché non è possibile modificare un ordine che è già stato pagato o spedito.
Analogamente, se un ordine è vuoto, invocare un metodo di pagamento potrebbe essere rigettato perché non ha senso pagare un ordine che non contiene prodotti.
Progettazione ad Oggetti
In un programma procedurale tradizionale, il processo di progettazione è tipicamente top-down. In altre parole, si parte dalla cima del programma, cioè dalla funzione principale main
, e si scende verso il basso, suddividendo il problema in sotto-problemi più piccoli e, quindi, sotto-procedure che li risolvono.
Quando si progetta un programma ad oggetti, invece, non esiste propriamente una cima da cui partire. Questo spesso confonde i principianti che si domandano da dove cominciare.
La risposta è: si identificano dapprima le classi e, successivamente, si aggiungono i metodi a ciascuna.
Una semplice regola empirica per identificare le classi consiste nel cercare i sostantivi durante l'analisi del problema. I metodi, d'altro canto, corrispondono ai verbi.
Ad esempio, in un sistema di elaborazione degli ordini, alcuni sostantivi potrebbero essere:
- Articolo
- Ordine
- Indirizzo di spedizione
- Pagamento
- Conto
Questi sostantivi potrebbero portare alla definizione delle classi Articolo
, Ordine
e così via.
Successivamente, bisogna cercare i verbi. Gli articoli vengono aggiunti agli ordini. Gli ordini vengono spediti o annullati. I pagamenti vengono applicati agli ordini. Con ogni verbo come "aggiungere", "spedire", "annullare" o "applicare", si identifica l'oggetto che ha la responsabilità principale di eseguire l'azione.
Ad esempio, quando un nuovo articolo viene aggiunto a un ordine, l'oggetto ordine dovrebbe essere responsabile di questa azione, poiché è quello che conosce il modo in cui gli articoli vengono archiviati e ordinati. In questo caso, il metodo aggiungi
dovrebbe appartenere alla classe Ordine
e prendere un oggetto Articolo
come parametro.
Ovviamente, la regola dei "sostantivi e verbi" è solo una linea guida; solo l'esperienza aiuterà a decidere quali sostantivi e verbi sono importanti nella costruzione delle classi.
Relazioni tra Classi
Le relazioni più comuni tra le classi sono:
- Dipendenza ("usa-un")
- Aggregazione ("ha-un")
- Ereditarietà ("è-un")
Iniziamo ad esaminarle:
-
Dipendenza ("usa-un")
La relazione di dipendenza è la più ovvia e anche la più generale. Ad esempio, la classe
Ordine
utilizza la classeConto
, poiché gli ordini devono accedere agli oggettiConto
per controllare lo stato del credito. Tuttavia, la classeArticolo
non dipende dalla classeConto
, poiché gli oggettiArticolo
non hanno bisogno di gestire i conti dei clienti. Dunque, una classe dipende da un'altra classe se i suoi metodi usano o manipolano oggetti di quella classe.Per una buona progettazione software, si dovrebbe minimizzare il numero di classi che dipendono l'una dall'altra. Se una classe
A
non è consapevole dell'esistenza di una classeB
, alloraA
non sarà influenzata da eventuali modifiche aB
. In termini di ingegneria del software, si vuole minimizzare l'accoppiamento o coupling tra classi. -
Aggregazione ("ha-un")
La relazione di aggregazione è facile da comprendere perché è concreta. Ad esempio, un oggetto
Ordine
contiene oggettiArticolo
. Il concetto di contenimento significa che gli oggetti della classeA
contengono oggetti della classeB
.Si noti bene che alcuni progettisti disprezzano il concetto di aggregazione e preferiscono il termine più generico "associazione". Tuttavia, dal punto di vista della programmazione, la relazione "ha-un" ha molto senso. L'uso dell'aggregazione è utile, poiché la notazione standard per le associazioni è meno chiara.
-
Ereditarietà ("è-un")
La relazione di ereditarietà esprime una relazione tra una classe più specifica e una classe più generica. Ad esempio, la classe
OrdineRapido
eredita dalla classeOrdine
. La classeOrdineRapido
ha metodi specializzati per la gestione delle priorità e un metodo diverso per il calcolo delle spese di spedizione, ma eredita altri metodi, come l'aggiunta di articoli e la fatturazione, dalla classeOrdine
.In generale, se la classe
A
estende la classeB
, alloraA
eredita i metodi diB
ma può aggiungere funzionalità proprie. Questo concetto sarà trattato più approfonditamente nelle lezioni successive.
Molti programmatori utilizzano la notazione UML (Unified Modeling Language) per rappresentare le relazioni tra classi nei diagrammi delle classi. Un esempio di diagramma UML è mostrato nella Figura che segue:
Studieremo la notazione UML in dettaglio nelle prossime lezioni.
Quello che possiamo però evincere dal diagramma di sopra, al momento, riguarda le relazioni che sussistono tra le classi, rappresentate come rettangoli.
Vediamo che la classe OrdineRapido
specializza la classe Ordine
, quindi vi è una relazione di tipo "è-un". La classe Ordine
contiene oggetti della classe Articolo
, quindi vi è una relazione di tipo "ha-un". Infine, la classe Ordine
dipende dalla classe Conto
, quindi vi è una relazione di tipo "usa-un".
In Sintesi
In questa lezione introduttiva sulla programmazione orientata agli oggetti abbiamo visto che un programma scritto in un linguaggio OOP è composto da oggetti che interagiscono tra loro.
In particolare, abbiamo visto che:
- Un oggetto è un'istanza di una classe e ha un comportamento, uno stato e un'identità.
- Una classe è un modello che definisce le caratteristiche e il comportamento di un oggetto.
- L'incapsulamento è un concetto fondamentale della OOP e consiste nel combinare i dati e il comportamento in un unico contenitore.
- La progettazione ad oggetti richiede di identificare dapprima le classi e successivamente di aggiungere i metodi a ciascuna.
- Le relazioni più comuni tra le classi sono la dipendenza, l'aggregazione e l'ereditarietà.
Nella prossima lezione, prima di imparare come creare oggetti personalizzati, vedremo come usare gli oggetti a partire da quelli forniti dalla libreria standard di Java.