web-dev-qa-db-ja.com

OOPで循環参照が必要と思われるこの実際のアクティビティをモデル化する適切な方法は何ですか?

Java循環参照に関するプロジェクトで問題に取り組んできました。問題のオブジェクトが相互に依存していて、知る必要があると思われる現実の状況をモデル化しようとしていますお互いについて。

プロジェクトは、ボードゲームをプレイする一般的なモデルです。基本的なクラスは特定されていませんが、チェス、バックギャモン、その他のゲームの詳細を扱うように拡張されています。 11年前にこれを6種類のゲームでアプレットとしてコーディングしましたが、問題は循環参照でいっぱいであることです。当時は、絡み合ったすべてのクラスを1つのソースファイルに詰め込んで実装していましたが、Javaではそれが悪い形式であることがわかりました。 Androidアプリとして同様のことを実装したいので、適切に処理したいと思います。

クラスは次のとおりです。

  • RuleBook:ボードの初期レイアウト、最初に移動した人などの他の初期ゲーム状態情報、使用可能な移動、提案された移動後のゲーム状態への影響、および現在または提案されている取締役会の職位。

  • ボード:動きを反映するように指示できるゲームボードの単純な表現。

  • MoveList:Moveのリスト。これは2つの目的で使用されます。特定の時点で使用可能なムーブの選択、またはゲームで行われたムーブのリストです。それは2つのほぼ同一のクラスに分けることができますが、それは私が尋ねている質問には関係がなく、さらに複雑になる可能性があります。

  • 移動:単一の移動。これには、アトムのリストとして移動に関するすべてが含まれています。ここからピースを拾い、そこに置き、そこからキャプチャされたピースを削除します。

  • 状態:進行中のゲームの完全な状態情報。ボードの位置だけでなく、MoveListや、誰が今移動するかなどのその他の状態情報。チェスでは、各プレーヤーのキングとルークが移動したかどうかを記録します。

たとえば、循環参照はたくさんあります。RuleBookは、特定の時間に利用可能なムーブを決定するためにゲームの状態を知る必要がありますが、ゲームの状態は、最初の開始レイアウトとムーブに伴う副作用についてRuleBookにクエリする必要があります。それが作られる(例えば次に動く人)。

RuleBookがすべてについて知る必要があるので、RuleBookを最上部にして、新しいクラスのセットを階層的に整理してみました。ただし、これにより、多くのメソッドをRuleBookクラスに移動(移動など)する必要が生じ、それがモノリシックになり、RuleBookがどうあるべきかを特に表すものではなくなります。

これを整理する適切な方法は何ですか? RuleBookをBigClassThatDoesAlmostEverythingInTheGameに変換して循環参照を回避し、実際のゲームを正確にモデル化する試みを放棄する必要がありますか?それとも、相互依存クラスにこだわり、コンパイラに何らかの方法でコンパイルさせて、実際のモデルを保持する必要がありますか?それとも私が見逃している明らかな有効な構造がありますか?

あなたが与えることができる助けをありがとう!

24
Damian Walker

Javaプロジェクトで循環参照に関する問題に取り組んでいます。

Javaのガベージコレクターは、参照カウント手法に依存しません。循環参照は、Javaでいかなる種類の問題も引き起こしません。 Javaで完全に自然な循環参照を排除するのに費やされた時間は、無駄な時間です。

私はこれをコード化しました[...]が、問題はそれが循環参照でいっぱいであることです。私はそれを絡み合ったすべてのクラスを単一のソースファイルに詰め込んで、[...]

必要はありません。すべてのソースファイルを一度にコンパイルするだけの場合(例:javac *.Java)、コンパイラはすべての前方参照を問題なく解決します。

または、相互依存クラスに固執し、コンパイラに何らかの方法でコンパイルするように促す必要があります[...]

