关于相互依赖关系存在许多类似的问题,但每个问题都让我不确定自己的设计。
我正在写一个国际象棋程序来学习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
}
}
作为一个参数传递一块所属的板子会起作用,但感觉不对。
提前致谢!
答案 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
。)
如果您现在想要在片段和电路板的位置之间存在依赖关系,您可以这样做:
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
变量。因此,整个电路板状态可以表示为仅8 Int
s,甚至可以表示为4 Long
s。这种优化会牺牲性能(位移)来支持内存使用。因此,对于生成大量Board
个实例并且有获得OutOfMemoryError
的危险的算法,此优化会更好。
通过将电路板和片段建模为特征而不是类,您可以轻松地交换实现,使用它们,并查看哪种实现最适合您的用例 - 无需更改一些算法使用板材。
底线:仅在需要时引入方法和变量等内容。 您不编写的每行代码都是一行不能包含错误的代码。 当对象不是绝对必要时,不要担心对象之间的相互依赖性。在棋盘模型的情况下,它们绝对没有必要。
首先关注简单性。每个方法,类和参数都应该证明它的存在。
例如,在我提出的模型中,位置总是有两个参数:row
和col
。由于只有64个可能的位置,我认为可以建立Position
类型。例如,它甚至可以是AnyVal
类型,可以编译为Int
。然后,您不需要嵌套结构来存储电路板。您可以存储64个电路板放置信息对象,就是这样。
仅引入必要的最低要求,并在需要时进行扩展。在极端情况下,从空特征开始并仅在没有它们的情况下才能继续添加方法。当你在它的时候,为每一种方法编写单元测试。这样,您应该获得一个良好,干净和可重复使用的解决方案。可重用性的关键是:尽可能避免使用功能。您引入的每个功能都限制了多功能性。放入严格要求的内容。