Modo pulito OOP di mappare un oggetto al suo presentatore


13

Sto creando un gioco da tavolo (come gli scacchi) in Java, dove ogni pezzo è del suo tipo (come Pawn, Rookecc.). Per la parte GUI dell'applicazione ho bisogno di un'immagine per ciascuno 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 pezzo ai presentatori 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 guru OOP prudente si acciglierebbe nel chiamare un getClass()metodo e suggerirebbe di usare un visitatore per esempio come questo:

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, PieceVisitor deve essere aggiornato con un nuovo metodo. Vorrei utilizzare il mio sistema come framework di giochi da tavolo in cui i nuovi pezzi potevano essere aggiunti attraverso un semplice processo in cui l'utente del framework avrebbe fornito l'implementazione sia del pezzo che del suo presentatore, e semplicemente lo avrebbe inserito nel framework. La mia domanda: esiste una soluzione OOP pulita senza instanceof, getClass()ecc. Che consentirebbe questo tipo di estensibilità?


Qual è lo scopo di avere una propria classe per ogni tipo di pezzo degli scacchi? Credo che un oggetto pezzo mantenga la posizione, il colore e il tipo di un singolo pezzo. Probabilmente avrei una classe Piece e due enumerazioni (PieceColor, PieceType) a tale scopo.
VIENI DAL

@COMEFROM bene, ovviamente diversi tipi di pezzi hanno comportamenti diversi, quindi c'è bisogno di un codice personalizzato che distingua tra dire torre e pedone. Detto questo, generalmente preferirei avere una classe di pezzi standard che gestisca tutti i tipi e usi gli oggetti Strategia per personalizzare il comportamento.
Jules,

@Jules e quale sarebbe il vantaggio di avere una strategia per ogni pezzo rispetto ad avere una classe separata per ogni pezzo che contiene il comportamento in sé?
lishaak,

2
Un chiaro vantaggio della separazione delle regole del gioco dagli oggetti con stato che i singoli pezzi di rilievo è che risolve immediatamente il problema. Generalmente non mescolerei affatto il modello di regole con il modello di stato quando implemento un gioco da tavolo a turni.
VIENI DAL

2
Se non vuoi separare le regole, puoi definire le regole di movimento nei sei oggetti PieceType (non le classi!). Comunque, penso che il modo migliore per evitare il tipo di problemi che stai affrontando sia quello di separare le preoccupazioni e di usare l'eredità solo quando è veramente utile.
VIENI DAL

Risposte:


10

esiste una soluzione OOP pulita senza instanceof, getClass () ecc. che consentirebbe questo tipo di estensibilità?

Si C'è.

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

Una tecnica più potente della domanda sul tipo è seguire Tell, non chiedere . E se ogni pezzo avesse PiecePresenterun'interfaccia 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 sono responsabili altre cose non chiedendole né prendendo decisioni basate su di essa. Tieni invece un riferimento a qualcosa che sa cosa fare di qualcosa e digli di fare qualcosa di ciò che sai.

Ciò consente il polimorfismo. Non ti interessa di cosa stai parlando. Non ti interessa quello che ha da dire. Ti importa solo che possa fare quello che ti serve.

Un buon diagramma che li mantiene in livelli separati, segue tell-Don't-ask e mostra come non accoppiare i layer a layer ingiustamente è questo :

inserisci qui la descrizione dell'immagine

Aggiunge un livello di caso d'uso che non abbiamo usato qui (e che possiamo sicuramente aggiungere) ma stiamo seguendo lo stesso modello che vedi nell'angolo in basso a destra.

Noterai anche che Presenter non usa l'ereditarietà. Usa la composizione. L'ereditarietà dovrebbe essere l'ultima via per ottenere il polimorfismo. Preferisco i disegni che preferiscono usare la composizione e la delega. Digita un po 'più la tastiera, ma è molto più potente.


2
Penso che questa sia probabilmente una buona risposta (+1), ma non sono convinto che la tua soluzione sia un buon esempio di architettura pulita: l'entità Rook ora detiene direttamente un riferimento a un presentatore. Non è questo tipo di accoppiamento che Clean Architecture sta cercando di prevenire? E ciò che la tua risposta non chiarisce del tutto: la mappatura tra entità e presentatori è ora gestita da chiunque istanzia l'entità. Questo è probabilmente più elegante delle soluzioni alternative, ma è un terzo posto da modificare quando vengono aggiunte nuove entità: ora non è un visitatore ma una fabbrica.
amon,

@amon, come ho detto, manca lo strato del caso d'uso. Terry Pratchett ha definito questo tipo di cose "bugie per i bambini". Sto cercando di non creare un esempio travolgente. Se si pensa che ho bisogno di essere preso a scuola, dove si tratta di Clean Architettura vi invito a portarmi a compito qui .
candied_orange

scusa, no, non voglio insegnarti, voglio solo capire meglio questo modello di architettura. Mi sono imbattuto letteralmente nei concetti di "architettura pulita" e "architettura esagonale" meno di un mese fa.
amon,

@amon in quel caso dai una buona occhiata e poi portami al compito. Mi piacerebbe se lo facessi. Sto ancora scoprendo parti di questo da solo. Al momento sto lavorando per aggiornare un progetto Python guidato da menu a questo stile. Una recensione critica di Clean Architecture è disponibile qui .
candied_orange

1
L'istanza di @lishaak può avvenire principalmente. Gli strati interni non conoscono gli strati esterni. Gli strati esterni conoscono solo le interfacce.
candied_orange,

5

Che dire di questo:

Il tuo modello (le classi di figure) ha metodi comuni che potresti aver bisogno anche in altri contesti:

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

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

King-White.png
Queen-Black.png

Quindi è possibile 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)));

Sono anche interessato a una soluzione generale per questo tipo di problemi quando devo allegare alcune informazioni (non solo immagini) a un insieme di classi potenzialmente in crescita. "

Penso che non dovresti concentrarti così tanto sulle lezioni . 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 fa 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 è di vedere questo come un caso aziendale separato con i propri livelli MVC, incluso un livello di persistenza che contiene il mapping.


Lo considero una soluzione molto pratica e concreta per questo particolare scenario. Sono anche interessato a una soluzione generale per questo tipo di problemi quando devo allegare alcune informazioni (non solo immagini) a un insieme di classi potenzialmente in crescita.
lishaak,

3
@lishaak: l'approccio generale qui è quello di fornire sufficienti metadati nei tuoi oggetti business in modo che una mappatura generale su una risorsa o elementi dell'interfaccia utente possa essere fatta automaticamente.
Doc Brown,

2

Vorrei creare una classe UI / view 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 una pedina 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 la completa separazione della logica e dell'interfaccia utente. È possibile passare il puntatore del pezzo logico a una classe di gioco che gestisca lo spostamento dei pezzi. L'unico inconveniente è che l'istanza dovrebbe avvenire in una classe UI.


OK, quindi diciamo che ne ho alcuni Piece* p. Come faccio a sapere che devo creare un PawnViewper visualizzarlo e non un RookViewo KingView? O devo creare immediatamente una vista o un presentatore di accompagnamento ogni volta che creo un nuovo pezzo? Questa sarebbe sostanzialmente la soluzione di @ CandiedOrange con le dipendenze invertite. In tal caso, il PawnViewcostruttore potrebbe anche prendere un Pawn*, non solo un Piece*.
amon,

Sì, mi dispiace, il costruttore PawnView prenderebbe una pedina *. E non devi necessariamente creare una PawnView e una Pawn allo stesso tempo. Supponiamo che esista un gioco che può avere 100 pedine ma solo 10 possono essere visive contemporaneamente, in questo caso puoi riutilizzare le visualizzazioni di pedine per più pedine.
Lasse Jacobs,

E sono d'accordo con la soluzione di @ CandiedOrange, anche se condividerei i miei 2 centesimi.
Lasse Jacobs,

0

Mi avvicinerei a questo facendo un Piecegenerico, dove il suo parametro è il tipo di un elenco che identifica il tipo di pezzo, ogni pezzo con un riferimento a un tale tipo. 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:

In primo luogo, applicabile alla maggior parte delle lingue tipizzate staticamente: se parametrizzi la tua tavola con il tipo di pezzo su xpect, non puoi quindi inserire il tipo sbagliato di pezzo in esso.

In secondo luogo, e forse più interessante, se stai lavorando in Java (o in altri linguaggi JVM), dovresti notare che ogni valore enum non è solo un oggetto indipendente, ma può anche avere una sua classe. Ciò significa che è possibile utilizzare gli oggetti del tipo di pezzo come oggetti Strategia per personalizzare 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 così, ma spero che tu abbia l'idea)


0

Sono un programmatore pragmatico e non mi interessa davvero cosa sia un'architettura pulita o sporca. Credo che i requisiti debbano essere gestiti in modo semplice.

Il tuo requisito è che la logica dell'app per gli scacchi sia rappresentata su diversi livelli di presentazione (dispositivi) come sul Web, sull'app mobile o persino sull'app console, quindi devi supportare questi requisiti. Puoi preferire l'uso di colori molto diversi, immagini dei pezzi 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 presenter deve essere passato su ciascun 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?


Prima di tutto, il pezzo deve sapere che il livello di presentazione è lì e che richiede immagini. Cosa succede se un livello di presentazione non richiede immagini? In secondo luogo, il pezzo deve essere istanziato dal livello UI, poiché un pezzo non può esistere senza un presentatore. Immagina che il gioco funzioni su un server in cui non è necessaria l'interfaccia utente. Quindi non è possibile creare un'istanza di un pezzo perché non si dispone dell'interfaccia utente.
lishaak,

È inoltre possibile definire il rappresentatore come parametro opzionale.
Freshblood,

0

C'è un'altra soluzione che ti aiuterà a sottrarre completamente l'interfaccia utente e la logica del dominio. La tua board dovrebbe essere esposta al tuo layer UI e il tuo layer UI può decidere come rappresentare pezzi e posizioni.

Per raggiungere questo obiettivo, è possibile utilizzare la stringa Fen . La stringa Fen è sostanzialmente informazione sullo stato della scheda e fornisce i pezzi correnti e le loro posizioni a bordo. Quindi la tua scheda può avere un metodo che restituisce lo stato corrente della scheda tramite la stringa Fen, quindi il tuo livello UI può rappresentare la scheda come desidera. Questo è come funzionano gli attuali motori di scacchi. I motori di scacchi sono un'applicazione console senza GUI ma li usiamo tramite GUI esterna. Il motore di scacchi comunica alla GUI tramite stringhe di fen e notazione di scacchi.

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


Bene, questa soluzione è sicuramente funzionale e apprezzo l'idea. Tuttavia, è molto limitato agli scacchi. Ho usato più gli scacchi come esempio per illustrare il problema generale (potrei averlo chiarito meglio nella domanda). La soluzione che proponi non può essere utilizzata in altri domini e, come dici correttamente, non c'è modo di estenderla con nuovi oggetti business (pezzi). Esistono davvero molti modi per estendere gli scacchi con più pezzi ....
lishaak,
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.