Maneira limpa de OOP de mapear um objeto para seu apresentador

13

Estou criando um jogo de tabuleiro (como xadrez) em Java, onde cada peça é seu próprio tipo (como Pawn, Rooketc.). Para a parte GUI do aplicativo, preciso de uma imagem para cada uma dessas peças. Desde fazendo pensa como

rook.image();

violar a separação da interface do usuário e da lógica de negócios, criarei um apresentador diferente para cada peça e, em seguida, mapeei os tipos de peça para seus apresentadores correspondentes, como

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

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

Por enquanto, tudo bem. No entanto, eu sinto que um guru prudente de OOP desaprovaria chamar um getClass()método e sugeriria o uso de um visitante, por exemplo:

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

Eu gosto dessa solução (obrigado, guru), mas tem uma desvantagem significativa. Sempre que um novo tipo de peça é adicionado ao aplicativo, o PieceVisitor precisa ser atualizado com um novo método. Eu gostaria de usar meu sistema como uma estrutura de jogo de tabuleiro, onde novas peças poderiam ser adicionadas através de um processo simples, em que o usuário da estrutura forneceria apenas a implementação da peça e de seu apresentador, e simplesmente a conectaria à estrutura. Minha pergunta: existe uma solução limpa de POO sem instanceof, getClass()etc. , que permita esse tipo de extensibilidade?

lishaak
fonte
Qual é o propósito de ter uma classe própria para cada tipo de peça de xadrez? Eu acho que um objeto de peça mantém a posição, a cor e o tipo de uma peça única. Eu provavelmente teria uma classe Piece e duas enumerações (PieceColor, PieceType) para esse fim.
Vem de
@ COMEFROM bem, obviamente, tipos diferentes de peças têm comportamentos diferentes, então é preciso haver um código personalizado que distinga entre digamos gralhas e peões. Dito isto, geralmente preferiria ter uma classe de peça padrão que lida com todos os tipos e usa objetos de estratégia para personalizar o comportamento.
Jules
@Jules e qual seria o benefício de ter uma estratégia para cada peça em vez de ter uma classe separada para cada peça que contenha o comportamento em si?
Lishaak # 23/17
2
Um benefício claro de separar as regras do jogo de objetos com estado que repercutem peças individuais é que ele resolve seu problema imediatamente. Geralmente, eu não misturava o modelo de regra com o modelo de estado ao implementar um jogo de tabuleiro baseado em turnos.
Venha de
2
Se você não quiser separar as regras, poderá definir as regras de movimento nos seis objetos PieceType (não nas classes!). Enfim, acho que a melhor maneira de evitar o tipo de problema que você está enfrentando é separar as preocupações e usar a herança apenas quando for realmente útil.
VENHA DE

Respostas:

10

existe uma solução limpa de POO sem instanceof, getClass () etc. que permita esse tipo de extensibilidade?

Sim existe.

Deixe-me perguntar isso. Nos exemplos atuais, você encontra maneiras de mapear tipos de peças para imagens. Como isso resolve o problema de uma peça ser movida?

Uma técnica mais poderosa do que perguntar sobre o tipo é seguir Tell, não pergunte . E se cada peça tivesse uma PiecePresenterinterface e tivesse a seguinte aparência:

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

A construção ficaria assim:

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

O uso seria algo como:

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

A idéia aqui é evitar assumir a responsabilidade de fazer qualquer coisa pela qual outras coisas sejam responsáveis, não perguntando sobre ela nem tomando decisões com base nela. Em vez disso, mantenha uma referência a algo que sabe o que fazer sobre algo e diga-o para fazer algo sobre o que você sabe.

Isso permite polimorfismo. Você não se importa com o que está falando. Você não se importa com o que tem a dizer. Você apenas se importa que ele possa fazer o que você precisa.

Um bom diagrama que os mantém em camadas separadas, segue o dizer não pergunte e mostra como não acoplar injustamente camada a camada é o seguinte :

insira a descrição da imagem aqui

Ele adiciona uma camada de caso de uso que não usamos aqui (e certamente podemos adicionar), mas estamos seguindo o mesmo padrão que você vê no canto inferior direito.

Você também notará que o Presenter não usa herança. Ele usa composição. A herança deve ser uma maneira de último recurso para obter polimorfismo. Eu prefiro designs que favorecem o uso de composição e delegação. É um pouco mais de digitação do teclado, mas é muito mais potente.

candied_orange
fonte
2
Acho que essa é provavelmente uma boa resposta (+1), mas não estou convencida de que sua solução seja um bom exemplo da Arquitetura Limpa: a entidade Rook agora, de maneira muito direta, faz referência direta a um apresentador. Não é esse tipo de acoplamento que a Arquitetura Limpa está tentando impedir? E o que sua resposta não esclarece: o mapeamento entre entidades e apresentadores agora é tratado por quem a instancia. Provavelmente, isso é mais elegante do que soluções alternativas, mas é um terceiro lugar a ser modificado quando novas entidades são adicionadas - agora, não é um Visitante, mas uma Fábrica.
amon
@ amon, como eu disse, está faltando a camada de casos de uso. Terry Pratchett chamou esse tipo de coisa de "mentiras para crianças". Estou tentando não criar um exemplo impressionante. Se você acha que preciso ir para a escola onde se trata de Arquitetura Limpa, convido você a me levar para a tarefa aqui .
Candied_orange 21/05
desculpe, não, eu não quero te ensinar, só quero entender melhor esse padrão de arquitetura. Eu literalmente me deparei com os conceitos de “arquitetura limpa” e “arquitetura hexagonal” menos de um mês atrás.
amon
@ amon, nesse caso, dê uma boa olhada e leve-me à tarefa. Eu adoraria se você fizesse. Eu ainda estou descobrindo partes disso sozinho. No momento, estou trabalhando na atualização de um projeto python orientado a menus para esse estilo. Uma revisão crítica da arquitetura limpa pode ser encontrada aqui .
Candied_orange 21/05
1
A instanciação @lishaak pode acontecer principalmente. As camadas internas não conhecem as camadas externas. As camadas externas conhecem apenas as interfaces.
Candied_orange 25/05
5

Que tal:

Seu modelo (as classes de figuras) também possui métodos comuns que podem ser necessários em outro contexto:

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

As imagens a serem usadas para exibir uma determinada figura obtêm nomes de arquivos por um esquema de nomenclatura:

King-White.png
Queen-Black.png

Em seguida, você pode carregar a imagem apropriada sem acessar informações sobre as classes java.

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

Também estou interessado na solução geral para esse tipo de problemas quando preciso anexar algumas informações (não apenas imagens) a um conjunto potencialmente crescente de classes ".

Eu acho que você não deveria se concentrar tanto nas aulas . Em vez disso, pense em termos de objetos de negócios .

E a solução genérica é um mapeamento de qualquer tipo. IMHO, o truque é mover esse mapeamento do código para um recurso mais fácil de manter.

Meu exemplo faz esse mapeamento por convenção, que é bastante fácil de implementar e evita adicionar informações relacionadas à exibição no modelo de negócios . Por outro lado, você pode considerá-lo um mapeamento "oculto" porque não é expresso em lugar algum.

Outra opção é ver isso como um caso de negócios separado com suas próprias camadas MVC, incluindo uma camada de persistência que contém o mapeamento.

Timothy Truckle
fonte
Eu vejo isso como uma solução muito prática e prática para este cenário em particular. Também estou interessado na solução geral para esse tipo de problema quando preciso anexar algumas informações (não apenas imagens) a um conjunto potencialmente crescente de classes.
Lishaak # 21/17
3
@lishaak: a abordagem geral aqui é fornecer metadados suficientes em seus objetos de negócios para que um mapeamento geral para um recurso ou elementos da interface do usuário possa ser feito automaticamente.
Doc Brown
2

Eu criaria uma classe UI / view separada para cada peça que contenha as informações visuais. Cada uma dessas classes de visualização por partes tem um ponteiro para o modelo / empresa correspondente, que contém a posição e as regras do jogo da peça.

Então, pegue um peão, por exemplo:

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

Isso permite a separação completa da lógica e da interface do usuário. Você pode passar o ponteiro da peça lógica para uma classe de jogo que lidaria com a movimentação das peças. A única desvantagem é que a instanciação teria que acontecer em uma classe de interface do usuário.

Lasse Jacobs
fonte
OK, então vamos dizer que eu tenho alguns Piece* p. Como sei que preciso criar um PawnViewpara exibi-lo, e não um RookViewou KingView? Ou preciso criar uma visualização ou apresentador imediatamente sempre que criar uma nova peça? Basicamente, essa seria a solução do @ CandiedOrange com as dependências invertidas. Nesse caso, o PawnViewconstrutor também pode usar a Pawn*, não apenas a Piece*.
amon
Sim, desculpe, o construtor PawnView levaria um Pawn *. E você não precisa necessariamente criar um PawnView e um Pawn ao mesmo tempo. Suponha que exista um jogo com 100 peões, mas apenas 10 possam ser visuais de uma só vez; nesse caso, você pode reutilizar as visualizações de peões para vários peões.
Lasse Jacobs
E eu concordo com a solução da @ CandiedOrange. Eu gostaria de compartilhar meus 2 centavos.
Lasse Jacobs
0

Eu abordaria isso tornando Piecegenérico, onde seu parâmetro é o tipo de uma enumeração que identifica o tipo de peça, cada peça tendo uma referência a um desses tipos. Em seguida, a interface do usuário poderia usar um mapa da enumeração como antes:

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

Isso tem duas vantagens interessantes:

Primeiro, aplicável à maioria das linguagens de tipo estaticamente: se você parametrizar sua placa com o tipo de peça a ser visualizada, não poderá inserir o tipo errado de peça nela.

Segundo, e talvez o mais interessante, se você estiver trabalhando em Java (ou outras linguagens JVM), observe que cada valor de enum não é apenas um objeto independente, mas também pode ter sua própria classe. Isso significa que você pode usar seus objetos de tipo de peça como objetos de estratégia para informar o comportamento da peça:

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

(Obviamente, as implementações reais precisam ser mais complexas que isso, mas espero que você entenda)

Jules
fonte
0

Sou programador pragmático e realmente não me importo com o que é arquitetura limpa ou suja. Acredito que os requisitos e devem ser tratados de maneira simples.

Sua exigência é que a lógica do seu aplicativo de xadrez seja representada em diferentes camadas de apresentação (dispositivos), como na Web, aplicativo móvel ou mesmo aplicativo de console, portanto, você precisa dar suporte a esses requisitos. Você pode preferir usar cores muito diferentes, imagens de peças em cada 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; }
}

Como você viu, o parâmetro do apresentador deve ser passado em cada dispositivo (camada de apresentação) de maneira diferente. Isso significa que sua camada de apresentação decidirá como representar cada peça. O que há de errado nesta solução?

Sangue fresco
fonte
Bem, primeiro, a peça precisa saber que a camada de apresentação está lá e que requer imagens. E se alguma camada de apresentação não exigir imagens? Segundo, a peça precisa ser instanciada pela camada da interface do usuário, pois uma peça não pode existir sem um apresentador. Imagine que o jogo seja executado em um servidor onde não há interface do usuário necessária. Então você não pode instanciar uma peça porque você não tem interface do usuário.
lishaak 27/05
Você também pode definir o representador como parâmetro opcional.
Freshblood 5/06
0

Há outra solução que o ajudará a abstrair completamente a lógica da interface do usuário e do domínio. Seu painel deve ser exposto à sua camada de interface do usuário e sua camada de interface do usuário pode decidir como representar peças e posições.

Para conseguir isso, você pode usar a sequência Fen . Fen string é basicamente uma informação do estado do tabuleiro e fornece as peças atuais e suas posições a bordo. Para que seu quadro possa ter um método que retorne o estado atual do quadro via seqüência de caracteres Fen, sua camada de interface do usuário poderá representar o quadro como desejar. É assim que os atuais mecanismos de xadrez funcionam. Os mecanismos de xadrez são aplicativos de console sem GUI, mas os usamos por meio de uma GUI externa. O mecanismo de xadrez se comunica com a GUI por meio de seqüências de caracteres fen e notação de xadrez.

Você está perguntando isso e se eu adicionar uma nova peça? Não é realista que o xadrez introduza uma nova peça. Isso seria uma grande mudança no seu domínio. Então, siga o princípio YAGNI.

Sangue fresco
fonte
Bem, esta solução é certamente funcional e eu aprecio a ideia. No entanto, é muito limitado ao xadrez. Usei o xadrez mais como exemplo para ilustrar o problema geral (talvez eu tenha deixado isso mais claro na questão). A solução que você propõe não pode ser usada em outro domínio e, como você diz corretamente, não há como estendê-la com novos objetos de negócios (peças). Há de fato muitas maneiras de estender xadrez com peças mais ....
lishaak