Diagrama de secuencia
En un diagrama de secuencia ponemos varios de los objetos o clases que forman parte de
nuestro programa y ponemos qué llamadas van haciendo unos a otros para realizar una
tarea determinada.
Hacemos un diagrama de secuencia por cada caso de uso o para una parte de un caso de uso
(lo que llamo subcaso de uso). En nuestro ejemplo de ajedrez, podemos hacer diagramas de
secuencia para "jugar partida" o bien para partes de "jugar partida", como puede ser "mover
pieza".
El detalle del diagrama depende de la fase en la que estemos, lo que pretendamos contar
con el diagrama y a quién. En una primera fase de diseño podemos poner clases grandes y
ficticias, que representen un paquete/librería o, si nuestro programa está compuesto por
varios ejecutables corriendo a la vez, incluso clases que representen un ejecutable.
Si estamos en una fase avanzada, estamos diseñando el programa y queremos dejar bien
atados los detalles entre dos programadores, que cada uno va a programar una de las clases
que participan, entonces debemos posiblemente ir al nivel de clase real de codificación y
método, con parámetros y todo, de forma que los programadores tengan claro que métdos
van a implementar, que deben llamar de la clase del otro, etc. Incluso si es un diagrama
para presentar al cliente, podemos hacer un diagrama de secuencia en el que sólo salga el
actor "jugador" y una única clase "juego ajedrez" que representa nuestro programa
completo, de forma que el cliente vea qué datos y en qué orden los tiene que meter en el
programa y vea qué salidas y resultados le va a dar el programa.
El siguiente puede ser un diagrama de secuencia de nuestro ejemplo del ajedrez a un nivel
de diseño muy preliminar.
Aquí ya hemos decidido que vamos a hacer tres librerías/paquetes, una para la interface de
usuario, otra para contener el tablero y reglas del ajedrez (movimiientos válidos y demás) y
otra para el algoritmo de juego del ordenador. Hemos puesto una clase representando cada
uno de los paquetes y hemos representado el caso de usa "mover pieza".
En el diagrama de secuencia no se ponen situaciones erróneas (movimientos inválidos,
jaques, etc) puesto que poner todos los detalles puede dar lugar a un diagrama que no se
entiende o difícil de leer. El diagrama puede acompañarse con un texto en el que se detallen
todas estas situaciones erróneas y particularidades. Si se quiere dejar muy claro que un
determinado error se contempla, por ejemplo, un movimiento no válido por el usuario,
entonces sí se puede poner en el diagrama de secuencia, siempre que no "embarulle"
demasiado.
En este diagrama sencillo ya vamos dejando algunas cosas claras, como por ejemplo, que la
interface de usuario hace llamadas y, por tanto, ve a los otros dos, mientras que algoritmo
sólo ve al tablero/reglas. El tablero/reglas aparentemente ve a la interface de usuario, pero
no nos interesa si seguimos un patrón modelo-vista-controlador, así que ese "Refresca
pantalla" lo implementaremos con un patrón observador, pero eso son detalles que quizás
no vienen al caso ahora.
OTRO
Qué es el modelo, la vista y el controlador
En casi cualquier programa que hagamos podemos encontrar tres partes bien diferenciadas
Por un lado tenemos el problema que tratamos de resolver. Este problema suele ser
independiente de cómo queramos que nuestro programa recoga los resultados o cómo
queremos que los presente. Por ejemplo, si queremos hacer un juego de ajedrez, todas las
reglas del ajedrez son totalmente independientes de si vamos a dibujar el tablero en 3D o
plano, con figuras blancas y negras tradicionales o figuras modernas de robots plateados y
monstruos peludos. Este código constituiría el modelo. En el juego del ajedrez el modelo
podría ser una clase (o conjunto de clases) que mantengan un array de 8x8 con las piezas,
que permita mover dichas piezas verificando que los movimientos son legales, que detecte
los jaques, jaque mate, tablas, etc. De hecho, las metodologías orientadas a objeto nos
introducen en este tipo de clases, a las que llaman clases del negocio.
Otra parte clara es la presentación visual que queramos hacer del juego. En el ejemplo del
ajedrez serían las posibles interfaces gráficas mencionados en el punto anterior. Esta parte
del código es la vista. La llamaré interface gráfica por ser lo más común, pero podría ser de
texto, de comunicaciones con otro programa externo, con la impresora, etc. Aquí
tendríamos, por ejemplo, la ventana que dibuja el tablero con las figuras de las piezas, que
permiten arrastrar con el ratón una pieza para moverla, botones, lista visual con los
movimientos realizados, etc.
La tercera parte de código es aquel código que toma decisiones, algoritmos, etc. Es código
que no tiene que ver con las ventanas visuales ni con las reglas de nuestro modelo. Esta
parte del código es el controlador. En nuestro código de ajedrez formarían parte de esto el
algoritmo para pensar las jugadas (el más complejo de todo el juego).
Dependencias entre modelo, vista y controlador
Si ordenamos estos tres grupos por probabilidad de ser reutilizable, tenemos un resultado
como el siguiente:
Lo más reutilizable y que es menos susceptible de cambio, es el modelo. Las reglas del
juego de ajedrez no cambian de un día para otro. Si tenemos un conjunto de clases que
mantengan en memoria el tablero y las reglas de movimiento de las piezas, es posible que
esas clases (o funciones y estructuras de datos) nos sirvan durante mucho tiempo sin
necesidad de tocarlas. En un punto intermedio está el controlador. Es posible que
mejoremos con cierta frecuencia nuestro algoritmo de juego del ajedrez, posiblemente
cada vez que saquemos una nueva versión de nuestro juego.
Finalmente, lo que más cambia, es la vista. De hecho, un mismo programa de ajedrez suele
darnos posibilidad de varias presentaciones. El modelo y el controlador serían los mismos,
pero habría varias vistas distintas.
Tras este tipo de ordenación, si queremos reaprovechar cosas en futuros programas de
ajedrez, está claro que el modelo debe ser independiente. Las clases (o funciones y
estructuras) del modelo no deben ver a ninguna clase de las otros grupos. De esta forma
podremos compilar el modelo en una librería independiente que podremos utilizar en
cualquier programa de ajedrez que hagamos. Es más, suponiendo que hagamos el programa
en C y queramos cambiarnos de plataforma (de linux a windows, por ejemplo), tenemos
bastantes posibilidades de que el código utilizado sea C standard y compile casi
directamente en cualquier plataforma. No tenemos librerias gráficas, de sockets ni otras
librerías avanzadas que suelen ser muy distintas, incluso dentro de una misma plataforma si
utilizamos distintos entornos de desarrollo (comparemos por ejemplo, los gráficos de visual
c++ con los de borland c++, ambos en PC/windows).
Siguiendo con el orden de posibilidad de reutilización, el controlador podría (y suele) ver
clases del modelo, pero no de la vista. Si en el juego del ajedrez el controlador es el que
analiza el tablero y hace los movimientos del ordenador, está claro que el controlador debe
ver el modelo que tiene las piezas y hacer en él los movimientos. Sin embargo, no debe ver
nada de la vista. De esta forma, el cambio de interface gráfica no implicará retocar el
algoritmo y recompilarlo, con los consiguientes riesgos de estropearlo además del trabajo
del retoque.
La vista es lo más cambiante, así que podemos hacer que vea clases del modelo y del
controlador. Si cambiamos algo del controlador o del modelo, es bastante seguro que
tendremos como mínimo que recompilar la interface gráfica.
Tras esto vemos claramente que cuando el jugador mueve una pieza en pantalla, la interface
gráfica se entera y hace al modelo que mueva la pieza. El modelo, como retorno de la
función o método llamado, puede devolver si el movimiento es válido o no. Si el
movimiento es válido, la interface gráfica puede decirle al controlador que mueva, para
simular el movimiento del ordenador. Incluso, como veremos más adelante, el controlador
puede enterarse de que se ha movido una pieza y de que es su turno, sin necesidad de que le
avise la vista.
El siguiente diagrama de secuencia muestra esto más o menos. Hay que fijarse que las
flechas sólo van en sentido de vista a modelo y controlador, y del controlador al modelo.
Comunicación en sentido inverso
Ahora surge una pregunta. Si el controlador decide hacer un movimiento en el modelo del
ajedrez, ¿cómo se entera la interface gráfica para visualizar dicho movimiento en pantalla?.
Debemos tener en cuenta que ni el modelo ni el controlador ven a la vista, por lo que no
pueden llamar a ninguna clase ni método de ella para que se actualize.
Para este tipo de problemas, tenemos otros patrones de diseño, por ejemplo, el patrón
observador. Debemos hacer una interface (en java sería un interface, en C++ sería una clase
con todos los métodos virtuales puros) que tenga métodos del estilo tomaMovimiento
(Movimiento), tomaJaque (), tomaTablas(), ganan (ColorGanador), y en general, para
cualquier cosa que pueda pasar en el tablero que pueda tener interés para alguien.
Llamemos a esta interface ObservadorTablero.
Esta interface formaría parte del modelo, de forma que las clases del modelo sí pueden
verla. La clase del modelo que mantiene el tablero, debe tener una lista de objetos que
implementen esta interface (en C++, clases que hereden de esta clase con métodos
virtuales). La clase del modelo debe tener además un par de métodos del estilo
anhadeObservador (ObservadorTablero) y eliminaObservador(ObservadorTablero).
Estos métodos añadirían o borrarían el parámetro que se les pasa de la lista y que es un
objeto que implementa la interface.
Tanto el controlador como la vista, deben implementar esta interface (heredar de ella en C+
+) y deben llamar al método anhadeObservador(this) del modelo. A partir de este
momento, cada vez que el modelo mueva una pieza, detecte un jaque, etc, debe llamar al
método tomaMovimiento(Movimiento),tomaJaque(), etc de todos los
ObservadorTablero que tenga en su lista.
De esta forma el modelo está avisando a las clases de la vista y del controlador sin
necesidad de verlas (sólo ve a la interface). De hecho, el modelo y la interface pueden estar
compiladas en una misma librería y el hacer nuevas clases que implementen la interface o
modificar las que ya la implementan, no es necesario recompilar la librería.
Si hay alguna cosa que pase en el controlador y deba enterarse la interface gráfica, habría
que implementar un mecanismo similar. Por ejemplo, el ordenador decide rendirse y la
interface gráfica debería mostrar un aviso indicándolo.
También, para aislar aún más las clases, suele ser habitual que el modelo (o incluso el
controlador) implementen (o hereden de) una interface del modelo con los métodos para
mover piezas y demás, de forma que ni la interface gráfica ni el controlador dependen de un
modelo concreto. Por ejemplo, la clase modeloAjedrez podría implementar (heredar) de
una interfaceModeloAjedrez. Las clases de controlador e interface gráfica verían a esta
interfaceModeloAjedrez, en vez de a modeloAjedrez. Las clases de la interface gráfica y
del controlador deberían tener métodos del estilo tomaModelo
(InterfaceModeloAjedrez), con el que se le pasa el modelo concreto que deben tratar.
Juntarlo todo
Para que todo esto funcione, es necesario que haya un programa principal a parte de todo
esto. El programa principal se debe encargar de instanciar las clases concretas del modelo,
controlador y vista que se van a usar y encargarse de llamar a todos los métodos del estilo
tomaModelo() y anhadeObservador(), es decir, hacer que se vean unas a otras de la
forma adecuada.
Si queremos hacer una interface gráfica totalmente nueva, bastará con hacerla de forma que
admita el mismo modelo y controlador que ya tenemos. Luego en el main tocaremos el new
de la interface gráfica para que lo haga de la nueva y ya está. Todo debería funcionar sin
tener necesidad siquiera de recompilar el modelo ni el controlador (el algoritmo para jugar
al ajedrez).
El ejemplo
Aquí tienes un ejemplo de un puzzle en java en el que se ha seguido (más o menos) esta
filosofía de programación. Puedes verlo funcionando como applet, ver los fuentes e incluso
bajártelos.
Como modelo está las clase Puzzle, que tiene métodos para mover las piezas y para
suscribirse a movimientos de piezas y a que el tablero esté ordenado. Avisará a clases que
implementen ObservadorMovimiento. He hecho también una clase Casilla, pero es
simplemente por hacer una estructura con los campos fila, columna y la comparación entre
dos estructuras para saber si corresponden a la misma casilla.
Como controlador, está Ordenador, que únicamente saber ordenar y desordenar el puzzle.
Por no complicarme la vida, el algoritmo de ordenación consiste en apuntar todos los
movimientos que se hacen en el puzzle y realizarlos en orden inverso. Ordenador, por
tanto, se suscribe en Puzzle a los movimientos, para apuntarlos y poder hacerlos luego al
revés. Se suscribe también a que el puzzle esté ordenado para borrar la lista de
movimientos. Otro detalle más: cuando Ordenador está ordenando el puzzle, se desuscribe
de los movimientos, para no ser avisado de sus mismos movimientos de ordenación y
montar un lio.Cuando termina de ordenar, se vuelve a suscribir.
Como vista, la clase GuiTablero es un lienzo (Canvas) de dibujo en el que se pintan las
piezas (unas imágenes .gif que hay por ahí). Los clicks de ratón se interpretan y llaman al
método mueve(fila,columna) de Puzzle. GuiTablero también se suscribe a los
movimientos del puzzle, de forma que cuando se realize un movimiento, se entera y repinta
la pantalla. La clase GuiTableroBotones contiene un GuiTablero y dos botones, uno para
ordenar y otro para desordenar. La pulsación de estos botones llamará a los métodos
ordena() y desordena() de Ordenador.
Finalmente he hecho dos posibles clases principales. AppletPuzzle hereda de JApplet, para
poder meter el puzzle en una página web y mainPuzzle hereda de JFrame, para poder
utilizarlo como aplicación independiente. Estas clases instancian algunas de las anteriores y
son además las encargadas de leer los ficheros .gif que hacen de piezas del puzzle. Pasan
las clases Image correspondientes a la vista.
Una observación
Si te fijas un poco en la API de java, verás que utilizan esta filosofía con frecuencia. Por
ejemplo JList es la vista de una lista, ListModel es la interface del modelo de lista,
DefaultListModel es una posible implementación de este modelo. JList tiene un método
setModel(ListModel) para pasarle el modelo. ListModel tiene métodos de
addListDataListener(ListDataListener) y removeListDataListener(ListDataListener),
con lo que se le está pasando a quién tiene que avisar cuando haya cambios en el modelo de
lista. JList tiene una clase interna que implementa ListDataListener, con lo que a través
de esa clase se enterará de los cambios en el modelo y los refrescará en pantalla.
Otra observación más
Todavía no llevo demasiado utilizando el patrón este y lo que he escrito aquí es la primera
idea que me he hecho sobre el tema. Es posible que algunas cosas no sean totalmente
correctas. De hecho, he leido hace un par de días otra explicación de este patrón, en el que
el controlador controla tanto al modelo como a la vista, de forma que el controlador es
capaz, en un momento dado, de llamar a métodos de la vista. Por supuesto, tanto modelo,
como controlador y vista implementan interfaces determinadas y sólo se ven entre ellos a
través de esas interfaces.
OTRO
OTRO
OTRO
OTRO