Nettoyer la façon OOP de mapper un objet à son présentateur

13

Je crée un jeu de plateau (comme les échecs) en Java, où chaque pièce est de son propre type (comme Pawn , Rooketc.). Pour la partie GUI de l'application, j'ai besoin d'une image pour chacune de ces pièces. Puisque faire pense comme

rook.image();

viole la séparation de l'interface utilisateur et de la logique métier, je vais créer un présentateur différent pour chaque pièce, puis mapper les types de pièces à leurs présentateurs correspondants, comme

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

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

Jusqu'ici tout va bien. Cependant, je sens qu'un gourou de la POO prudent froncerait les sourcils en appelant ungetClass() méthode et suggérerait d'utiliser un visiteur par exemple comme ceci:

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

J'aime cette solution (merci, gourou), mais elle a un inconvénient majeur. Chaque fois qu'un nouveau type de pièce est ajouté à l'application, PieceVisitor doit être mis à jour avec une nouvelle méthode. Je voudrais utiliser mon système comme cadre de jeu de société où de nouvelles pièces pourraient être ajoutées grâce à un processus simple où l'utilisateur du cadre ne fournirait que la mise en œuvre de la pièce et de son présentateur, et le brancherait simplement dans le cadre. Ma question: existe-t-il une solution propre de POO sans instanceof, getClass()etc. qui permettrait ce type d'extensibilité?

lishaak
la source
Quel est le but d'avoir une propre classe pour chaque type de pièce d'échecs? Je pense qu'un objet pièce détient la position, la couleur et le type d'une pièce unique. J'aurais probablement une classe Piece et deux énumérations (PieceColor, PieceType) à cet effet.
VENIR DU
@COMEFROM bien, évidemment, différents types de pièces ont des comportements différents, il doit donc y avoir un code personnalisé qui distingue les tours et les pions par exemple. Cela dit, je préfère généralement une classe de pièces standard qui gère tous les types et utilise des objets de stratégie pour personnaliser le comportement.
Jules
@Jules et quel serait l'avantage d'avoir une stratégie pour chaque pièce par rapport à une classe séparée pour chaque pièce contenant le comportement en soi?
lishaak
2
Un avantage évident de séparer les règles du jeu des objets avec état qui représentent des pièces individuelles différentes est que cela résout votre problème immédiatement. Je ne mélangerais généralement pas le modèle de règle avec le modèle d'état lors de la mise en œuvre d'un jeu de plateau au tour par tour.
VENIR DU
2
Si vous ne voulez pas séparer les règles, vous pouvez définir les règles de mouvement dans les six objets PieceType (pas les classes!). Quoi qu'il en soit, je pense que la meilleure façon d'éviter le genre de problèmes auxquels vous êtes confronté est de séparer les préoccupations et d'utiliser l'héritage uniquement lorsque c'est vraiment utile.
VENIR DE

Réponses:

10

existe-t-il une solution de POO propre sans instanceof, getClass () etc. qui permettrait ce type d'extensibilité?

Oui il y a.

Permettez-moi de vous poser cette question. Dans vos exemples actuels, vous trouvez des façons de mapper des types de pièces à des images. Comment cela résout-il le problème du déplacement d'une pièce?

Une technique plus puissante que de demander le type est de suivre Tell, don't ask . Et si chaque pièce prenait une PiecePresenterinterface et ressemblait à ceci:

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

La construction ressemblerait à ceci:

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

L'utilisation ressemblerait à quelque chose comme:

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

L'idée ici est d'éviter de prendre la responsabilité de faire quelque chose dont d'autres choses sont responsables en ne posant pas de questions à ce sujet ni en prenant des décisions en fonction de cela. Au lieu de cela, tenez une référence à quelque chose qui sait quoi faire à propos de quelque chose et dites-lui de faire quelque chose à propos de ce que vous savez.

Cela permet le polymorphisme. Vous ne vous souciez pas de ce à quoi vous parlez. Vous ne vous souciez pas de ce qu'il a à dire. Vous vous souciez juste qu'il puisse faire ce que vous avez besoin de faire.

Un bon schéma qui maintient ces derniers dans des couches séparées, suit le dire-ne-demande, et montre comment ne pas la couche deux à la couche est injustement ce :

entrez la description de l'image ici

Il ajoute une couche de cas d'utilisation que nous n'avons pas utilisée ici (et peut certainement ajouter) mais nous suivons le même modèle que vous voyez dans le coin inférieur droit.

Vous remarquerez également que Presenter n'utilise pas l'héritage. Il utilise la composition. L'héritage devrait être un moyen de dernier recours pour obtenir le polymorphisme. Je préfère les designs qui privilégient la composition et la délégation. C'est un peu plus de frappe au clavier, mais c'est beaucoup plus de puissance.

candied_orange
la source
2
Je pense que c'est probablement une bonne réponse (+1), mais je ne suis pas convaincu que votre solution soit un bon exemple de l'architecture propre: l'entité Rook détient désormais très directement une référence à un présentateur. N'est-ce pas ce genre de couplage que la Clean Architecture essaie d'empêcher? Et ce que votre réponse ne dit pas tout à fait: la correspondance entre les entités et les présentateurs est désormais gérée par celui qui instancie l'entité. C'est probablement plus élégant que les solutions alternatives, mais c'est un troisième endroit à modifier lorsque de nouvelles entités sont ajoutées - maintenant, ce n'est plus un visiteur mais une usine.
amon
@amon, comme je l'ai dit, la couche de cas d'utilisation est manquante. Terry Pratchett a appelé ce genre de choses "des mensonges aux enfants". J'essaie de ne pas créer d'exemple écrasant. Si vous pensez que j'ai besoin d'être emmené à l'école où il s'agit de l'architecture propre, je vous invite à me prendre à la tâche ici .
candied_orange
désolé, non, je ne veux pas vous scolariser, je veux juste mieux comprendre ce modèle d'architecture. Je suis littéralement tombé sur les concepts d '«architecture propre» et «d'architecture hexagonale» il y a moins d'un mois.
amon
@amon dans ce cas, donnez-lui un bon coup d'oeil dur et ensuite prenez-moi à la tâche. J'adorerais si tu le faisais. Je suis toujours en train de comprendre des parties de cela moi-même. En ce moment, je travaille sur la mise à niveau d'un projet python piloté par menu vers ce style. Un examen critique de l'architecture propre peut être trouvé ici .
candied_orange
1
L'instanciation de @lishaak peut se produire principalement. Les couches internes ne connaissent pas les couches externes. Les couches externes ne connaissent que les interfaces.
candied_orange
5

Que dire de cela:

Votre modèle (les classes de figures) a des méthodes communes dont vous pourriez également avoir besoin dans un autre contexte:

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

Les images à utiliser pour afficher une certaine figure obtiennent des noms de fichiers par un schéma de dénomination:

King-White.png
Queen-Black.png

Ensuite, vous pouvez charger l'image appropriée sans accéder aux informations sur les classes java.

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

Je suis également intéressé par une solution générale pour ce type de problèmes lorsque j'ai besoin de joindre des informations (pas seulement des images) à un ensemble de classes potentiellement croissant. "

Je pense que vous ne devriez pas trop vous concentrer sur les cours . Pensez plutôt en termes d' objets métier .

Et la solution générique est une cartographie de toute nature. À mon humble avis, l'astuce consiste à déplacer ce mappage du code vers une ressource plus facile à maintenir.

Mon exemple fait ce mappage par convention qui est assez facile à implémenter et évite d'ajouter des informations liées à la vue dans le modèle économique . D'un autre côté, vous pouvez le considérer comme un mappage "caché" car il n'est exprimé nulle part.

Une autre option consiste à voir cela comme une analyse de rentabilisation distincte avec ses propres couches MVC, y compris une couche de persistance qui contient le mappage.

Timothy Truckle
la source
Je vois cela comme une solution très pratique et terre-à-terre pour ce scénario particulier. Je suis également intéressé par une solution générale pour ce type de problèmes lorsque j'ai besoin de joindre des informations (pas seulement des images) à un ensemble de classes potentiellement croissant.
lishaak
3
@lishaak: l'approche générale ici consiste à fournir suffisamment de métadonnées dans vos objets métier afin qu'un mappage général vers une ressource ou des éléments d'interface utilisateur puisse être effectué automatiquement.
Doc Brown
2

Je créerais une classe d'interface utilisateur / vue distincte pour chaque pièce contenant les informations visuelles. Chacune de ces classes à la pièce a un pointeur sur son homologue modèle / entreprise qui contient la position et les règles de jeu de la pièce.

Alors prenez un pion par exemple:

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

Cela permet une séparation complète de la logique et de l'interface utilisateur. Vous pouvez passer le pointeur de pièce logique à une classe de jeu qui gérerait le déplacement des pièces. Le seul inconvénient est que l'instanciation devrait se produire dans une classe d'interface utilisateur.

