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

时间:2015-08-19 15:30:43

标签: scala oop dependencies

关于相互依赖关系存在许多类似的问题,但每个问题都让我不确定自己的设计。

我正在写一个国际象棋程序来学习Scala。板和它之间的密切关系让我想知道一个Piece对象是否应该包含对它所属的板的引用。当我用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?

上述链接的选定答案是不同的 - 它将相互依赖从类定义移动到接口。我不明白为什么会更好。

这里的设计反映了我目前的做法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
  } 
}

作为一个参数传递一块所属的板子会起作用,但感觉不对。

提前致谢!

1 个答案:

答案 0 :(得分:0)

我冒昧地重新设计你的课程。

我注意到的第一件事:你的Piece并不是真的。 说左上方有一个白色的主教。 如果我将它移动到右上角的字段,它会变成另一个片段吗? - 显然不是。 因此,棋盘上棋子的位置不是其身份的一部分。

所以我会将类Piece重构为:

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

(我在这里使用了一个trait而不是一个抽象类,这样我就可以让实现者知道如何实现这两个方法。)

过程中丢失的信息应该换成不同的类型:

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
  2. case class PiecePlacement(piece:Piece, row:Int, col:Int, board:Board) {...}

    1. 让棋盘创建展示位置实例:
    2. case class Board(...) { def place(piece:Piece, row:Int, col:Int):(Board,PiecePlacement) = ??? }

      请注意,place的返回值不仅返回新的PiecePlacement实例,还返回新的Board实例,因为我们希望使用不可变实例。

      现在,如果你看一下这个问题,应该提出一个问题:place甚至返回PiecePlacement。呼叫者有什么好处呢?这几乎只是董事会内部信息。因此,您可能会重构place方法,使其仅返回新的Board。然后,你可以完全没有Placement类型,从而消除相互依赖。

      您可能需要注意的另一件事是place方法无法实现。返回的Board必须是新实例,但返回的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
      }
      

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

      例如:每个董事会职位只有13种可能的状态。每个棋子类型一个,双方为两个,加上一个为空状态。这些状态是非常可枚举的,因此您可以通过枚举它们进行优化。

      另一个潜在的优化:由于电路板位置仅需要4位进行建模,因此在编码时,电路板的一整行都适合Int变量。因此,整个电路板状态可以表示为仅8 Int s,甚至可以表示为4 Long s。这种优化会牺牲性能(位移)来支持内存使用。因此,对于生成大量Board个实例并且有获得OutOfMemoryError的危险的算法,此优化会更好。

      通过将电路板和片段建模为特征而不是类,您可以轻松地交换实现,使用它们,并查看哪种实现最适合您的用例 - 无需更改一些算法使用板材。

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

      首先关注简单性。每个方法,类和参数都应该证明它的存在。

      例如,在我提出的模型中,位置总是有两个参数:rowcol。由于只有64个可能的位置,我认为可以建立Position类型。例如,它甚至可以是AnyVal类型,可以编译为Int。然后,您不需要嵌套结构来存储电路板。您可以存储64个电路板放置信息对象,就是这样。

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