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

Advertisements

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s