MaCocoa 004

Capitolo 004 - Object Oriented Programming

Dove si conclude il paradigma.

Sorgenti: Copiare da una sola fonte è plagio, da tante, ricerca: un vecchio libro sullo Smalltalk, un po' dal libro Apple su Objective C.

Primo inserimento: Settembre-Ottobre 2001

Il nome delle cose

Un oggetto è composto indissolubilmente di una struttura di dati privata che descrive lo stato dell'oggetto e da un insieme di segmenti di codice, chiamati metodi, che accedono in lettura e scrittura alla struttura dati interna dell'oggetto. Utilizzare i metodi è l'unico modo per accedere alla struttura dati interna dell'oggetto. Tanto per dirne una, ciò non è vero per il C++, che può accedere tranquillamente e direttamente ai dati interni (perlomeno una parte).

L'insieme di tutti i metodi di un oggetto è la sua interfaccia verso il mondo esterno. Una rappresentazione abbastanza utilizzata per rappresentare un oggetto e i suoi metodi è quella di utilizzare un cerchio per racchiudere la struttura dati propria dell'oggetto, ed un guscio attorno ad esso suddiviso in tanti pezzetti, uno per ciascun metodo, per raffigurare l'interfaccia dell'oggetto.

SingleObject.gif

Gli oggetti sono contrassegnati da un identificatore; nei linguaggi procedurali ogni variabile è identificata attraverso il nome della variabile stessa; ugualmente nella OOP gli oggetti hanno un nome attraverso il quale possono essere utilizzati. L'identificatore può essere una costante, vale a dire l'oggetto pre-esiste nella ambiente operativo del programma. Nella maggior parte dei casi, gli identificatori sono in realtà dei puntatori o dei contenitori degli oggetti cui si riferiscono.

Riassumendo. Un oggetto è fondamentalmente un tipo di dati astratto: consiste in una struttura dati ed in un insieme di operazioni che si possono svolgere su questi dati. Chi usa l'oggetto non deve conoscere la struttura interna, né come siano realizzate le funzionalità messe a disposizione. Tutto quello che occorre sapere per usare un oggetto è che cosa è e come usarlo: un identificatore che ce lo faccia riconoscere e un'interfaccia esterna disponibile a tutti.

Questione di metodo

Per poter utilizzare un oggetto non c'è altra strada che attivare uno dei metodi messi a disposizione dall'oggetto. Questa operazione è chiamata inviare un messaggio all'oggetto. Un messaggio è quindi una richiesta inviata ad oggetto allo scopo di attivare una delle operazioni disponibili, realizzata attraverso uno dei metodi appartenenti all'interfaccia dell'oggetto.

L'invio di un messaggio coinvolge tre enti: il ricevente, l'oggetto destinatario del messaggio, identificato da un nome; un selettore, ovvero una stringa che identifica il metodo di cui si richiede l'attivazione; gli argomenti, opzionali, da passare come parametri al metodo.

Supponendo di avere un oggetto identificato da LaMiaFinestra, si può richiedere l'apertura della finestra tramite un messaggio del tipo

LaMiaFinestra apri

Dove LaMiaFinestra è l'identificatore della mia particolare finestra, ed apri il selettore del metodo. Se c'è anche una serranda, possiamo dire

LaMiaSerranda apri: 50%

In cui l'oggetto LaMiaSerranda è ancora aperta, ma questa volta esiste un argomento che dice di quanto deve essere aperta (la metà). Notiamo per inciso una caratteristica dei linguaggi OOP: lo stesso identificatore di metodo (la stringa 'apri') è utilizzata in due contesti diversi, applicata a due oggetti diversi. Non c'è in questo caso alcun conflitto di nomi, in quanto il primo apri si rivolge ad oggetti di tipo finestra, mentre il secondo ad oggetti di tipo serranda.

