Limpie la forma OOP de asignar un objeto a su presentador

13

Estoy creando un juego de mesa (como el ajedrez) en Java, donde cada pieza es de su propio tipo (como Pawn , Rook , etc.). Para la parte GUI de la aplicación necesito una imagen para cada una de estas piezas. Ya que hacer piensa como

rook.image();

viola la separación de la interfaz de usuario y la lógica empresarial, crearé un presentador diferente para cada pieza y luego mapearé los tipos de piezas a sus presentadores correspondientes como

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

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

Hasta ahora todo bien. Sin embargo, siento que un gurú de OOP prudente frunciría el ceño al llamar al método getClass() y sugeriría usar un visitante como este:

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

Me gusta esta solución (gracias, gurú), pero tiene un inconveniente importante. Cada vez que se agrega un nuevo tipo de pieza a la aplicación, PieceVisitor debe actualizarse con un nuevo método. Me gustaría usar mi sistema como un marco de juego de mesa donde se podrían agregar nuevas piezas a través de un proceso simple en el que el usuario del marco solo proporcionaría la implementación tanto de la pieza como de su presentador, y simplemente lo conectaría al marco. Mi pregunta: ¿hay una solución de POO limpia sin instanceof , getClass() etc. que permitiría este tipo de extensibilidad?

    
pregunta lishaak 21.05.2017 - 16:30

6 respuestas

10
  

¿hay una solución de POO limpia sin instancia de, getClass () etc. que permitiría este tipo de extensibilidad?

Sí, hay.

Déjame preguntarte esto. En sus ejemplos actuales, está encontrando formas de asignar tipos de piezas a imágenes. ¿Cómo resuelve esto el problema de mover una pieza?

Una técnica más poderosa que preguntar sobre el tipo es seguir Dígale, no pregunte . ¿Qué pasaría si cada pieza tomara una interfaz PiecePresenter y se pareciera a esto?

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 construcción se vería así:

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

El uso se vería algo así como:

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

La idea aquí es evitar asumir la responsabilidad de hacer algo de lo que otras cosas son responsables al no preguntar ni tomar decisiones basadas en ello. En su lugar, haga una referencia a algo que sepa qué hacer con respecto a algo y dígale que haga algo sobre lo que sabe.

Esto permite el polimorfismo. No te importa lo que estás hablando. No te importa lo que tenga que decir. Solo te importa que pueda hacer lo que necesitas que se haga.

Un buen diagrama que los mantiene en capas separadas, sigue las indicaciones de no hacer preguntas y muestra cómo no acoplar capas a capas injustamente es this :

Agrega una capa de caso de uso que no hemos usado aquí (y ciertamente podemos agregar) pero estamos siguiendo el mismo patrón que se ve en la esquina inferior derecha.

También notará que el Presentador no usa herencia. Utiliza la composición.  La herencia debe ser un último recurso para obtener polimorfismo. Prefiero diseños que favorezcan el uso de la composición y la delegación. Es un poco más de teclado, pero es mucho más potente.

    
respondido por el candied_orange 21.05.2017 - 17:28
5

¿Qué pasa con eso?

Su modelo (las clases de figura) tienen también métodos comunes que podría necesitar en otro contexto:

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

Las imágenes que se usarán para mostrar una figura determinada obtienen nombres de archivos mediante un esquema de denominación:

King-White.png
Queen-Black.png

Luego puede cargar la imagen apropiada sin acceder a la información sobre las clases de java.

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

También me interesa la solución general para este tipo de problemas cuando necesito adjuntar información (no solo imágenes) a un conjunto potencialmente creciente de clases ".

Creo que no deberías enfocarte tanto en clases . Más bien piense en términos de objetos de negocio .

Y la solución genérica es un mapeo de cualquier tipo. En mi humilde opinión, el truco es mover esa asignación del código a un recurso que es más fácil de mantener.

Mi ejemplo hace este mapeo por convención que es bastante fácil de implementar y evita agregar información relacionada con la vista al modelo de negocio . Por otro lado, podría considerarlo un mapeo "oculto" porque no se expresa en ninguna parte.

Otra opción es ver esto como un caso de negocio separado con sus propias capas MVC, incluida una capa de persistencia que contiene la asignación.

    
respondido por el Timothy Truckle 21.05.2017 - 16:52
2

Yo crearía una clase de UI / vista por separado para cada pieza que contenga la información visual. Cada una de estas clases de vistas parciales tiene un puntero a su contraparte modelo / negocio que contiene la posición y las reglas de juego de la pieza.

Tomemos un peón, por ejemplo:

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

Esto permite una separación completa de la lógica y la interfaz de usuario. Puede pasar el puntero de la pieza lógica a una clase de juego que manejaría el movimiento de las piezas. El único inconveniente es que la creación de instancias tendría que ocurrir en una clase de UI.

    
respondido por el Lasse Jacobs 21.05.2017 - 18:51
0

Me acercaría a esto haciendo que Piece genérico, donde su parámetro es el tipo de enumeración que identifica el tipo de pieza, cada pieza tiene una referencia a uno de estos tipos. Luego, la IU podría usar un mapa de la enumeración 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); }

Esto tiene dos ventajas interesantes:

Primero, aplicable a la mayoría de los idiomas escritos de manera estática: si parametriza su tablero con el tipo de pieza a xpect, no puede insertar el tipo incorrecto de pieza.

Segundo, y quizás más interesante, si está trabajando en Java (u otros lenguajes JVM), debe tener en cuenta que cada valor de enumeración no es solo un objeto independiente, sino que también puede tener su propia clase. Esto significa que puede usar sus objetos de tipo de pieza como objetos de Estrategia para el cliente, el comportamiento de la pieza:

 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, las implementaciones reales deben ser más complejas que eso, pero espero que tengas la idea)

    
respondido por el Jules 21.05.2017 - 19:27
0

Soy un programador pragmático y realmente no me importa lo que es arquitectura limpia o sucia. Creo que los requisitos y debe ser manejado de manera sencilla.

Su requerimiento es que la lógica de su aplicación de ajedrez esté representada en diferentes capas de presentación (dispositivos) como en la web, la aplicación móvil o incluso la aplicación de consola, por lo que debe ser compatible con estos requisitos. Es posible que prefiera utilizar colores muy diferentes, piezas de imágenes en 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 ha visto, el parámetro de presentador se debe pasar a cada dispositivo (capa de presentación) de manera diferente. Eso significa que su capa de presentación decidirá cómo representar cada pieza. ¿Qué está mal en esta solución?

    
respondido por el Freshblood 26.05.2017 - 00:57
0

Hay otra solución que te ayudará a abstraer completamente la UI y la lógica del dominio. Su tablero debe estar expuesto a su capa de UI y su capa de UI puede decidir cómo representar piezas y posiciones.

Para lograr esto, puedes usar cadena de Fen . La cadena Fen es básicamente información del estado del tablero y proporciona las piezas actuales y sus posiciones a bordo. Así que su placa puede tener un método que devuelva el estado actual de la placa a través de la cadena Fen, entonces su capa UI puede representar la placa como desee. Así es como funcionan los motores de ajedrez actuales. Los motores de ajedrez son aplicaciones de consola sin GUI, pero los usamos a través de una GUI externa. El motor de ajedrez se comunica con la GUI a través de las cuerdas fen y la notación de ajedrez.

¿Estás preguntando qué sucede si agrego una nueva pieza? No es realista que el ajedrez introduzca una nueva pieza. Eso sería un gran cambio en tu dominio. Así que sigue el principio YAGNI.

    
respondido por el Freshblood 26.05.2017 - 23:23

Lea otras preguntas en las etiquetas