懸念の分離の維持


8

私は最初のC#アプリを作成していますが、問題の分離に少し問題があります。コンセプトは理解していますが、正しくやっているのかわかりません。私の質問を説明する簡単な例としてこれを持っています。ゲームなどのアプリには、プログラムやゲームなどのメインループを実行するメインクラスがあります。私の質問は、このクラスのクラスのすべてのインスタンスへのすべての参照を維持し、それがそれらが相互作用する唯一の方法にすることですか?

たとえば、プレーヤー、カード、ボードを使用したカードゲームです。プレーヤーがカードをボードに置きたいが、PlayerクラスにはカードのList <>しかなく、ゲームボードについて何もわからないとしましょう。ただし、メインのGameクラスは、プレーヤー、カード、およびゲームボードについて知っています。ボードにカードを配置するのがゲームクラス次第なのか、それとも、プレーヤーのアクションであるため、それはプレーヤークラス内にあるべきであるということの方が理にかなっています。

例:

public class Game{
    private GameBoard gameBoard;
    private Player[] players;

   public Game(){
     gameBoard = new GameBoard(10,10);
     Player player1 = new Player();
     Player player2 = new Player();
     players = {player1, player2};
   }

   // Create method here?
   public void PlayerPlaceCard(int x, int y, int cardIndex){
      gameBoard.grid[1,1] = player1.cards[cardIndex];
   }
}

public class Player {
     public List<Cards> cards = new List<Cards>();

     public Player(){
     }

     // Or place method here?
     public PlaceCard(Card card, int x, int y, GameBoard gameBoard){
     }
}

public class GameBoard{
    public Card[,] grid;

    public GameBoard(int x, int y){
       // Make the game board
    }
}

public class Card{
   public string name;
   public string value;
}

Gameはすべてのことを知っているので、私にとっては、Gameにメソッドを持つ方が理にかなっています。しかし、コードを追加していくと、ゲームがかなり肥大化し、PlayerDoesThis()関数をたくさん書いています。

アドバイスをありがとう

回答:


12

ここでの鍵は、懸念の分離だけでなく、単一の責任の原則でもあります。2つは基本的に同じコインの異なる側面です。SOCをトップダウンだと思うとき(これらの懸念があるので、どうすればそれらを分離できますか)、SRPはよりボトムアップです(このオブジェクトがありますが、単一の懸念?それは分割されるべきか?その懸念はすでにあまりにも多く分割されていますか?)

あなたの例では、次のエンティティとその責任があります:

  • ゲーム:これはプログラムを「実行」させるコードです。
  • GameBoard:プレイエリアの状態を維持します。
  • カード:ゲームボード上の単一のエンティティ。
  • プレーヤー:ゲームボードの状態を変更するアクションを実行します。

各エンティティの単一の責任について考えると、線がより明確になります。

ゲームなどのアプリには、プログラムやゲームなどのメインループを実行するメインクラスがあります。私の質問は、このクラスのクラスのすべてのインスタンスへのすべての参照を維持し、それがそれらが相互作用する唯一の方法にすることですか?

ここで、覚えておくべき2つの問題があります。最初に決定することは、他のエンティティについてどのエンティティが知っているかです。どのエンティティが他のエンティティに属していますか?

上で概説した責任を見てください。プレーヤーは、ゲームボードの状態を変更するアクション実行します。つまり、プレーヤーはゲームボードにメッセージを送信(メソッドを呼び出す)します。これらのメッセージには、カードが含まれている可能性があります。たとえば、プレーヤーはカードをボードの手札に置いたり、既存のカードの状態を変更したりします(たとえば、カードを裏返すか、新しい場所に移動します)。

明らかに、プレイヤーはあなたが質問で行った仮定に反するゲームボードについて知っている必要があります。それ以外の場合、プレイヤーはゲームにメッセージを送信する必要があり、そのメッセージはゲームボードに中継されます。プレイヤーはゲームボード上でアクションを実行するので、プレイヤーはゲームボードについて知っている必要があります。これにより、カップリングが増加します。プレーヤーがメッセージを直接送信する代わりに、2人の俳優がそのメッセージの送信方法を知っている必要があります。デメテル法則は、オブジェクトが別のオブジェクトに作用する必要がある場合、このシナリオでは、結合を減らすためにパラメーターを介して他のオブジェクトを渡す必要があることを意味します。