はい。アプリケーションクラスは相互に依存していることが予想されます。すべてのJava同じパッケージに属するソースファイルを一度にコンパイルすることは巧妙なハックではありません。まさにJavaがsupposedが機能する。

47
Atsby

当然のことながら、循環依存関係は設計の観点からは疑わしい慣行ですが、禁止されていません。また、純粋に技術的な観点からは、あなたはそれらを考慮しているように、必ずしも問題があるではありませんbe:ほとんどのシナリオで完全に合法であり、状況によっては不可避であり、まれなケースでは、それらが有用なものと見なされることさえあります。

実際、Javaコンパイラーが循環依存関係を拒否するシナリオはごくわずかです。(注:それ以上の可能性があるため、現時点では次のことしか考えられません。)

  1. 継承:クラスAを拡張してクラスBを拡張することはできません。クラスBはクラスAを拡張します。これは、論理的な観点からはまったく意味がないため、これを使用できないことは完全に合理的です。

  2. メソッドローカルクラス:メソッド内で宣言されたクラスは、相互に循環参照できない場合があります。これはおそらくJavaコンパイラの制限に他なりません。おそらくそのようなことを行う能力は、それをサポートするためにコンパイラに入る必要がある追加の複雑さを正当化するには十分に役に立たないためです(ほとんどのJavaプログラマは、メソッド内でクラスを宣言できること、さらには複数のクラスを宣言できること、そしてこれらのクラスが相互に循環参照することを認識していません。)

したがって、循環依存関係を最小限に抑えるための探求は、設計の純粋さのための探求であり、技術的な正確さのための探求ではないことを認識し、それを排除することが重要です。

私が知る限り、循環依存関係を排除する還元主義的なアプローチは存在しません。つまり、循環参照を持つシステムを取得し、それらを順番に適用して終了するための単純な事前定義の「簡単な」ステップのみで構成されるレシピはありません。循環参照のないシステムを使用してください。あなたは自分の心を働かせ、デザインの性質に依存するリファクタリング手順を実行する必要があります。

あなたが手元にある特定の状況では、必要なのはおそらく「ゲーム」または「ゲームロジック」と呼ばれる新しいエンティティであり、他のすべてのエンティティを認識しているようです(他のエンティティはそれを認識していませんが、 )他のエンティティがお互いを知る必要がないように。

たとえば、RuleBookエンティティがGameStateエンティティについて何でも知っている必要があるのは不合理に思えます。ルールブックはプレイするために私たちが相談するものであり、プレイに積極的に関与するものではないためです。したがって、ルールブックとゲームの状態の両方を調べて、どのような動きが利用できるかを判断する必要があるのは、この新しい「ゲーム」エンティティであり、これにより循環依存関係が排除されます。

今、私はこのアプローチであなたの問題がどうなるかを推測できると思います:ゲームに依存しない方法で「ゲーム」エンティティをコーディングすることは非常に難しいので、おそらく1つだけでなく2つになるでしょう「RuleBook」および「Game」エンティティなど、ゲームの異なるタイプごとにカスタムメイドの実装を必要とするエンティティ。これは、そもそも「RuleBook」エンティティーを持つ目的を無効にします。さて、これについて言えることは、多分、たぶん、多種多様なゲームをプレイできるシステムを書くというあなたの最初の願望は高貴だったかもしれませんが、おそらくよく考えられていません。私があなたの立場にいるなら、すべての異なるゲームの状態を表示するための共通のメカニズムと、これらすべてのゲームのユーザー入力を受信するための共通のメカニズムを使用することに焦点を当てたでしょう、そして私は異なるゲームの実装を許可したでしょう各ゲームの実装における最大の柔軟性を保証するために、非常に少ないコードで共通して、大きく変化します。

22
Mike Nakis

ゲーム理論は、ゲームを以前の動き(それらをプレイした人を含む値のタイプ)のリストと関数ValidMoves(previousMoves)として扱います

