棋盘和棋子之间相互依赖
关于相互依赖的问题有很多类似的问题,但每个问题都让我不确定自己的设计。
我正在写一个国际象棋程序来学习Scala。 董事会与其董事会之间的密切关系使我怀疑一个Piece对象是否应该包含对它所属的董事会的引用。 这是我用Java编写国际象棋程序时的做法。
但是,这意味着一块电路板只有在其部件才能完全定义,反之亦然。 如果董事会实例是一个变量,您可以在构建董事会时增加一些变量,但这种代码性不成问题,但这不符合不变性。
有关:
这里的方法似乎建议定义棋盘对象中棋子移动的所有规则:https://gamedev.stackexchange.com/questions/43681/how-to-avoid-circular-dependencies-between-player-and-world
这里表决得分最高的答案也有类似的建议:两个互相依赖的对象。 那不好吗?
上述链接的选择答案是不同的 - 它将相互依赖从类定义移动到接口。 我不明白为什么这样更好。
这里的设计反映了我目前的做法 :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
}
}
作为一个参数传递一块作品所属的板子会起作用,但感觉不对。
提前致谢!
我花了一些自由来重新设计你的课程。
我注意到的第一件事:你的Piece
并不是真正的作品。 假设左上角有一位白人主教。 如果我将它移动到右上角的领域,它会变成另一块吗? - 很明显不是。 因此,棋子在棋盘上的位置不是其身份的一部分。
所以我会重构类Piece
:
trait Piece {
def side:Side.Value
def piece_type:PieceType.Value
}
(我在这里使用了一个特征而不是抽象类,这样我就可以让实现者了解如何实现这两种方法。)
在这个过程中丢失的信息应该放在一个不同的类型中:
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
。)
如果你现在想要在棋子和棋盘的位置之间有依赖关系,你可以这样做:
PiecePlacement
: case class PiecePlacement(piece:Piece, row:Int, col:Int, board:Board) {...}
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
变量。 因此,整个董事会的状态可以表示为只有八个Int
,甚至可以是四个Long
。 这种优化可以平衡性能(位移),有利于内存使用。 因此,这种优化对于生成大量Board
实例并有陷入OutOfMemoryError
危险的算法会更好。
通过将板子和零件建模为特征而不是类,您可以轻松地交换实现,与它们一起玩耍,并查看哪种实现最适合您的使用情况 - 而无需更改一些利用该算法的算法板和件。
底线:仅在需要时才引入方法和变量等内容。 您不写的每行代码都是一行不能包含错误的代码。 不要担心对象之间的相互依赖关系,当它们不是绝对必要时。 而在棋盘模型的情况下,它们绝对没有必要。
首先关注简单。 每一种方法,类别和参数都应该证明它的存在。
例如,在我提出的模型中,位置总是有两个参数: row
和col
。 由于只有64个可能的职位,我会争辩做一个Position
类型。 例如,它甚至可以是将被编译为Int
的AnyVal
类型。 然后,你不需要嵌套结构来存储板。 您只能存储64个电路板放置信息对象,就是这样。
只介绍必要的最低限度,并在需要时延长。 在极端的情况下,只有在没有它们的情况下才能继续下去,从空的特质开始并添加方法。 当你处理它时,为每一种方法编写单元测试。 这样,你应该找到一个好的,干净的和可重用的解决方案。 可重用性的关键是:尽可能避免功能。 您引入的每个功能都会限制多功能性。 放入刚才所要求的内容。
链接地址: http://www.djcxy.com/p/51043.html