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.

söndag 26 december 2010

En första version av klassen Board

Mina förberedelser innan jag satte igång att koda Scala var att jag införskaffade boken Programming in Scala som nu även finns i en 2nd edition (som PDF, boken kommer i början av 2011) plus att jag läst lite bloggar då och då i ett par månaders tid. Jag hann läsa de första två hundra sidorna innan jag insåg att det var dags att praktisera. Bra ändå att skaffa sig en första överblick av språket. Scala är onekligen ett kraftfullt språk och detta är bara början på en spännande resa. Steget från Java till Scala känns ungefär lika stort som att gå från C till Java!

Till att börja med behövde jag en utvecklingsmiljö och då föll valet på IntelliJ IDEA som nu kommit i version 10 och som enligt rykte ska vara den bästa IDE:n för Scala för tillfället. Efter att ha installerat verktyget och pluginen för Scala var det dags att börja skriva min första klass. Som den testdrivne utvecklare jag försöker vara tänker jag använda mig av ScalaTest som jag hört ska vara bra. Programmet som ska "konverteras" hette i C++ versionen TetrisAnalyzer som skrevs mellan åren 2001 och 2002 och dels en halvfärdig omskrivning till Java som jag lagt på SourceForge med namnet TetrisAi där numera även Scala-version ligger. Då Java-versionen använde sig av Maven väljer jag här att fortsätta med det och hoppas att det är så man gör i Scala-värden!

Bestämmer mig för att börja med att skriva testet för klassen Board. Då jag redan har en halvfärdig version i Java kan jag utgå från den koden som ser ut enligt följande:

package net.sf.tetrisai;

import static org.junit.Assert.assertEquals;

import org.junit.Test;

public class BoardTest {

    @Test
    public void clearLines() {
        Board board = new Board(
            "#----------#",
            "#----x-----#",
            "#xxxxxxxxxx#",
            "#-x--x----x#",
            "#xxxxxxxxxx#",
            "############");

        board.clearLines(1, 4);
  
        Board expectedBoard = new Board(
            "#----------#",
            "#----------#",
            "#----------#",
            "#----x-----#",
            "#-x--x----x#",
            "############");

        assertEquals(expectedBoard, board);
    }
}

Innan jag börjar med att implementera metoden clearLines vill jag få ordning på den interna lagringen av min Board. Tänker därför tills vidare köra metoden print från mitt test för att se att den skriver ut samma possition som jag skickar in. Testet av metoden clearLines fortsätter jag med vid nästa blogginlägg!

Så här ser det halvfärdiga testet ut som just nu saknar en assert:

package net.sf.tetrisai

import org.scalatest.junit.AssertionsForJUnit
import org.junit.Test

class BoardTest extends AssertionsForJUnit {

  @Test def clearLines() {
    val board = Board(Array(
      "#----------#",
      "#----x-----#",
      "#xxxxxxxxxx#",
      "#-x--x----x#",
      "#xxxxxxxxxx#",
      "############")
    )

    board.print()
  }
}

I Java-koden kunde man skicka in en array direkt i konstruktorn, men vad jag har sett hittils måste man i Scala ange att det är en array (rad 9). Om någon läsare av denna blogg har en bättre lösning vill jag gärna att ni lägger in kommentarer!

Våran Java-version av klassen Board har två konstruktorer (se listning nedan), en som används av "den vanlig koden" (rad 10) och en som används av testerna (rad 20), fullständiga koden hittar du här:

...

public class Board {
    private int width;
    private int height;
    private int[][] grid;

    private static int DEFAULT_DOT = 1;

    public Board(int width, int height) {
        if (width < 4) {
            throw new IllegalStateException("Illegal size of board, minimum with is 4.");
        }

        this.width = width;
        this.height = height;
        grid = new int[height][width];
    }

    public Board(String... boardRows) {
        this(boardRows[0].length() - 2, boardRows.length - 1);

        for (int y = 0; y < height; y++) {
            String row = boardRows[y];

            for (int x = 1; x <= width; x++) {
                if (!row.substring(x, x + 1).equals("-")) {
                    grid[y][x - 1] = DEFAULT_DOT;
                }
            }
        }
    }

