在OOP中似乎需要循环引用的现实活动建模的正确方法是什么?


24

我一直在努力解决Java项目中有关循环引用的问题。我正在尝试对现实世界中的情况进行建模,在这种情况下,所讨论的对象似乎是相互依赖的,并且需要彼此了解。

该项目是玩棋盘游戏的通用模型。基本类是非特定类,但已扩展为处理国际象棋,西洋双陆棋和其他游戏的特定类。11年前,我用六种不同的游戏将其编码为applet,但问题是它充满了循环引用。那时,我通过将所有相互交织的类填充到一个源文件中来实现它,但是我意识到这在Java中是不好的形式。现在,我想实现与Android应用类似的功能,并且我想正确地执行操作。

这些类是:

  • RuleBook:可以针对诸如棋盘的初始布局,其他初始游戏状态信息(例如谁先移动,可用的移动,可用的移动,提议的移动后游戏状态发生了什么)等问题进行询问的对象。当前或提议的董事会职位。

  • 棋盘:游戏棋盘的简单表示,可以指示其反映移动。

  • MoveList:移动列表。这是双重目的的:在给定的位置选择可用的动作,或者在游戏中进行的动作列表。它可以分为两个几乎相同的类,但这与我要问的问题无关,并且可能会使它进一步复杂化。

  • 搬家:一招。它以原子列表的形式包含了有关移动的所有内容:从这里拿起一块,放到那里,从那里取出被捕获的一块。

  • 状态:正在进行的游戏的完整状态信息。不仅是董事会的位置,而且还有MoveList以及其他状态信息,例如现在要移动的人。在国际象棋中,将记录每个棋手的国王和乌鸦是否已移动。

例如,循环引用比比皆是:RuleBook需要了解游戏状态以确定给定时间可用的棋步,但是游戏州需要查询RuleBook的初始开始布局以及一次棋步会带来哪些副作用它是制造出来的(例如,下一个移动者)。

我尝试分层组织新的类集,其中RuleBook位于顶部,因为它需要了解所有信息。但这导致必须将许多方法移至RuleBook类中(例如进行移动),从而使其成为整体,并不能特别代表RuleBook应该是什么。

那么组织此活动的正确方法是什么?我应该将RuleBook变成BigClassThatDoesAlmostEverythingInTheGame以避免循环引用,而放弃对真实游戏进行精确建模的尝试吗?还是应该坚持使用相互依赖的类并哄骗编译器以某种方式对其进行编译,同时保留我的实际模型?还是我缺少一些明显的有效结构?

谢谢你提供的所有帮助!


7
如果将RuleBook例如State作为参数,并返回有效值MoveList,即“我们现在在这里,下一步可以做什么?”,该怎么办?
jonrsharpe 2015年

@jonrsharpe说了什么。在玩真正的棋盘游戏时,规则书也不知道正在玩任何实际的游戏。我什至可能会引入另一个类来实际计算移动,但这可能取决于RuleBook类的大小。
塞巴斯蒂安·范登布罗克

4
避免使用上帝对象(BigClassThatDoesAlmostEverythingInTheGame)比避免使用循环引用更为重要。
user281377

2
@ user281377不一定是互斥的目标!
jonrsharpe 2015年

1
您可以显示建模尝试吗?例如图?
用户

Answers:


47

我一直在努力解决Java项目中有关循环引用的问题。

Java的垃圾收集器不依赖于引用计数技术。循环引用在Java中不会引起任何问题。在Java中消除完美自然的循环引用所花费的时间是浪费时间。

我对此进行了编码,但问题是它充满了循环引用。那时,我通过将所有相互交织的类填充到一个源文件中来实现它,[...]

不必要。如果仅一次编译所有源文件(例如javac *.java),则编译器将解析所有正向引用而不会出现问题。

还是应该坚持使用相互依赖的类,并哄骗编译器以某种方式对其进行编译,[...]

是。应用程序类应相互依赖。一次编译属于同一软件包的所有Java源文件并不是一个聪明的技巧,这恰恰是Java 应该工作的方式。


24
“循环引用不会在Java中引起任何类型的问题。” 就编译而言,这是正确的。但是,循环引用被认为是不良设计
印章

22
循环引用在许多情况下是很自然的,这就是Java和其他现代语言使用复杂的垃圾收集器而不是简单的引用计数器的原因。
user281377

