Skill essenziali per lo sviluppatore Agile

Essential skills for the agile developer

Essential skills for the Agile Developer

Essential Skills for the Agile Developer è un libro scritto a quattro mani (Alan Shalloway, Scott Bain, Ken Pugh e Amir Kolsky) che si pone come obiettivo quello di aiutare il lettore a migliorare la qualità e il design del codice da lui prodotto.

Già da questa introduzione si capisce come sia un libro rivolto a programmatori più o meno esperti, che desiderano migliorare le proprie capacità soprattutto per quanto riguarda la programmazione ad oggetti, ed hanno già le conoscenze e competenze necessarie per comprendere gli esempi forniti all’interno del libro ed eseguire gli esercizi proposti al termine di ogni capitolo.

In questo articolo troverai alcuni dei passaggi che ho ritenuto più interessanti del libro. Se desideri saperne di più, puoi acquistare il libro su Amazon in tre versioni:

I consigli che ho trovato più interessanti

Guida alla rimozione del codice duplicato

Viene fornito un esempio, cioè la ricerca elemento per elemento di oggetti all’interno di un vettore, il cui codice è duplicato in quanto si trova sia nel metodo di visualizzazione che in quello di eliminazione.

E’ un classico esempio di errore di duplicazione: se dovessi aggiornare qualcosa inerente il vettore lo dovrei fare piu volte (es. se cambio implementazione dell’elenco da un vettore ad un array o ad un’altra struttura dati).

Inoltre in generale meno duplicazione implica meno codice e quindi meno codice da debuggare e meno possibilità di errori, oltre ad essere piu leggibile.

E se avessi subroutine simili ma non uguali? Devo individuare cosa è uguale e cosa no, cercare di isolare le variazioni (es. parametrizzandole o mettendole in subroutine a parte) e poi rimuovere la ridondanza della parte in comune

Erano poi presenti altri esempi ad esercizi (con soluzioni), anche per casi più complessi, ad esempio la creazione di un’interfaccia comune, che venisse adottata dal codice in comune tra i metodi originali, e poi implementata da classi diverse che racchiudono le differenze delle subroutine da semplificare (cioè quelle dalle quali estrarre il codice duplicato).

Trasformare commenti in codice

Se uso nomi più espressivi per classi, metodi ed attributi posso risparmiarmi i commenti ed includerli dentro i nomi delle variabili.

In alternativa posso estrarre del codice e metterlo in un metodo a parte, appositamente per dargli un nome consono, in modo che il commento diventi superfluo!

Inoltre i commenti del tipo //costruttore sono inutili.

Al giorno d’oggi i programmatori abusano della possibilità di commentare il codice, a discapito della chiarezza del codice. Scrivere codice chiaro è più importante perchè magari in futuro questo verrà aggiornato ma non i commenti, creando grattacapi ad altre persone che dovessero mettere mani al codice. Se non ci sono commenti ma esiste solo il codice, il problema non si pone.

Quindi non bisogna mai commentare nulla? Assolutamente no, ci sono dei casi nei quali i commenti sono obbligatori:

  • per motivi di prestazione vengono eseguite operazioni in un ordine non logico e poco comprensibile, spiegherò le motivazioni
  • vengono utilizzati pattern poco conosciuti o poco usati, che lo sviluppatore originale stesso del codice potrebbe anche dimenticare
  • esprimere pre e post condizioni

Make it work, make it right, make it fast

Il codice per prima cosa deve funzionare, poi se va funziona si provvederà a sistemare i casi non base, le eccezioni, ecc., e solo alla fine si procede all’ottimizzazione.

In particolare l’ultima parte delle frasi precedenti richiama la famosa citazione di Donald Knuth (Professore americano, autore del più famoso libri sugli algoritmi):

PREMATURE OPTIMIZATION IS THE ROOT OF ALL EVIL

Consigli per il refactoring

  1. Aggiungere static e final per le variabili quando è appropriato e non è stato inserito durante la prima scrittura del codice
  2. Quando è presente del codice inutilizzato, vuol dire che occorre rifattorizzare qualche cosa
  3. Quando ho delle variabili con un nome generico perchè potrebbero assumere significati diversi a seconda delle situazioni, probabilmente c’è un errore di design a monte.
  4. Quando ho delle variabili che definiscono il tipo di un oggetto, si rifattorizza creando delle sottoclassi.
  5. If-then-else complessi o annidati possono essere eliminati utilizzando le sottoclassi, lasciando alle diverse implementazioni delle classi il compito che prima era dell’if di discriminare le operazioni
  6. Metodi troppo lunghi devono essere scomposti
  7. Metodi che fanno cose diverse ma hanno nomi uguali, vanno modificati

