GUI Frameworks - part 1/3
July 1st, 2006 di Aaron Brancottiin coding, gui, programmazione | Letture: 3041
Alla base della costruzione, nel senso della programmazione, della GUI (Graphical User Interface) di una applicazione sottendono svariati concetti, alcuni dei quali molto più filosofici che tecnici.
Il presente documento -suddiviso in tre articoli- è dedicato a chi, pur non essendo un programmatore, è comunque incuriosito da questo ben preciso problema informatico.
Prima di Iniziare
Prima di addentrarci nella descrizione tecnica di cosa sia una GUI e di come venga costruita, è necessario introdurre alcuni concetti ampiamente utilizzati nella trattazione.
Programmazione ad Oggetti
A partire dall’introduzione della programmazione cosiddetta “ad oggetti” da parte di linguaggi ormai mitologici (Smalltalk-80 per citarne uno), il mondo informatico è stato rivoluzionato. È opportuno spiegare brevemente come tale metodologia, nativa in linguaggi come Java, C++ e molti altri, permetta una organizzazione del lavoro migliore di vari ordini di grandezza rispetto alla programmazione non object-oriented di C, Basic e di tutti i “vecchi” linguaggi.
Ogni GUI che si rispetti è fortemente basata sulla programmazione ad oggetti.
La chiave della programmazione ad oggetti (OOP, Object Oriented Programming) consiste nella capacità intrinseca del linguaggio di programmazione di descrivere monoliticamente delle entità – gli Oggetti, appunto – che racchiudono tutto ciò che serve a loro per svolgere il lavoro per il quale vengono progettati. Lo sviluppo del software si estrinseca quindi in una attività di creazione e interconnessione di componenti simili a scatole nere, ognuna delle quali progettata per risolvere un ben preciso problema. È questa una architettura fondamentalmente client-server, nel senso che alcuni oggetti chiedono dei servizi ad altri che sono in grado di svolgerli (non nel senso “network” del termine).
Incapsulazione, Ereditarietà, Polimorfismo
Più specificatamente, i linguaggi di programmazione ad oggetti supportno i seguenti tre concetti, o direttamente (C++, Java) o mediante delle convenzioni adottate da tutti gli sviluppatori (Python):
Incapsulazione: un oggetto è monolitico. Racchiude sia dati, ovvero variabili che ne descrivono lo stato in un dato istante, che metodi, ovvero funzioni che modificano questo stato. Inoltre è possibile specificare quali di questi dati e metodi siano accessibili dall’esterno; tipicamente, da “fuori” non si ha accesso direttamente ai dati di un oggetto, ma se ne manipola lo stato chiamando gli opportuni metodi.
I metodi pubblici, ovvero quelli visibili da “fuori” costituiscono l’interfaccia dell’Oggetto verso i suoi clienti. Si noti che, fino a quando non modifichiamo questa interfaccia, siamo liberi di modificare le caratteristiche interne del nostro Oggetto senza che i clienti dello stesso se ne accorgano.
Così ad esempio un oggetto Dado potrebbe avere un dato intero, privato, che ne rappresenta il valore, e due metodi – Lancia e ValoreAttuale – che permettono ad un generico cliente di interrogarlo. L’implementazione è assolutamente nascosta: non sappiamo quale algoritmo, delle varie decine riportate dal Knuth nei suoi giganteschi libroni, venga utilizzato per generare il “prossimo” numero casuale .
Volendo utilizzare 100 dadi contemporaneamente, non dovremo fare altro che creare 100 istanze della classe Dado; ognuna avrà metodi e variabili di stato indipendenti da tutte le altre. È evidente come questo approccio semplifichi in maniera drastica situazioni in cui gli oggetti con i quali abbiamo a che fare sono “complessi”: usarne più di uno contemporaneamente è pressoché immediato. L’incapsulazione apre quindi la strada alla forte riusabilità del codice object-oriented.
Ereditarietà: è possibile specificare che un oggetto erediti dati e metodi da uno o più progenitori (a seconda del linguaggio si parla di ereditarietà semplice o multipla). Questo permette di costruire nuove classi di oggetti molto velocemente, che ereditano per default stati e comportamenti dei progenitori.
Solitamente di questi oggetti vengono poi definiti solo dati e metodi “nuovi”, o ridefiniti dati e metodi ereditati che per qualche motivo “non vanno bene” così come sono. Questa ultima caratteristica è il polimorfismo: la capacità che hanno oggetti diversi di manifestare comportamenti diversi in risposta all’invocazione dello stesso metodo.
Ad esempio, una Finestra e un Cassetto si possono aprire, ma dal punto di vista fisico succedono due cose completamente diverse: una rotazione nel primo caso, una traslazione nel secondo. Mediante OOP è possibile catturare queste complessità del mondo esterno.
Così una classe StrumentoMusicale, con un metodo Suona, può essere utilizzata come progenitrice di Chitarra e Batteria; ridefinendo opportunamente il metodo Suona nelle due classi derivate, potremo ottenere “pling” come risultato di Chitarra.Suona, mentre otterremo “thump” come risultato di Batteria.Suona.
Non solo: il polimorfismo ci permette di andare ancora più in là. Poiché Strumento è superclasse sia di Chitarra che di Batteria, una variabile di tipo StrumentoMusicale può “contenere” in realtà una Chitarra o una Batteria. Non importa sapere esplicitamente di che tipo sia veramente quella variabile: quando le chiederemo di suonare, farà il suono giusto… questo meccanismo è intrinseco dei linguaggi object-oriented ed è la chiave del funzionamento a volte apparentemente magico dell’OOP: funziona e non si capisce il perché – il che non è detto sia sempre un vantaggio…
Design Patterns
Nel mondo reale, è stato osservato, i problemi tendono a ripresentarsi. Con l’andare del tempo, emergono soluzioni migliori di altre. Per pattern si intende proprio questo: la soluzione ad un problema ricorrente che, dopo anni e molti mal di testa, si è dimostrata migliore a livello comunitario. Il concetto viene dal mondo dell’architettura, ma ha trovato una collocazione ben precisa nello sviluppo del software, che è per definizione un gigantesco e continuo tentativo di risoluzione di problemi.
Patterns Ricorrenti nelle GUI
In particolare, quando si ha a che fare con la creazione di GUI, si ripresentano regolarmente i seguenti pattern:
Observer
Una GUI è una complessa interazione di molti oggetti, dove potenzialmente ognuno è interessato allo stato di molti altri: quando schiacci un bottone certi menù si devono disabilitare ed altre finestre si devono aprire e altre diecimila cose. Al posto di far si che ogni oggetto tenga d’occhio esplicitamente tutti gli altri che gli interessano in una relazione molti-a-molti che tende a diventare immediatamente incontrollabile, si utilizza un Observer.
È questo un pattern in cui un singolo oggetto Observer mantiene una lista di oggetti da osservare, e per ogni oggetto osservato mantiene anche la lista di oggetti interessati ai cambi di stato di quell’oggetto. Ogni volta che un oggetto cambia stato questo informa l’Observer, e solo lui; l’Observer propaga l’informazione a tutti gli oggetti interessati a quel cambio di stato. Se quando schiacciamo il bottone non si disabilita il menù sappiamo immediatamente dove andare a mettere le mani.
Factory
Una parte consistente della programmazione consiste nel codice per creare oggetti. È buona regola, anziché incorporare questo codice nell’oggetto stesso – cosa che tende a spargere il codice di creazione in giro per tutto il programma – utilizzare il concetto di Factory, ovvero di un unico oggetto in grado di creare intere categorie di altri oggetti. Avremo così, ad esempio, una GUIFactory con dei metodi CreateButton, CreateMenu etc.
Singleton
Esistono situazioni per cui si vuol essere sicuri che di una certa classe di oggetti possa esistere una e una sola istanza. Tipicamente questo avviene quando la classe utilizza risorse esclusive, come ad esempio una stampante. Questo per impedire a eventuali clienti di creare erroneamente più istanze dello stesso oggetto, fatto che potrebbe portare a malfunzionamenti e comportamenti inspiegabili
Framework, Toolkit, Libreria…
Iniziando a parlare finalmente di GUI, bisogna chiarire la differenza tra Libreria e Framework.
Fondamentalmente, un framework è una evoluzione object-oriented di toolkit e librerie; là dove le seconde sono più semplicemente collezioni di funzioni e oggetti che in qualche modo possono sveltire il nostro lavoro, il primo è un impianto ben più complesso che modifica sostanzialmente il modo in cui scriviamo il nostro programma, incapsulandone – ecco la natura object-oriented – tutte quelle parti che “vanno già bene”.
Un framework, per default, rappresenta un programma che non fa nulla; sta a noi poi ridefinirne varie parti, come da dettami dell’OOP, per arrivare al “nostro” programma.
In tutti i casi, impadronirsi di un ambiente di sviluppo per GUI significa per un programmatore imparare a districarsi in un dedalo di alcune decine/centinaia di classi, dovendo imparare più o meno a memoria delle API (Application Programming Interfaces) di qualche migliaio di funzioni, metodi e nomi di classi. È evidente come, senza il paradigma della Programmazione a Oggetti – che ci permette di attribuire precise responsabilità e funzionalità ad ogni classe, rendendo molto più logica l’organizzazione del lavoro – lo sforzo sarebbe inaccettabile. Anche così, imparare ad usare bene un tipico GUI framework implica un tempo di apprendimento che va dai sei mesi ad un anno-uomo.
Concetti Base
La creazione di programmi GUI-Oriented si poggia su numerosi concetti che si discostano notevolmente dalla programmazione “classica”, dove un programma ha un inizio, un ben preciso flusso di esecuzione e una fine.
Programmazione a Eventi
La più grande differenza tra un programma dotato di una GUI e uno “a consolle”, come quelli di una volta, è probabilmente il concetto di Event Driven Programming. Un programma a consolle inizia, si ferma ogni tanto aspettando degli input, fa i calcoli che deve fare e finisce.
Un programma GUI-oriented invece consiste in un ciclo infinito, nel quale gli input dall’esterno (movimenti del mouse, pressioni dei tasti, dati disponibili sulla rete…) vengono incessantemente comunicati al programma sotto forma di eventi. In questo loop il programma aspetta il prossimo evento, lo interpreta e lo gira all’oggetto di competenza, che è tipicamente l’oggetto che in quel momento è “attivo” (si veda oltre il concetto di focus). Un esempio chiarirà questo concetto:
- L’utente schiaccia un tasto della tastiera.
- Il Sistema Operativo genera un evento EventKeyPressed e lo passa all’applicazione attiva;
- se in quel momento l’oggetto attivo dell’applicazione attiva è un bottone, il framework passa a lui l’evento;
- Se per il bottone l’evento in questione ha senso – ad esempio perchè abbiamo battuto Enter e quindi volevamo “schiacciarlo” – allora il bottone consuma l’evento e il framework ritorna nel suo ciclo infinito ad aspettare il prossimo evento;
- Altrimenti il framework passa l’evento a qualche altro oggetto – ad esempio, se il bottone fa parte di un dialog, lo passa al dialog stesso. Se, risalendo nella gerarchia, il framework non trova nessun oggetto per il quale l’evento ha senso, lo butta via.
Ogni input genera eventi: movimenti del mouse, pressioni dei tasti, ridimensionamento e spostamento delle finestre, il sistema operativo che decide che è ora di far partire lo screen saver… il nostro programma, ovvero il framework sul quale il nostro programma è costruito, va avanti fino a quando non riceve un evento ben preciso: un EventQuit, che lo fa uscire dal loop e gli permette finalmente di raggiungere i Verdi Pascoli Dei Processi Terminati.
La programmazione ad eventi prevede quindi che ogni oggetto che riceve eventi risponda a questi (se interessato) eseguendo del codice racchiuso in ben precise funzioni, chiamate Event Handlers. La maggior parte di quello che dobbiamo fare, quando scriviamo un programma event-driven, è (ri)definire opportunamente ed eventualmente gli handlers delle nostre classi, derivate dalle classi “neutre” del framework, specificando in questi il comportamento che desideriamo.
Se la programmazione a eventi è un paradigma potentissimo, è anche fonte di notevoli grattacapi quando si ignorano alcune regole base: ad esempio, mai dare per scontato che gli eventi arrivino in un certo ordine. In questo fa scuola Microsoft, che a causa delle diverse politiche di schedulazione e gestione degli eventi dei suoi vari sistemi operativi ha reso a volte estremamente difficoltoso lo sviluppo di programmi correttamente funzionanti. Addirittura, si parla, oltre che di compatibilità cross-platform, anche di compatibilità cross-Windows, poiché non è raro che un programma funzioni su W2000 e non su NT, o W98, o XP, o….
Viste e Documenti
Un concetto chiave delle GUI è la separazione tra Vista e Documento. Si tratta, in ultima analisi, di un ulteriore pattern: mantenere separato il codice che descrive i dati che il nostro programma elabora da quello che li visualizza e che ci permette, attraverso la GUI, di modificarli.
Questa separazione ha un vantaggio evidente: un singolo insieme di dati può essere visualizzato in molti modi diversi – si pensi a dei numeri visualizzati come grafici a torta, istogrammi o grafici X-Y, oppure a una singola scena tridimensionale che può essere osservata indipendentemente da diversi punti di vista.
Avere questa separazione ci permette di aprire innumerevoli viste diverse sullo stesso documento, con evidenti vantaggi. Si pone però il problema di mantenere il tutto sincronizzato: quando modifichiamo il documento attraverso una certa vista, tutte le altre viste devono aggiornarsi opportunamente.
Infatti, il concetto/pattern qui presentato è spesso generalizzato e conosciuto come MVC, ovvero Model-View-Controller. L’idea è quella di introdurre esplicitamente un terzo livello di “oggetti”, i controllers appunto, che hanno lo scopo di mantenere sincronizzati documenti e relative viste e che sono fondamentalmente degli Observer.
Nella sua versione più semplice, ogni vista si registra presso il documento di sua competenza, richiedendo di essere avvertita quando questo cambia.
Un altro vantaggio immediato del modello MVC è che, avendo tutti i dati della nostra applicazione in un solo posto – il Documento – è più facile implementare la persistenza dello stesso, ovvero la capacità di salvare e caricare il suo stato. Se ogni vista gestisse anche le variabili di stato ad essa pertinenti, sarebbe molto più complicato tenere la GUI coerente e per salvare lo stato della nostra applicazione dovremmo esaminare decine o centinaia di oggetti.
SDI e MDI
Direttamente dalla nozione di Documento nascono due tipi diversi di applicazione GUI: quelle SDI, Single Document Interface, e quelle MDI, Multiple Document Interface.
Il concetto è molto semplice: fermo restando che un singolo documento può essere “guardato” da più viste, vogliamo che la nostra applicazione possa lavorare con più documenti contemporaneamente? Solitamente le applicazioni SDI sono composte da una singola finestra, mentre quelle MDI hanno una finestra principale nella quale si possono aprire delle sotto-finestre, ma qui le variazioni sono pressoché infinite:
- Esempi di MDI sono Photoshop (che può aprire contemporaneamente più immagini diverse, ognuna nella sua finestra), i multi-editor come Visual Studio o UltraEdit, FrontPage, Mozilla Firefox che permette di aprire pagine HTML diverse in tab diversi.
- Esempi di SDI sono Outlook, che anche se ha diecimila finestre diverse sono tutte viste su un unico database contenente tutte le mail, i contatti, i to-do etc. Explorer permette di clonare la propria finestra, ma sempre sullo stesso documento, quindi è SDI.
Attenzione: è ovviamente possibile aprire più documenti diversi contemporaneamente con un programma SDI, a patto che il programma sia avviabile più volte (deve supportare l’istanziazione multipla), il che non è assolutamente dato per scontato… se il programma è costruito in modo da essere un Singleton.
Ad esempio Word può essere lanciato più volte, Outlook no.
Beh, questo primo articolo 1/3 era soltanto una introduzione alle problematiche di sviluppo (programazione) di una GUI. Presto potrete leggere le successive puntate ed entrare nel vivo dell’ambiente di realizzazione della User Interface.




