Imparare Ruby col TDD 2

Continuo quanto fatto nel post precendente e inizio col rifattorizzare il codice (i tests erano verdi no?) :

Penso sia ora che i packages si prendano le loro responsabilità, non sono solo nomi, hanno delle relazioni tra essi e in ogni caso voglio alleggerire la classe Dependencer di alcune delle sue responsabilità di printing, in particolare quella che riguarda il print delle rows dei packages.

Creo quindi una classe Package, che sostituisco alle stringhe nell’array @packages di Dependencer :

def setup
@tableMaker = Dependencer.new
@tableMaker.addPackage(Package.new(“aspects”))
@tableMaker.addPackage(Package.new(“client”))
@tableMaker.addPackage(Package.new(“grouping”))
@tableMaker.addPackage(Package.new(“factories”))
@tableMaker.addPackage(Package.new(“baselib”))
end

Run. Rosso : manca la classe.

class Package
@name

def initialize(name)
@name = name
end

def to_str
return @name
end
end

Run. Verde. Ora la mia classe Package sostituisce le stringhe che rappresentavano i nomi dei packages, ma non fa altro, è un data holder, devo dargli una responsabilità, quella di fare la print della row, quindi muovo il metodo Dependencer.printRow in Package e sposto anche i tests che lo riguardano dentro una test case tutta per Package :

require ‘test/unit’
require ‘dependencer’

class DependenciesTableTest < Test::Unit::TestCase

@tableMaker

def setup
@tableMaker = Dependencer.new
@tableMaker.addPackage(Package.new(“aspects”))
@tableMaker.addPackage(Package.new(“client”))
@tableMaker.addPackage(Package.new(“grouping”))
@tableMaker.addPackage(Package.new(“factories”))
@tableMaker.addPackage(Package.new(“baselib”))
end

def testFirstRowContainsRootPackage
@tableMaker.addRoot(“sample”)
assert_equal(“!|Module Dependencies|sample|”,@tableMaker.printHeader)
end

def testFirstRowContainsJustTheFixture
assert_equal(“!|Module Dependencies|”,@tableMaker.printHeader)
end

def testTitlesRowContainsAllPackages
assert_equal(“||aspects|client|grouping|factories|baselib|”,@tableMaker.printPackagesRow)
end

def testNoPackagesReturnsFirstEmptyColumn
emptyTableMaker = Dependencer.new
assert_equal(“||”,emptyTableMaker.printPackagesRow)
end

def testTitlesRowContainsAllPackages
@tableMaker.addRoot “sample”
assert_equal(“||.aspects|.client|.grouping|.factories|.baselib|”,@tableMaker.printPackagesRow)
end

def testWholeTableIsGeneratedCorrectly
assert_equal(“!|Module Dependencies|\n” +
“||aspects|client|grouping|factories|baselib|\n” +
“|aspects||||||\n” +
“|client||||||\n” +
“|grouping||||||\n” +
“|factories||||||\n” +
“|baselib||||||\n”,@tableMaker.printWholeTable);
end
end

class PackageTest < Test::Unit::TestCase

@aspectsPackage
@clientPackage

def setup
@aspectsPackage = Package.new “aspects”
@clientPackage = Package.new “client”
end

def testFirstRowIsCorrectlyFormatted
assert_equal(“|aspects||||||”,@aspectsPackage.printRow(5))
end

def testSecondRowIsCorrectlyFormatted
assert_equal(“|client||||||”,@clientPackage.printRow(5))
end
end

Spostati i tests faccio il refactoring. Ecco cosa è cambiato :

class Dependencer
….
def printWholeTable
table = printHeader + “\n”
table += printPackagesRow + “\n”
#Look ‘ma! Without index!
for package in @packages
table += package.printRow(@packages.size)+”\n”
end
return table
end
end

class Package
….
def printRow(packagesSize)
#Look ‘ma, without magic numbers!
emptyColumns = “|”*(packagesSize)
return “|”+@name+ “|” + emptyColumns
end
end

Run. Verde. Nuovo test.
Passiamo a generare delle belle X in girùla. Aggiorno il setup di PackageTest, per avere tutte le informazioni necessarie.

@aspectsPackage
@clientPackage
@packages

def setup
@aspectsPackage = Package.new “aspects”
@clientPackage = Package.new “client”
@packages = Array.new
@packages << @aspectPackage
@packages << @clientPackage
@packages << Package.new(“grouping”)
@packages << Package.new(“factories”)
@packages << Package.new(“baselib”)
end

E aggiungo il test :