Mantenere il codice snello

E’ importantissimo il principio della singola responsabilità: quando una classe include molti tipi di funzionalità differenti, siamo di sicuro violando il principio della responsabilità singola.

Design by contract

Insieme di best-practices nella design object oriented che aggiunge dei vincoli in modo da facilitare nella creazione di un design corretto

  • Le classi devono specificare cosa è vero prima e cosa è vero dopo l’esecuzione dei metodi pubblici (ovvero le precondizioni e postcondizioni)
  • Se un metodo ha delle precondizioni e queste non vengono rispettate, è colpa del client, non deve presentare al suo interno le eccezioni, dovrà pensare a tutto il client, il quale deve assicurarsi che siano rispettate
  • Principio di sostituzione di Liskov: posso sostituire un tipo con un suo sottotipo senza causare malfunzionamenti nel programma. Nel design-by-contract ho ben esplicitato cosa mi aspetto che non cambi dai singoli metodi.

Scegliere le ereditarietà con cura

Potrei fare una ereditarietà sbagliata: es. metodo che poi non mi serve, in quel caso devo toglierla, posso utilizzare la delegation o delega.

La delega è un modo per estendere e riusare le funzionalità di una classe senza ricorrere all’ereditarietà: simulo l’ereditarietà utilizzando un’istanza della classe originale per fornire le funzionalitù originali, inoltre ne aggiunge di nuove. Serve per rappresentare concetti comeIS-A-ROLE-PLAYED-BY e non il IS-A-KIND-OF per cui si usa l’ereditarietà.

DELEGATOR ——–USES—–> DELEGATEE

Ha il vantaggio di poter essere modificate a runtime, ed il modo più semplice per creare una delegation è utilizzare una classe attributo di un’altra classe.

Handling inappropriate references

Se la classe A ha un riferimento a B, allora quando riuseremo A, questa tirerà in ballo B. Se a sua volta B ha un riferimento a C, eccetera. Ma se C non ha significato nel nuovo ambiente, non potremo utilizzare A. Quindi bisogna stare attenti ai riferimenti o le classi diventano difficilmente riutilizzabili.

Primo passo: evitare riferimenti a classi troppo specifiche o di sistema (non riutilizzabili altrove)

Secondo passo: evitare riferimenti circolari o mutui.

Viene poi introdotto il “Dependency Inversion Principle“: un modulo di alto livello non dovrebbe dipendere su un modulo di basso livello: se questo succede, bisogna estrarli entrambi in un concetto astratto e lasciarli dipendere da quel concetto.

ES. Se un programma copia un carattere dalla tastiera alla stampante, modificandolo per supportare device qualsiasi (non solo stampanti) non è elegante: il metodo copy dipende dalla tastiera. Un’implementazione migliore prevede un’interfaccia per i lettori ed una per gli scrittori, in modo che il programma sia adattabile facilmente alla scrittura e lettura da altri device.

Separate Database, User Interface and Domain Logic<

Separare: il database su una classe a parte, così come UI e logica di business: devo dividere il codice in 3 layer.

Il layer del dominio logico ha riferimenti da entrambi gli altri , ma non viceversa: questo rende facile riutilizzarlo.

Il database layer ha un riferimento al domain logic layer, ma non all’UI layer. Non importa se il sistema è text based, GUI based o web based, questo layer non può essere riutilizzato.

Gestire un progetto software ricorrendo a user stories o casi d’uso (scenari)

Una userstory è una descrizione, una narrativa da parte dell’utente dalla sua prospettiva. Partendo dalla descrizione testuale non strutturata del problema, ricaverò un’elenco di punti, il cosiddetto flusso.

Il caso d’uso descrive però solo il comportamento esterno del sistema, tralasciando quello interno: non devo specificare che succede dentro ne parlare di database, records, o campi, sto parlando dell’interazione utente-sistema.