3
Java能够解析循环引用非常棒,并且在许多情况下它们都是自然的,这是绝对正确的。但是OP提出了一种特殊的情况,应该加以考虑。纠结的意大利面条代码可能不是解决此问题的最佳方法。

3
请不要散布无关的编程语言的未经证实的FUD。Python自古以来就一直支持参考循环的GC(docs,也适用于SO:herehere)。
Christian Aichinger 2015年

2
恕我直言,这个答案只是平庸的,因为关于循环引用,没有一个词对OP有用。
布朗

22

当然,从设​​计的角度来看,循环依赖是一种有问题的做法,但并不是禁止的,从纯粹的技术角度来看,它们甚至不一定有问题,就像您认为它们是合法的那样:在大多数情况下,它们在某些情况下是不可避免的,在极少数情况下,甚至可以将它们视为有用的东西。

实际上,很少有Java编译器会拒绝循环依赖的情况。(注意:可能还有更多,我现在只能想到以下内容。)

  1. 在继承中:您不能拥有A类扩展类,而B类又扩展了B类,并且完全不能这样做,因为从逻辑的角度来看,这种选择绝对没有任何意义。

  2. 在方法局部类中:方法内声明的类可能不会相互循环引用。这可能只是Java编译器的局限性,可能是因为执行此操作的能力不足以证明编译器支持该编译器需要额外的复杂性。(大多数Java程序员甚至都不知道您可以在方法中声明一个类,更不用说声明多个类,然后让这些类相互循环引用了。)

因此,重要的是要认识到并消除它,即最小化循环依赖性的追求是对设计纯度的追求,而不是对技术正确性的追求。

据我所知,没有消除循环依赖关系的简化论方法,这意味着除了简单的预定“无脑”步骤外,没有其他方法可以组成带有循环引用的系统,一个接一个地应用它们,然后结束。没有循环引用的系统。您必须全神贯注地工作,并且必须执行依赖于设计性质的重构步骤。

在您遇到的特定情况下,在我看来,您需要的是一个新的实体,也许称为“游戏”或“ GameLogic”,它了解所有其他实体(而其他任何实体都不知道, ),这样其他实体就不必彼此了解。

例如,在我看来,您的RuleBook实体需要了解GameState实体的任何知识都是不合理的,因为规则书是我们为玩游戏而要咨询的东西,而不是积极参与游戏的东西。因此,正是这个新的“游戏”实体需要同时查阅规则书和游戏状态,才能确定哪些移动可用,并且消除了循环依赖。

现在,我想我可以猜出这种方法会带来什么问题:以一种与游戏无关的方式编码“ Game”实体会非常困难,因此您很可能最终不仅会遇到一个问题,而且还会遇到两个问题每个不同类型的游戏都需要具有定制实现的实体:“ RuleBook”和“ Game”实体。反过来,这不利于拥有“ RuleBook”实体的目的。好吧,我对此只能说的是,也许您可​​能最初只是想编写一个可以玩许多不同类型游戏的系统,但是想法很不理想。如果我不知所措,我会专注于使用一种通用的机制来显示所有不同游戏的状态,以及一种通用的机制来接收所有这些游戏的用户输入,


1
谢谢迈克。您对Game实体的弊端是正确的;使用旧的applet代码,我能够用新的RuleBook子类和适当的图形设计来绘制新游戏。
Damian Walker

10

博弈论将游戏视为先前动作(值类型,包括玩过的人)和函数ValidMoves(previousMoves)的列表

我会在游戏的非UI部分尝试遵循这种模式,并将诸如棋盘设置之类的东西视为动作。

然后,UI可以是标准的OO内容,并通过一种方式引用逻辑


更新以压缩评论

考虑一下国际象棋。象棋游戏通常记录为动作列表。http://en.wikipedia.org/wiki/Portable_Game_Notation

动作列表定义游戏的完整状态远胜于棋盘画面。

例如,我们开始为Board,Piece,Move等以及诸如Piece.GetValidMoves()之类的方法制作对象

首先,我们看到我们必须参考董事会,然后再考虑铸造。仅当您尚未移动国王或新手时才可以这样做。因此,我们需要在国王和车手上使用MovedAlready标志。同样,典当在其第一步中可以移动2个正方形。

然后我们看到,在对国王进行有效的举动时取决于国王的存在和状态,因此董事会需要在上面加上部分并参考这些部分。我们正在处理您的循环引用问题。

