Паттерн представление-модель-контроллер(Model-view-controller mvc)
MVC относится к составным паттернам. В этом паттерне выделяют:
Модель(model)- средство хранения данных приложения,
Представление(view) – визуальное представление данных модели или пользовательский интерфейс.
Контроллер (controller)- связывает модель и представление.
Данная схема проектирования часто используется для построения каркаса приложения.
Применение MVC может выполнить следующие задачи:
К одной модели можно присоединить несколько представлений, не затрагивая при этом реализацию модели. Например, некоторые данные могут быть одновременно представлены в виде электронной таблицы, гистограммы и круговой диаграммы.
Не изменяя реализацию представлений, можно поменять реакции на действия пользователя (нажатие мышью на кнопке, ввод данных), для этого достаточно использовать другой контроллер.
Ряд разработчиков специализируется только в одной из областей: либо разрабатывают графический интерфейс, либо разрабатывают бизнес-логику. Поэтому возможно добиться того, что программисты, занимающиеся разработкой модели, вообще не будут осведомлены о том, какое представление будет использоваться.
Наиболее типичная реализация отделяет представление от модели путем установления между ними протокола взаимодействия, используя аппарат событий (подписка/оповещение). При каждом изменении внутренних данных модель оповещает все зависящие от неё представления. Для этого используется паттерн «наблюдатель». При обработке реакции пользователя представление выбирает, в зависимости от нужной реакции, нужный контроллер, который обеспечит ту или иную связь с моделью. Для этого используется шаблон «стратегия», или вместо этого может быть модификация с использованием шаблона «команда» А для возможности однотипного обращения с подобъектами сложно-составного иерархического вида может использоваться шаблон «компоновщик».
Применим схему MVC для создания графического редактора.
Моделью данных в нашем приложении будет некоторое хранилище фигур с набором функций для работы с этими фигурами.
Представление данных – это панель для рисования.
Контроллер – это некоторый класс, связывающий панель и модель данных.
Создадим модель данных, которые будут храниться в графическом редакторе. Во второй главе был создан проект редактора, в котором пользователь может нарисовать одну фигуру – закрашенную или незакрашенную, прямоугольник или овал. В этом проекте не было разделения на представление и модель. Панель, на которой рисовалась фигура, являлась одновременно моделью и представлением данных. Данными являлась рисуемая фигура(shape).
Пусть в модели все рисуемые фигуры будут храниться в ArrayList<MyShape> list. Фигура, которая рисуется в данный момент - activeShape. Для добавления activeShape в коллекцию фигур служит метод add, для создания activeShape – метод setNewActiveShape. При создании новой фигуры используется порождающий паттерн «прототип», который будет рассмотрен в следующей главе. При изменении размеров рисуемой фигуры (activeShape) работает метод setShapeSize, который устанавливает размер фигуры в соответствие с координатами, хранящимися в массиве точек. Метод draw рисует все фигуры, которые хранятся в list. Для реализации функции «рисование» данная модель обладает всеми необходимыми функциями.
public class Model extends Observable{
ArrayList<MyShape> list;
private MyShape activeShape;
Model(){
list = new ArrayList<MyShape>();
}
void add(){
list.add(activeShape);
}
void setShapeSize(Point2D[]p){
activeShape.setShapeSize(p);
notifyPanel();
}
void setActiveShape(MyShape s){
activeShape = s;
}
void setNewActiveShape(){
activeShape = activeShape.clone();
}
void draw(Graphics g){
Graphics2D g2 = (Graphics2D)g;
for(MyShape x:list)x.draw(g2);
}
void notifyPanel(){
setChanged();
notifyObservers();
}
}
Заметим, что модель наследует класс Observable и является наблюдаемым объектом. В функции notifyPanel вызываются методы setChanged, который устанавливает состояние происшедших изменений, и notifyObservers, который оповещает подписчиков модели об изменениях. Метод notifyPanel вызывается в функции setShapeSize после изменения размеров рисуемой фигуры. Таким образом, панель оповещается о том, что надо перерисовать экран и отобразить новые размеры фигуры.
Создадим представление - панель, которая будет отображать информацию из описанной выше модели. В функции панели будет входить обработка событий мыши и передача «мышиных» координат соответствующим методам контроллера. Также панель будет перерисовывать экран и реализовывать метод update.
public class MyPanel extends JPanel implements Observer{
Controller controller;
//конктруктор
MyPanel(Controller contr){
controller = contr;
this.addMouseListener(new MouseAdapter() {
@Override
public void mousePressed(MouseEvent arg0) {
controller.executePress(arg0.getPoint());
}
});
addMouseMotionListener(new MouseMotionAdapter() {
@Override
public void mouseDragged(MouseEvent arg0) {
controller.executeDrag(arg0.getPoint());
}
} );
}
//конец конструктора
@Override
public void paintComponent(Graphics g){
super.paintComponent(g);
controller.draw(g);
}
public void update(Observable arg0, Object arg1) {
repaint();
}
}
Как видим представление(MyPanel) получилось достаточно простое. Вся функциональность должна выполняться в модели и контроллере. MyPanel наследует интерфейс Observer и является наблюдателем. При изменении наблюдаемого объекта вызывается функция update, которая перерисовывает экран с помощью repaint.
Рассмотрим контроллер. При создании контроллера(Controller) использовался паттерн singleton, который позволяет создавать в проекте только один экземпляр данного класса. Этот паттерн мы рассмотрим позже. Класс Controller имеет доступ к модели, хранит массив точек p, необходимых для реализации действий с фигурами, хранит «текущую фигуру» shape , т.е. характеристики фигуры, которая будет рисоваться. Для формирования свойств shape служат методы setRectangularShape и setColorBehavior.
public class Controller {
private Model model;
private Activity action;
private MyShape shape;
private static Controller controller;
private Controller(Model m){
model=m;
shape=new MyShape();
}
void setActivity(Activity act){
action = act;
}
void setMyShape(MyShape s){
shape= s;
model.setActiveShape(shape);
}
void setRectangularShape(RectangularShape r){
shape.setShape(r);
model.setActiveShape(shape);
}
void setColorBehavior(ColorBehavior b){
shape.setColorBehavior(b);
model.setActiveShape(shape);
}
void executePress(Point2D point){
action.executePress(model, point);
}
void executeDrag(Point2D point){
action.executeDrag(model, point);
}
void draw(Graphics g){
model.draw(g);
}
public static Controller getInstance(Model model){
if(controller ==null)
controller = new Controller(model);
return controller;
}
}
Для выполнения действий над фигурами служит переменная action. При реализации различных действий над фигурами использовался паттерн стратегия, описанный в 1 главе. В нашей реализации редактора надо различать два действия над фигурами – рисовать фигуру или перемещать фигуру. Обрабатываются два события мыши – mousePressed в методе executePress и mouseDragged в методе executeDrag. При рисовании фигуры в методе executePress необходимо добавить новую фигуру в модель и в executeDrag изменять её координаты. При перемещении фигуры в методе executePress надо найти в модели фигуру, содержащую координаты мыши и в executeDrag перемещать эту фигуру. В зависимости от типа переменной action эти действия будут реализованы. Как этого достичь?
Создадим интерфейс Activity.
public interface Activity {
void setPoint(Point2D[]p);
void executePress(Model model,Point2D p);
void executeDrag(Model model,Point2D p);
}
Пусть этот интерфейс наследует класс DrawAction, отвечающий за функцию рисования. В методе setPoint устанавливается ссылка на массив точек (созданный в контроллере). В методе executePress в нулевом элементе массива запоминается точка «mousePressed» и вызываются методы модели, которые создают новую фигуру с характеристиками activeShape model.setNewActiveShape (activeShape передана в модель контроллером ранее в методе setMyShape). В методе executeDrag в первом элементе массива точек запоминается вторая координата мыши (mouseDragged) и вызывается model.setShapeSize, который перерисовывает фигуру с новыми границами.
public class DrawAction implements Activity{
Point2D [] p;
MyShape shape;
DrawAction(){
p=new Point2D[2];
}
public void setPoint(Point2D[]p) {
this.p = p;
}
public void executePress(Model model,Point2D point) {
p[0]=point;
model.setNewActiveShape();
model.add();
}
public void executeDrag(Model model, Point2D point) {
p[1]=point;
model.setShapeSize(p);
}
}
В качестве упражнения напишите класс MoveAction, который будет отвечать за передвижение фигур. В класс Model необходимо добавить:
метод findShape(Point2D p) для поиска фигуры, содержащей точку р. Для этого в классе MyShape для объекта shape нужно вызвать метод contains(p), который возвращает true, если фигура содержит точу;
метод moveShape(Point2D[]p), который будет менять координаты у фигуры, выбранной в методе findShape. Ниже приведен код функции, чтобы не затруднять читателя расчетами новых координат.
void moveShape(Point2D[]p){
double deltaX = p[0].getX()-p[1].getX();
double deltaY = p[0].getY()-p[1].getY();
if(movedShape!=null){
double xMin = movedShape.getMinX()-deltaX;
double yMin = movedShape.getMinY()-deltaY;
double xMax = movedShape.getMaxX()-deltaX;
double yMax = movedShape.getMaxY()-deltaY;
movedShape.setShapeSize(xMin, yMin, xMax, yMax);
p[0]=p[1];
notifyPanel();
}
}
Для соединения выше перечисленных классов необходим еще один класс – компоновщик, который реализует паттерн «компоновщик». Пусть таким классом будет класс MyFrame, который в дальнейшем будет реализовывать меню для выбора действий и фигур. В этом классе создаются – модель, контроллер, панель и фигура. Методом model.addObserver(panel) панель назначается наблюдателем над моделью. В строке controller.setMyShape(new MyShape(new Rectangle2D.Double(),new ColorShape(Color.GREEN))) назначается рисуемая фигура. В строке controller.setActivity(new DrawAction()) назначается действие. В результате может быть нарисовано множество различных зеленых прямоугольников.
public class MyFrame extends JFrame{
Model model;
MyPanel panel;
Controller controller;
MyFrame(){
model = new Model();
shape = new MyShape(new Rectangle2D.Double(),new ColorShape(Color.GREEN));
//singleton
controller = Controller.getInstance(model);
controller.setMyShape(shape);
controller.setActivity(new DrawAction());
panel = new MyPanel(controller);
model.addObserver(panel);
add(panel);
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(300, 300);
setVisible(true);
}
}