La suddivisione nel flusso principale e flussi secondari mi permette di strutturare meglio il problema, ad esempio stimando il tempo necessario per realizzare ciascun punto. Posso così dare dei punteggi di difficoltà ai singoli punti (il libro parla di story points. Alla fine moltiplico la somma degli story point per il tempo che stimo ci voglia a fare un punto che richiede 1 solo story point, ed ho una stima del tempo necessario per il completamento del progetto.

Questa è una prima stima, in realtà posso fare una stima piu precisa misurando quanto tempo effettivo ci metto a fare 1 story point e poi moltiplicando per il numero totale degli story points.

Nella realizzazione effettiva, che succede?

Posso decidere di rimandare degli scenari ad una iterazione successiva, se non sono fondamentali subito, per completare le parti chiave al piu presto, oppure posso aumentare i developers (il miglioramento non è detto che sia lineare, anzi è quasi sicuro che non lo sia, se raddoppio il team ci metterò 2/3 del tempo, non la metà, ad esempio, a causa di costi di coordinamento e comunicazione,ecc.).

Se non riesco a stimare il tempo necessario per una certa user story, posso agire in tre direzioni:

  • suddividerla in più user story
  • chiedere a chi ha l’esperienza e la conoscenza necessaria per stimare il tempo di aiutarmi
  • eseguire degli esperimenti

Iterazioni

Un progetto agile è implementato in iterazioni. All’inizio di ogni iterazione, lasceremo l’utente scegliere le user story che vuole implementate nell’iterazione, che di solito saranno quelle più importanti.

Se un’user story viene eseguita prima di un’altra da cui dipende, si ricorrerà ai cosiddetti stub o parti hard coded, cioè a simulazioni ultra-semplificate della user story mancante.

Se un’attività richiede piu del tempo disponibile in un’iterazione (es. 2.5 punti settimanali), posso sempre spezzare l’attività in 2-3 parti e farne solo alcune in questa iterazione.

Invece di buttare giù tutte le specifiche subito, farò vedere all’utente l’user story e gli farò ripecorrere il processo, così da essere sicuro di individuare subito eventuali problemi. Inoltre eventuali dubbi potranno essere chiariti direttamente, con evidenti vantaggi rispetto allo stilare un semplice foglio pieno di requisiti.

Design delle classi utilizzando le schede CRC

Una scheda CRC (dove la sigla rappresenta Classi, Responsabilità e Collaborazioni) è un foglio di carta nel quale sono appuntati dei concetti del mondo reale che stiamo per modellare in classi, con alcune responsabilità legate a tali oggetti e delle collaborazioni con altre classi necessarie per adempiere alle proprie responsabilità.

Partendo dal flusso delle attività (o degli eventi), indico per ciascuna classe quali sono le responsabilità (cioè le cose che deve fare) e le collaborazioni (cioè le altre classi assieme alle quali portare a termine le proprie responsabilità).

Se escono fuori troppe responsabilità (>4) è possibile riunirne alcune in una più astratta oppure creare un’altra classe.

Le schede CRC servono per esplorare alternative di design, se una alternativa non mi convince posso buttare tutto. Servono per creare un design velocemente, non servono nè come documentazione finale nè devono essere perfette, le aggiusterò dopo al momento dell’implementazione.

Acceptance test

Mentre richiedo maggiori dettagli sulla user story allo stakeholder, mostrandogli le CRC cards, è possibile avere una conferma che ci si sta muovendo nella direzione giusta.

L’acceptance test invece dell’implementazione (o test funzionale) prevede che sia controllato solo se il funzionamento esterno del sistema va a buon fine, ignorando quello che succede all’interno. Ad esempio arrivano le mail di conferma finali? Viene visualizzato a video quello che mi aspetto?

Questi controlli vanno fatti in più casi:

  • nel caso vada tutto bene
  • nel caso qualcosa vada male
  • nel caso si verifichi una eccezione (es. ID DUPLICATO) gestita

E` quindi necessario creare dei test cases, e poi eseguirli manualmente. Questo però richiede molto tempo e sforzi. Inoltre ad ogni modifica del codice i test andrebbero rieseguiti: diventa necessario automatizzare il tutto.

I test vengono automatizzati scrivendo del codice che provveda all’esecuzione dei test cases dell’acceptance test autonomamente, una classe con un metodo per ogni test case.

Tutti i metodi che eseguono test cases non devono includere domain logic (ordinamento, controlli, eccetera), eventuali metodi che includano elementi della domain logic vanno spostato negli oggetti della domain logic, fuori dai test, e quindi verranno richiamati dal metodo della classe test.

E’ importante scrivere bene i test cases: modifiche e problemi successivi possono essere evitati tenendo bene in considerazione quello che può e quello che non può succedere ed agire di conseguenza. E` possibile anche farsi aiutare dagli stakeholder in questa attività, per individuare casi critici o limite (es. email max 25 caratteri).

Una volta scritti test cases accurati, sarà possibile eseguirli a piacimento, modificandoli leggermente se cambia qualcosa nell’interfaccia della domain logic, ma comunque in modo veloce.

Per farmi aiutare dagli utenti/stakeholder a scrivere l’acceptance test posso usare uno strumento come FIT. L’utente scrive i vincoli, poi lancerò l’esecuzione con Ant e poca programmazione. http://fit.c2.com/

Acceptance test a user interface:

Partendo dalla descrizione della user story, concordo assieme al customer/stakeholder uno storyboard, dei prototipi di interfaccia da seguire. Come si testano le interfacce grafiche?

  1. Individuare gli elementi della UI da testare
  2. Tesare gli elementi separatamente: preparare un test case e programmare un metodo specifico che simuli l’azione di input dell’utente sulla UI
  3. Talvolta si potrebbe pensare che non sia la strada più semplice testare tutta o almeno più elementi assieme (es. dropdown list, o schermata dei dettagli): tuttavia è possibile testare separatamente i due dialog e prendere un input statico (es. invece di utilizzare nel test una prima dialog che determina l’input per la seconda, si utilizza un input statico per la seconda dialog, ad esempio una stringa, e successivamente viene testata a parte la primadialog

In pratica è consigliato scrivere del codice per ogni use case, in modo da automatizzare il test. Test manuali (molto costosi), oppure replay based GUI (questi ultimi potrebbero non funzionare ed ogni volta che cambio la GUI diventano inutili, rendendo impossibile il regression testing*) sono sconsigliati..

*regression testing
Consiste nell’eseguo un’intera serie di test sul codice in occasione di ogni nuova modifica, alla ricerca di errori introdotti con la modifica stessa. Cioè cerco errori causati dall’introduzione delle novità.
L’aspetto più difficile è generare un insieme di test cases che copra tutti i problemi possibili, cercando di non impiegare troppo tempo, includendo una serie di test automatici e casi di prova. Per evitare tempo del testing e ridurre la complessità della libreria di test di regressione, è possibile eliminare dalla libreria di test di regressione tutti quei test cases che non possono essere stati intaccati dalla modifica che si intende testare.

Unit test

Test case per il comportamento di una singola classe, o meglio di una unità. E’ diverso dall’acceptance test perchè non testa il comportamento esterno del sistema, dal punto di vista del client, ma il comportamento interno della classe.

In java si utilizza JUnit, includendo le librerie corrispondenti ed utilizzando metodi come assertEquals(X,Y) si controlla se X ed Y sono uguali, e viene lancia un’eccezione in caso contrario.

Inoltre JUnit fornisce una GUI, nella classe TestRunner, che mostra i risultati del test e l’avanzamento degli stessi, eventualmente con lo stacktrace dell’eccezione.

Il TestUnit va svolto per ogni metodo ed ogni classe, a meno che il metodo sia troppo semplice per causare errori (es. costruttori), resterà solo il metodo equals per esempio, scrivo il test e lo lancio.

JUnit permette di lanciare intere Suite di test, cioè impostare tutti i test in un metodo unico e poi eseguirli in un colpo solo. Per creare nuovi test sarà necessario andare ad aggiungerli alla suite esistente, e questi verranno accodati a tutti i test preesistenti.

I test sono programmi scritti per essere eseguiti in modalità batch (non interattiva) e testare i metodi di una classe, osservando se la risposta attesa è ottenuta. Sono una componente fondamentale della software engineering, sono gli sviluppatori stessi che scrivono test per ogni classe prodotta. Un test deve essere completo, predicendo ogni aspetto che potrebbe non funzionare.

Quando eseguire l’Unit Test? Appena si è finito di scrivere o modificare la classe oggetto dell’unit test, o una classe ad essa collegato. Inoltre se si deve eseguire un acceptance test, questo è buona norma sia svolto dopo gli unit test, per evitare bug.

Code Unit Test First

L’interfaccia e l’output di un oggetto sono più importanti della loro implementazione, prima scrivo le prime. Cioè:

    1. stabilisco ciò che bisogna fare
    2. scrivo un unit test per la nuova funzionalità che deve svolgere la mia classe; tale funzionalità deve rappresentare il più piccolo incremento possibile (piccoli step)
    3. eseguo l’unit test, se funziona vado alla prossima funzionalità

La cosa fondamentale è questa: non implementare 2 cose contemporaneamente, e quindi non provare a fixare 2 cose contemporaneamente, ma solo una. Adottando questo approccio, lo sviluppo è un ciclo di testing->ricerca del bug->fix->test->feedback positivo, e quindi si può procedere con la feature successiva.

Test Driven Development

E` un’estremizzazione dell’approccio precedente.

      1. Scrivo prima gli Unit Test, riferiti a classi che non esistono ancora. Non userò DATABASE nei miei test (non mettere nei test dati non necessari, database connections e tutto quello che non serve o complica ulteriormente il tutto), userò classi di storage, che poi al loro interno implementerò come mi pare. Si fa ricorso ad interfacce per rendere più semplice la scrittura dei test: partendo dai test elimino dei dettagli implementatiivi dai test che in realtà sarebbero ininfluenti. Scrivendo prima i test, è più facile scrivere un comportamento per volta, il che rende più facile il tutto, e viceversa non testare la stessa cosa 2 volte in 2 test diversi: es. quando testo la validazione non testerò anche la scrittura su storage, che la testerò da un’altra parte…il segreto è dividere lo sviluppo (e quindi il testing) in parti piccole, per realizzare cicli di sviluppo da pochi minuti.
      2. Scrivo lo scheletro delle mie classi, solo il codice necessario per far compilare unit test: classi e metodi vuoti.
      3. Lancio il test e lo vedo fallire (su JUnit ho la caratteristica barra rossa)
      4. Scrivo il codice necessario (l’implementazione della classe) per far passare il test
      5. Lancio il test e questo viene superato
      6. Rifattorizzazione ed ottimizzazione: rimozione eventuale doppioni di codice, codice inutile, riscrivere il codice per aumentare l’espressività delle variabili ecc, rifattorizzando in modo da aumentare coesione e ridurre accoppiamento: è possibile aggiungere i design pattern, ma uno alla volta, non tutti assieme
      7. Ripetere gli step precedenti per altre funzionalità

La comunicazione durante lo sviluppo di un software

Esistono diversi modelli di comunicazione per i requisiti:

      • L’analista scrive al cliente per capire i requisiti, e scrive delle specifiche SRS (Software Requirements Specifications). A questo punto queste SRS andranno comprese dagli sviluppatori, che se non capiscono chiederanno chiarimenti all’analista che potrebbe chiedere di nuovo al cliente
      • Gli sviluppatori parlano direttamente col cliente, i requisiti sono poi scritti come acceptance test. Se ci sono dubbi c’è comunicazione diretta sviluppatore->cliente
      • L’analista parla al cliente e si fa dare i requisiti principali, poi scrive una breve descrizione di ogni feature e la manda agli sviluppatori. Per i dettagli gli sviluppatori si interfacceranno direttamente col cliente, ad esempio con meeting settimanali.

Esistono anche diversi modelli per la comunicazione del design tra sviluppatori:

      • diagrammi UML
      • codice facile da comprendere, il design è intuito dal codice
      • Developers parlano del design mentre disegnano su una lavagna
      • Developers usano CRC cards, simile al discorso della lavagna ma posso spostare gli elementi del design più agevolmente

Quale di questi modelli sia il migliore dipende dai casi, il primo metodo è il più pesante, include un sacco di dettagli, il secondo è il più leggero ma è difficile da capire per gli altri sviluppatori.

Questi sono solo alcuni tra i principali argomenti trattati in Essential Skills for the Agile Developer. Per una trattazione più completa e per scoprire quali concetti, esempi ed esercizi sono presenti nel libro ed assenti nel mio riassunto, ti invito a comprare il libro su Amazon, al miglior prezzo.

Lascia un commento

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *