Test di gerarchie
Scritto da Michele Della Torre il 19 aprile 2008 – 15:52Il Ruby Tower Defence sta procedendo molto lentamente per una cronica mancanza di tempo e per la tipica ansia da tema: alla superiori quando il professore di italiano dettava la traccia non si sapeva mai come iniziare, ma una volta scritte le prime frasi si procedeva in modo spedito.
Nonostante questo qualcosa di interessante è arrivato.
Una delle prime classi che ho definito è stata Monster che, tra le varie cose, espone il metodo draw(surface) per disegnarsi sulla superficie di destinazione, con il relativo test, e ora sto definendo la classe Tower ed anch’essa dovrà essere mostrata a schermo, quindi necessiterà di un metodo di disegno, come indicato dal seguente test:
class TowerTest < Test::Unit::TestCase def setup @tower_image = SurfaceMock.new([20, 20]) @position = Position.new(100, 200) @tower = Tower.new(@position, @tower_image) end def test_draw destination_surface = SurfaceMock.new([500, 500]) @tower.draw(destination_surface) assert_equal(destination_surface, @tower_image.target) assert_equal([90, 190], @tower_image.destination) end end
Il fatto che i due test siano praticamente identici, e quindi esista una duplicazione, ci deve spingere a cercare un modo per rimuoverla anche perché appare evidente che tale duplicazione si troverà anche nelle due classi. Procediamo per passi.
Iniziamo a duplicare il codice tra Monster e Tower in modo da far passare entrambi i test:
class Tower def initialize(position, image) @image = image @position = position end def draw(destination_surface) @image.blit( destination_surface, top_left_image_corner() ) end private def top_left_image_corner() new_top = @position.x - @image.height / 2 new_left = @position.y - @image.width / 2 return [new_top, new_left] end end
class Monster initialize(initial_health, initial_position, image, speed) @image = image @position = position [...] end def draw(destination_surface) @image.blit( destination_surface, top_left_image_corner() ) end private def top_left_image_corner() new_top = @position.x - @image.height / 2 new_left = @position.y - @image.width / 2 return [new_top, new_left] end [...] end
In questo caso credo che sia ragionevole dire che sia Tower che Monster siano due Sprite e che il metodo draw sia definito nella superclasse, quindi applichiamo un Extract Superclass a Tower, portando tutto nella nuova classe Sprite.
class Sprite def initialize(position, image) @image = image @position = position end def draw(destination_surface) @image.blit( destination_surface, top_left_image_corner() ) end private def top_left_image_corner() new_top = @position.x - @image.height / 2 new_left = @position.y - @image.width / 2 return [new_top, new_left] end end
class Tower < Sprite [...] end
Testiamo tutto: barra verde. Facciamo lo stesso con Monster e ci accorgiamo che in Sprite bisogna definire l’attributo position, in particolare in lettura deve essere pubblico, mentre in scrittura deve essere disponibile solo alle sotto classi, quindi protected: questo perchè Monster si muove e ha bisogno di aggiornare la posizione (il codice relativo non è stato riportato qui)
class Sprite public attr_reader :position protected attr_writer :position [...] end
class Monster < Sprite [...] end
Lanciamo i test e tutti passano. In questo momento abbiamo tolto la duplicazione dalle classi sotto test, ma non da quelle di test.
Iniziamo a scrivere il test della classe Sprite:
class SpriteTest < Test::Unit::TestCase def setup @image = SurfaceMock.new([20, 20]) @position = Position.new(100, 200) @sprite = Sprite.new(@position, @image) end def test_draw destination_surface = SurfaceMock.new([500, 500]) @sprite.draw(destination_surface) assert_equal(destination_surface, @image.target) assert_equal([90, 190], @image.destination) end end
rendiamo quindi il membro @sprite accessibile sia in scrittura che in lettura dalle sottoclassi e ricordiamoci il principio di sostituzione di Liskov che dice che le sottoclassi non devono invalidare il contratto delle superclassi o, per dirlo in un altro modo, tutti i test validi per la superclasse devono esserlo anche per la sottoclasse. Per definire questa relazione nel programma è sufficiente far estendere ai test delle sottoclassi il test della superclasse; quindi ora modifichiamo MonsterTest e TowerTest, ricordandoci di richiamare sempre il setup e di usare le variabili position e image della superclasse, di assegnare a sprite l’elemento da testare e di cancellare i test duplicati:
class TowerTest < SpriteTest def setup super()
@tower = Tower.new(self.position, self.image) self.sprite = @tower end end
Lanciamo i test e vediamo una barra verde: i test della superclasse vengono lanciati perchè ereditati nella sottoclasse.
Una nota: Sprite e SpriteTest in Java o C# sarebbe state classi astratte, ma in Ruby questo concetto non esiste, per cui sono classi semplici.