   ...
}
Vad jag märkte när jag skulle skriva om detta till Skala var att Scala hanterar konstruktorer annorlunda än t ex Java. Scala har en huvudkonstruktor. Om man vill ha fler konstruktorer får man lägga till this metoder som i sin första sats måste anropa huvudkonstruktorn, se Multiple Constructors. Jag testade göra på detta vis först men stötte då på problemet med att jag ville göra saker med de inkomna argumenten (en array med String i vårat fall) innan huvudkonstruktorn anropades. Lösningen är att utnyttja att varje klass i Scala även får ha en kompis (object) med samma namn som klassen och som hanterar all statisk information. Detta object lägger man i samma fil som den vanliga klassen och genom att definiera apply metoder (en i vårat fall) kan man köra dessa genom att bara ange klassens namn utan att använda sig av new syntaxen. Hade vi haft en this metod i klassen Board kunde vi ha skrivit (strippad version)
val board = new Board(Array("#----------#",...))
men nu kan vi skriva (strippad version):
val board = Board(Array("#----------#",...))
När jag implementerar Scala-version av Board blir resultatet detta, notera att sådant som i Java skulle ha varit static här ligger i våran object, raderna 3-26 och själva klassen definieras från och med rad 31:
package net.sf.tetrisai

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

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

    require(lines(height) == ("#" * (width + 2)))

    val board: Array[Array[Byte]] = Array.tabulate(height, width) (
        ((y, x) => (getBoardValue(x, y, width, height, lines)))
    )
    new Board(width, height, board)
  }

  def getBoardValue(x: Int, y: Int, width: Int, height: Int, lines: Array[String]):Byte = {
    require(lines(y).length - 2 == width)

    if (lines(y)(x+1) == 'x') Occupied
    else Empty
  }
}

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

  def print() {
    for (y <- 0 to height-1) {
      Predef.print("#")
      for (x <- 0 to width-1)
        Predef.print(Board.Characters(board(y)(x)))
      Predef.println("#")
    }
    Predef.println("#" * (width + 2))
  }
}
Värt att notera:
  • Konstanter placeras i objektet. Namngivningsstandarden är att en konstant ska börja med stor bokstav och vara i CamelCase (rad 4-6)
  • ScalaTest använder sig av nyckelordet require i stället för som i Java assert (rad 12)
  • Operatorn == motsvaras av metoden equals i Java, vill man jämföra objektreferenser får man använda operatorn === (rad 12)
  • Det går att repetera en sträng x antal gånger genom att t ex skriva "#" * x (rad 12) snyggt!
  • På rad 14 använder jag mig av en mycket vacker konstruktion som finns i Scala nämligen möjligheten att skicka funktioner som argument. Här anropar jag metoden tabulate med storleken på arrayen följt av variablerna y, x följt av värdet för varje kombination av y, x vilket i detta fallet är resultatet av metoden getBoardValue. På detta sätt har vi här trollat bort en nästad loop som vi annars hade behövt göra och på köpet fått koden mer läsbar!
  • I Scala kan man utelämna nyckelordet return om värdet man vill returnera är det sista som görs i metoden (rad 17)
  • På rad 34 har jag definierat metoden print. Då jag har definierat en lokal version av print utan parametrar, måste jag hårt ange på rad 36, 38, 39 och 41 att det är systemmetoden print som jag vill köra och den ligger i objektet Predef som alltid finns importerad i Scala och gör att man normalt kan skriva print i stället för som här Predef.print
  • Jag har kämpat med att försöka skriva den nästade loopen på rad 35-40 på ett funktionellt sätt, men gav upp. Grejen här var att jag ville göra något en gång i början och slutet av varje rad (rad 33 och 40) och det hade jag problem med att få till. Kan någon se en bättre lösning så kom med förslag! Denna version är ändå i mitt tycke ganska läsbar.
  • En tumregel är att man ska deklarera "magic values" som konstanter vilket jag gjort på rad 4-5. Dock har jag (till vidare) valt att inte definiera strängen "#" som en konstant, detta för att höja läsbarheten av koden vilket jag anser väger över i detta fallet!
Våra två klasser Board och BoardTest ser för närvarande ut så här i vårat repository.

Det var allt för denna gång!