Summa sidvisningar

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!

Inga kommentarer:

Skicka en kommentar