ゲームのUI以外の部分については、このパターンを試してみて、ボードのセットアップなどを動きとして扱います。

次に、UIを標準にすることができますOOロジックへの片道参照を伴うもの


コメントを要約するための更新

チェスを検討してください。チェスのゲームは通常、移動のリストとして記録されます。 http://en.wikipedia.org/wiki/Portable_Game_Notation

ムーブのリストは、ボードの写真よりもゲームの完全な状態を定義します。

たとえば、ボード、ピース、移動などのオブジェクトや、Piece.GetValidMoves()などのメソッドの作成を開始するとします。

最初にピースがボードを参照する必要があることがわかりますが、次にキャスティングを検討します。これは、キングまたはルークをまだ動かしていない場合にのみ実行できます。したがって、キングとルークにMovedAlreadyフラグが必要です。同様に、ポーンは最初の移動で2マス移動できます。

次に、キャスティングでは、キングの有効な動きがルークの存在と状態に依存することがわかります。そのため、ボードには駒があり、それらの駒を参照する必要があります。循環参照の問題に取り組んでいます。

ただし、Moveを不変の構造体として定義し、ゲームの状態を以前のMoveのリストとして定義すると、これらの問題は消えます。キャスティングが有効かどうかを確認するには、城と王の移動の存在の移動リストを確認します。ポーンが前を通ることができるかどうかを確認するために、他のポーンが以前に移動中に2つの移動を行ったかどうかを確認できます。ルール->移動以外の参照は必要ありません

今チェスは静的なボードを持っており、ピースは常に同じ方法で設定されます。しかし、別のセットアップを許可するバリアントがあるとしましょう。おそらくハンディキャップとしていくつかの部分を省略しています。

「ボックスからスクエアXへ」というセットアップムーブをムーブとして追加し、そのムーブを理解するためにルールオブジェクトを適合させても、ゲームを一連のムーブとして表すことができます。

同様に、ゲーム内でボード自体が静的でない場合、チェスに四角を追加したり、ボードから四角を削除して、それらを移動できないようにするとします。これらの変更は、ルールエンジンの全体的な構造を変更したり、類似のBoardSetupオブジェクトを参照したりすることなく、移動として表すこともできます。

10
Ewan

オブジェクト指向プログラミングで2つのクラス間の循環参照を削除する標準的な方法は、そのうちの1つで実装できるインターフェースを導入することです。したがって、あなたの場合、RuleBookStateを参照し、次にInitialPositionProviderRuleBookによって実装されるインターフェース)を参照することができます。これにより、テストのために異なる(おそらくより単純な)初期位置を使用するStateを作成できるため、テストが容易になります。

8
Jules

循環参照とあなたのケースの神オブジェクトは、ゲームの状態の制御とゲームのルールモデルからゲームフローの制御を分離することで簡単に削除できると思います。そうすることで、おそらく多くの柔軟性が得られ、不要な複雑さを取り除くことができます。

ルールブックやゲームの状態にこの責任を負わせる代わりに、ゲームの流れを制御し、実際の状態の変化を処理するコントローラー(必要に応じて「ゲームマスター」)が必要だと思います。

ゲーム状態オブジェクトは、それ自体を変更したり、ルールを認識したりする必要はありません。クラスは、アプリケーションの残りの部分で簡単に処理(作成、検査、変更、永続化、ログ、コピー、キャッシュなど)された効率的なゲーム状態オブジェクトのモデルを提供する必要があるだけです。

ルールブックは、進行中のゲームについて知ったり、いじったりする必要はありません。どのムーブが合法であるかを判別できるようにするためには、ゲームステートのビューだけが必要であり、ムーブがゲームステートに適用されたときに何が起こるか尋ねられたときに、結果のゲームステートで応答する必要があります。また、初期レイアウトを要求されたときに、ゲームの開始状態を提供することもできます。