次に、どの状態をどこに保存しますか?ゲームはここでのドライバーです。直接またはプロキシ経由で(たとえば、ファクトリーまたはゲームが呼び出すコンストラクターで)すべてのオブジェクトに折り目を付ける必要があります。次の論理的な質問は、どのオブジェクトに他のどのオブジェクトが必要かということです。これは基本的に私が上で尋ねたものですが、それを求める別の方法です。

私がそれを設計する方法は次のとおりです:

  • ゲームは、ゲームに必要なすべてのオブジェクトを作成します。

  • ゲームはカードをシャッフルし、カードが表すゲーム(ポーカー、ソリティアなど)ごとにカードを分割します。

  • ゲームはカードを最初の場所に配置します。ゲームボード上にあるものや、プレーヤーの手にあるものなどです。

  • その後、ゲームはターンを表すメインループに入ります。

各ターンは次のようになります。

  • ゲームは現在のプレーヤーにメッセージを送信し(メソッドを呼び出し)、ゲームボードへの参照を提供します。

  • プレーヤーは、実行するプレイを決定するために必要な内部ロジック(コンピュータープレーヤー)またはユーザー操作を実行します。

  • プレーヤーは、ゲームボードの状態を変更するように求めるメッセージを、ゲームボードに送信します。

  • ゲームボードは、移動が(それが有効であるかどうかを決定する責任が有効なゲームの状態を維持するため)。

  • コントロールがゲームに戻り、ゲームは次に何をするかを決定します。勝利条件を確認しますか?ドロー?次のプレイヤー?次のターン?プレイされている特定のカードゲームに依存します。

ボードにカードを配置するのがゲームクラス次第なのか、それとも、プレーヤーのアクションであるため、それはプレーヤークラス内にあるべきであるということの方が理にかなっています。

両方:ゲームは初期設定を担当しますが、プレイヤーはボード上でアクションを実行します。GameBoardは、有効な状態を保証する責任があります。たとえば、クラシックソリティアでは、山の上のカードだけが表向きになることがあります。


元のポイントに戻ります。あなたは懸念を適切に分離しています。適切なオブジェクトを識別しました。あなたがつまずいたのは、メッセージがシステムをどのように流れるか、そしてどのオブジェクトが他のオブジェクトへの参照を保持する必要があるかを理解することでした。私はそれを疑似コードであるこのように設計します:

class Game {
  main();
}

class GameBoard {
  // Data structures specific to the game being played. There is a
  // lot of hand-waving here to give the general idea without
  // getting bogged down in the implementation.
  Map<Card, Location> cards;

  GameBoard(Map<Card, Location>);

  // Return false if the move is invalid.
  bool flip(Card);
  bool move(Card, Location);
}

class Card {
  // Make Rank and Suit enums.
  Suit suit;
  Rank rank;
  bool faceUp;
}

class Player {
  Set<Card> hand;

  Player(Set<Card>);
  void takeTurn(GameBoard);
}

1
良い答え、私が感じていないことが一つだけ不足しています。OPの例では、彼はゲームボードにx、yを渡して、カードを配置する場所を正確に伝えています。プレーヤーがゲームボードを呼び出してカードを配置する一方で、そのx、yを決定するのはゲームボードでなければなりません。ボードの座標について知ることをプレーヤーに要求すると、漏れやすい抽象化が作成されます。
David Arno

@DavidArnoカードが長方形のグリッド内の場所でプレイできる場合、プレイヤーは座標ではなく、どのカードでプレイするかをどのように示すべきですか?(これらは画面の座標ではなく、グリッドの座標です。)
PaŭloEbermann、2015

1
カードの配置方法に関する特定の詳細は、このデザインでは非常にマイナーな部分となるLocationクラスによって抽象化される必要があります。確かに、座標は機能するかもしれません。「パイルを破棄する」などの名前付きの場所を使用する方が適切な場合もあります。質問に述べられているように設計をマッピングする場合、実装の詳細は重要ではありません。

@スノーマンありがとう、この答えは正解です。Playerが常にGameBoardに基づいて動作している場合、クラス内にローカル参照を作成する方が理にかなっていますか?それは、コンストラクターの間に設定されますか?そうすれば、GameBoardを毎回Playerに渡す必要がなくなります(ボードとのやり取りがたくさんあります)。
トーン31

@DavidArnoプレーヤーは実際にゲームボードに配置する場所を指示する必要があり、ゲームボードはそれを検証する必要があります。プレイヤーはカードを拾って移動することができます。
トーン31
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.