java - ventajas - Separación de modelo y vista/controlador en una aplicación de dibujo
modelo vista controlador php (4)
Consideraciones Básicas
Antes que nada, hagamos algunas definiciones para simplificar.
Entity
es un objeto de modelo de dominio, que define toda la estructura y el comportamiento, es decir, la lógica. EntityUI
es el control gráfico que representa la Entity
en la interfaz de usuario.
Entonces, básicamente, para las clases de Shape
creo que ShapeUI
debe ser muy consciente de la estructura de la Shape
. La estructura se compone principalmente de los puntos de control, supongo. En otras palabras, al tener toda la información sobre los puntos de control (quizás vectores en el futuro), ShapeUI
podrá dibujarse en la IU.
Sugerencias iniciales
Lo que sugeriría para las clases de Shape
, es que la clase Shape
define todo el comportamiento. La clase ShapeUI
tendrá conocimiento de la clase Shape
y mantendrá una referencia a la que está representando, por la cual tendrá acceso a los puntos de control, así como también podrá manipularlos, por ejemplo, establecer sus ubicaciones. El patrón Observer
simplemente solicita ser utilizado en este contexto. Particularmente, la clase Shape
puede implementar el Observable
y ShapeUI
implementará Observer
y se suscribirá al objeto Shape
correspondiente.
Entonces, básicamente, lo que sucederá en este caso, el objeto ShapeUI
manejará todas las operaciones de UI, y será responsable de actualizar los parámetros de Shape
, por ejemplo, las ubicaciones de los puntos de control. Posteriormente, tan pronto como ocurre una actualización de ubicación, el objeto Shape
ejecuta su lógica en el cambio de estado y luego ciegamente (sin darse cuenta de ShapeUI
) notifica a ShapeUI
sobre el estado actualizado. De manera correspondiente, ShapeUI
dibujará el nuevo estado. Aquí obtendrá modelos y vistas de bajo acoplamiento.
En cuanto a las Tools
, mi propia opinión es que cada Tool
debe saber cómo manipular cada tipo de Shape
, es decir, la lógica de manipulación por forma debe implementarse dentro de la clase de Tool
. Para desacoplar la vista y el modelo, es prácticamente el mismo que para Shape
. La clase ToolUI
maneja dónde se hace clic en el cursor, en qué ShapeUI
se hizo clic, en qué punto de control se hizo clic, etc. Al obtener esta información, ToolUI
pasará al objeto Tool
apropiado, que luego aplicará la lógica basada en los parámetros recibidos
Manejo de diferentes tipos de formas
Ahora, cuando se trata de la Tool
que trata a las diferentes Shape
a su manera, creo que el patrón Abstract Factory
interviene. Cada herramienta implementará una Abstract Factory
en la que proporcionaremos implementaciones de manipulación para cada tipo de Shape
.
Resumen
En base a lo que sugerí, aquí está el borrador del Modelo de Dominio:
Para extraer toda la idea de mis sugerencias, también estoy publicando el Diagrama de secuencia para un caso de uso específico:
Usando ToolUI
el usuario hace clic en ShapeUI
''s ControlPointUI
Estoy trabajando en una aplicación de dibujo vectorial (en java) y estoy luchando con la separación entre mis clases de modelo y las clases de vista / controlador.
Algunos antecedentes:
Puedes dibujar diferentes formas:
rectángulos, líneas y segmentos circulares
Hay 4 herramientas para manipular las formas en el lienzo:
scale-tool, move-tool, rotate-tool y morph-tool
Para esta pregunta, la herramienta de transformación es la más interesante: te permite cambiar una forma arrastrando uno de sus puntos y ajustando las otras propiedades como se muestra en este gráfico:
Estas reglas de transformación son diferentes para cada forma y creo que son parte de la lógica de negocios del modelo, pero de alguna manera deben estar expuestas a la vista / controlador (las clases de herramienta) para que puedan aplicar la correcta.
Además, las formas se representan internamente a través de diferentes valores: - El rectángulo se almacena como centro, ancho, alto, rotación - La línea se almacena como punto inicial y final - El segmento circular se almacena como centro, radio, ángulo1, ángulo2
Planeo agregar más formas en el futuro, como estrellas, burbujas de discurso o flechas, cada una con sus propios puntos de control.
También planeo agregar más herramientas en el futuro, como rotar o escalar grupos de formas.
Los puntos de control para cada herramienta son diferentes. Por ejemplo, cuando usa la herramienta de escala, no puede agarrar el punto central, pero cada punto de control de escala debe asociarse con un punto de pivote (o múltiples para que el usuario pueda elegir).
Para las formas simples como rectángulo, línea y empanada, los puntos de control son los mismos para cada instancia de la clase, pero las formas de futuro como una ruta bezier o una estrella (con conteo de puntos configurable) tendrían una cantidad diferente de puntos de control por instancia.
Entonces, la pregunta es ¿cuál es una buena manera de modelar e implementar estos puntos de control?
Como son ligeramente diferentes para cada herramienta y transportan datos específicos de la herramienta / controlador, pertenecen a la herramienta / controlador de alguna manera. Pero como también son específicos para cada tipo de forma y tienen una lógica de dominio muy importante, también pertenecen al modelo.
Me gustaría evitar la explosión combinatoria de agregar un tipo especial de punto de control para cada combinación de herramienta / forma cada vez que se agrega una herramienta o forma.
Actualización: Para dar otro ejemplo: en el futuro, puede ocurrir que tenga una idea para una nueva forma que quiero apoyar: el arco. Es similar al segmento circular pero se ve un poco diferente y se comporta completamente diferente al arrastrar los puntos de control.
Para implementar esto, me gustaría poder crear una clase ArcShape implementando mi interfaz Shape y listo.
En principio, es una buena idea hacer que el modelo coincida con la interfaz de dibujo. Entonces, por ejemplo, en Java Swing, se pueden dibujar rectángulos con el método drawRect
que toma como argumentos la x
, y
de la esquina superior izquierda, el ancho y la altura. Por lo tanto, normalmente querrá modelar un rectángulo como { x-UL, y-UL, width, height }
.
Para las rutas arbitrarias, incluidos los arcos, Swing proporciona el objeto GeneralPath con métodos para trabajar con una secuencia de puntos conectados por líneas o curvas cuadráticas / Bezier. Para modelar una ruta general, puede proporcionar una lista de puntos, una regla de devanado y los parámetros necesarios de una curva cuadrática o una curva de Bezier.
No esperaría que surgiera un buen diseño sin tener que descifrar la codificación y abordar problemas reales. Pero si no sabes por dónde empezar aquí está mi propuesta.
inteface Shape {
List<Point> getPoints(ToolsEnum strategy); // you could use factory here
}
interface Point {
Shape rotate(int degrees); // or double radians if you like
Shape translate(int x, int y);
void setStrategy(TranslationStrategy strategy);
}
interface Origin extends Point {}
interface SidePoint extends Point {}
interface CornerPoint extends Point {}
Luego implemente las extensiones de la interfaz de Point
como clases internas en cada forma concreta.
Supongo que el siguiente flujo de usuario:
- Herramienta seleccionada -
currentTool
dentro del controlador establecido en el valor apropiado de enum. - El usuario selecciona / recorta una forma - se llaman
getPoints
, dependiendo de la herramienta, se pueden filtrar algunos tipos de puntos. Por ejemplo, solo se devolvieron los puntos de esquina para las operaciones de transformación. Inyectar estrategias apropiadas para los puntos expuestos. - A medida que el usuario arrastra el punto,
translate
llamado y tienes una nueva forma transformada con una herramienta determinada.
Si entiendo correctamente, aquí tenemos lo que tenemos:
- diferentes figuras que tienen puntos de control
- la IU permite dibujar figuras y arrastrar los puntos de control
Mi consejo aquí es decir que lo que caracteriza a una figura va en la capa Modelo, y que la parte UI va en la vista / controlador uno.
Un paso más para el modelo:
las figuras deben implementar una interfaz:
public interface Figure { List<Segment> segments(); List<ControlPoint> controlPoints(); void drag(ControlPoint point, Pos newPos); void rotate(ControlPoint point, Pos newPos, Pos center); // or rotate(Pos center, double angle); }
Segment
es una abstracción que puede representar un segmento de línea, un arco o una curva de Bezierun
ControlPoint
tiene sentido para una implementación deFigure
y tiene unaPos
actualpublic interface ControlPoint{ Figure parent(); void drag(Pos newPos); // unsure if it must exist in both interfaces Pos position(); ToolHint toolHint(); }
el
ToolHint
debe ser una indicación de qué herramienta puede usar el punto de control y para qué uso; según su requisito, la herramienta de rotación debe considerar el centro como especial.- a
Pos
representa las coordenadas x, y
De esta forma, la IU no tiene que saber nada sobre cuáles son realmente las cifras.
Para draw
una Figure
, la UI obtiene la lista de Segment
y simplemente dibuja independientemente cada segmento y agrega una marca en cada punto de control. Cuando se arrastra un punto de control, la interfaz de usuario le da una nueva posición a la Figure
y la vuelve a dibujar. Debería poder borrar una Figure
antes de volver a dibujarla en su nueva posición, o alternativamente (más simple pero más lento) podría volver a dibujar todo en cada operación
Con el método de drag
, solo podemos arrastrar un punto de control simple en una sola forma. Es fácilmente extensible, pero se deberán agregar extensiones para cada herramienta. Por ejemplo, ya agregué el método de rotate
que permite rotar una forma moviendo un punto de control con un centro de definición. También podría agregar un método de escala .
Formas múltiples
Si desea aplicar una transformación a un conjunto de formas, puede usar una subclase del rectángulo. Construye un rectángulo con lados paralelos para coordinar ejes que contienen todas las formas. Recomiendo agregar un método en la Figure
que devuelva un rectángulo (razonablemente pequeño) que contiene paréntesis laterales para coordinar los ejes para facilitar la creación del rectángulo de múltiples formas. Luego, cuando aplica una transformación al rectángulo englobing, simplemente informa la transformación a todos sus elementos. Pero llegamos a transformaciones que no se pueden realizar arrastrando puntos de control, porque el punto que se arrastra no pertenece a la forma interna.
Transformaciones internas
Hasta ahora, solo he tratado con la interfaz entre la interfaz de usuario y el modelo. Pero con las formas múltiples, vimos que necesitamos aplicar transformaciones afines arbitrarias (traslación de un punto de un rectángulo englobado o escalado del rectángulo englobado) o rotación. Si optamos por implementar la rotación como rotate(center, angle)
la rotación de una forma incluida ya está hecha. Entonces, simplemente tenemos que implementar la transformación afín
class AffineTransform {
private double a, b, c, d;
/* creators, getters, setters omitted, but we probably need to implement
one creator by use case */
Pos transform(Pos pos) {
Pos newpos;
newpos.x = a * pos.x + b;
newpos.y = c * pos.y + d;
return newpos;
}
}
De esta forma, para aplicar una transformación afín a una Figure
, solo tenemos que implementar la transform(AffineTransform txform)
de forma tal que simplemente apliquemos todos los puntos que definen la estructura.
La figura es ahora:
public interface Figure {
List<Segment> segments();
List<ControlPoint> controlPoints();
void drag(ControlPoint point, Pos newPos);
void rotate(Pos center, double angle);
// void rotate(ControlPoint point, double angle); if ControlPoint does not implement Pos
Figure getEnclosingRectangle();
void transform(AffineTransform txform);
}
Resumen :
Son solo las ideas generales, pero debería ser lo básico para permitir que las herramientas actúen sobre formas arbitrarias, con un bajo acoplamiento