Lasse Jacobs
la source
OK, alors disons que j'en ai Piece* p. Comment savoir que je dois créer un PawnViewpour l'afficher, et non un RookViewou KingView? Ou dois-je créer une vue ou un présentateur d'accompagnement immédiatement chaque fois que je crée une nouvelle pièce? Ce serait essentiellement la solution de @ CandiedOrange avec les dépendances inversées. Dans ce cas, le PawnViewconstructeur peut également prendre un Pawn*, pas seulement un Piece*.
amon
Oui, je suis désolé, le constructeur PawnView prendrait un pion *. Et vous n'avez pas nécessairement à créer un PawnView et un Pawn en même temps. Supposons qu'il existe un jeu qui peut avoir 100 pions mais seulement 10 peuvent être visuels à la fois, dans ce cas, vous pouvez réutiliser les pions pour plusieurs pions.
Lasse Jacobs
Et je suis d'accord avec la solution de @ CandiedOrange, je pensais juste partager mes 2 cents.
Lasse Jacobs
0

J'aborderais cela en rendant Piecegénérique, où son paramètre est le type d'une énumération qui identifie le type de morceau, chaque morceau ayant une référence à un tel type. L'interface utilisateur pourrait alors utiliser une carte de l'énumération comme précédemment:

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

Cela présente deux avantages intéressants:

Tout d'abord, applicable à la plupart des langages typés statiquement: si vous paramétrez votre planche avec le type de pièce à attendre, vous ne pouvez pas y insérer le mauvais type de pièce.

Deuxièmement, et peut-être plus intéressant encore, si vous travaillez en Java (ou dans d'autres langages JVM), vous devez noter que chaque valeur d'énumération n'est pas seulement un objet indépendant, mais elle peut également avoir sa propre classe. Cela signifie que vous pouvez utiliser vos objets de type pièce comme objets de stratégie pour personnaliser le comportement de la pièce:

 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),
                    ....

(De toute évidence, les implémentations réelles doivent être plus complexes que cela, mais j'espère que vous avez l'idée)

Jules
la source
0

Je suis un programmeur pragmatique et je ne me soucie vraiment pas de ce qu'est une architecture propre ou sale. Je crois que les exigences et elles doivent être traitées de manière simple.

Votre exigence est que la logique de votre application d'échecs soit représentée sur différentes couches de présentation (appareils) comme sur le Web, une application mobile ou même une application console, vous devez donc prendre en charge ces exigences. Vous pouvez préférer utiliser des couleurs très différentes, des images de pièces sur chaque appareil.

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

Comme vous l'avez vu, le paramètre du présentateur doit être transmis différemment sur chaque périphérique (couche de présentation). Cela signifie que votre couche de présentation décidera comment représenter chaque pièce. Quel est le problème dans cette solution?

Sang frais
la source
Eh bien d'abord, la pièce doit savoir que la couche de présentation est là et qu'elle nécessite des images. Que faire si une couche de présentation ne nécessite pas d'images? Deuxièmement, la pièce doit être instanciée par la couche d'interface utilisateur, car une pièce ne peut pas exister sans présentateur. Imaginez que le jeu s'exécute sur un serveur où aucune interface utilisateur n'est requise. Ensuite, vous ne pouvez pas instancier un morceau parce que vous n'avez pas d'interface utilisateur.
lishaak
Vous pouvez également définir le représentant comme paramètre facultatif.
Freshblood
0

Il existe une autre solution qui vous aidera à abstraire complètement l'interface utilisateur et la logique du domaine. Votre carte doit être exposée à votre couche d'interface utilisateur et votre couche d'interface utilisateur peut décider comment représenter les pièces et les positions.

Pour ce faire, vous pouvez utiliser la chaîne Fen . La chaîne de fenêtre est essentiellement des informations sur l'état du tableau et donne les pièces actuelles et leurs positions à bord. Ainsi, votre carte peut avoir une méthode qui retourne l'état actuel de la carte via la chaîne Fen, puis votre couche d'interface utilisateur peut représenter la carte comme elle le souhaite. C'est en fait ainsi que fonctionnent les moteurs d'échecs actuels. Les moteurs d'échecs sont des applications console sans interface graphique, mais nous les utilisons via une interface graphique externe. Le moteur d'échecs communique avec l'interface graphique via des chaînes de fen et une notation d'échecs.

Vous demandez que si j'ajoute un nouveau morceau? Il n'est pas réaliste que les échecs introduisent une nouvelle pièce. Ce serait un énorme changement dans votre domaine. Suivez donc le principe YAGNI.

Sang frais
la source
Eh bien, cette solution est certainement fonctionnelle et j'apprécie l'idée. Cependant, il est très limité aux échecs. J'ai utilisé les échecs davantage comme exemple pour illustrer le problème général (j'ai peut-être clarifié cela dans la question). La solution que vous proposez ne peut pas être utilisée dans un autre domaine et comme vous le dites correctement, il n'y a aucun moyen de l'étendre avec de nouveaux objets métier (pièces). Il existe en effet de nombreuses façons d'étendre les échecs avec plus de pièces ....
lishaak