Summary: first lesson with my new class. Teaching TDD, letting the students get a glimpse of how skilled they are in programming; which is unfortunately not much.
Per il secondo anno insegno Tecnologia e Applicazioni Internet all’Insubria. Il mio obiettivo per questo corso è di insegnare come sviluppare applicazioni web, applicando le pratiche tecniche di Extreme Programming. In particolare, vorrei insegnare Test-Driven Development e i principi di Object-Oriented Design, per come li capisco. Il mio meta-obiettivo per questo corso è fare in modo che lo studente diventi il doppio più bravo a programmare.
Per questo corso uso Java e non Rails. Il motivo di questa scelta è che Rails, per quanto sia una spanna sopra a tutti i web framework in Java, è purtuttavia un framework e in quanto tale è una stampella, una gruccia, che ti permette di stare in piedi ma certo non ti aiuta quando vuoi imparare a camminare da solo, men che meno a correre. Per imparare a camminare da soli bisogna imparare a programmare a oggetti.
In Aula
Per la prima lezione ho spiegato il TDD da solo, senza la complicazione delle servlet. Mi sono sforzato di pensare a un esempio che fosse piccolo a sufficienza per fare una demo di fronte agli studenti, in un pomodoro o poco più. Ho deciso di fare un “calcolatore a riga di comando”, ovvero un programma che presa una stringa come “2 + 3” come argomento sulla riga di comando, stampi “5.0” su standard output.
Il primo test che ho scritto:
@Test
public void twoAndThreeIsFive() throws Exception {
Calculator calculator = new Calculator();
double result = calculator.add(2, 3);
assertEquals(5.0, result, EPSILON);
}
Abbastanza semplice da far passare. Ma non era sufficiente, perché dalla riga di comando gli argomenti arrivano come stringhe e non come interi già parsati. Per cui ho scritto un secondo test che ha fatto emergere una classe Parser
@Test
public void willParseTwoAndFive() throws Exception {
Calculator calculator = new Calculator();
String result = new Parser(calculator).calculate("2 + 5");
assertEquals("7.0", result);
}
Perché creare una seconda classe a questo punto? Non sarebbe bastato mettere il metodo “parse” nella classe Calculator? Avrei potuto, però in questo modo il metodo “add” sarebbe diventato un metodo ad uso interno della classe. Come avrei fatto a testarlo? Avrei dovuto buttare via il test su add, oppure tenere add come “public” anche se in realtà serve solo internamente. Oppure usare qualche brutto trucco come dare ad “add” visibilità protected oppure package.
Invece, tenendo il Calculator come classe a sè che si occupa solo di fare conti, mentre Parser si occupa di leggere e scrivere stringhe, posso tenere “add” come metodo pubblico di Calculator. Martin direbbe che ho applicato il “Single Responsibility Principle.” Per me è stato decisivo pensare “se no mi tocca testare un metodo privato”.
Poi non ero ancora soddisfatto. Nel TDD quello che facciamo è sviluppare un isola felice di codice a oggetti, che però a un certo punto si deve scontrare con la realtà procedurale del mondo esterno. In questo caso il “mondo esterno” è il main, che deve creare e invocare i nostri oggetti. Per me è fondamentale che il main non contenga nessuna logica, ma soltanto la creazione di un certo numero di oggetti, collegati insieme. Se faccio restituire il risultato a Parser#calculate, poi al main resta la responsabilità di invocare System.out.println() per stampare.
Il mio obiettivo è ridurre al minimo la logica nel main, in modo che il main, che è per sua natura più difficile da testare unitariamente, sia così semplice da risultare ovviamente corretto. O comunque, per essere sicuro che il main se fallisce, fallisce sempre, e se funziona, funziona sempre. In questo modo posso essere ragionevolmente certo che se il mio main contiene un errore, me ne accorgerò. Gli errori di cablaggio, come li chiama Hevery, sono facili da trovare.
Allora ho applicato il principio “Tell, don’t Ask,” e ho passato l’OutputStream come collaboratore alla Parser#calculate.
@Test
public void willParseTwoAndFive() throws Exception {
Calculator calculator = new Calculator();
OutputStream stream = new ByteArrayOutputStream();
new Parser(calculator, stream).calculate("2 + 5");
assertEquals("7.0\n", stream.toString());
}
In questo modo è come se dicessi a Parser, “questa è la tua stringa da calcolare, questo è lo stream dove devi scrivere il risultato, adesso arrangiati, non ne voglio sapere nulla.”
Un pattern che si può riconoscere in questo design è una versione embrionale di collecting parameter. In generale cerco di evitare di avere metodi che restituiscono dati. Di solito è più efficace dire agli oggetti di fare cose, piuttosto che chiedere dati. Questo è il principio “tell, don’t ask“.
Possiamo anche vedere l’oggetto Parser come un adapter: adatta l’interfaccia del Calculator, basata su numeri, alle necessità di main, che lavora con stringhe.
Tutto ciò, beninteso, non significa che per programmare bisogna ad ogni piè sospinto cercare nel manualone dei pattern uno o più pattern da ficcare dentro al nostro codice. Al contrario, quello che ho fatto io è stato di scrivere il codice che mi sembrava più appropriato per risolvere il mio problema, e poi, ragionandoci sopra, ho riconosciuto dei pattern in quello che avevo scritto.
In laboratorio
La seconda parte della lezione si è svolta in laboratorio. Ho proposto un semplice esercizio, di scrivere un programma che concatena le righe di due file a una a una, un po’ come fa il comando paste(1) di Unix. Ho visto subito che per la maggior parte degli studenti questo esercizio era troppo difficile, per cui sono subito passato a suggerire come primo test una versione semplificata del problema.
@Test
public void pasteLinesFromArrays() throws Exception {
List a = Arrays.asList("aa", "bb");
List b = Arrays.asList("xx", "zz");
List result = new ArrayList();
Concatenator concatenator = new Concatenator();
concatenator.concatenate(result, a, b);
assertEquals(Arrays.asList("aaxx", "bbzz"), result);
}
I miei studenti sono al terzo anno di Informatica triennale. Nel nostro corso di laurea, il linguaggio di programmazione di riferimento è Java. Purtroppo, ho dovuto osservare che per la grande maggioranza dei miei circa 40 studenti, scrivere il codice che fa passare questo esercizio è un problema difficile. E nessuno (mi pare) è stato in grado di estendere il codice per fare passare anche il secondo test:
@Test
public void listsCanBeOfDifferentLength() throws Exception {
List a = Arrays.asList("a");
List b = Arrays.asList("b", "c");
List result = new ArrayList();
Concatenator concatenator = new Concatenator();
concatenator.concatenate(result, a, b);
assertEquals(Arrays.asList("ab", "c"), result);
}
Non so che cosa pensare. Questi esercizi mi sembrano di un livello di difficoltà paragonabile al famoso “problema” FizzBuzz, che viene usato nei colloqui di lavoro per scremare quelli che non sanno programmare per niente da quelli che forse sono capaci di fare qualcosa. Al terzo anno mi aspetterei qualche cosa di più. Sto cercando di ricordare me stesso al terzo anno di università. Sono sicuro che sarei riuscito a risolvere questo problema.
Ma non importa. Venerdì prossimo continuerò con esercizi di questo tipo. Piano piano miglioreremo. Sono sicuro che, alla fine del corso, gli studenti che avranno continuato a frequentare raggiungeranno l’obiettivo di diventare (almeno) il doppio più bravi a programmare.