Questa 'confusione' di nomi risulterà molto utile quando si tratta di polimorfismo e binding. Il risultato dell'invio di un messaggio è sempre un oggetto. L'oggetto prodotto può essere utile oppure no. Nel secondo caso, non ci faremo alcun scrupolo a non utilizzarlo (e l'ambiente operativo lo distruggerà subito al posto nostro). Nel primo caso, invece, bisogna procurare un posto dove immagazzinare l'oggetto, ad esempio in una variabile creata apposta.

Classi ed oggetti

Gli oggetti sono divisi in vari tipi; come esiste un generico oggetto automobile dal quale poi si ricavano la mia auto, la tua auto, l'auto di tizio, eccetera, esiste un modello per gli oggetti di un certo tipo, la Classe.

La classe definisce il tipo degli oggetti, ovvero come l'oggetto viene costruito. Attraverso la classe, sono definite la struttura dei dati interni e i metodi afferenti all'oggetto. La definizione dei metodi richiede anche l'esplicita indicazione delle operazioni che i metodi eseguono: in pratica, la classe raccoglie il codice realizzativo di tutti i metodi dell'oggetto, che sono in comune tra tutti gli oggetti.

Anche la struttura dati è comune a tutti gli oggetti di una data classe, anche se ovviamente i valori contenuti all'interno di questa struttura dati sono diversi da oggetto ad oggetto. La classe automobile insomma contiene tutti metodi per descrivere ed operare sulle automobili, ed il posto per i dati afferenti ad ogni singola automobile (il colore, la cilindrata, eccetera). Ogni singolo oggetto poi raccoglie nel proprio interno i valori (rosso, milledue, eccetera).

Un oggetto della classe Automobile si dice Istanza della classe. Una classe è una rappresentazione platonica di un oggetto. Quando in un linguaggio OOP mi serve un oggetto, devo istanziare una data classe, per dire che mi serve un oggetto fatto in un certo modo. L'ambiente operativo allora produce una copia della classe, e la fornisce sotto forma di oggetto, istanza della classe richiesta.

Una classe raccoglie dunque le proprietà comuni di un insieme di oggetti, e specifica il comportamento di tutti gli elementi. Un programmatore OOP trova nella classe il suo lavoro principale. Si tratta di definire la classe, la sua struttura dati, e definire compiutamente tutti i metodi che la classe è in grado di realizzare. In pratica, definisce un tipo di oggetto e i messaggi che tale oggetto è in grado di ricevere.

Una volta definita la classe, il programmatore (ma anche no, può essere un altro programmatore) passa ad utilizzare la classe, creando oggetti di quella classe e inviando loro i messaggi. La parte interessante è che nel costruire classi, non mi interesso di chi dovrà utilizzare la classe.

Sono un produttore di automobili: posso certamente presumere quali saranno le richieste dei clienti, ma tutto quello che posso fare è produrre un modello di automobile e rendere disponibile la sua interfaccia.

Viceversa, chi utilizza le classi, non è interessato (anzi, il linguaggio OOP glielo proibisce espressamente) ai dettagli interni di realizzazione della classe, ma semplicemente gli interessa sapere cosa fa quell'oggetto.

Sono allora un cliente di automobili: io voglio un'automobile per andare a fare gite fuori porta, e non mi interessano i dettagli costruttivi dell'auto, se non limitatamente alle questioni di interfaccia (quanto consuma il motore, il colore, se fa rumore, quanto costa, dettagli del genere). Ma che l'auto sia costruita in Italia assemblando parti costruite in Giappone, o che sia costruita in Thailandia utilizzando parti costruite in Brasile, la cosa non mi interessa (fatta salva, ovviamente, la qualità dell'interfaccia).

Ereditarietà

Ogni oggetto appartiene ad una classe , che ne specifica il funzionamento. Una delle caratteristiche fondamentali della OOP è l'ereditarietà. Per spiegarla, faccio un esempio.

Abbiamo la Classe Automobile. Qualcuno l'ha scritta per noi, e quindi abbiamo già a disposizione tutto quello che ci serve per viaggiare in automobile. Però adesso vogliamo una macchina speciale, un Taxi.