コントローラーは、ゲームの状態とルールブック、およびおそらくゲームモデルの他のいくつかのオブジェクトを認識する必要がありますが、詳細をいじる必要はありません。

6
COME FROM

ここでの問題は、どのクラスがどのタスクを処理するかについて明確な説明を提供していないことだと思います。私は、各クラスが何をすべきかについての良い説明であると私が考えることを説明し、次に、アイデアを示す一般的なコードの例を示します。コードの結合が少ないため、循環参照は実際にはありません。

まず、各クラスの機能を説明します。

GameStateクラスには、ゲームの現在の状態に関する情報のみを含める必要があります。これには、ゲームの過去の状態や将来の動きの可能性についての情報は含まれていません。チェスのどのマスにどの駒があるか、またはバックギャモンのどのポイントにいくつのチェッカーが何個あるかに関する情報のみが含まれている必要があります。 GameStateには、チェスでのキャスティングやバックギャモンでの2倍のキューブに関する情報など、いくつかの追加情報を含める必要があります。

Moveクラスは少しトリッキーです。プレイの結果として得られるGameStateを指定することで、プレイするムーブを指定できると思います。したがって、移動はGameStateとして実装できると想像できます。ただし、移動中(たとえば)、ボード上の単一の点を指定することで、移動を指定する方がはるかに簡単であると想像できます。 Moveクラスには、これらのいずれかのケースを処理するのに十分な柔軟性が必要です。したがって、Moveクラスは実際には、移動前のGameStateを受け取り、新しい移動後のGameStateを返すメソッドとのインターフェースになります。

現在、RuleBookクラスは、ルールに関するすべてを知る責任があります。これは3つに分類できます。最初のGameStateが何であるかを知る必要があり、どの動きが合法であるかを知る必要があり、プレイヤーの1人が勝ったかどうかを通知できる必要があります。

GameHistoryクラスを作成して、行われたすべての移動と発生したすべてのGameStatesを追跡することもできます。単一のGameStateがその前にあるすべてのGameStatesを認識する責任を負わないことを決定したため、新しいクラスが必要です。

これで、これから説明するクラス/インターフェースを終了します。 Boardクラスもあります。しかし、異なるゲームのボードは非常に異なるため、ボードで一般的に何ができるかを理解するのは難しいと思います。次に、ジェネリックインターフェイスを提供し、ジェネリッククラスを実装します。

最初はGameStateです。このクラスは特定のゲームに完全に依存しているため、汎用のGamestateインターフェイスまたはクラスはありません。

次はMoveです。すでに述べたように、これは移動前の状態を取り、移動後の状態を生成する単一のメソッドを持つインターフェースで表すことができます。このインターフェースのコードは次のとおりです。

package boardgame;

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

    T makeResultingState(T preMoveState) throws IllegalArgumentException;

}

型パラメーターがあることに注意してください。これは、たとえば、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は、1人または他のプレーヤーがすでに勝利したかどうかを説明する責任があるだけで、ゲームの途中で誰がより良いポジションにいるのかについては責任がないことに注意してください。誰がより良い立場にいるかを判断することは、独自のクラスに移動する必要がある複雑なことです。したがって、StateEvaluationクラスは、実際には次のように指定された単純な列挙型です。

package boardgame;

/**
 *
 */
public enum StateEvaluation {

    UNFINISHED,
    PLAYER_ONE_WINS,
    PLAYER_TWO_WINS,
    DRAW,
    ILLEGAL_STATE
}

最後に、GameHistoryクラスについて説明します。このクラスは、ゲームで到達したすべての位置とプレイされた動きを記憶する責任があります。それができるはずの主なことは、演奏されたMoveを記録することです。 Movesを元に戻す機能を追加することもできます。以下の実装があります。

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の仕事です。したがって、GameGameHistoryに現在の状態を尋ね、RuleBookが合法的な動きを伝える必要がある場合にこの情報をGameに提供します。誰かが勝った場合。

