In questo paragrafo vogliamo descrivere la differenza strutturale più importante che distingue i processi di sviluppo con le piattaforme Instant Developer da quelli utilizzati in qualsiasi altro strumento di programmazione. Vogliamo inoltre analizzare i vantaggi che porta.
Normalmente lo sviluppo di software avviene memorizzando righe di codice in tanti file di testo. Questo metodo non rispetta la natura relazionale del software: ogni riga di codice contiene implicitamente delle dipendenze verso altre righe scritte in altri file, ma queste informazioni vengono perse e non sono disponibili in una forma trattabile dal sistema stesso.
Facciamo due esempi di quali relazioni implicite vengono perse durante la scrittura del codice. Nel primo esempio analizziamo una semplicissima funzione JavaScript:
function sommaInt (n1, n2) {
let s = n1 + n2;
return parseInt(s);
}
In questa funzione, viene persa l’informazione relativa ai tipi di parametri di input (numerici) e di output (numerico intero). Questa perdita è relativa al tipo di linguaggio JavaScript, che non è tipizzato staticamente, ed è superabile usando altri tipi di linguaggio più specifici.
Passiamo ad un esempio più complesso, ma presente in ogni progetto software che utilizza un database.
create table Prodotti (
codice: integer not null,
famiglia: varchar not null,
nome: varchar not null,
descrizione: varchar not null,
primary key (codice)
}
async function getNomeProdotto (codiceProdotto) {
let rs = await database.query(
“select nome from prodotti where codice = “ + parseInt(codiceProdotto));
return rs.rows[0].nome;
}
Questo secondo esempio mostra le relazioni implicite fra il file che contiene la definizione dei dati del database e una funzione JavaScript che utilizza tali dati. Avendo scritto la query in una stringa, sono state implicitamente definite le seguenti relazioni:
- Il campo nome della tabella prodotti si chiama effettivamente nome.
- la tabella prodotti si chiama effettivamente prodotti.
- il campo codice della tabella prodotti si chiama effettivamente codice.
- la chiave primaria della tabella prodotti è costituita da un unico campo: codice.
- La sanificazione del campo di input (codiceProdotto) viene ottenuta tramite la funzione parseInt in quanto tale campo è numerico intero.
- Il tipo di ritorno della funzione è di tipo stringa.
La mancanza di una definizione esplicita di queste relazioni causa una rigidità del software che aumenta esponenzialmente al crescere complessità dello stesso.
Infatti, se diventasse necessario modificare il tipo di dati della colonna codice, oppure cambiare la chiave primaria della tabella aggiungendo ad esempio anche il campo famiglia, ci si dovrebbe ricordare di modificare manualmente la funzione getNomeProdotto per adattarla alle nuove specifiche. Ma questo adattamento può risultare in una modifica all’interfaccia della funzione e, in questo caso, diventerebbe necessaria una ulteriore fase di modifica manuale del software per verificare ed adattare tutti i punti in cui è stata usata quella funzione. E così via, in maniera ricorsiva, ogni modifica ad ogni riga di codice può rendere necessario analizzare manualmente gli impatti e correggere ulteriormente il codice.
Questa è la causa del cosiddetto debito tecnico, ma aspetto ancora più negativo, è la causa dell’irrigidimento del software che nel tempo non è più in grado di essere modificato senza costi eccessivi. Il software, non potendo più evolvere per adattarsi alle mutevoli esigenze dell’ambiente, diventa inutile e deve essere completamente sostituito con un’operazione costosissima e spesso impossibile.
Come si può risolvere questo problema così importante? Utilizzando una nuova tecnologia di memorizzazione delle definizioni e del codice del progetto che tenga conto della natura relazionale descritta in precedenza e permetta al sistema di fornire le informazioni necessarie a comprendere l’impatto di una modifica, o, meglio ancora, ad eseguirla automaticamente. Questa tecnologia deve tenere conto contemporaneamente di tutti gli aspetti del progetto software, non si può limitare a gestire i vari pezzi in maniera scollegata: la struttura del database non può essere separata dal codice applicativo che lo utilizza.
Ecco perché Instant Developer Cloud memorizza i dati di un progetto software non in tanti file di testo, ma utilizzando un grafo memorizzato in un unico -grande- oggetto JavaScript avente come nodi tutti gli oggetti definiti nel progetto, e come archi le relazioni fra di essi.
Quali sono le parti di progetto memorizzate nei nodi di questo grafo? In sostanza tutto ciò che è presente, che in qualche modo è stato definito. Facciamo alcuni esempi:
- Le videate dell’applicazione
- Gli elementi grafici che compongono le videate.
- Gli script che gestiscono gli eventi degli elementi grafici.
- Le classi.
- Le proprietà ed i metodi delle classi.
- I database, le tabelle, i campi, le relazioni e gli indici.
- I documenti e i relativi metodi, proprietà ed eventi.
- Le librerie di sistema.
- I pacchetti.
- Le risorse (immagini, video, file…).
L’albero del progetto mostra una parte dei nodi del grafo che descrive il sistema in fase di sviluppo
Oltre a tutti questi elementi, anche tutto il codice presente in ogni script dell’applicazione viene memorizzato come parte del grafo. Quando selezioniamo nell’albero degli oggetti uno script o un metodo, l’editor di codice ricostruisce una versione testuale della parte di grafo dedicata al metodo e la mostra in un editor di codice JavaScript.
Anche il codice è parte del grafo del progetto
Quando il codice viene modificato, il grafo sottostante viene ricostruito in tempo reale, in modo tale che tutto quello che viene scritto nell’IDE rimanga sempre in forma relazionata.
Nell’immagine precedente possiamo vedere che all’interno del nodo onLoad, che rappresenta lo script lanciato all’apertura di una istanza della videata ShareList, vengono memorizzati i seguenti tipi di nodi:
- I parametri del metodo, ognuno con le proprie caratteristiche.
- Le righe di codice del metodo
- Per ogni riga, ogni token o parola chiave del linguaggio.
Ogni nodo del grafo ha una serie di caratteristiche che dipendono dal tipo di nodo stesso. Ad esempio, un elemento grafico memorizza tutte le proprietà dell’elemento stesso, che a sua volta dipendono dal tipo di elemento in funzione di come esso è stato definito nelle librerie.
I nodi che rappresentano le righe e i token di codice portano importantissime informazioni di collegamento con gli oggetti presenti nel progetto. In questo modo, infatti è possibile far funzionare gli algoritmi di type inference e di gestione delle referenze. Come esempio di questo possiamo notare nell’immagine precedente che aprendo un menu contestuale a partire dal token refresh appare una voce di menu che permette di saltare alla definizione del metodo nella videata. Questa operazione non avviene tramite matching di stringhe, ma proprio come referenza diretta del token al nodo metodo refresh nell’albero degli oggetti.
Vantaggi del modello relazionale #
La modalità di memorizzazione relazionale rende possibili alcune delle caratteristiche più interessanti e utili di Instant Developer Cloud fra le quali:
Instant refactoring: modificando il nome o le caratteristiche di un elemento del progetto, le conseguenze di questa modifica vengono apportate automaticamente in tutto il codice presente nel progetto. Se, ad esempio, si cambia il nome al metodo refresh in aggiorna, anche le righe di codice che lo chiamavano vengono cambiate di conseguenza. Tali modifiche vengono apportate in automatico perchè sono sicure, non richiedono alcuna revisione manuale.
Type inference: Instant Developer Cloud è in grado di recuperare in tempo reale i tipi riferiti dalla maggior parte delle espressioni JavaScript, anche se questo linguaggio, per sua natura, non ha alcuna informazione sui tipi a design time. In questo modo è possibile proporre un intellisense globale esteso all’intero progetto: dal database, alle librerie di componenti, fino ad ogni elemento di back-end e front-end.
Intellisense globale sull’intero progetto
Sequenzializzatore asincrono: una delle difficoltà maggiori della scrittura di applicazioni JavaScript complesse risiede nella gestione delle operazioni asincrone, che, per loro natura, richiedono di descrivere il flusso di un processo complesso tramite callback, come mostrato nell’esempio seguente.
L’inferno delle callback
Questo modo di scrivere il codice viene spesso chiamato l’inferno delle callback perché rende estremamente difficile riconoscere il significato del codice, oltre che essere altamente imprevedibile per quanto riguarda la gestione delle eccezioni. Cosa accade infatti in caso di eccezione JavaScript? Quale sarà la prossima riga di codice ad essere eseguita?
Fin dal 2015 Instant Developer Cloud contiene un framework per la gestione automatica di questo problema. Tale framework converte il codice asincrono in un codice sequenzializzato tramite yield. Le righe di codice che coinvolgono operazioni asincrone vengono quindi sospese dal costrutto yield e poi riprese in automatico al termine dell’operazione asincrona. Anche la gestione delle eccezioni può essere scritta in modo normale, senza preoccuparsi del livello di asincronicità in cui esse avvengono.
Per poter funzionare, l’algoritmo di sequenzializzazione asincrona ha bisogno di informazioni dettagliate sui tipi di oggetti utilizzati nel codice, in quanto il codice stesso deve cambiare di forma se vengono coinvolte o meno operazioni asincrone. Quindi un metodo che inizialmente non è asincrono e lo diventa in seguito, causa modifiche automatiche anche in tutti i punti in cui tale metodo è stato utilizzato.
Gestione globale delle referenze: quando il progetto software diventa complesso, è necessario poter rilevare dove un determinato oggetto viene utilizzato. Ad esempio si potrebbe essere interessati a conoscere tutti gli utilizzi di un determinato campo (ad esempio di nome “id”) di una tabella del database. Instant Developer Cloud è in grado di estrarre queste informazioni in modo rapido e sicuro navigando il grafo dei nodi del progetto.
Effetti del modello relazionale #
L’esperienza di programmazione con Instant Developer Cloud offre quindi il vantaggio di “tenere sempre in mano” un progetto, anche di classe enterprise, con un utilizzo minimo di risorse (tempo e costi). Questo vale nella fase iniziale di sviluppo, e vale ancora di più nella fase di manutenzione nel tempo, quando è più facile perdere conoscenza delle relazioni fra i vari elementi del software.