oop -- 棋盘和棋子之间的相互依赖

oop -- mutual dependency between chessboard and its pieces

有很多关于相互依赖的类似问题,但每一个都让我不确定自己的设计。

我正在编写一个象棋程序来学习Scala。棋盘与其棋子之间的密切关系让我想知道棋子对象是否应该包含对其所属棋盘的引用。这是我在 Java 中编写国际象棋程序时的方法。

然而,这意味着一块板只有在它有它的部分时才会被完全定义,反之亦然。如果 Board 实例是一个变量,您可以在构建电路板时向其添加片段,则这种相互依赖性没有问题,但这违背了不变性。

相关:

此处的方法似乎建议在棋盘对象中定义棋子移动的所有规则: https://gamedev.stackexchange.com/questions/43681/how-to-avoid-circular-dependencies-between-player-and-world

这里投票最高的答案有类似的建议: Two objects with dependencies for each other. Is that bad?

上面 link 的选择答案不同——它将相互依赖从 class 定义转移到接口。我不明白为什么这样更好。

这里的设计反映了我目前的做法https://sourcemaking.com/refactoring/change-bidirectional-association-to-unidirectional

我的代码:

abstract class Piece(val side: Side.Value, val row: Int, val col: Int){
  val piece_type: PieceType.Value //isInstanceOf() could accomplish the same
  def possible_moves(board: Board): List[Move]
}

class Board (val pieces: Array[Array[Piece]]){
  def this(){
    this(DefaultBoard.setup) //An object which builds the starting board
  } 
}

必须将棋子所属的棋盘作为参数传递是可行的,但感觉不对。

提前致谢!

我冒昧地重新设计了您的 classes。

我注意到的第一件事:你的 Piece 并不是真正的一块。 假设左上角有一位白主教。 如果我把它移到右上角,它会变成另一块吗? - 明显不是。 因此,棋子在棋盘上的位置不是其身份的一部分。

所以我会将 class Piece 重构为:

trait Piece {
  def side:Side.Value
  def piece_type:PieceType.Value
}

(我在这里使用了特征而不是抽象 class,这样我就可以让实现者了解如何实现这两个方法。)

在这个过程中丢失的信息应该放在不同的类型中:

case class PiecePlacement(piece:Piece, row:Int, col:Int) {
  def possible_moves(board:Board):Seq[Move] = ??? // Why enfore a list here?
}

现在我们可以这样定义一个棋盘:

case class Board(pieces:IndexedSeq[IndexedSeq[Piece]] = DefaultBoard.setup)

(注意我是如何用默认参数值替换辅助构造函数的,并且还使用了不可变的 IndexedSeq 而不是可变的 Array。)

如果你现在还想在棋子的放置和棋盘之间建立依赖关系,你可以这样做:

  1. 将板添加到 PiecePlacement:

case class PiecePlacement(piece:Piece, row:Int, col:Int, board:Board) {...}

  1. 让看板创建放置实例:

case class Board(...) { def place(piece:Piece, row:Int, col:Int):(Board,PiecePlacement) = ??? }

注意place的return值return不仅是新的PiecePlacement实例,还有新的Board实例,因为我们想要使用不可变实例。

现在,如果你看这个,应该提出一个问题,为什么 place 甚至 return 是 PiecePlacement。调用者会从中得到什么好处?这几乎只是董事会内部信息。因此,您可能会重构 place 方法,使其 return 只是新的 Board。然后,您可以完全不使用 Placement 类型,从而消除相互依赖。

您可能要注意的另一件事是无法实现 place 方法。 returned Board 必须是新实例,但 returned PiecePlacement 必须包含新的 Board 实例。由于新的 Board 实例还包含 PiecePlacement 实例,因此永远无法以完全不可变的方式创建它。

所以我真的会听从@JörgWMittag 的建议并摆脱相互引用。开始为你的棋盘和棋子定义特征,并且只包含绝对最少的必要信息。例如:

trait Board {
  def at(row:Int, col:Int):Option[Piece]
  def withPieceAt(piece:Piece, row:Int, col:Int):Board
  def withoutPieceAt(row:Int, col:Int):Board
}

sealed trait Move
case class Movement(startRow:Int, startCol:Int, endRow:Int, endCol:Int) extends Move
case class Capture(startRow:Int, startCol:Int, endRow:Int, endCol:Int) extends Move

sealed trait PieceType {
  def possibleMoves(board:Board, row:Int, col:Int):Seq[Move]
}
object Pawn extends PieceType {...}
object Bishop extends PieceType {...}

sealed trait Piece {
  def side:Side.Value
  def pieceType:PieceType
}
case class WhitePiece(pieceType:PieceType) {
  def side:Side.White
}
case class BlackPiece(pieceType:PieceType) {
  def side:Side.Black
}

现在您可以开始编写代码,使用这些特征来推理潜在的移动等。 此外,您可以编写实现这些特征的 classes。 您可以从一个简单的实现开始,然后根据需要进行优化。

例如:每个棋盘位置只有13种可能的状态。每个棋子类型一个,两侧乘以两个,再加上空状态一个。这些状态是非常可枚举的,因此您可以通过枚举它们来优化。

另一个潜在的优化:由于棋盘位置只需要 4 位来建模,所以棋盘的一整行在编码时适合 Int 变量。因此,整个棋盘状态可以表示为八个 Int,甚至可以表示为四个 Long。这种优化会牺牲性能(移位)以支持内存使用。因此,这种优化对于生成大量 Board 实例并有获得 OutOfMemoryError.

的危险的算法会更好

通过将棋盘和棋子建模为特征而不是 classes,您可以轻松地交换实现,试用它们,并查看哪种实现最适合您的用例 - 而无需更改一些利用棋盘和棋子的算法。

底线:仅在需要时引入方法和变量等内容。 你不写的每一行代码都是不能包含错误的代码行。 当它们不是绝对必要的时候,不要担心对象之间的相互依赖。而在棋盘模型的情况下,它们绝对不是必需的。

以简洁为先。每一个方法,class 和参数都应该证明它的存在。

例如,在我提出的模型中,一个位置总是有两个参数:rowcol。因为只有 64 个可能的位置,所以我建议使用 Position 类型。例如,它甚至可以是将被编译为 IntAnyVal 类型。然后,您就不需要嵌套结构来存储电路板。您可以只存储 64 个电路板放置信息对象,仅此而已。

只引入必要的最低限度,并在需要时进行扩展。在极端情况下,从空特征开始,只有当你真的不能没有它们时才添加方法。当你在做的时候,为每一个方法编写单元测试。这样,您应该得到一个好的、干净的和可重用的解决方案。可重用性的关键是:尽可能避免使用特性。您引入的每一个功能都会限制多功能性。只输入严格要求的内容。