Aufrechterhaltung der Trennung von Bedenken

8

Ich mache meine erste C # -App und habe ein bisschen Schwierigkeiten mit der Trennung von Bedenken. Ich verstehe das Konzept, aber ich weiß nicht, ob ich es richtig mache. Ich habe dies als schnelles Beispiel, um meine Frage zu veranschaulichen. In einer App wie einem Spiel gibt es eine Hauptklasse, in der die Hauptschleife ausgeführt wird, z. B. Programm oder Spiel. Meine Frage ist, behalte ich jeden Verweis auf jede Instanz einer Klasse in dieser Klasse bei und mache dies zur einzigen Art und Weise, wie sie interagieren?

Zum Beispiel ein Kartenspiel mit einem Spieler, Karten und einem Brett. Angenommen, der Spieler möchte Karten auf das Brett legen, aber die Spielerklasse hat nur eine Liste <> von Karten und keine Ahnung vom Spielbrett. Die Hauptspielklasse kennt jedoch die Spieler, die Karten und das Spielbrett. Sollte es an der Spielklasse liegen, die Karten auf das Brett zu legen, oder ist es sinnvoller, dass sie, da es sich um die Aktion des Spielers handelt, innerhalb der Spielerklasse liegen sollte.

Beispiel:

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;
}

Für mich ist es sinnvoller, die Methode im Spiel zu haben, weil das Spiel über alles Bescheid weiß. Aber wenn ich mehr Code hinzufüge, wird das Spiel ziemlich aufgebläht und ich schreibe viele PlayerDoesThis () -Funktionen.

Vielen Dank für jeden Rat

Töne31
quelle

Antworten:

12

Der Schlüssel liegt hier nicht nur in der Trennung von Anliegen , sondern auch im Prinzip der Einzelverantwortung . Die beiden sind im Grunde verschiedene Seiten derselben Medaille: Wenn ich SOC denke, denke ich von oben nach unten (ich habe diese Bedenken, wie trenne ich sie?), Während SRP mehr von unten nach oben ist (ich habe dieses Objekt, hat es eine einzelnes Anliegen? Sollte es geteilt werden? Sind seine Anliegen bereits zu stark gespalten?).

In Ihrem Beispiel haben Sie die folgenden Entitäten und deren Verantwortlichkeiten:

  • Spiel: Dies ist der Code, der das Programm zum "Los" bringt.
  • GameBoard: Behält den Status des Spielbereichs bei.
  • Karte: eine einzelne Einheit auf dem Spielbrett.
  • Spieler: Führt Aktionen aus, die den Status des Spielbretts ändern.

Sobald Sie über die Einzelverantwortung jedes Unternehmens nachdenken, werden die Linien klarer.

In einer App wie einem Spiel gibt es eine Hauptklasse, in der die Hauptschleife ausgeführt wird, z. B. Programm oder Spiel. Meine Frage ist, behalte ich jeden Verweis auf jede Instanz einer Klasse in dieser Klasse bei und mache dies zur einzigen Art und Weise, wie sie interagieren?

Hier sind wirklich zwei Punkte zu beachten. Die erste Entscheidung ist, was Entitäten über andere Entitäten wissen. Welche Entitäten gehören zu anderen Entitäten?

Schauen Sie sich die oben beschriebenen Verantwortlichkeiten an. Die Spieler führen Aktionen aus , die den Status des Spielbretts ändern. Mit anderen Worten, Spieler senden Nachrichten an (Aufrufmethoden auf) dem Spielbrett. Bei diesen Nachrichten handelt es sich wahrscheinlich um Karten: Beispielsweise kann ein Spieler eine Karte in seine Hand auf das Brett legen oder den Status einer vorhandenen Karte ändern (z. B. eine Karte umdrehen oder an einen neuen Ort verschieben).

Es ist klar, dass ein Spieler über das Spielbrett Bescheid wissen muss, was der Annahme widerspricht, die Sie in Ihrer Frage gemacht haben. Andernfalls muss der Spieler eine Nachricht an das Spiel senden, die diese Nachricht dann an das Spielbrett weiterleitet. Da die Spieler Aktionen auf dem Spielbrett ausführen , müssen sie über das Spielbrett Bescheid wissen. Dies erhöht die Kopplung: Anstatt dass der Spieler die Nachricht direkt sendet, müssen jetzt zwei Akteure wissen, wie sie diese Nachricht senden. Das Demeter-Gesetz impliziert, dass in diesem Szenario, wenn ein Objekt auf ein anderes Objekt einwirken muss, dieses andere Objekt über einen Parameter übergeben werden sollte, um die Kopplung zu verringern.