とにかく、この答えの要点は、各クラスが何を担当するのかを合理的に決定したら、各クラスを少数の責任に集中させ、各責任を一意のクラスに割り当ててから、クラスを分離する傾向があり、すべてがコーディングしやすくなります。うまくいけば、それは私が与えたコード例から明らかです。

5
Brian Moths

私の経験では、循環参照は一般に、設計が十分に検討されていないことを示しています。

あなたのデザインでは、RuleBookが州について「知る」必要がある理由がわかりません。確かに、あるメソッドに対してparameterとして状態を受け取る可能性がありますが、なぜknow(つまり、インスタンス変数として保持する)状態への参照が必要なのでしょうか?それは私には意味がありません。 RuleBookは、その仕事をするために特定のゲームの状態を「知る」必要はありません。ゲームのルールは、ゲームの現在の状態によって変化しません。それで、あなたはそれを正しく設計しなかったか、またはあなたはそれを正しく設計しましたが、それを正しく説明していません。

3
user541686

循環依存は必ずしも技術的な問題ではありませんが、コードのにおいであると考える必要があります。これは通常 単一の責任の原則 の違反です。

循環依存関係は、Stateオブジェクトからあまりにも多くのことを実行しようとしているという事実から生じます。

ステートフルオブジェクトは、そのローカル状態の管理に直接関連するメソッドのみを提供する必要があります。最も基本的なロジック以外のものが必要な場合は、おそらくより大きなパターンに分割する必要があります。一部の人々はこれについて異なる意見を持っていますが、一般的な経験則として、データのゲッターとセッター以外のことをしていると、やりすぎです。

この場合、StateFactoryを知っているRulebookを用意することをお勧めします。おそらく、StateFactoryを使用して新しいゲームを作成する別のコントローラークラスがあるでしょう。 Stateは確かにRulebookを知らないはずです。 Rulebookは、ルールの実装によってはStateについて知っている場合があります。

1
00500005

ルールブックオブジェクトを特定のゲーム状態にバインドする必要はありますか?または、ゲーム状態が与えられた場合、その状態からどのような動きが利用できるかを報告するメソッド(および、それを報告した後、問題の状態について何も覚えていない)?利用可能な動きについて尋ねられるオブジェクトにゲームの状態の記憶を保持させることによって得られるものがない限り、参照を永続化する必要はありません。

場合によっては、ルールを評価するオブジェクトに状態を維持させることで利点が得られる可能性があります。このような状況が発生すると思われる場合は、「審判」クラスを追加し、ルールブックに「createReferee」メソッドを提供することをお勧めします。ルールブックは、1つのゲームについて尋ねられても50でもかまわないのとは異なり、レフェリーオブジェクトは1つのゲームを担当することを期待しています。関係するゲームに関連するすべての状態をカプセル化することは期待されていませんが、ゲームについて有用と思われる情報をキャッシュすることができます。ゲームが「元に戻す」機能をサポートしている場合、以前のゲームの状態とともに保存できる「スナップショット」オブジェクトを作成する手段をレフェリーに含めると役立つ場合があります。そのオブジェクトは、作成時にゲームの状態のコピーが与えられると、スナップショットが作成されたときに元のレフェリーが持っていた状態と一致する状態を持つ新しいレフェリーを生成できる必要があります。

コードのルール処理とゲーム状態処理の間で何らかの結合が必要な場合、レフリーオブジェクトを使用すると、メインのルールブックとゲーム状態のそのような結合outを維持できるようになります。クラス。また、新しいルールに、ゲーム状態クラスが関連するとは見なさないゲーム状態の側面を考慮させることもできます(たとえば、「オブジェクトXは、場所Zにいた場合、Yを実行できません」というルールが追加された場合"、レフェリーを変更して、ゲーム状態クラスを変更せずに、どのオブジェクトが位置Zに移動したかを追跡することができます)。

0
supercat