但是,如果我们将Move定义为一个不变的结构,并且将游戏状态定义为先前的动作列表,则这些问题将消失。要查看是否有效,我们可以检查城堡和国王移动存在的移动列表。要查看该pawn是否可以被传入,我们可以检查另一个pawn是否在此之前进行了两次移动。除了“规则”->“移动”外,无需引用

现在,国际象棋有一个固定的棋盘,并且总是以相同的方式来设置这些棋子。但可以说我们有一个变体,允许我们进行替代设置。也许忽略了一些障碍。

如果我们将设置动作添加为“从框到X的移动”并调整“规则”对象以理解该动作,那么我们仍然可以将游戏表示为一系列动作。

类似地,如果在您的游戏中棋盘本身不是静态的,则说我们可以在棋盘上添加正方形或从棋盘上删除正方形,以使它们不能移动。这些更改也可以表示为“移动”,而无需更改规则引擎的整体结构或不必引用类似的BoardSetup对象


这将使ValidMoves的实现复杂化,这将降低您的逻辑速度。
塔米尔(Taemyr),2015年

不完全是,我假设电路板的设置是可变的,所以您需要以某种方式对其进行定义。如果将设置移动转换为其他结构或对象以帮助计算,则可以根据需要缓存结果。一些游戏的棋盘会随着比赛的进行而变化,某些有效的移动可能取决于之前的移动,而不是当前位置(例如,国际象棋的cast击)
Ewan,

1
仅通过移动历史记录就可以避免添加标志和填充物的复杂性。循环说100下棋来获得当前的棋盘设置并不昂贵,您可以
Ewan,2015年

1
您还避免更改对象模型以反映规则。即对于国际象棋,如果您使validMoves-> Piece + Board,您将无法进行掷骰,传递,典当和棋子提升的第一步,并且必须向对象添加额外的信息或引用第三个对象。您还会迷失方向,去发现诸如支票的概念
Ewan,2015年

1
@Gabe boardLayout是所有人的函数priorMoves(即,如果我们确实将其维护为状态,则除了每个之外,没有其他贡献thisMove)。因此,Ewan的建议实质上是“削减中间人”-有效的举动是所有先验的直接作用,而不是validMoves( boardLayout( priorMoves ) )
OJFord

8

在面向对象的编程中,删除两个类之间的循环引用的标准方法是引入一个接口,然后可以由其中一个实现。因此,在您的情况下,您可能RuleBook要先引用State,然后再引用InitialPositionProvider(这将是由实现的接口RuleBook)。这也使测试变得更容易,因为您可以创建一个State使用不同(可能更简单)初始位置进行测试的。


6

我相信,通过将游戏流程的控制与游戏的状态和规则模型分开,可以轻松地删除圆形引用和您所关注的上帝对象。这样,您可能会获得很大的灵活性并摆脱不必要的复杂性。

我认为您应该有一个控制游戏流程并处理实际状态更改的控制器(如果需要,可以是“游戏大师”),而不是赋予规则书或游戏状态此责任。

游戏状态对象不需要更改自身或了解规则。该类只需要提供易于处理(创建,检查,更改,持久,记录,复制,缓存等)的模型,并为其余应用程序提供有效的游戏状态对象。

规则书不需要了解或玩弄任何正在进行的游戏。它只需要查看游戏状态就可以判断哪些移动是合法的,并且只需要在被问到将移动应用于游戏状态时会发生什么时回答结果的游戏状态即可。当要求初始布局时,它还可以提供游戏开始状态。

控制器必须知道游戏状态,规则簿以及游戏模型的其他一些对象,但不必弄乱细节。


4
完全是我的想法。OP 在同一类中混合了太多的数据过程。最好将它们分开。是一个很好的话题。顺便说一句,当我阅读“查看游戏状态”时,我认为是“对该功能的争论”。如果可以的话,+ 100。
jpmc26

5

我认为这里的问题是您没有清楚地说明哪些类将处理哪些任务。我将描述我认为是每个类应该做什么的很好的描述,然后我将给出一个通用代码示例来说明这些想法。我们将看到代码的耦合较少,因此它实际上没有循环引用。

让我们从描述每个类的功能开始。

GameState类应该只包含关于游戏的当前状态信息。它不应包含有关游戏的过去状态或将来可能发生的动作的任何信息。它只应包含有关棋盘上哪些棋子上的棋子,西洋双陆棋中哪些棋子​​上的棋子数和类型的信息。该GameState会必须包含一些额外的信息,如关于在国际象棋或关于步步高加倍的立方体易位信息。

