Saubere OOP-Methode zum Zuordnen eines Objekts zu seinem Präsentator

13

Ich erstelle ein Brettspiel (wie Schach) in Java, wo jedes Stück seine eigene Art ist (wie Pawn, Rooketc.). Für den GUI-Teil der Anwendung benötige ich für jedes dieser Teile ein Bild. Da denkt das ja gerne

rook.image();

Verstößt die Trennung von Benutzeroberfläche und Geschäftslogik, erstelle ich für jedes Stück einen anderen Präsentator und ordne dann die Stücktypen den entsprechenden Präsentatoren zu

private HashMap<Class<Piece>, PiecePresenter> presenters = ...

public Image getImage(Piece piece) {
  return presenters.get(piece.getClass()).image();
}

So weit, ist es gut. Ich spüre jedoch, dass ein umsichtiger OOP-Guru beim Aufrufen einer getClass()Methode die Stirn runzelt und die Verwendung eines Besuchers zum Beispiel so vorschlägt:

class Rook extends Piece {
  @Override 
  public <T> T accept(PieceVisitor<T> visitor) {
    return visitor.visitRook(this);
  }
}

class ImageVisitor implements PieceVisitor<Image> {
  @Override  
  public Image visitRook(Rook rook) {
    return rookImage;
  } 
}

Ich mag diese Lösung (danke, Guru), aber sie hat einen wesentlichen Nachteil. Jedes Mal, wenn der Anwendung ein neuer Stücktyp hinzugefügt wird, muss der PieceVisitor mit einer neuen Methode aktualisiert werden. Ich möchte mein System als Brettspiel-Framework verwenden, in dem neue Teile durch einen einfachen Prozess hinzugefügt werden können, bei dem der Benutzer des Frameworks nur die Implementierung des Teils und seines Präsentators bereitstellt und es einfach in das Framework einfügt. Meine Frage: Gibt es eine saubere OOP Lösung ohne instanceof, getClass()etc. , die für diese Art von Erweiterbarkeit ermöglichen würden?

lishaak
quelle
Was ist der Zweck, eine eigene Klasse für jede Art von Schachfigur zu haben? Ich gehe davon aus, dass ein Stückobjekt die Position, die Farbe und den Typ eines einzelnen Stücks enthält. Zu diesem Zweck hätte ich wahrscheinlich eine Piece-Klasse und zwei Enums (PieceColor, PieceType).
KOMMEN SIE VOM
@COMEFROM Nun, offensichtlich haben verschiedene Arten von Teilen unterschiedliche Verhaltensweisen. Es muss also einen benutzerdefinierten Code geben, der zwischen Sprichwörtern und Bauern unterscheidet. Das heißt, ich hätte im Allgemeinen lieber eine Standardstückklasse, die alle Typen behandelt und Strategieobjekte verwendet, um das Verhalten anzupassen.
Jules
@Jules und was wäre der Vorteil einer Strategie für jedes Teil gegenüber einer separaten Klasse für jedes Teil, das das Verhalten in sich enthält?
Lishaak
2
Ein klarer Vorteil der Trennung der Spielregeln von statusbehafteten Objekten, die einzelne Teile darstellen, besteht darin, dass das Problem sofort gelöst wird. Bei der Implementierung eines rundenbasierten Brettspiels würde ich das Regelmodell im Allgemeinen überhaupt nicht mit dem Zustandsmodell mischen.
KOMMEN SIE VOM
2
Wenn Sie die Regeln nicht trennen möchten, können Sie die Bewegungsregeln in den sechs PieceType-Objekten definieren (keine Klassen!). Wie auch immer, ich denke, der beste Weg, Probleme zu vermeiden, besteht darin, Bedenken zu trennen und Vererbung nur dann einzusetzen, wenn sie wirklich nützlich ist.
KOMMEN SIE VOM

Antworten:

10

Gibt es eine saubere OOP-Lösung ohne instanceof, getClass () usw., die diese Art von Erweiterbarkeit ermöglichen würde?

Ja da ist.

Lass mich dich das fragen. In Ihren aktuellen Beispielen finden Sie Möglichkeiten zum Zuordnen von Objekttypen zu Bildern. Wie löst dies das Problem, dass ein Teil bewegt wird?

Eine wirkungsvollere Technik als nach dem Typ zu fragen, ist, Tell zu folgen , nicht zu fragen . Was ist, wenn jedes Teil eine PiecePresenterSchnittstelle hat und so aussieht:

class PiecePresenter implements PieceOutput {

  BoardPresenter board;
  Image pieceImage;

  @Override
  PiecePresenter(BoardPresenter board, Image image) {
    public void display(int rank, int file) {
      board.display(pieceImage, rank, file);
    } 
  }
}

Die Konstruktion würde ungefähr so ​​aussehen:

rookWhiteImage = new Image("Rook-White.png");
PieceOutput rookWhiteOutPort = new PiecePresenter(boardPresenter, rookWhiteImage);
PieceInput rookWhiteInPort = new Rook(rookWhiteOutPort);
board[0, 0] = rookWhiteInPort;

Die Verwendung würde ungefähr so ​​aussehen:

board[rank, file].display(rank, file);

Hier geht es darum, keine Verantwortung dafür zu übernehmen, etwas zu tun, wofür andere Dinge verantwortlich sind, indem sie nicht danach fragen oder darauf basierende Entscheidungen treffen. Halten Sie stattdessen einen Verweis auf etwas bereit, das weiß, was mit etwas zu tun ist, und weisen Sie es an, etwas zu tun, was Sie wissen.

Dies ermöglicht Polymorphismus. Sie kümmern sich nicht darum, womit Sie sprechen. Es ist dir egal, was es zu sagen hat. Sie kümmern sich nur darum, dass es das kann, was Sie tun müssen.

Ein gutes Diagramm , das diese in getrennten Schichten hält, folgt sagen-nicht-fragen, und zeigt , wie man nicht Paar Schicht zu Unrecht ist die Schicht dies :

Bildbeschreibung hier eingeben

Es wird eine Use-Case-Ebene hinzugefügt, die wir hier nicht verwendet haben (und die wir sicherlich hinzufügen können), aber wir folgen demselben Muster, das Sie in der unteren rechten Ecke sehen.

Sie werden auch feststellen, dass Presenter keine Vererbung verwendet. Es nutzt Komposition. Vererbung sollte der letzte Ausweg sein, um Polymorphismus zu erreichen. Ich bevorzuge Designs, die Komposition und Delegation bevorzugen. Es ist ein bisschen mehr Tastatur-Eingabe, aber es ist viel mehr Leistung.

kandierte_orange
quelle
2
Ich denke, dies ist wahrscheinlich eine gute Antwort (+1), aber ich bin nicht davon überzeugt, dass Ihre Lösung ein gutes Beispiel für die saubere Architektur ist: Die Rook-Entität enthält jetzt sehr direkt einen Verweis auf einen Präsentator. Ist es nicht diese Art von Kopplung, die die saubere Architektur zu verhindern versucht? Und was Ihre Antwort nicht ganz ausdrückt: Die Zuordnung zwischen Entitäten und Präsentatoren wird jetzt von demjenigen vorgenommen, der die Entität instanziiert. Dies ist wahrscheinlich eleganter als alternative Lösungen, aber es ist ein dritter Ort, der geändert werden muss, wenn neue Entitäten hinzugefügt werden - jetzt ist es kein Besucher, sondern eine Fabrik.
amon
@amon, wie gesagt, die Use-Case-Ebene fehlt. Terry Pratchett nannte solche Dinge "Lügen für Kinder". Ich versuche, kein überwältigendes Beispiel zu schaffen. Wenn Sie denken , ich brauche zur Schule gebracht werden , wo es zu reinigen Architektur kommt Ich lade Sie ein , mich zu Aufgabe zu übernehmen hier .
candied_orange
Entschuldigung, nein, ich möchte Sie nicht schulen, ich möchte nur dieses Architekturmuster besser verstehen. Ich bin vor weniger als einem Monat buchstäblich auf die Konzepte „saubere Architektur“ und „hexagonale Architektur“ gestoßen.
amon
@amon in diesem Fall schau es dir genau an und nimm mich dann zur Aufgabe. Ich würde es lieben, wenn du es tust. Ich finde immer noch Teile davon selbst heraus. Im Moment arbeite ich daran, ein menügesteuertes Python-Projekt auf diesen Stil zu aktualisieren. Eine kritische Überprüfung der Clean Architecture finden Sie hier .
candied_orange
1
@ lishaak Instanziierung kann in main passieren. Die inneren Schichten kennen die äußeren Schichten nicht. Die äußeren Schichten kennen nur Schnittstellen.
candied_orange
5

Was ist damit:

Ihr Modell (die Abbildung Klassen) haben eine gemeinsame Methoden, die Sie möglicherweise auch in anderen Kontext benötigen:

interface ChessFigure {
  String getPlayerColor();
  String getFigureName();
}

Die Bilder, die zum Anzeigen einer bestimmten Figur verwendet werden sollen, erhalten Dateinamen anhand eines Namensschemas:

King-White.png
Queen-Black.png

Anschließend können Sie das entsprechende Image laden, ohne auf Informationen zu den Java-Klassen zugreifen zu müssen.

new File(FIGURE_IMAGES_DIR,
         String.format("%s-%s.png",
                       figure.getFigureName(),
                       figure.getPlayerColor)));

Ich bin auch an einer allgemeinen Lösung für diese Art von Problemen interessiert, wenn ich Informationen (nicht nur Bilder) an eine potenziell wachsende Gruppe von Klassen anhängen muss. "

Ich denke, Sie sollten sich nicht so sehr auf den Unterricht konzentrieren . Denken Sie lieber in Geschäftsobjekten .

Und die generische Lösung ist ein Mapping jeglicher Art. IMHO besteht der Trick darin, diese Zuordnung vom Code zu einer Ressource zu verschieben, die einfacher zu warten ist.

In meinem Beispiel wird dieses Mapping gemäß Konvention durchgeführt, das recht einfach zu implementieren ist und das es vermeidet, sichtbezogene Informationen in das Geschäftsmodell einzufügen . Auf der anderen Seite könnte man es als "verstecktes" Mapping betrachten, da es nirgendwo zum Ausdruck kommt.

Eine andere Möglichkeit besteht darin, dies als separaten Geschäftsfall mit eigenen MVC-Layern einschließlich eines Persistenz-Layers zu betrachten, der das Mapping enthält.

Timothy Truckle
quelle
Ich sehe dies als eine sehr praktische und bodenständige Lösung für dieses spezielle Szenario. Ich bin auch an einer allgemeinen Lösung für diese Art von Problemen interessiert, wenn ich Informationen (nicht nur Bilder) an eine potenziell wachsende Gruppe von Klassen anhängen muss.
lishaak
3
@lishaak: Der allgemeine Ansatz besteht darin, genügend Metadaten in Ihren Geschäftsobjekten bereitzustellen, damit eine allgemeine Zuordnung zu einer Ressource oder zu Elementen der Benutzeroberfläche automatisch erfolgen kann.
Doc Brown
2

Ich würde eine separate UI / View-Klasse für jedes Stück erstellen, die die visuellen Informationen enthält. Jede dieser Teilsichtklassen hat einen Zeiger auf ihr Modell / Geschäftsgegenstück, der die Position und die Spielregeln des Teils enthält.

Also nimm einen Bauern zum Beispiel:

class Pawn : public Piece {
public:
    Vec2 position() const;
    /**
     The rest of the piece's interface
     */
}