Un Taxi è tale e quale ad un'automobile, però ha qualche caratteristica diversa (il colore, ad esempio) ed in più (c'è un tassametro a bordo). Le caratteristiche diverse possono non solo essere attributi (il colore, il numero dei passeggeri) ma anche funzionali: pensiamo ad esempio che il 'costo per chilometro' va calcolato in modo diverso. Con un'auto normale, abbiamo il prezzo della benzina e l'ammortamento del costo, con un taxi abbiamo una tariffa a tempo e/o a distanza. Data però la grande somiglianza che esiste tra un'automobile normale ed un taxi, pare brutto costruire una nuova classe da zero solo per trattare queste piccole differenze.

L'idea è allora di prendere la classe Automobile e specializzarla, definendo la classe Taxi come una figlia della classe Automobile, di cui eredita tutte le caratteristiche. Inoltre, e soprattutto, la classe Taxi aggiunge di suo qualcos'altro.

Ad esempio, ha bisogno di un metodo per la Prenotazione (cosa che un'auto normale normalmente non ha), e di modificare pesantemente il metodo del costo orario. Nel primo caso la classe Taxi aggiunge un nuovo metodo, nel secondo sovrascrive un metodo precedente (tecnica detta di overloading).

Tutto ciò avviene generalmente con molta semplicità: la definizione della classe Taxi si fa normalmente all'interno di un linguaggio OOP richiamando la definizione della classe da cui ereditare, e definire esplicitamente le nuove caratteristiche (dati interni, o metodi) o le modifiche a quelle esistenti (metodi; non è possibile modificare le strutture dati esistenti).

La classe Automobili si dice essere la superclasse della classe Taxi, e viceversa, la classe Taxi è una sottoclasse di Automobili. Una sottoclasse eredita la struttura dati della superclasse, e può aggiungere nuovi dati, ma non può cancellarne.

Le istanze della sottoclassi avranno dunque una struttura dati più grande o al più uguale a quella della superclasse. Per quanto riguarda i metodi, la sottoclasse eredita tutti i metodi della superclasse, e ne può aggiungere di nuovi o sovrascrivere, in tutto o in parte, quelli vecchi. Non si può cancellare un metodo della superclasse, ma se lo riscrivo facendogli fare nulla, è come se lo avessi cancellato.

Gerarchia delle classi

Il procedimento può essere reiterato, ed anzi, normalmente, è proprio costruendo sottoclassi e sottoclassi che si scrive un programma. Si viene così a costruire una gerarchia di classi, in cui ogni classe ha almeno una superclasse, e può avere (o anche no) diverse sottoclassi. Esiste normalmente un unico capo di questa gerarchia delle classi, tipicamente un oggetto primitivo, il cui unico scopo è di fornire un modello base per definire altre classi. L'oggetto base non ha normalmente struttura dati (almeno significativa) e possiede giusto quei metodi di uso generico che servono a far nascere e morire un oggetto.

Non esiste un limite superiore al numero di livelli di sottoclassi che si possono costruire. Anzi; l'attività di un programmatore OO consiste nel mettere assieme un programma assemblando una serie di classi. Più classi conosce, più è probabile che trovi quelle che gli servono, senza bisogno di scriverne lui stesso. Se proprio non trova la classe adatta, se la scrive lui, cercando la classe più simile che gli riesce di trovare, in modo da sovrascrivere il numero minimo di metodi e cercando di utilizzare il lavoro già fatto.

Sono a questo punto in grado di definire meglio Cocoa:

è un insieme di classi predefinite da Apple per la costruzione di programmi funzionanti all'interno del sistema operativo Mac Os X.

Scrivere un programma utilizzando Cocoa significa mettere assieme un po' di classi e, attraverso lo scambio di opportuni messaggi, realizzare le operazioni richieste dall'utente.

Binding

Un messaggio richiede un oggetto destinatario ed una indicazione del metodo richiesto. È ovviamente una cosa buona e giusta che l'oggetto destinatario sia in grado di rispondere al messaggio, altrimenti possono nascere guai. Esistono fondamentalmente due metodi per fare questo controllo (binding), statico o dinamico.

Il binding statico richiede che sia noto da subito il destinatario (al momento della compilazione): il compilatore controlla che tra i metodi dell'oggetto destinatario sia presente quello richiesto. Questo processo porta ad una efficienza in sede di esecuzione, ma occorre essere precisi in sede di stesura del codice.

Questo è l'approccio tipico della metodologia di programmazione procedurale: il controllo di tipo e la verifica della fattibilità dell'operazione è controllato in sede di compilazione. Chi ha programmato in C ricorderà certamente i messaggi di errore dovuti al fatto che si richiedono operazioni non lecite sui tipi quali assegnare numeri floating point a variabili reali.

Chi invece si è avvicinato alla programmazione usando ad esempio Hypercard o Applescript, ha scoperto che un contenitore può contenere qualsiasi cosa, ed è solo in sede di esecuzione (all'ultimo momento possibile) che l'ambiente operativo si accorge di operazioni non lecite (ad esempio, estrarre il terzo elemento di una lista, quando il dato non è una lista...). Questo è un esempio di binding dinamico, in cui il controllo che una data operazione sia possibile e lecita per una dato oggetto avviene all'ultimo momento, cioè all'invio del messaggio.

Questo approccio è estremamente flessibile (posso chiedere ad un oggetto il suo colore, senza dovermi preoccupare se l'oggetto è un automobile o un taxi), ha qualche problema di prestazioni (devo fare il collegamento messaggio-procedura da eseguire ogni volta che invio il messaggio) e qualche problema in più (se mando il messaggio all'oggetto sbagliato, potrei accorgermene quando è troppo tardi). Ovviamente, la cosa migliore sarebbe avere a disposizione entrambe le modalità, ed in effetti così spesso accade (per lo meno, in Objective C).

Polimorfismo

Dicevo prima che una delle caratteristiche divertenti del binding dinamico è di chiedere il colore ad un oggetto senza doversi preoccupare di cosa sia l'oggetto destinatario. Questo è un esempio di polimorfismo.

Il prerequisito del polimorfismo è proprio il fatto di poter avere metodi con lo stesso nome in oggetti anche completamente diversi. Se ho un insieme di oggetti diversi che devo dipingere di colore giallo, per sapere di quanta pittura ho bisogno, invio ad ogni oggetto un messaggio che chiede loro la superficie da colorare. A questo livello, non ho alcun interesse sulla natura degli oggetti, mi basta sapere che il primo oggetto ha una superficie di tre metri quadri, il secondo di venti centimetri quadri, ed il terzo di due metri quadri. Solo che il primo oggetto è una porta d'automobile, il secondo una maniglia ed il terzo una scatola di cartone.

Fare la stessa cosa con un linguaggio procedurale avrebbe richiesto molto lavoro ed un sottile distinguo ogni volta che chiedo la superficie ad un oggetto. Con il polimorfismo è molto più semplice. Programmare classi con metodi aventi lo stesso nome è una possibilità, non un vincolo. Quindi il polimorfismo ha senso solo se serve.

Il vantaggio del polimorfismo si ha dove si riconosce un comportamento simile tra due classi diverse tra loro, per cui ha senso avere lo stesso nome. Avere lo stesso nome ingenera confusione dove i metodi hanno in realtà compiti concettualmente diversi.

Gerarchia e reti di oggetti

Un programma scritto secondo la metodologia OOP è soggetto a due linee guida strutturali.

La prima struttura è stata evidenziata esplicitamente, ed è la gerarchia delle classi. Esiste un oggetto base, la madre di tutti gli oggetti, dal quale discendono tutti gli oggetti, attraverso il meccanismo delle sottoclassi. Di sottoclasse in sottoclasse, ogni oggetto che costituisce il nostro programma può venire classificato (!) per quanto riguarda il suo funzionamento. Ma questa gerarchia dà un'idea di come funziona ogni singolo oggetto, ma non giustifica il funzionamento dell'intero programma.

Abbiamo detto che il programma sono un po' di oggetti che si scambiano messaggi. Ovviamente, gli oggetti non si scambiano messaggi a caso, ma con criterio. Esiste quindi tra gli oggetti una rete di collegamenti che rendono conto delle strade percorsi dai messaggi, che costituisce una buona parte delle fatiche della costruzione di un programma completo.

La rete dei collegamenti non deve essere statica, ovvero non devono essere indicati fin dall'inizio tutti i possibili collegamenti che esistono tra gli oggetti, ma è possibile creare collegamenti al volo, mantenerli per po', chiuderli, cose così.

Ci sono linee di comunicazione completamente transitorie, come un oggetto che nasce, manda un messaggio che richiede una data operazione, e poi muore avendo compiuto il suo dovere; un po' come collegarsi in internet tramite il telefono da casa: il mio oggetto-computer scambia messaggi (pacchetti di dati) con l'oggetto-web finché rimango collegato.

In altri casi invece, piuttosto interessanti, la comunicazione è continua e sempre attiva; come essere collegati in permanenza ad internet tramite linea dedicata: il computer potenzialmente continua a scambiare dati col web.

Una rete permanente di linee di comunicazione è indispensabile per costruire strutture di oggetti cooperanti; una finestra è composta da un oggetto-titolo, due oggetti-barre di scorrimento laterali, un oggetto-pulsante di chiusura, un oggetto- contenuto della finestra stessa, eccetera. In pratica, una finestra è un gruppo di oggetti che cooperano tra loro per dare luogo alle funzionalità che tutti conosciamo. Il legame tra i vari oggetti è statico: l'oggetto barra di scorrimento comunica con l'oggetto contenuto per informarlo del fatto che deve mostrare una certa porzione di documento, l'oggetto pulsante di zoom comunica a tutti di portarsi nella posizione di zoom,e via così.

Queste comunicazioni tra oggetti sono strutturali, nel senso che partecipano alla struttura del programma: sono l'ossatura su cui il programma si basa. Il mezzo più comodo (ma non l'unico) è di immagazzinare nella struttura dati dell'oggetto gli altri oggetti con cui l'oggetto in questione ha relazioni stabili. Cocoa chiama questa variabile outlet: un outlet è dunque la linea dedicata che intercorre fra un oggetto ed un altro oggetto con cui comunica.

È bene precisare che questo concetto non è proprio della OOP, ovvero, si fa OOP anche senza il concetto di outlet. Tuttavia, un outlet è molto comodo proprio nelle situazioni in cui l'interfaccia utente è modellata per oggetti; in particolare, diventa un mezzo potentissimo quando la variabile outlet è gestita più o meno automaticamente dall'ambiente operativo. L'inizializzazione degli outlet (quando la struttura degli oggetti è costruita opportunamente, ad esempio attraverso Interface Builder, vedremo poi) avviene in maniera automatica, per cui le connessioni sono già pronte e disponibili quando si tratta di usarle. Ovviamente, una variabile outlet non deve necessariamente rimanere fissa; l'oggetto cui si riferisce può cambiare durante il corso del programma, sia automaticamente sia per ragioni connesse allo sviluppo del programma stesso.

Il paradigma MVC

Un vecchio concetto (deriva dallo Smalltalk), molto utile soprattutto quando si ha a che fare con programmi che interagiscono con l'utente, è il paradigma MVC, Model-View-Controller. Questo modo di vedere le cose è utile per separare tra loro alcune funzionalità fondamentali di una applicazione. Il paradigma non fa parte della OOP, ma si tratta ancora una volta di un concetto utile.

L'idea è di dividere gli oggetti in tre categorie, gli oggetti Modello, gli oggetti Vista e gli oggetti Controllori.

Gli oggetti Modello contengono appunto un modello del funzionamento, sono la base della conoscenza, sono i depositari del modo di lavorare. Come tali, raramente hanno una rappresentazione visibile. Un oggetto in grado di trattare elenchi di indirizzi, ad esempio, ordinarli, manipolarli, eccetera, è un oggetto Modello. Sono oggetti molto importanti, che definiscono la struttura dell'applicazione.

Gli oggetti Vista rappresentano qualcosa direttamente visibile su di uno schermo, ed in generale rappresentano un modo per esporre e rendere disponibile qualcosa. Gli elementi dell'interfaccia utente come finestre, menu, pulsanti, controlli in genere, sono tutti oggetti Vista. Poiché in definitiva un oggetto Vista rappresenta dei dati e modalità di interazione, i modi di rappresentazione sono mutevoli e diversi, tanto che spesso ci si trova a doverne costruirne di nuovi, magari a partire da oggetti già fatti.

Ovviamente Cocoa ne mette a disposizione moltissimi, e moltissimo si può fare semplicemente arrangiando opportunamente gli oggetti Vista messi a disposizione. Utilizzare oggetti Vista già costruiti rende un'applicazione consistente con le altre applicazioni. Mettere a punto gli oggetti Vista e come questi sono legati tra loro è la parte più divertente dell'applicazione, ed esistono applicazioni apposite per fare ciò con facilità.

Gli oggetti Controllore si trovano a metà strada tra gli oggetti Vista e gli oggetti Modello. Se gli oggetti Vista sono solo Forma e gli oggetti Modello sono pura sostanza, chi si preoccupa di mediare tra questi due estremi sono gli oggetti controllore. L'insieme degli oggetti vista che costituisce una rappresentazione dei dati e un modo di interazione con l'utente si riferisce generalmente ad un oggetto controllore.

Quando, ad esempio, si fa clic sopra il pulsante Cerca di un dialogo per la ricerca di stringhe all'interno di un testo, l'oggetto pulsante nulla sa fare se non inviare un messaggio all'oggetto controllore segnalando l'intenzione dell'utente di cercare qualcosa. L'oggetto controllore si preoccupa di verificare cosa è successo, recupera le altre informazioni dagli altri oggetti vista (il testo da cercare), verifica lo stato corrente (ad esempio, se deve fare attenzione alle maiuscole/minuscole), raccoglie tutte le informazioni al contesto, ne verifica la consistenza, e poi, finalmente, interagisce coll'oggetto Modello (che è a questo punto il depositario di tutta la conoscenza) inviandogli un messaggio di ricerca testo.

Come si può capire dall'esempio, un oggetto Controllore è molto legato all'applicazione specifica, in quanto è l'oggetto che consente l'esecuzione delle operazioni.

La ripartizione degli oggetti in tre categorie consente di semplificare ancor di più il processo di progettazione di una applicazione. Avendo completamente separato gli oggetti Vista, la costruzione di una interfaccia utente consiste nel disporre opportunamente una serie predefinita di oggetti Vista all'interno di finestre (anch'essi oggetti Vista...); visto che questi oggetti si portano dietro struttura dati e funzionalità, risulta facile e veloce costruire una interfaccia utente quasi funzionante: vedremo che Interface Builder è lo strumento per fare ciò.