Move班是有点棘手。我要说的是,我可以指定要播放的动作,方法是指定该动作GameState所产生的结果。因此,您可以想象一下,仅可将举动实施为GameState。但是,例如在go中,您可以想象通过指定板上的单个点来指定移动要容易得多。我们希望我们的Move班级足够灵活以处理这两种情况。因此,Move该类实际上将成为具有方法的接口,该方法需要执行pre-move GameState并返回新的post-move GameState

现在,RuleBook班级负责了解有关规则的所有信息。这可以分为三部分。它需要知道最初的名字GameState,它需要知道哪些动作是合法的,并且需要能够判断其中一名玩家是否赢了。

您也可以创建一个GameHistory班级来跟踪所有已执行的动作以及GameStates已发生的所有动作。需要一个新的类,因为我们认为一个GameState人不应该负责了解所有GameState先于它的。

这将结束我将讨论的类/接口。您也有一Board堂课。但是我认为不同游戏中的棋盘差异很大,很难看到棋盘通常可以做什么。现在,我将继续给出通用接口并实现通用类。

首先是GameState。由于此类完全取决于特定的游戏,因此没有通用的Gamestate接口或类别。

接下来是Move。如我所说,这可以用一个接口来表示,该接口具有一个采用移动前状态并产生移动后状态的方法。这是此接口的代码:

package boardgame;

/**
 *
 * @param <T> The type of GameState
 */
public interface Move<T> {

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

注意,有一个类型参数。这是因为,例如,a ChessMove将需要了解预移动的详细信息ChessGameState。因此,例如,类声明ChessMove

class ChessMove extends Move<ChessGameState>

您已经定义了一个ChessGameState类的地方。

接下来,我将讨论泛型RuleBook类。这是代码:

package boardgame;

import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public interface RuleBook<T> {

    T makeInitialState();

    List<Move<T>> makeMoveList(T gameState);

    StateEvaluation evaluateState(T gameState);

    boolean isMoveLegal(Move<T> move, T currentState);

}

同样,GameState该类还有一个类型参数。由于RuleBook应该知道初始状态是什么,因此我们放置了一种给出初始状态的方法。由于RuleBook应该知道哪些举动是合法的,因此我们有方法来测试某举动在给定状态下是否合法,并给出给定状态的合法举动列表。最后,有一种方法可以评估GameState。请注意,RuleBook只能负责描述一个或其他玩家是否已经赢得比赛,而不是谁在比赛中处于更好位置。决定谁的位置更好是一件复杂的事情,应该移入自己的班级。因此,StateEvaluation该类实际上只是一个简单的枚举,如下所示:

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

最后,让我们描述一下GameHistory课程。该课程负责记住游戏中到达的所有位置以及所进行的移动。它应该能够做的主要事情是录制一个Move演奏过的音乐。您还可以添加用于撤消Move的功能。我在下面有一个实现。

package boardgame;

import java.util.ArrayList;
import java.util.List;

/**
 *
 * @param <T> The type of GameState
 */
public class GameHistory<T> {

    private List<T> states;
    private List<Move<T>> moves;

    public GameHistory(T initialState) {
        states = new ArrayList<>();
        states.add(initialState);
        moves = new ArrayList<>();
    }

    void recordMove(Move<T> move) throws IllegalArgumentException {
        moves.add(move);
        states.add(move.makeResultingState(getMostRecentState()));
    }

    void resetToNthState(int n) {
        states = states.subList(0, n + 1);
        moves = moves.subList(0, n);
    }

    void undoLastMove() {
        resetToNthState(getNumberOfMoves() - 1);
    }

    T getMostRecentState() {
        return states.get(getNumberOfMoves());
    }

    T getStateAfterNthMove(int n) {
        return states.get(n + 1);
    }

    Move<T> getNthMove(int n) {
        return moves.get(n);
    }

    int getNumberOfMoves() {
        return moves.size();
    }

}

最后,我们可以想象创建一个Game将所有内容捆绑在一起的课程。这个Game类应该公开,使人们可以看到当前什么样的方法GameState是,看看谁,如果任何人有一个,看看有什么动作可以玩,玩一招。我在下面有一个实现

package boardgame;

import java.util.List;

/**
 *
 * @author brian
 * @param <T> The type of GameState
 */
public class Game<T> {

    GameHistory<T> gameHistory;
    RuleBook<T> ruleBook;

    public Game(RuleBook<T> ruleBook) {
        this.ruleBook = ruleBook;
        final T initialState = ruleBook.makeInitialState();
        gameHistory = new GameHistory<>(initialState);
    }