Wo speichern Sie als nächstes welchen Zustand? Das Spiel ist hier der Treiber, es muss alle Objekte entweder direkt oder über einen Proxy knittern (z. B. eine Factory oder einen Konstruktor, den das Spiel aufruft). Die logische nächste Frage ist, welche Objekte welche anderen Objekte benötigen. Dies ist im Grunde das, was ich oben gefragt habe, aber eine andere Art, es zu fragen.

So würde ich es gestalten:

  • Das Spiel erstellt alle für das Spiel erforderlichen Objekte.

  • Das Spiel mischt die Karten und teilt sie nach dem jeweiligen Spiel auf (Poker, Solitaire usw.).

  • Das Spiel legt die Karten an ihren ursprünglichen Positionen ab: vielleicht einige auf dem Spielbrett, andere in den Händen der Spieler.

  • Das Spiel geht dann in seine Hauptschleife, die eine Runde darstellt.

Jede Runde würde so aussehen:

  • Das Spiel sendet eine Nachricht an den aktuellen Spieler (ruft eine Methode auf) und gibt einen Verweis auf das Spielbrett.

  • Der Player führt die interne Logik (Computer-Player) oder Benutzerinteraktion aus, die erforderlich ist, um zu bestimmen, welches Spiel ausgeführt werden soll.

  • Der Spieler sendet eine Nachricht an das Spielbrett, in der er aufgefordert wird, den Status des Spielbretts zu ändern.

  • Das Spielbrett entscheidet , ob der Zug gültig ist oder nicht (es ist verantwortlich für die Aufrechterhaltung der gültigen Spielzustand).

  • Die Kontrolle kehrt zum Spiel zurück und entscheidet dann, was als nächstes zu tun ist. Auf Gewinnbedingungen prüfen? Zeichnen? Nächster Spieler? Nächste Runde? Hängt vom jeweiligen Kartenspiel ab.

Sollte es an der Spielklasse liegen, die Karten auf das Brett zu legen, oder ist es sinnvoller, dass sie, da es sich um die Aktion des Spielers handelt, innerhalb der Spielerklasse liegen sollte.

Beide: Das Spiel ist für die Ersteinrichtung verantwortlich, aber der Spieler führt Aktionen auf dem Brett aus. GameBoard ist für die Gewährleistung eines gültigen Status verantwortlich. Beispielsweise kann im klassischen Solitaire nur die oberste Karte auf einem Stapel offen liegen.


Zurück zu meinem ursprünglichen Punkt: Sie haben die richtigen Trennungen von Bedenken. Sie haben die richtigen Objekte identifiziert. Was Sie auslöste, war herauszufinden, wie Nachrichten durch das System fließen und welche Objekte Verweise auf andere Objekte enthalten sollten. Ich würde es so gestalten, was Pseudocode ist:

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);
}

quelle
1
Gute Antwort, nur eine Sache fehlt mir. Im Beispiel des OP übergibt er ein x, y an das Spielbrett, um genau anzugeben, wo die Karte platziert werden soll. Während der Spieler das Spielbrett aufruft, um eine Karte zu platzieren, sollte es das Spielbrett sein, das über x, y entscheidet. Wenn der Spieler die Brettkoordinaten kennen muss, entsteht eine undichte Abstraktion.
David Arno
@DavidArno Wenn die Karten an Orten in einem rechteckigen Raster gespielt werden können, wie sollte der Spieler angeben, an welchen von denen er sie spielen soll, wenn nicht durch Koordinaten? (Dies sind keine Bildschirmkoordinaten, sondern Gitterkoordinaten.)
Paŭlo Ebermann
1
Die spezifischen Details zum Platzieren der Karte sollten irgendwie von einer Standortklasse abstrahiert werden, was in diesem Design ein sehr untergeordnetes Element wäre . Sicher, Koordinaten könnten funktionieren. Es könnte auch geeigneter sein, benannte Orte wie "Stapel wegwerfen" zu verwenden. Die Implementierungsdetails sind unwichtig, wenn das Design wie in der Frage angegeben abgebildet wird.
@Snowman Danke, diese Antwort ist genau richtig. Wenn der Spieler immer auf dem GameBoard agiert, wäre es dann nicht sinnvoller, eine lokale Referenz innerhalb der Klasse zu haben, die während des Konstruktors festgelegt wird? Auf diese Weise muss GameBoard nicht jedes Mal an den Spieler weitergegeben werden (es wird viele Interaktionen mit dem Board geben).
Töne31
@DavidArno Der Spieler muss dem Spielbrett tatsächlich mitteilen, wo er es platzieren soll. Das Spielbrett muss es validieren. Der Spieler kann Karten aufnehmen und bewegen.
Töne31