Ejemplos java y C/linux

Tutoriales

Enlaces

Licencia

Creative Commons License
Esta obra está bajo una licencia de Creative Commons.
Para reconocer la autoría debes poner el enlace https://old.chuidiang.org

Separación modelo vista controlador

Vamos a explicar en qué consiste la separación modelo-vista-controlador a la hora de hacer un programa. Aunque en la explicación se utiliza como ejemplo un juego de ajedrez, al final se presenta un código en java para un puzzle (más sencillo que un juego de ajedrez).

Objetivo

Un problema muy común para los programadores es la reutilización del código que ya tienen hecho. A veces hay que resolver un problema parecido a algo que ya tenemos hecho, mejorar el aspecto de un programa, mejorar su algoritmo, etc. Esta tarea se facilita mucho si a la hora de programar tenemos la precaución de separar el código en varias partes que sean susceptibles de ser reutilizadas sin modificaciones.

Qué es el modelo, la vista y el controlador

En casi cualquier programa que hagamos podemos encontrar tres partes bien diferenciadas

Dependencias entre modelo, vista y controlador    

Si ordenamos estos tres grupos por probabilidad de ser reutilizable, tenemos un resultado como el siguiente:

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.

diagrama de secuencia modelo vista controlador

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.

Estadísticas y comentarios

Numero de visitas desde el 4 Feb 2007:

Aviso Legal