    T getCurrentState() {
        return gameHistory.getMostRecentState();
    }

    List<Move<T>> getLegalMoves() {
        return ruleBook.makeMoveList(getCurrentState());
    }

    void doMove(Move<T> move) throws IllegalArgumentException {
        if (!ruleBook.isMoveLegal(move, getCurrentState())) {
            throw new IllegalArgumentException("Move is not legal in this position");
        }
        gameHistory.recordMove(move);
    }

    void undoMove() {
        gameHistory.undoLastMove();
    }

    StateEvaluation evaluateState() {
        return ruleBook.evaluateState(getCurrentState());
    }

}

注意,在此类中,RuleBook并不负责了解电流GameState是什么。那是GameHistory工作。因此,Game询问GameHistory当前状态是什么,并将此信息提供给RuleBook何时Game需要说明法律动作是什么或是否有人赢得了。

无论如何,此答案的要点是,一旦您合理地确定了每个班级的职责,并且使每个班级专注于少量职责,然后将每个职责分配给一个唯一的班级,则这些班级往往会解耦,所有内容都易于编写。希望从我给出的代码示例中可以明显看出这一点。


3

以我的经验,循环引用通常表明您的设计不是经过深思熟虑的。

在您的设计中,我不明白为什么RuleBook需要“了解”状态。当然,它可能会接收到State作为某些方法的参数,但是为什么它需要知道(例如,作为实例变量)对State的引用?这对我来说没有意义。RuleBook不需要“知道”任何特定游戏的状态即可完成其工作。游戏规则不会根据游戏的当前状态而改变。因此,要么您设计不正确,要么设计正确,但解释不正确。


+1。您购买了一个物理棋盘游戏,您会获得一本规则手册,该手册无需描述状态即可。
unperson325680

1

循环依赖关系不一定是技术问题,但应将其视为代码异味,这通常违反了单一职责原则

循环依赖源于您试图对State对象执行过多操作的事实。

任何有状态对象都应仅提供与管理该本地状态直接相关的方法。如果除了最基本的逻辑之外还需要其他任何内容,则应将其分解为更大的模式。某些人对此有不同的看法,但是作为一般经验法则,如果您除了对数据进行getter和setter方法之外要做的事情还很多。

在这种情况下,最好使用StateFactory,它可能知道Rulebook。您可能还有另一个控制器类,可以使用该类StateFactory来创建新游戏。State绝对不应该知道RulebookRulebook可能会State根据您的规则的执行情况知道一个。


0

是否有必要将规则簿对象绑定到特定的游戏状态,或者用一种方法给一个规则簿对象提供某种意义,该方法可以在给定游戏状态的情况下报告该状态下的可用举动(并且,报告了这一点后,对有关国家一无所知)?除非让被询问有关可用移动的对象保留对游戏状态的记忆,否则没有任何必要,它不需要持久保留引用。

在某些情况下,使规则评估对象保持状态可能会有好处。如果您认为可能会出现这种情况,建议您添加一个“裁判”类,并让规则手册提供“ createReferee”方法。与规则手册不同,规则手册并不关心被问到一场比赛还是五十场比赛,而裁判员则期望主持一场比赛。不会希望封装与该游戏相关的所有状态,但是可以缓存有关它认为有用的游戏的任何信息。如果游戏支持“撤消”功能,则让裁判包括产生“快照”对象的方法可能会有所帮助,该对象可以与较早的游戏状态一起存储;该对象应该

如果代码的规则处理和游戏状态处理方面之间可能需要某种耦合,则使用裁判对象将可以使这种耦合脱离主要规则手册和游戏状态类。也可能使新规则考虑游戏状态类认为不相关的游戏状态方面(例如,如果添加了一条规则,说“如果对象X曾经到过位置Z,则对象X不能做Y。 ”,则可以更改裁判的位置,以跟踪哪些对象已到达位置Z,而无需更改比赛状态等级)。


-2

处理此问题的正确方法是使用接口。不要让两个类彼此了解,而要让每个类实现一个接口并在另一个类中引用该接口。假设您有A类和B类需要相互引用。拥有A类实现接口A和B类实现接口B,那么您可以引用A类的接口B和B类的接口A。与类B一样,类A可以位于自己的项目中。这些接口位于单独的项目中这两个其他项目都参考。


2
这似乎只是重复在之前一个小时之前发布的先前答案中提出和解释的观点
gna
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.