Reputation: 38531
I have written the following class and corresponding companion object:
class Tile(val tileCoordinate: Int, val pieceOnTile: Piece) {
override def toString(): String = {
if(isOccupied()) {
pieceOnTile.toString()
}
"-"
}
def isOccupied(): Boolean = pieceOnTile != null
}
object Tile {
def apply(coordinate: Int, piece: Piece): Tile = {
new Tile(coordinate, piece)
}
def apply(coordinate: Int): Tile = {
new Tile(coordinate, ??) // what is the best design here?An Option?
}
}
My question is, when the factory method to create a Tile
without a piece
on it is invoked, what is the appropriate argument to pass in to the Tile
constructor? I don't want to pass in null
, that seems like a poor design choice. Should I have the main constructor take an Option[Piece]
and pass in None
?
That seems sort of ugly too because when I want to create a tile I'd need to say:
val t = Tile(1, Some(new Knight()));
Upvotes: 0
Views: 1696
Reputation: 12104
Personally I would ditch the class
and companion object
completely, and instead go for a case class
:
case class Tile(tileCoordinate: Int, pieceOnTile: Option[Piece] = None) {
override def toString(): String = pieceOnTile.map(_.toString).getOrElse("-")
}
This allows you to later do the following when checking for values:
... match {
case Tile(c, Some(p)) => ...
case Tile(c, None) => ...
}
Having the second parameter default to None
allows you to call it as Tile(coord)
, Tile(coord, None)
and Tile(coord, Some(piece))
Upvotes: 4
Reputation: 368
Another alternative could be to define empty/occupied cases as separate case classes:
sealed abstract class Tile(val isOccupied: Boolean)
case object EmptyTile extends Tile(false) {
override def toString: String = "-"
}
case class OccupiedTile(piece: Piece) extends Tile(true) {
override def toString: String = piece.toString
}
You can use EmptyTile
, create OccupiedTile(piece)
and you can check a Tile either by the isOccupied
flag or by pattern matching.
One possible advantage of this approach is that if you add more methods that assume a contained piece, you will not have to add checks of occupiedness to each method because you could define them only in OccupiedTile
in the first place.
Upvotes: 2
Reputation: 1124
I would code the following:
class Tile(val tileCoordinate: Int, val pieceOnTile: Option[Piece]) {
// A little bit more scala-idiomatic, and a probable bug corrected
override def toString(): String = pieceOnTile.map(_.toString).getOrElse("-")
def isOccupied : Boolean = pieceOnTile.isDefined
}
object Tile {
def apply(coordinate: Int, piece: Piece): Tile = {
new Tile(coordinate, Some(piece))
}
def apply(coordinate: Int): Tile = {
new Tile(coordinate, None)
}
}
Upvotes: 3
Reputation: 55569
If pieceOnTile
is optional, then you should use Option
for sure. If you don't like the ugliness of having to wrap the Piece
s in Some
when creating new instances of Tile
, then hide it within apply
:
class Tile(val tileCoordinate: Int, val pieceOnTile: Option[Piece]) { ... }
object Tile {
def apply(coordinate: Int, piece: Piece): Tile = new Tile(coordinate, Some(piece))
def apply(coordinate: Int): Tile = new Tile(coordinate, None)
}
Then even isOccupied
will look more scala-like:
def isOccupied() : Boolean = pieceOnTile.isDefined
Upvotes: 2