Паттерн наблюдатель
Вы знакомы с понятием подписка. Сколько людей мучаются от бесконечных SMS, посылаемых различными магазинами, банками и т.д. о проходящих у них акциях, распродажах, кредитах. Эта ситуация описывается паттерном наблюдатель. У нас есть наблюдаемый объект – магазин и множество наблюдателей за этим магазином – владельцы мобильных телефонов. Как только в магазине возникает какое-то неординарное событие (распродажа, акция, скидки), владельцы телефонов получают сообщение об этом событии. Создадим два интерфейса Observer и Observable. Метод update интерфейса Observer предназначен для передачи сообщения клиентам. Методы registerObserver и removeObserver интерфейса Observable предназначены для регистрации и удаления клиентов магазина. Метод notifyObservers служит для уведомления клиентов о различных событиях в магазине. package observer; public interface Observer { public void update (String sms); } package observer; public interface Observable { public void registerObserver(Observer o); public void removeObserver(Observer o); public void notifyObservers(); } Класс Shop (магазин) наследует интерфейс Observable(доступный для наблюдения) и содержит список всех клиентов магазина (observers), дату очередной акции магазина (date) и посылаемое клиенту сообщение (message). Метод sendMessage создает сообщение, приписывая к сообщению текущую дату. При каждом изменении переменной message вызывается метод notifyObservers, который рассылает сообщение всем подписчикам, т.е. клиентам, зарегистрированным методом registerObserver. package observer; import java.util.ArrayList; import java.util.Date; public class Shop implements Observable{ Date date; String message; private ArrayList<Observer> observers = new ArrayList<Observer>(); void setDate(){ date = new Date(); } String sendMessage(){ setDate(); return message+date; } void setMessage(String sms){ message= sms; notifyObservers(); } public void registerObserver(Observer o) { observers.add(o); } public void removeObserver(Observer o) { int i = observers.indexOf(o); if (i>=0) observers.remove(i); } public void notifyObservers() { for (Observer observer: observers){ observer.update(sendMessage()); } } }
Класс клиент Client наследует интерфейс Observer и содержит список всех сообщений, полученных клиентом (sms). В методе update очередное сообщение добавляется в список полученных sms. В методе disply выводятся все сообщения присланные клиенту. package observer;
import java.util.ArrayList;
public class Client implements Observer { ArrayList<String> sms; String name; Client(String name){ sms=new ArrayList<String>(); this.name = name; } public void update(String str) { sms.add(str); } void disply(){ for(String x:sms) System.out.println("Client "+name+" receive the message "+x); } } Теперь необходимо создать магазин и его клиентов. Каждый клиент регистрируется в магазине - magazine.registerObserver(client[i]). Затем всем клиентам рассылаются сообщения с разной датой magazine.setMessage("Sale "). Демонстрируем все полученные клиентами сообщения for(int i=0;i<4;i++){ client[i].disply(); }
package observer; public static void main(String[] args) throws InterruptedException { Shop magazine = new Shop(); Client [] client = new Client[4]; for(int i=0;i<4;i++){ client[i] = new Client("Cl "+i); magazine.registerObserver(client[i]); } for(int i=0;i<5;i++){ Thread.sleep(1000); magazine.setMessage("Sale "); } for(int i=0;i<4;i++){ client[i].disply(); } } } В java.util имеется реализация наблюдателя и наблюдаемого объекта – интерфейс java.util.observer и класс java.util.observable. Интерфейс содержит метод update, а наблюдаемый объект может добавлять /удалять наблюдателей методами addObserver/deleteObserver. Также наблюдаемый объект сообщает наблюдателям о совершенных изменениях методом notifyObservers. Перед вызовом этого метода необходимо сообщить о том, что наблюдаемые объекты изменились - setChanged. Метод setChanged полезен в некоторых случаях. Например, изменение наблюдаемых параметров может происходить очень часто - раз в 10 милисекунд, а отражать это изменение нужно раз в секунду, тогда вызовом метода setChanged эта проблема решается. Рассмотрим простой пример реализации наблюдателя с использованием java.util. Пусть у нас имеется наблюдаемый объект TestObservable. Этот объект содержит только од ну переменную – строку name. Метод modify утверждает, что объект TestObservable изменился. Понятно, что данный объект изменяется только при создании. public class TestObservable extends java.util.Observable { private String name = ""; public TestObservable(String name) { this.name = name; }
public void modify() { setChanged(); }
public String getName() { return name; } } Наблюдатель – экземпляр класса TestObserver, наследующий интерфейс java.util.Observable, перегружает метод update. В методе update используется объект типа Observable и объект типа Object. Вызвавший update объект «о» предоставляет метод getName для доступа к наблюдаемой строке name. Также в функции update доступен объект, переданный в качестве параметра функции notifyObservers объекта типа Observable. В данном случае таким параметром является строка. public class TestObserver implements java.util.Observer { private String name = "";
public TestObserver(String name) { this.name = name; } public void update(java.util.Observable o,Object arg) { String str = "Called update of " + name; str += " from " + ((TestObservable)o).getName(); str += " with argument " + (String)arg; System.out.println(str); } } Рассмотрим реализацию паттерна наблюдатель. В функции main создаются объекты: to – наблюдаемый объект, о1 - наблюдатель Observer 1, о2 - наблюдатель Observer 2. Вызов метода addObserver для объекта to to.addObserver(o1) и to.addObserver(o2) назначает наблюдателей объекта to. Вызов метода modify, устанавливает состояние – «наблюдаемый объект был изменен». После этого notifyObservers сообщает наблюдателям об изменении наблюдаемого объекта.
public class Test { public Test() { } public static void main(String[] args) { Test test = new Test(); TestObservable to = new TestObservable("Observable"); TestObserver o1 = new TestObserver("Observer 1"); TestObserver o2 = new TestObserver("Observer 2"); to.addObserver(o1); to.addObserver(o2); to.modify(); to.notifyObservers("Notify argument"); } } В качестве упражнения напишите пример с рассылкой сообщений с использованием java.util.observer и класс java.util.observable. Паттерн представление-модель-контроллер(Model-view-controller MVC) MVC относится к составным паттернам. В этом паттерне выделяют:
Данная схема проектирования часто используется для построения каркаса приложения. Применение MVC может выполнить следующие задачи: 1. К одной модели можно присоединить несколько представлений, не затрагивая при этом реализацию модели. Например, некоторые данные могут быть одновременно представлены в виде электронной таблицы, гистограммы и круговой диаграммы. 2. Не изменяя реализацию представлений, можно поменять реакции на действия пользователя (нажатие мышью на кнопке, ввод данных), для этого достаточно использовать другой контроллер. 3. Ряд разработчиков специализируется только в одной из областей: либо разрабатывают графический интерфейс, либо разрабатывают бизнес-логику. Поэтому возможно добиться того, что программисты, занимающиеся разработкой модели, вообще не будут осведомлены о том, какое представление будет использоваться. 4. Наиболее типичная реализация отделяет представление от модели путем установления между ними протокола взаимодействия, используя аппарат событий (подписка/оповещение). При каждом изменении внутренних данных модель оповещает все зависящие от неё представления. Для этого используется паттерн «наблюдатель». При обработке реакции пользователя представление выбирает, в зависимости от нужной реакции, нужный контроллер, который обеспечит ту или иную связь с моделью. Для этого используется шаблон «стратегия», или вместо этого может быть модификация с использованием шаблона «команда» А для возможности однотипного обращения с подобъектами сложно-составного иерархического вида может использоваться шаблон «компоновщик».
Применим схему 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); } }
|