Per inciso, i famosi ambienti di sviluppo di tipo Visual (Hypercard ne è stato il predecessore) non sono altro che l'estremizzazione di questo paradigma: l'ambiente di programmazione consente la disposizione degli elementi dell'interfaccia (gli oggetti Vista), ai quali si associano in qualche modo, non proprio pulitamente, gli oggetti Controllore (lo script) e Modello (un po' disperso tra le funzionalità realizzate).

Non sempre il paradigma MVC è utile (molti programmatori ne fanno tranquillamente a meno); ad esempio, programmi in cui gli oggetti Vista sono fondamentali ed il collegamento con il modello molto stretto (non ci sono molti controlli e verifiche, ed il contesto è meno importante), si può eliminare il livello Controllore.

Certo è che l'uso del paradigma MVC porta nella maggior parte dei casi ad una applicazione di facile ed immediata strutturazione, comprensione e manutenzione. La separazione tra la parte frivola (Vista) e la parte seria (Modello) permette di concentrare lo sforzo in una sola direzione (e di dividere con facilità il lavoro tra più programmatori), mentre il controllore rimane la parte più difficile da fare: difficile perché ci sono tante cose di cui tenere conto, ma non perché ci siano cose difficili di per sé.

Licenza Creative Commons
Eccetto dove diversamente specificato, i contenuti di questo sito sono rilasciati sotto Licenza Creative Commons.
Pagina a cura di Livio Sandel (macocoa2012@gmail.com).