Forma limpia de OOP de mapear 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, Rooketc.). 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 asignaré 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 aquí todo bien. Sin embargo, siento que un gurú prudente de OOP frunciría el ceño al llamar a un getClass()método y sugeriría usar un visitante, por ejemplo, así:

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 en el que 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 se conectaría al marco. Mi pregunta: ¿existe una solución de programación orientada a objetos limpios y sin instanceof, getClass()etc., que permita este tipo de extensibilidad?

lishaak
fuente
¿Cuál es el propósito de tener una clase propia para cada tipo de pieza de ajedrez? Creo que un objeto pieza mantiene la posición, el color y el tipo de una sola pieza. Probablemente tendría una clase Piece y dos enumeraciones (PieceColor, PieceType) para ese propósito.
VENIDO DEL
@COMEFROM, obviamente, diferentes tipos de piezas tienen comportamientos diferentes, por lo que debe haber algún código personalizado que distinga entre dichos grajos y peones. Dicho esto, generalmente preferiría tener una clase de pieza estándar que maneje todos los tipos y use objetos de Estrategia para personalizar el comportamiento.
Julio
@Jules y ¿cuál sería el beneficio de tener una estrategia para cada pieza en lugar de tener una clase separada para cada pieza que contiene el comportamiento en sí mismo?
lishaak
2
Un beneficio claro de separar las reglas del juego de los objetos con estado que representan piezas individuales es que resuelve su problema de inmediato. En general, no mezclaría el modelo de reglas con el modelo de estado al implementar un juego de mesa por turnos.
VENIDO DEL
2
Si no desea separar las reglas, puede definir las reglas de movimiento en los seis objetos PieceType (¡no clases!). De todos modos, creo que la mejor manera de evitar el tipo de problemas que enfrenta es separar las preocupaciones y usar la herencia solo cuando sea realmente útil.
VENIDO DEL

Respuestas:

10

¿Existe una solución OOP 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 una pieza que se mueve?

Una técnica más poderosa que preguntar sobre el tipo es seguir a Tell, no preguntar . ¿Qué pasaría si cada pieza tomara una PiecePresenterinterfaz y se viera así?

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 así:

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 al respecto 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 con respecto a lo que sabe.

Esto permite el polimorfismo. No te importa lo que estás hablando. No te importa lo que tenga que decir. Solo le importa que pueda hacer lo que necesita hacer.

Un buen diagrama que los mantiene en capas separadas, sigue a tell-don't-ask y muestra cómo no acoplar injustamente capa a capa es esto :

ingrese la descripción de la imagen aquí

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

También notará que el presentador no usa la herencia. Utiliza composición. La herencia debería ser una forma de último recurso para obtener el polimorfismo. Prefiero diseños que favorecen el uso de composición y delegación. Es un poco más de teclado, pero es mucho más poder.

naranja confitada
fuente
2
Creo que esta es probablemente una buena respuesta (+1), pero no estoy convencido de que su solución sea un buen ejemplo de la Arquitectura limpia: la entidad Rook ahora tiene una referencia muy directa a un presentador. ¿No es este tipo de acoplamiento lo que la arquitectura limpia está tratando de evitar? Y lo que su respuesta no explica bien: la asignación entre entidades y presentadores ahora es manejada por quien crea una instancia de la entidad. Probablemente sea más elegante que las soluciones alternativas, pero es un tercer lugar para ser modificado cuando se agregan nuevas entidades; ahora, no es un Visitante sino una Fábrica.
amon
@amon, como dije, falta la capa de caso de uso. Terry Pratchett llamó a este tipo de cosas "mentiras a los niños". Estoy tratando de no crear un ejemplo que sea abrumador. Si crees que necesito que me lleven a la escuela en lo que respecta a la arquitectura limpia, te invito a llevarme a la tarea aquí .
candied_orange
lo siento, no, no quiero enseñarte, solo quiero entender mejor este patrón de arquitectura. Literalmente me encontré con los conceptos de "arquitectura limpia" y "arquitectura hexagonal" hace menos de un mes.
amon
@amon en ese caso dale una buena mirada y luego llévame a la tarea. Me encantaría si lo hicieras. Todavía estoy descubriendo partes de esto yo mismo. En este momento estoy trabajando en actualizar un proyecto de Python controlado por menú a este estilo. Una revisión crítica de Clean Architecture se puede encontrar aquí .
candied_orange
1
La instanciación de @lishaak puede suceder en main. Las capas internas no conocen las capas externas. Las capas externas solo conocen las interfaces.
candied_orange
5

¿Qué hay de eso?

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

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

Las imágenes que se utilizarán para mostrar una determinada figura obtienen nombres de archivo mediante un esquema de nomenclatura:

King-White.png
Queen-Black.png

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

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

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

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

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 negocios separado con sus propias capas MVC, incluida una capa de persistencia que contiene la asignación.

Timothy Truckle
fuente
Veo esto como una solución muy práctica y práctica para este escenario en particular. También estoy interesado en una solución general para este tipo de problemas cuando necesito adjuntar información (no solo imágenes) a un conjunto de clases potencialmente creciente.
lishaak
3
@lishaak: el enfoque general aquí es proporcionar suficientes metadatos en sus objetos de negocio para que se pueda realizar una asignación general a un recurso o elementos de la interfaz de usuario de forma automática.
Doc Brown
2

Crearía una clase de UI / vista separada para cada pieza que contiene la información visual. Cada una de estas clases de vista de pieza tiene un puntero a su contraparte de modelo / negocio que contiene la posición y las reglas del juego de la pieza.

Entonces tome 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 la 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 instanciación tendría que suceder en una clase de IU.

Lasse Jacobs
fuente
OK, entonces digamos que tengo un poco Piece* p. ¿Cómo sé que tengo que crear un PawnViewpara mostrarlo, y no un RookViewo KingView? ¿O tengo que crear una vista acompañante o presentador inmediatamente cada vez que creo una nueva pieza? Esa sería básicamente la solución de @ CandiedOrange con las dependencias invertidas. En ese caso, el PawnViewconstructor también podría tomar un Pawn*, no solo un Piece*.
amon
Sí, lo siento, el constructor de PawnView tomaría un Pawn *. Y no necesariamente tiene que crear un PawnView y un Pawn al mismo tiempo. Supongamos que hay un juego que puede tener 100 Peones, pero solo 10 pueden ser visuales a la vez, en este caso puede reutilizar las vistas de peones para múltiples Peones.
Lasse Jacobs
Y estoy de acuerdo con la solución de @ CandiedOrange, aunque compartiría mis 2 centavos.
Lasse Jacobs
0

Me acercaría a esto haciendo Piecegenérico, donde su parámetro es el tipo de una enumeración que identifica el tipo de pieza, cada pieza tiene una referencia a uno de esos tipos. Luego, la interfaz de usuario 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 lenguajes tipados estáticamente: si parametriza su tablero con el tipo de pieza a esperar, no puede insertar el tipo de pieza incorrecto en él.

En segundo lugar, 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 pieza como objetos de Estrategia para mostrarle al 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 entiendas la idea)

Jules
fuente
0

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

Su requisito 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 cumplir con estos requisitos. Es posible que prefiera usar colores muy diferentes, imágenes de piezas 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 del presentador debe pasarse en cada dispositivo (capa de presentación) de manera diferente. Eso significa que su capa de presentación decidirá cómo representar cada pieza. ¿Qué hay de malo en esta solución?

Sangre fresca
fuente
Bueno, primero, la pieza tiene que saber que la capa de presentación está allí y que requiere imágenes. ¿Qué pasa si alguna capa de presentación no requiere imágenes? Segundo, la pieza tiene que ser instanciada por la capa UI, ya que una pieza no puede existir sin un presentador. Imagine que el juego se ejecuta en un servidor donde no se necesita interfaz de usuario. Entonces no puede crear una instancia de una pieza porque no tiene interfaz de usuario.
lishaak
También puede definir representanter como parámetro opcional.
Freshblood
0

Hay otra solución que lo ayudará a abstraer la interfaz de usuario y la lógica de dominio por completo. Su tablero debe estar expuesto a su capa de interfaz de usuario y su capa de interfaz de usuario puede decidir cómo representar piezas y posiciones.

Para lograr esto, puedes usar la cuerda Fen . La cadena Fen es básicamente información del estado del tablero y proporciona las piezas actuales y sus posiciones a bordo. Por lo tanto, su placa puede tener un método que devuelva el estado actual de la placa a través de la cadena Fen, luego su capa de interfaz de usuario puede representar la placa como lo 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 cadenas de fen y notación de ajedrez.

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

Sangre fresca
fuente
Bueno, esta solución es ciertamente funcional y aprecio la idea. Sin embargo, está muy limitado al ajedrez. Usé el ajedrez más como ejemplo para ilustrar el problema general (puede que lo haya aclarado en la pregunta). La solución que propone no se puede utilizar en otro dominio y, como usted dice correctamente, no hay forma de extenderla con nuevos objetos comerciales (piezas). De hecho hay muchas maneras de extender ajedrez con piezas más ....
lishaak