Pulisce il modo OOP di mappare un oggetto al suo presentatore

13

Sto creando un gioco da tavolo (come gli scacchi) in Java, in cui ogni pezzo è di tipo proprio (come Pawn , Rook ecc.). Per la parte GUI dell'applicazione ho bisogno di un'immagine per ognuno di questi pezzi. Dal momento che fare pensa come

rook.image();

viola la separazione dell'interfaccia utente e della logica aziendale, creerò un presentatore diverso per ogni pezzo e quindi mapperò i tipi di pezzi ai corrispondenti corrispondenti come

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

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

Fin qui tutto bene. Tuttavia, sento che un prudente guru OOP aggrotta le sopracciglia chiamando un metodo getClass() e suggerirebbe di utilizzare un visitatore ad esempio in questo modo:

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

Mi piace questa soluzione (grazie, guru), ma ha uno svantaggio significativo. Ogni volta che un nuovo tipo di pezzo viene aggiunto all'applicazione, PezzoVisitor deve essere aggiornato con un nuovo metodo. Mi piacerebbe usare il mio sistema come una struttura di gioco da tavolo in cui nuovi pezzi potrebbero essere aggiunti attraverso un semplice processo in cui l'utente del framework fornirebbe solo l'implementazione sia del pezzo che del suo presentatore, e semplicemente collegarlo al framework. La mia domanda: esiste una soluzione OOP pulita senza instanceof , getClass() ecc. Che consentirebbe questo tipo di estensibilità?

    
posta lishaak 21.05.2017 - 16:30
fonte

6 risposte

10

is there a clean OOP solution without instanceof, getClass() etc. which would allow for this kind of extensibility?

Sì, c'è.

Lascia che ti chieda questo. Nei tuoi esempi attuali trovi modi per mappare i tipi di pezzi alle immagini. In che modo questo risolve il problema di un pezzo che viene spostato?

Una tecnica più potente che chiedere informazioni sul tipo è seguire Tell, non chiedere . Cosa accadrebbe se ogni pezzo avesse un'interfaccia PiecePresenter e assomigliasse a questo:

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 costruzione sarebbe simile a questa:

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

L'uso sarebbe simile a:

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

L'idea qui è di evitare di assumersi la responsabilità di fare qualsiasi cosa di cui altre cose sono responsabili non chiedendole né prendendo decisioni basate su di essa. Invece, tieni un riferimento a qualcosa che sa cosa fare su qualcosa e digli di fare qualcosa per quello che sai.

Questo consente il polimorfismo. Non si cura di cosa stai parlando. Non ti importa cosa ha da dire. Ti interessa solo che possa fare ciò che ti serve.

Un buon diagramma che mantiene questi in livelli separati, segue tell-don-ask, e mostra come non accoppiare strato a livello ingiustamente è questo :

Aggiunge un layer case use che non abbiamo usato qui (e può certamente aggiungere) ma stiamo seguendo lo stesso pattern che vedi nell'angolo in basso a destra.

Noterai che Presenter non usa l'ereditarietà. Usa la composizione.  L'ereditarietà dovrebbe essere un modo estremo per ottenere il polimorfismo. Preferisco i progetti che preferiscono l'uso di composizione e delega. È un po 'più digitato sulla tastiera ma è molto più potente.

    
risposta data 21.05.2017 - 17:28
fonte
5

Che dire di questo:

Il tuo modello (le classi figura) hanno metodi comuni che potrebbero essere necessari anche in altri contesti:

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

Le immagini da utilizzare per visualizzare una certa figura ottengono i nomi dei file secondo uno schema di denominazione:

King-White.png
Queen-Black.png

Quindi puoi caricare l'immagine appropriata senza accedere alle informazioni sulle classi java.

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

I am also interested in general solution for this kind of problems when I need to attach some information (not only images) to a potentially growing set of classes."

Penso che non dovresti concentrarti su classes così tanto. Piuttosto, pensa in termini di oggetti business .

E la soluzione generica è una mappatura di qualsiasi tipo. IMHO il trucco è spostare quella mappatura dal codice a una risorsa che è più facile da mantenere.

Il mio esempio esegue questa mappatura per convenzione che è abbastanza facile da implementare ed evita di aggiungere informazioni relative alla vista nel modello di business . D'altra parte potresti considerarlo una mappatura "nascosta" perché non è espressa da nessuna parte.

Un'altra opzione è vedere questo come un caso aziendale separato con i suoi livelli MVC, incluso un livello di persistenza che contiene il mapping.

    
risposta data 21.05.2017 - 16:52
fonte
2

Creerei una classe UI / vista separata per ogni pezzo che contiene le informazioni visive. Ognuna di queste classi pieceview ha un puntatore al suo modello / controparte commerciale che contiene la posizione e le regole di gioco del pezzo.

Quindi prendi un pedone per esempio:

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

Ciò consente una separazione completa della logica e dell'interfaccia utente. Puoi passare il puntatore del pezzo logico a una classe di gioco che gestirà lo spostamento dei pezzi. L'unico inconveniente è che l'istanziazione dovrebbe avvenire in una classe dell'interfaccia utente.

    
risposta data 21.05.2017 - 18:51
fonte
0

Mi avvicinerei a questo facendo Piece generico, dove il suo parametro è il tipo di un'enumerazione che identifica il tipo di pezzo, ogni pezzo che ha un riferimento a uno di questi tipi. Quindi l'interfaccia utente potrebbe utilizzare una mappa dall'enumerazione come prima:

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

Questo ha due vantaggi interessanti:

Innanzitutto, applicabile alla maggior parte delle lingue tipizzate in modo statico: se parametrizzi la tua scheda con il tipo di pezzo su xpect, non puoi inserire il tipo di pezzo sbagliato in essa.

Secondo, e forse più interessante, se stai lavorando in Java (o in altre lingue JVM), dovresti notare che ogni valore enum non è solo un oggetto indipendente, ma può avere anche una sua classe. Ciò significa che è possibile utilizzare gli oggetti del tipo di oggetto come oggetti Strategia per soddisfare il comportamento del pezzo:

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

(Ovviamente le implementazioni effettive devono essere più complesse di quelle, ma si spera che tu abbia l'idea)

    
risposta data 21.05.2017 - 19:27
fonte
0

Sono un programmatore pragmatico e non mi interessa davvero cosa sia l'architettura pulita o sporca. Credo nei requisiti e dovrebbe essere gestito in modo semplice.

La tua esigenza è che la logica delle tue app di scacchi sia rappresentata su diversi livelli di presentazione (dispositivi) come su web, app per dispositivi mobili o persino app per console, quindi devi supportare questi requisiti. Potresti preferire usare colori molto diversi, ritagliare le immagini su ciascun dispositivo.

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

Come hai visto, il parametro relatore deve essere trasmesso su ogni dispositivo (livello di presentazione) in modo diverso. Ciò significa che il tuo livello di presentazione deciderà come rappresentare ogni pezzo. Cosa c'è di sbagliato in questa soluzione?

    
risposta data 26.05.2017 - 00:57
fonte
0

C'è un'altra soluzione che ti aiuterà a astrarre completamente l'interfaccia utente e la logica del dominio. La tua scheda deve essere esposta al tuo livello di interfaccia utente e il tuo livello di interfaccia utente può decidere come rappresentare pezzi e posizioni.

Per raggiungere questo obiettivo, puoi utilizzare Stringa di coda . La stringa di Fen è fondamentalmente informazioni sullo stato della scheda e fornisce i pezzi correnti e le loro posizioni a bordo. Quindi la tua tavola può avere un metodo che restituisce lo stato attuale della tavola tramite una stringa di Fen, quindi il tuo livello di interfaccia utente può rappresentare la scheda a suo piacimento. Questo è in realtà il modo in cui funzionano gli attuali motori di scacchi. I motori di scacchi sono l'applicazione della console senza GUI, ma li usiamo tramite GUI esterna. Il motore di scacchi comunica alla GUI tramite stringhe e notazione degli scacchi.

Stai chiedendo che cosa succede se aggiungo un nuovo pezzo? Non è realistico che gli scacchi introdurranno un nuovo pezzo. Sarebbe un enorme cambiamento nel tuo dominio. Quindi segui il principio YAGNI.

    
risposta data 26.05.2017 - 23:23
fonte

Leggi altre domande sui tag