Summa sidvisningar

fredag 31 december 2010

Dela upp klassen Board

Vill börja med en liten rättning från föregående inlägg. Där skrev jag att this metoder i sin första sats måste anropa huvudkonstruktorn, det får den göra, men den kan lika gärna anropa en annan this medod!

När jag tittade på resultatet av klassen Board inser jag att den har väldigt mycket hantering av enskilda rader i det normalt 10x20 stora griden. Hanteringen kommer att utökas ytterligare när vi inför metoden clearLines. Inser att det skulle vara mycket bättre att skapa en egen klass av varje rad för att kapsla in den hanteringen och samtidigt få klassen Board mer läsbar.

Gör samtidigt en annan förändring då jag byter ut metoden print mot asText. Metoden print har sidoeffekter vilket ibland kan vara nödvändigt men i det här fallet är det bättre att returnera en sträng. Det har också den fördelen att vi kan testa metoden! Vårat test av klassen BoardLine ser ut enligt följande:

package net.sf.tetrisai

import org.junit.Test
import org.scalatest.junit.{JUnitSuite, ShouldMatchersForJUnit, AssertionsForJUnit}

class BoardLineTest extends JUnitSuite with ShouldMatchersForJUnit {

  @Test def asText() {
    val boardLine = BoardLine("#---x-x-x--#")

    boardLine.asText should be ("#---x-x-x--#")
  }
}

Syntaxen should be är i mitt tycke ett bra exempel på hur man kan skriva läsbar kod i Scala. Dock är det mycket magi när man kommer som ny Javautvecklare. Bra inlägg här som beskriver magin bakom denna syntax.

Vår nya klass BoardLine blev så här:
package net.sf.tetrisai

object BoardLine {
  val Empty: Byte = 0
  val Occupied: Byte = 1
  val Characters = Array("-", "x")

  def apply(textLine: String) = {
    val width = textLine.length - 2

    val line: Array[Byte] = Array.tabulate(width) (
        ((x) => (if (textLine(x+1) == 'x') Occupied else Empty))
    )
    new BoardLine(line)
  }
}

/**
 * Represents a row in the game board.
 */
class BoardLine(line: Array[Byte]) {
  require(line.length >= 4)

  def asText() = "#" + (line.foldLeft("") { (text, n) => text + BoardLine.Characters(n) }) + "#"
}

Rad 24 kräver en förklaring. Det denna kodrad gör är att loopa arrayen line som består av Bytes (som troligen kompileras till vanliga 8-bitarstal av Scala-kompilatorn) och gör om värdena till "-" eller"x" plus lägger till "#" i början och slutet. Här har jag hittat den fiffiga metoden foldLeft där man ger ett startvärde som första argument ("" i vårat fall) följt av "(text, n)" där text representerar resultatet (med "" som ingångsvärde) och n representerar aktuellt värde i arrayen för index 0 till och med 9 vilket är värdena line(0), line(1)...line(9). Från arrayen [0,0,0,1,0,1,0,1,0,0] beräknas resultatet "#---x-x-x--#".

Dags att skriva testet för klassen Board där vi dels testar att brädet inte är mindre än 4x4 och dels testar metoden asText:
package net.sf.tetrisai

import org.junit.Test
import org.scalatest.junit.{JUnitSuite, ShouldMatchersForJUnit, AssertionsForJUnit}

class BoardTest extends JUnitSuite with ShouldMatchersForJUnit {

  @Test def tooLow() {
    evaluating {
      Board(Array(
        "#----------#",
        "#----x-----#",
        "#-x--x----x#",
        "############"))
    } should produce[IllegalArgumentException]
  }

  @Test def tooNarrow() {
    evaluating {
      Board(Array(
        "#---#",
        "#---#",
        "#---#",
        "#-x-#",
        "#####"))
    } should produce[IllegalArgumentException]
  }

  @Test def asText() {
    val boardArray = Array(
      "#----------#",
      "#----------#",
      "#----x-----#",
      "#-x--x----x#",
      "############")

    val board = Board(boardArray)

    board.asText should be (boardArray.mkString("\n"))
  }
}

De första två testerna tooLow och tooNarrow använder sig av konstruktionen evaluating { ... } should produce[Exception] som förklaras här. Konstruktionen är rätt självförklarande men går i kort ut på att vi kan specificera vilken exception som vi förväntar oss i det omslutande kodblocket.

För att kunna använda formatet shuld be i testet asText måste vi ärva från ShouldMatchersForJUnit. Metoden mkString på rad 39 fungerar så att den skapar en ny sträng genom att lägga in radmatning ("\n") mellan alla element i boardArray.

Efter att ha refaktorerat ut kod till BoardLine ser våran Board ut så här:
package net.sf.tetrisai

object Board {
  def apply(lines: Array[String]) = {
    val width = lines(0).length - 2
    val height = lines.length - 1

    require(lines(height) == bottomTextLine(width))

    val boardLines: Array[BoardLine] = Array.tabulate(height) (
      ((y) => (BoardLine(lines(y))))
    )
    new Board(width, height, boardLines)
  }

  def bottomTextLine(width: Int) = "#" * (width + 2)
}

/**
 * Represents the game board.
 * Standard size is 10x20 (width x height).
 */
class Board(width: Int, height: Int, lines: Array[BoardLine]) {
  require(height >= 4)

  def bottomTextLine = Board.bottomTextLine(width)
  def asText() = lines.map { _.asText }.mkString("\n") + "\n" + bottomTextLine
}

Det stora som skett här är att klassen nu lagrar en array med BoardLine i stället för array med array av Byte. Metoden asText på rad 27 använder sig av metoden map. Den fungerar i vårat fall så att den skapar en ny array utifrån arrayen lines där den aplicerar metoden asText på varje element.

Inga kommentarer:

Skicka en kommentar