def testClientCanDependOnAspects
@clientPackage.canDependOn(@aspectsPackage)
assert_equal(“|client|X|||||”,@clientPackage.printRow(@packages.size))
end

Run. Rosso. Manca il metodo “canDependOn”, presto detto presto fatto :
class Package

@name
@allowedDependencies
….
def initialize(name)
@name = name
@allowedDependencies = Array.new
end
….
def canDependOn(package)
@allowedDependencies << package
end
end

Run. Rosso, ovvio, printRow continua a ritornare tutto senza la X. Inizio a smanettare su printRow, ma è subito evidente che ho bisogno di un’informazione che mi manca, e cioè se i packages da cui posso dipendere sono tra quelli delle colonne della tavola, quindi devo passarmi la lista dei packages, non solo il size.

def testFirstRowIsCorrectlyFormatted
assert_equal(“|aspects||||||”,@aspectsPackage.printRow(@packages)
end

def testSecondRowIsCorrectlyFormatted
assert_equal(“|client||||||”,@clientPackage.printRow(@packages)
end

def testClientCanDependOnAspects
@clientPackage.canDependOn(@aspectsPackage)
assert_equal(“|client|X|||||”,@clientPackage.printRow(@packages)
end

E quindi modifico la printRow per far passare i tests :
def printRow(packages)
columns = “”
for package in packages
if @allowedDependencies.include?(package)
columns += “X”
end
columns += “|”
end
return “|”+@name+ “|” + columns
end

Run. Verde. Questa ha funzionato al secondo colpo in effetti, avevo scambiato un “+=” con un “=” Refactoring? Penso non serva.

Però, c’è un però, una volta sbattuto tutto dentro Fitnesse la tavola non ha funzionato. Acceptance fallita! Già, mi sono dimenticato che, in presenza di una root, anche il nome del package all’ inizio della sua row deve iniziare con il punto.
Scriviamo un test allora …

def testPackageBeginsWithDotIfRootIsSet
assert_equal(“|.client||||||”,@clientPackage.printRow(Root.new(“sample”),@packages))
end

Va da sè che ritorna rosso, printRow non ha due argomenti, solo uno, quindi modifico e implemento quello che penso farà passare il mio test :
def printRow(root,packages)
columns = “”
for package in packages
if @allowedDependencies.include?(package)
columns += “X”
end
columns += “|”
end
return “|”+ root.dot + @name+ “|” + columns
end

Verde.

Ora è tempo di un piccolo test di acceptance all inclusive :

def testWholeTableIsGeneratedCorrectlyWithRootAndDependencies

@grouping.canDependOn(@aspects)
@grouping.canDependOn(@baselib)
@aspects.canDependOn(@baselib)

@tableMaker.addRoot(“sample”)

assert_equal(“!|Module Dependencies|sample|\n” +
“||.aspects|.client|.grouping|.factories|.baselib|\n” +
“|.aspects|||||X|\n” +
“|.client||||||\n” +
“|.grouping|X||||X|\n” +
“|.factories||||||\n” +
“|.baselib||||||\n”,@tableMaker.printWholeTable);
end

Verde! Applausi grazie.

Qualche conclusione alla fine di questa maratona :

  • Ruby è molto intuitivo, considerato che la mia conoscenza di Ruby era limitata a chiamare in sequenza dentro un file dei comandi equivalenti a quelli unix, a concatenare stringhe e usare variabili locali, sono rimasto stupito di quante poche volte ho dovuto cercare nell’help e su Google rispetto alle volte che ho potuto invece buttare dentro qualcosa che “forse si scrive così” e azzeccarci sul colpo.
  • Grazie al test first l’applicazione è molto piccola (100 linee esatte!), la proporzione tra linee di test e linee di soluzione è 1:1 (107 linee di test).
  • Sono certo che si sarebbe potuto fare meglio, o quantomeno molto più Rubish e meno Javish, non so come, ma considerate le differenze sostanziali tra i due linguaggi mi stupirei che una soluzione che sfrutti a pieno Ruby possa assomigliare così tanto a una soluzione Java/C++. Per questo avrei bisogno di feedback da parte di un esperto di Ruby, oppure aspettare di saperne molto di più e reimplementare questa stessa piccola applicazione, al momento di certo uso un po’ meglio Ruby, ma non -penso- in Ruby, traduco da Java a Ruby più che altro.
  • Per scrivere queste 207 linee mi ci sono volute circa 4 ore, due ieri sera e due oggi, di queste il 60% almeno è andato a finire nel testo e nei copia-incolla per questi posts, quindi penso mi ci sia voluta un’ora e tre quarti per tests e soluzione.
  • La soluzione è ottimistica e non robusta : penso proprio che a input sbagliati non ritorni errori, quanto output sbagliati, quindi è fail-slow (l’errore lo noti solo quando inserisci la tabella in fitnesse e la fai girare, o forse nemmeno allora). Ad ogni modo non è testata per la robustezza, solo per la correttezza.

Comments

una cosa soltanto, vedendo ora questo post (ma com’è che me lo sono perso per tre mesi? mah). Non serve che “dichiari” le variabili distanza, ed effettivamente non stai facendo quello che pensi. class C @foo def initialize @foo=”foo” end end è equivalente a class C #niente def initialize @foo=”foo” end end la presenza delle brutte chiocciole davanti alle variabili d’istanza è sufficiente a far capire a ruby quel che stai facendo.
Posted by riffraff on 09/27/2006 04:23:14 PM

Si, ti ringrazio molto per il commento, me l’avevano già detto e l’avevo tolto. Ora quando scrivo in Ruby non le “dichiaro” più, ma il dependencer è morto con Windows. Devo ancora trovare il tempo di riprenderlo da capo secondo i suggerimenti che ho avuto su XP-IT. Conto di farlo comunque, è un esercizio che mi stava piacendo (e poi mi era davvero utile per creare le tabelle di dipendenze su FitNesse :) )
Posted by ratta on 09/27/2006 04:31:14 PM

Imparare Ruby col TDD

Allora, devo imparare Ruby un po’ meglio di quanto i miei primi esperimenti mi abbiano insegnato (negli scorsi mesi ho fatto in modo di sostituire gli scripts unix e ANT che usavo con degli scripts di Ruby, per imparare il linguaggio).

Ho quindi pensato di sviluppare un altro scriptino, ma questa volta configurabile a tal punto da richiedere un deciso approccio ad oggetti : si tratta di generare una tabella per la JDepend fixture dello zio Bob (http://www.butunclebob.com/ArticleS.UncleBob.JdependFixture). Questa fixture è proprio simpatica e permette di imporre che alcuni packages non siano dipendenti da altri, e rileva le ciclicità appoggiandosi su JDepend (http://clarkware.com/software/JDepend.html).

Trovo però che sia molto pesante dover andare a scrivere quei tabelloni, che, se io ho 12 unità di rilascio, sono grossi 12×12. Voglio quindi creare una classe in Ruby che, opportunamente configurata, crei questi tabelloni per me, e me li stampi in un file o magari in console, così che io possa fare un bel copia incolla sul mio fitnesse. In più voglio poter definire delle “librerie di base”, cioè delle unità di rilascio che non devono avere collegamenti efferenti verso alcuna altra unità da me rilasciata, ma che possono essere usate da chiunque, magari anche meglio, voglio poter definire programmaticamente quali dipendenze mi attendo.

Ecco un esempio di quello che intendo creare :
!|Module Dependencies|sample|
||.aspects|.client|.grouping|.factories|.baselib|
|.aspects|||||X|
|.client|||||X|
|.grouping|X||||X|
|.factories|X|X|||X|
|.baselib||||||

Inizio quindi con lo scrivere un test di ruby per ottenere la prima linea, che ha “!|Module Dependencies|” fisso e poi la root. Per lanciare i tests uso un profilo di run di Eclipse, in particolare “Test::Unit” che mi mette a disposizione il plugin rubyeclipse (http://rubyeclipse.sourceforge.net/).

require ‘test/unit’
require ‘dependencer’

class DependenciesTableTest < Test::Unit::TestCase

@tableMaker = Dependencer.new

def setup
@tableMaker.addRoot(“sample”)
end

def testFirstRowContainsRootPackage
assert_equal(“!|Module Dependencies|sample|”,@tableMaker.printHeader)
end

end

Run, manca la classe Dependencer, e la aggiungo :

class Dependencer
end

Run. E già il test mi dice che non capisco nulla di Ruby : Exception: undefined method `addRoot’ for nil:NilClass

A quanto pare setup viene eseguito prima delle inizializzazioni delle variabili d’istanza, quindi metto l’inizializzazione del Dependencer (nome terribile, lo so :) ) dentro al setup.
def setup
@tableMaker = Dependencer.new
@tableMaker.addRoot(“sample”)
end

Run… manca il metodo addRoot. Aggiungo in Dependencer :
@root

def addRoot(root)
@root = root
end

Run. Manca printHeader. Aggiungo in Dependencer con la mia interpretazione javista di una concatenazione di stringhe :
def printHeader
return “!|Module Dependencies|” + @root + “|”
end

Run. Verde! Ruby è decisamente intuitivo nella sua sintassi di base.

Refactoring… direi nulla, non c’è nemmeno un if.

Prossimo test, se non c’è una root il risultato di printHeader dovrebbe essere : !|Module Dependencies|
def testFirstRowContainsJustTheFixture
assert_equal(“!|Module Dependencies|”,@tableMaker.printHeader)
end

Run. Exception: cannot convert nil into String
Comprensibile, @root non è inizializzata, e io ci butto dentro la soluzione più stupida che ci sia :
def printHeader
if @root != nil
return “!|Module Dependencies|” + @root + “|”
else
return “!|Module Dependencies|”
end
end

Run. Verde! Ha funzionato di nuovo! Devo dire che ruby è davvero intuitivo.
Refactoring…
1 – duplicazione della costante “!|Module….”
Per questo estraggo la costante
2 – if
Qui ci metterei volentieri un NullObject pattern, ma il vantaggio, visto che l’if per adesso è solo uno, sarebbe davvero minimo, e quindi lo lascio così, per adesso.

Ma come si faranno le costanti con Ruby?? Chiediamo a google. Ah, ecco, basta iniziare con una maiuscola :
FIXTURE_NAME = “Module Dependencies”
…..
def printHeader
if @root != nil
return “!|” + FIXTURE_NAME + “|” + @root + “|”
else
return “!|” + FIXTURE_NAME + “|”
end
end

A questo punto devo ottenere la prima riga, quella dei nomi dei packages : ||.aspects|.client|.grouping|.factories|.baselib|
Notare però che il punto serve solo se è settata la root. Quindi per ora voglio :
def testTitlesRowContainsAllPackages
assert_equal(“||aspects|client|grouping|factories|baselib|”,@tableMaker.printPackagesRow)
end

e nel setup :
def setup
@tableMaker = Dependencer.new
@tableMaker.addPackage(“aspects”)
@tableMaker.addPackage(“client”)
@tableMaker.addPackage(“grouping”)
@tableMaker.addPackage(“factories”)
@tableMaker.addPackage(“baselib”)
end

Run. Manca il metodo addPackage. Qui voglio una lista… ah, pare che si chiami Array, che in Ruby sono a lunghezza variabile. E per aggiungere un elemento al fondo dell’array? Naah, questo è davvero ganzo :
def initialize
@packages = Array.new
end
….
def addPackage(package)
@packages << package
end

Quelle doppie freccette aggiungono un elemento al fondo dell’array! Bellezza dell’overloading degli operatori.
Notare la initialize, Ruby, a differenza di Java, pare che non inizializzi le variabili di instanza se non dentro a un metodo esplicito, per fortuna la initialize viene invocata automaticamente. E io che credevo che fosse solo questione di setup di TestCase!

Run. Manca “printPackagesRow”.
Proviamo con questo :
def printPackagesRow
row = “|”
for package in @packages
row += “|” + package
end
row += “|”
end

Run. Verde! Continuo ad azzeccarci al primo colpo, giuro! Che poi sono stupefatto, mi sono appena reso conto di aver dimenticato i returns… sarà una delle tante cose che Ruby “intuisce”. Ma preferisco essere esplicito e aggiungo il return.

Però se non ci sono packages settati non è che ho dei guai? Io voglio che ritorni la colonnina vuota :
def testNoPackagesReturnsFirstEmptyColumn
emptyTableMaker = Dependencer.new
assert_equal(“||”,emptyTableMaker.printPackagesRow)
end

Run. Verde di nuovo. Ma non è che questi unit test di ruby sono truccati? ;)

Passiamo alle cose difficili, così magari avrò dei rossi.
Se la root è presente ci vogliono i punti!
def testRootIsPresentThusPackagesAreDotted
@tableMaker.addRoot “sample”
assert_equal(“||.aspects|.client|.grouping|.factories|.baselib|”,@tableMaker.printPackagesRow)
end

Run. Rosso, oh là, mancano i punti. Un altro if entra in gioco!

def printPackagesRow
row = “|”
for package in @packages
row += “|”
if @root != nil
row += “.”
end
row += package
end
row += “|”
end

Run. Verde. Ok, ma questo root inizia a essere fastidioso. E’ ora di rifattorizzare per il NullObject direi. Forse in Ruby c’è un modo ancora più furbo, ma ne so troppo poco per ora e mi attengo alla tradizione. Definisco due classi :

class Root
@root

def initialize(root)
@root = root
end

def dot
return “.”
end

def to_str
return @root + “|”
end
end

class NoRoot
def dot
return “”;
end
def to_str
return “”;
end
end

Entrambe hanno il metodo “dot” e fanno l’override della funzione to_str (che sarebbe la toString)

Cambio di conseguenza il Dependencer per sfruttare queste due classi :

class Dependencer

FIXTURE_NAME = “Module Dependencies”

@root
@packages

def initialize
@root = NoRoot.new
@packages = Array.new
end

def addRoot(root)
@root = Root.new(root)
end

def addPackage(package)
@packages << package
end

# printHeader è davvero dimagrita!
def printHeader
return “!|” + FIXTURE_NAME + “|” + @root
end

#E anche printPackagesRow è molto più snella!
def printPackagesRow
row = “|”
for package in @packages
row += “|” + @root.dot + package
end
row += “|”
end

end

Run. Verde, refactoring ok.

Ora passiamo alla costruzione delle righe interne, testo la prima :

def testFirstRowIsCorrectlyFormatted
assert_equal(“|aspects||||||”,@tableMaker.printRow(0))
end

Run. Manca printRow, manco a dirlo.

def printRow(row)
emptyColumns = “|”*@packages.size
return “|”+@packages[row]+emptyColumns
end

Qui in Java avrei probabilmente usato una for, ma con Ruby certe cose sono molto più compatte, notare la linea in grassetto.

Run. Rosso : Test::Unit::AssertionFailedError: <“|aspects||||||”> expected but was <“|aspects|||||”>.

Manca un trattino! (Ne ho generati cinque in coda ad aspects, mentre invece dovevo generarne sei)

Piccola modifica, con un magic number…. brrrr

def printRow(row)
emptyColumns = “|”*(@packages.size + 1)
return “|”+@packages[row]+emptyColumns
end

Run. Verde.
Refactor? Mah, al massimo ci potrei estrarre un metodo printEmptyColumns, per dare un po’ più di significato a quel +1, ma per adesso lascio stare. Mi viene però il sospetto che Dependencer stia assumendo un po’ troppe responsabilità, va bene la generazione di header e titolo della tabella, ma forse le righe dovrebbero essere generate dai rispettivi packages. Mi tengo questo pensiero da parte.

Provo con la seconda riga :
def testSecondRowIsCorrectlyFormatted
assert_equal(“|client||||||”,@tableMaker.printRow(1))
end

Ho un po’ barato, perchè ottengo subito un verde.. eh sì, in teoria per far passare il test della prima riga mi bastava scrivere :
def printRow(row)
emptyColumns = “|”*(@packages.size + 1)
return “|”+@packages[0]+emptyColumns
end

Ma non esageriamo con l’ingenuità dell’implementazione, se no questo post diventa chilometrico davvero.

Ora provo a generare la pagina completa (questo sarebbe un po’ il mio test di acceptance, ma è anche un test unitario per creare il metodo printWholeTable) :
def testWholeTableIsGeneratedCorrectly
assert_equal(“!|Module Dependencies|\n” +
“||aspects|client|grouping|factories|baselib|\n” +
“|aspects||||||\n” +
“|client||||||\n” +
“|grouping||||||\n” +
“|factories||||||\n” +
“|baselib||||||\n”,@tableMaker.printWholeTable);
end

Run. Rosso, manca printWholeTable. Implementazione banale :

def printWholeTable
table = printHeader + “\n”
table += printPackagesRow + “\n”
for i in 0..@packages.size – 1
table += printRow(i)+”\n”
end
return table
end

Run. Verde. Si però quella for è davvero brutta, in particolare “@packages.size -1” non mi piace, pensare che Ruby ha il bellissimo “for … in” che tanto mi piace in Java e tanto mi piace anche qui.

Ora procederò ad aggiungere la possibilità di definire programmaticamente delle relazioni “buone” tra i packages, cioè le “X” nella tabella. Voglio che sia comodo, quindi metodi molto semplici.

Per adesso chiudo qui, a breve la continuazione, in cui credo proprio che dovrò iniziare a far collaborare più di un oggetto.

Intanto prendo il risultato e lo metto in Fitnesse, per vedere se anche per lui è corretto.

Fitnesse acconsente.

Comments

In Ruby, la parola “return” è opzionale. Se manca, il valore di ritorno del metodo è il valore dell’ultima espressione calcolata. Non sono sicuro che mi abituerò mai, ma nei metodi di una sola riga la cosa non mi sembra più tanto anormale – si vede che mi sto rubyzzando.
Posted by Paolo “Nusco” Perrotta on 09/07/2006 08:03:09 AM