class PawnView : public PieceView {
public:
    PawnView(Piece* piece) { _piece = piece; }
    void drawSelf(BoardView* board) const{
         board.drawPiece(_image, _piece->position);
    }
private:
    Piece* _piece;
    Image _image;
}

Dies ermöglicht eine vollständige Trennung von Logik und Benutzeroberfläche. Sie können den Zeiger der Logikfigur an eine Spielklasse übergeben, die die Bewegung der Figuren handhaben würde. Der einzige Nachteil ist, dass die Instanziierung in einer UI-Klasse erfolgen müsste.

Lasse Jacobs
quelle
OK, also nehmen wir mal an, ich hätte welche Piece* p. Woher weiß ich, dass ich ein erstellen muss PawnView, um es anzuzeigen, und nicht ein RookViewoder KingView? Oder muss ich sofort eine begleitende Ansicht oder einen Präsentator erstellen, wenn ich ein neues Stück erstelle? Das wäre im Grunde die Lösung von @ CandiedOrange mit den umgekehrten Abhängigkeiten. In diesem Fall PawnViewkönnte der Konstruktor auch a nehmen Pawn*, nicht nur a Piece*.
amon
Ja, es tut mir leid, der PawnView-Konstruktor würde einen Pawn * nehmen. Und Sie müssen nicht unbedingt eine Pfandansicht und eine Pfand gleichzeitig erstellen. Angenommen, es gibt ein Spiel, das 100 Bauern haben kann, aber nur 10 können gleichzeitig sichtbar sein. In diesem Fall können Sie Bauernansichten für mehrere Bauern wiederverwenden.
Lasse Jacobs
Und ich stimme der Lösung von @ CandiedOrange zu, obwohl ich meine 2 Cent teilen würde.
Lasse Jacobs
0

Ich würde mich dem nähern, indem ich Piecegenerisch mache , wobei sein Parameter der Typ einer Aufzählung ist, die den Typ eines Stücks identifiziert, wobei jedes Stück einen Verweis auf einen solchen Typ hat. Dann könnte die Benutzeroberfläche eine Karte aus der Aufzählung wie zuvor verwenden:

public abstract class Piece<T>
{
    T type;
    public Piece (T type) { this.type = type; }
    public T getType() { return type; }
}
enum ChessPieceType { PAWN, ... }
public class Pawn extends Piece<ChessPieceType>
{
    public Pawn () { super (ChessPieceType.PAWN); }

Dies hat zwei interessante Vorteile:

Erstens gilt dies für die meisten statisch typisierten Sprachen: Wenn Sie Ihr Board mit der Art der Figur auf xpect parametrieren, können Sie nicht die falsche Art der Figur in das Board einfügen.

Zweitens und vielleicht interessanter, wenn Sie in Java (oder anderen JVM-Sprachen) arbeiten, sollten Sie beachten, dass jeder Aufzählungswert nicht nur ein unabhängiges Objekt ist, sondern auch eine eigene Klasse haben kann. Dies bedeutet, dass Sie Ihre Objekttypen als Strategieobjekte verwenden können, um das Verhalten des Objekts zu bestimmen:

 public class ChessPiece extends Piece<ChessPieceType> {
    ....
   boolean isMoveValid (Move move)
    {
         return getType().movePatterns().contains (move.asVector()) && ....


 public enum ChessPieceType {
    public abstract Set<Vector2D> movePatterns();
    PAWN {
         public Set<Vector2D> movePatterns () {
              return Util.makeSet(
                    new Vector2D(0, 1),
                    ....

(Offensichtlich müssen die tatsächlichen Implementierungen komplexer sein, aber Sie haben hoffentlich die Idee)

Jules
quelle
0

Ich bin ein pragmatischer Programmierer und es ist mir wirklich egal, was saubere oder schmutzige Architektur ist. Ich glaube Anforderungen und es sollte einfach gehandhabt werden.

Ihre Anforderung ist, dass Ihre Schach-App-Logik auf verschiedenen Präsentationsebenen (Geräten) wie im Web, in mobilen Apps oder sogar in Konsolen-Apps dargestellt wird, sodass Sie diese Anforderungen unterstützen müssen. Sie können es vorziehen, sehr unterschiedliche Farben und Einzelbilder auf jedem Gerät zu verwenden.

public class Program
{
    public static void Main(string[] args)
    {
        new Rook(new Presenter { Image = "rook.png", Color = "blue" });
    }
}

public abstract class Piece
{
    public Presenter Presenter { get; private set; }
    public Piece(Presenter presenter)
    {
        this.Presenter = presenter;
    }
}

public class Pawn : Piece
{
    public Pawn(Presenter presenter) : base(presenter) { }
}

public class Rook : Piece
{
    public Rook(Presenter presenter) : base(presenter) { }
}

public class Presenter
{
    public string Image { get; set; }
    public string Color { get; set; }
}

Wie Sie gesehen haben, sollte der Presenter-Parameter auf jedem Gerät (Presentation Layer) unterschiedlich übergeben werden. Das heißt, Ihre Präsentationsebene entscheidet, wie jedes Stück dargestellt wird. Was ist an dieser Lösung falsch?

Frisches Blut
quelle
Zunächst muss das Stück wissen, dass die Präsentationsebene vorhanden ist und dass Bilder erforderlich sind. Was ist, wenn für einige Präsentationsebenen keine Bilder erforderlich sind? Zweitens muss das Stück von der UI-Ebene instanziiert werden, da ein Stück ohne einen Präsentator nicht existieren kann. Stellen Sie sich vor, das Spiel läuft auf einem Server, auf dem keine Benutzeroberfläche erforderlich ist. Dann können Sie ein Teil nicht instanziieren, da Sie keine Benutzeroberfläche haben.
Lishaak
Sie können den Repräsentanten auch als optionalen Parameter definieren.
Freshblood
0

Es gibt eine andere Lösung, mit der Sie die Benutzeroberfläche und die Domänenlogik vollständig abstrahieren können. Ihr Board sollte Ihrer UI-Ebene ausgesetzt sein, und Ihre UI-Ebene kann entscheiden, wie Teile und Positionen dargestellt werden sollen.

Um dies zu erreichen, können Sie Fen String verwenden . Die Fen-Zeichenfolge enthält im Grunde genommen Informationen zum Board-Status und gibt aktuelle Stücke und ihre Positionen an Board an. So kann Ihre Karte eine Methode haben, die den aktuellen Status der Karte über die Fen-Zeichenfolge zurückgibt. Dann kann Ihre UI-Ebene die Karte so darstellen, wie sie es wünscht. So funktioniert die aktuelle Schachengine. Schach-Engines sind Konsolenanwendungen ohne GUI, aber wir verwenden sie über eine externe GUI. Die Schachengine kommuniziert mit der GUI über Fenstrings und Schachnotation.

Sie fragen sich, was ist, wenn ich ein neues Stück hinzufüge? Es ist nicht realistisch, dass Schach eine neue Figur einführt. Das wäre eine große Veränderung in Ihrer Domain. Folgen Sie also dem YAGNI-Prinzip.

Frisches Blut
quelle
Nun, diese Lösung funktioniert auf jeden Fall und ich schätze die Idee. Es ist jedoch sehr auf Schach beschränkt. Ich habe Schach eher als Beispiel verwendet, um das allgemeine Problem zu veranschaulichen (das habe ich vielleicht in der Frage klarer ausgedrückt). Die von Ihnen vorgeschlagene Lösung kann nicht auf einem anderen Gebiet verwendet werden, und wie Sie richtig sagen, gibt es keine Möglichkeit, sie mit neuen Geschäftsobjekten (Teilen) zu erweitern. Es gibt in der Tat viele Möglichkeiten, das Schachspiel um weitere Teile zu erweitern ...
lishaak