You are here

Negli incasinamenti di codice… trovare una direzione

> INDICE DEL CORSO

Avvertenza: questa pagina non rientra nel modulo introduttivo del corso. E' rivolta a chi ha già un po' di esperienza con Codename One.

How to write good codeDurante le nostre attività di coding, è normale dover modificare il codice scritto da noi (anche a distanza di tempo) o da altri per aggiungere, togliere o modificare funzionalità, correggere bug, tentare migliorie. Il problema è che spesso è difficile capire da che parte iniziare e come proseguire, specialmente su progetti complessi fatti da centinaia di classi e da una moltitudine di metodi interagenti tra di loro. Provo qui a riunire alcune mie riflessioni e indicazioni, riferite alle app multipiattaforma fatte con Codename One, con l’auspicio che almeno uno dei punti sotto indicati possa tornare utile a qualcuno.

In futuro potrei ritoccare questa lista o aggiungere suggerimenti che qualcuno dei miei lettori vorrà condividere. Per il momento non inserisco esempi di codice, questa lista è una sorta di brainstorming personale che regalo ai miei lettori.

  • Creati uno script che automaticamente ti permetta di fare backup regolari di tutto il tuo codice sorgente (qui un esempio di script, da personalizzare), archiviandolo in file compressi con l'indicazione del giorno e dell'orario, e conserva questi backup regolari per mesi o per anni (magari tenendone anche una copia online, su un hard disk esterno o su altri supporti idonei ad un disaster recovery): prima o poi potrebbe tornarti utile poter far confronti con un codice risalente a un periodo precedente, specialmente se una certa funzionalità prima funzionante dovesse smettere di funzionare (ovvero nel caso di una "regressione").
     
  • Sempre a proposito di regressioni, è utile anche mantenere una copia dei sorgenti di Codename One risalenti a periodi differenti (qui un esempio di script per scaricarli): ciò ti permetterà di verificare se una eventuale regressione è dovuta a modifiche al tuo codice o a modifiche al codice di Codename One (come in questo caso). Ovviamente ciò presume che tu possa compilare la tua app non con le librerie di default, ma con i sorgenti di Codename One (le istruzioni sono alla slide 6 di queste slides), il che è relativamente semplice con Netbeans.
     
  • Cerca nell’interfaccia grafica parole o frasi che hai buona speranza di ritrovare direttamente nel codice o nel resource bundle, poi fai una ricerca estesa su tutto il codice per scoprire dove è stato inserito quel che stai cercando: questo ti darà un’indicazione su dove iniziare a guardare. Netbeans permette agevolmente di cercare una stringa in tutto il codice dell’app oppure solo in specifici tipi di files.
     
  • A proposito del resource bundle sopra citato, nelle mie app lo implemento attraverso uno o più file json collocati nella root del package principale, in modo che sia semplice fare ricerche, modifiche o aggiunte di stringhe. L’uso del resource bundle di “default”, cioè all’interno del Codename One Designer, mi pare invece più difficile da gestire su progetti complessi e non integrato negli strumenti di ricerca di Netbeans. Aggiungo un dettaglio che a qualcuno potrebbe interessare: l'uso di questi json, abbinati a specifici script da linea di comando, permette di tradurre automaticamente l'app con Google Translate in qualunque lingua (fermo restando che una revisione manuale delle traduzioni è sempre opportuna); perlomeno c'ero riuscito tempo addietro.
     
  • Concentra la tua analisi nella maniera più circostanziata possibile su ciò che ti interessa capire e modificare, riducendo al minimo indispensabile il codice che andrai ad analizzare. Quali sono le classi e i metodi che ti interessano?
     
  • Qual è l’input o l’insieme degli input? Anche le interazioni dell’utente con l’interfaccia grafica costituiscono un input.
     
  • Come e dove vengono salvati i dati? Vengono salvati nello Storage? Sono inviati a un server tramite chiamate Rest? Sono solo in variabili temporanee, perse dopo un kill dell’app? Se hai dubbi su "se e cosa" viene salvato nello Storage o nel File System Storage, prima di eseguire l'app svuota la cartella nascosta in cui il simulatore salva i dati (su Linux è in /home/<nomeUtente>/.cn1/) e tienila d'occhio mentre esegui l'app. Per il transito dei dati in entrata e in uscita tramite chiamate Rest, usa il "Network monitor".
     
  • Tutto il codice gira nell’EDT o ci sono altri thread? Se ci sono altri thread, perché ci sono stati messi? Questi richiamano a loro volta l’EDT?
     
  • Ci sono timer? Se sì, perché?
     
  • Ci sono callback richiamati dopo specifici eventi?
     
  • Considerando tutti questi aspetti, prova a fare uno o più diagrammi del flusso di esecuzione del codice oppure a scriverti una descrizione a parole. Non importa che siano diagrammi UML, l’importante è che siano chiari per te. I tools (sovente a pagamento) per diagrammare il codice Java, nella maggioranza dei casi, sono inutili o, peggio, confondenti, anche perché non sono stati creati per Codename One. E’ meglio che ti fai manualmente i grafici che ti interessano, se proprio ne hai bisogno. Nel caso dei flow chart, utili per diagrammare singoli algoritmi, un ottimo strumento è code2flow.com. Comunque, per tuo uso personale, normalmente vanno bene semplici diagrammi temporanei fatti a mano su carta.
     
  • Se hai dubbi sull'ordine di esecuzione del codice, ovvero su "chi chiama cosa", l'uso della funzionalità "Debug Project", abbinata all'uso di breakpoints e all'esecuzione riga per riga, può essere utile, almeno in parte.
     
  • Quando leggi il codice senza eseguirlo, spesso potrebbe esserti utile poter saltare da un punto all'altro del codice che ti sei precedentemente segnato. Per non perderti, l'uso dei "breakpoints" forniti da Netbeans, anche al di fuori dell'attività di debugging, può tornarti utile per saltare velocemente da un punto all'altro (a proposito, una volta inserito un breakpoint, questo può essere disabilitato senza cancellarlo). Nel tab relativo ai breakpoints, questi vengono mostrati nell'ordine in cui li hai inseriti (e non nell'ordine di numero di riga). Purtroppo Netbeans manca della possibilità di associare, nel tab dei breakpoints, una descrizione a parole per ogni breakpoint: al massimo puoi scriverla su carta o in un editor esterno, oppure aggiungere un commento nel codice Java.
     
  • Se devi aggiungere, togliere o modificare una funzionalità, considera tutta la tua indagine fatta finora e valuta come e dove ha più senso fare modifiche. Se possibile, collauda ogni singola modifica. Meno codice hai bisogno di toccare e meglio è. Ricorda che tra gli scopi della OOP (Programmazione Orientata agli Oggetti) ci sono proprio quelli di agevolare il riuso del codice e di rendere le modifiche il più possibile “locali”, ovvero coinvolgenti piccole porzioni di codice.
     
  • Ogni volta che metti mano a un codice funzionante per fare modifiche, fai prima uno (o più) snapshot (supponendo che il tuo ambiente di lavoro si trovi dentro una macchina Virtualbox), in modo da poter sempre ritornare al codice funzionante da cui sei partito, annullando le modifiche successive.
     
  • Se hai dubbi sul ruolo di certe parti di codice, inserisciti temporaneamente un logging accurato delle variabili. Usa gli strumenti di debug forniti da Netbeans, usa spesso anche le funzionalità “Navigation → Go to Declaration” e “Navigation → Go to Source”. Aiutati con il “Network Monitor” con il “Component Inspector” del Simulatore di Codename One. Potrebbe esserti utile anche la GroovyConsole. Implementati anche l’invio del logging via email quando lo ritieni necessario (soprattutto per i test su dispositivi reali). Nei miei progetti complessi, faccio in modo che se l’utente dell’app è registrato come “sviluppatore”, allora gli do accesso ad una schermata dove poter vedere i log normali, quelli nativi (tramite la cn1lib “native logs”), con pulsante per inviarseli via email.
     
  • Se noti che c’è del codice che ti sembra strano, inutile o con uno stile discutibile, prima di cambiarlo o di toglierlo riflettici bene. Nel dubbio lascialo stare. Magari chi l’ha scritto aveva le sue ragioni... e se l’avevi scritto tu, magari quando l’avevi scritto avevi in mente qualcosa che ora non ricordi.
     
  • Se stai cercando la causa di un bug, e proprio non riesci a trovarla, come soluzione estrema potresti progressivamente cancellare o disattivare quanto più codice puoi, per ridurre via via il codice potenzialmente contenente il bug. Ripeti più volte questa riduzione di codice. Se a un certo punto, dopo una cancellazione di codice, il bug sparirà, allora avrai trovato il pezzo di codice incriminato. Se avrai fatto uno snapshot prima di iniziare, in qualunque momento potrai tornare al codice di partenza.
     
  • Se l'app ha problemi di lentezza nel rispondere all'input dell'utente, o se usa quantità esagerate di memoria (per quanto possibile misurabili con strumenti specifici come quelli offerti da Xcode), purtroppo quel che hai eventualmente studiato in un corso di "complessità degli algoritmi" potrebbe esserti poco utile. Non sto dicendo che avere una cognizione della classe di complessità del codice che hai scritto o che stai scrivendo sia sbagliato, o che quando si scrive codice ricorsivo non bisogna stare attenti a non saturare lo stack, affermo piuttosto che le cause di lentezza o di uso eccessivo della memoria vanno spesso cercate altrove, sia a causa delle specificità delle app mobili, sia perché il codice Java delle app fatte con Codename One viene transcompilato. A tal proposito:
    • A volte è proprio il caso di procedere per prove ed errori (come argomentato in questa discussione).
    • Altre volte può essere utile verificare quali operazioni deve fare l'app prima di visualizzare un Form, e meno ne fa meglio è, anche perché il rischio di un crash è concreto; nel dubbio, può essere utile l'API invokeWithoutBlocking.
    • Altre volte ancora bisogna stare attenti alle immagini: quando è possibile, preferisci le EncodedImage e, per il ridimensionamento, i metodi offerti dalla classe ImageIO, perché hanno un impatto contenuto sulla Ram, diversamente da altri formati di immagine e da altri metodi di ridimensionamento, come spiegato qui e in altri post analoghi. Se devi visualizzare molte immagini nello stesso Form, mantieni al minimo necessario la loro risoluzione per non sovraccaricare la Ram.
    • Infine, talvolta può essere opportuno fare segnalazioni su Github, perché la causa di un rallentamento potrebbe essere imputabile a un update di Codename One con esiti non voluti (come in questo caso) oppure è possibile richiedere un miglioramento di certe funzionalità (come in quest'altro caso).
  • Se hai bisogno di metter mano a codice nativo che implementa un’interfaccia nativa, allora usa gli IDE nativi: usare Netbeans per scrivere Objective-C per iOS o codice Java con le API di Android non è infatti una buona idea, anzi.
     
  • In generale, non fidarti solo del simulatore: ogni tanto, fai qualche prova sui dispositivi reali che possiedi e magari anche su altri modelli in una device farm (tipo BrowserStack App Live).
     
  • Se vuoi segnalare un bug di Codename One su Github, un’anomalia o fare una domanda su Stack Overflow, dai la possibilità a chi ti leggerà di poterti davvero rispondere: fornisci un “test case” che riproduce il problema, ovvero il codice minimo necessario, meglio se di poche righe.
     
  • Se vuoi inviare una PR (Pull Request) al progetto Github di Codename One per aggiungere funzionalità che ti interessano o correggere un bug, fai in modo che ogni PR sia comprensibile, minima nella quantità di codice che apporta o che modifica, e il più possibile isolata da altre funzionalità. Puoi inviare PR anche per correggere o integrare la documentazione. Nel dubbio, meglio aprire una discussione e chiedere se il tuo codice interessa, piuttosto che inviare direttamente una PR.
     
  • Possono venirti in mente molti modi per “raggirare” i limiti imposti dalle API correnti o dall'attuale struttura del codice dell'app che stai analizzando, struttura che potrebbe esser troppo complicato modificare perché basata su presupposti difficili da cambiare. Qualunque strategia creativa ti venga in mente, però, assicurati di documentarla o quantomeno di renderla evidente a chi leggerà il tuo codice. Cerca anche di aver ben chiaro se quello che stai facendo sarà o non influenzato da aggiornamenti futuri dell'implementazione delle API: fare affidamento sull'attuale implementazione delle API, infatti, oltre a violare i principi della encapsulation e dell'information hiding, potrà avere esiti imprevedibili.
     
  • Per aggiungere informazioni a oggetti che di per sé non offrono metodi specifici per memorizzare quel che ci interessa, ci sono i metodi "putClientProperty" e "getClientProperty" forniti da qualunque Component di Codename One. Un’altra strategia è l'uso di nomi di files, di nomi di PropertyBase o di nomi di Component con significati speciali, prefissi o suffissi speciali, interpretati da appositi metodi che possiamo scrivere a tale scopo.
     
  • In un'applicazione complessa, il numero dei CSS e degli elementi dei resource bundle può essere nell'ordine delle migliaia: un buon modo per evitare "collisioni", cioè ripetere più volte involontariamente gli stessi nomi, è quello di usare prefissi che identificano a cosa ci stiamo riferendo, sia per i CSS sia per le keyword del resource bundle. Per una questione di organizzazione, può essere utile anche avere più files CSS, ad es. un file CSS generico, uno specifico per iOS e uno specifico per Android.
     
  • A volte, quando ho avuto bisogno di mostrare scritte che risultassero identiche per l’utente, ma differenti a livello di codice, ho inserito all’interno delle stringhe uno o più caratteri “zero width space”.
     
  • Idealmente ogni metodo dovrebbe ricevere tutti e solo i parametri di cui ha bisogno come input. A volte, però, ciò potrebbe risultare complicato o quasi impossibile per vari motivi. In questi casi può essere d’aiuto la classe ComponentSelector, che permette di ottenere facilmente tutti i Component di un Form che hanno un certo UIID (cioè stile CSS), un certo nome o altre caratteristiche; è anche possibile ottenere tutti i Component che appartengono ad una determinata classe.
     
  • Quando aggiungi classi, cerca di essere il più ordinato possibile nella scelta dei package e dei nomi.
     
  • Crea il database interno dell'app tramite Property Business Objects: se la loro struttura non è troppo complessa, quando li salvi nello Storage puoi esportarli come JSON piuttosto che come oggetti Java esternalizzabili, in modo da facilitarti la lettura di ciò che l'app salva. Occhio però che ci sono casi nei quali il formato JSON non è adatto, come ho dimostrato qui nel caso dell'eccessivo uso di memoria e qui nel caso di uso di interfacce. Per casi semplici o anche per test momentanei, comunque, il JSON va bene. I Property Business Objects salvati come oggetti Java, comunque, sono sufficientemente (ma non semplicemente) leggibili con i normali editor di testo forniti con Linux (tipo Xed, Gedit, Kwrite e simili).

Francesco Galgani,
Natale 2019

Classificazione: