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 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?
fuente
Respuestas:
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
PiecePresenter
interfaz y se viera así?La construcción se vería así:
El uso se vería así:
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 :
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.
fuente
¿Qué hay de eso?
Su modelo (las clases de figuras) también tienen métodos comunes que podría necesitar en otro contexto:
Las imágenes que se utilizarán para mostrar una determinada figura obtienen nombres de archivo mediante un esquema de nomenclatura:
Luego puede cargar la imagen apropiada sin acceder a información sobre las clases de Java.
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.
fuente
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:
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.
fuente
Piece* p
. ¿Cómo sé que tengo que crear unPawnView
para mostrarlo, y no unRookView
oKingView
? ¿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, elPawnView
constructor también podría tomar unPawn*
, no solo unPiece*
.Me acercaría a esto haciendo
Piece
gené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: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:
(Obviamente, las implementaciones reales deben ser más complejas que eso, pero espero que entiendas la idea)
fuente
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.
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?
fuente
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.
fuente