Наблюдатель (Observer)

Как и другие формы обратного вызова, эта содержит специальную точку (hook point), в которой вы можете изменить код. Различие состоит в полностью динамической природе наблюдателя. Он часто используется в специальных случаях изменений, основанных на других объектах с изменяемым состоянием, но сам также является базисом управления событиями. В любой момент вы захотите разделить исходный код вызова от вызываемого кода для получения действительно динамического подхода.

Шаблон наблюдателя решает довольно общую проблему: Что если группе объектов нужно обновить себя, когда некоторый объект изменил состояние? Это можно рассмотреть в аспекте "модель-вид", встречаемом в Smalltalk MVC (model-view-controller), или почти эквивалентной "Архитектуре Документ-Вид". Предположим, что у вас есть некоторые данные ("документ") и несколько вариантов представления, скажем, графическое и текстовое представления. Когда вы меняете данные, оба представления должны знать об этом, чтобы обновить себя, вот этому-то и способствует наблюдатель. Это достаточно общая проблема, для решения которой была сделана часть стандартной библиотеки java.util.

Существуют два типа объектов, используемых для реализации шаблона наблюдателя в Java. Класс Observable хранит след каждого, кто хочет быть информирован о возникновении изменений, не зависимо от того, менялось "состояние" или нет. Когда кто-либо говорит "ОК, все должны проверить и, возможно, обновить себя", класс Observable выполняет эту задачу, вызывая метод notifyObservers( ) для каждого наблюдателя из списка. Метод notifyObservers( ) является частью базового класса Observable.

Реально есть две "вещи, которые меняются" в шаблоне наблюдателя: количество наблюдающих объектов и способ, которым происходит обновление. Таким образом, шаблон наблюдателя позволяет вам изменять обе из этих вещей без влияния на окружающий код.

Наблюдатель (Observer) - это "интерфейсный" класс, который имеет только одну член-функция update(). Эта функция вызывается объектом, который наблюдается, когда этот объект решает, что пришло время обновить всех наблюдателей. Аргументы в функции не обязательны. Вы можете создать метод update( ) без аргументов и он все равно будет соответствовать шаблону наблюдателя. Однако, более общим случаем является передача в объект-наблюдатель объекта, который является причиной обновления (так как объект Наблюдателя может быть зарегистрирован не в одном наблюдаемом объекте), и любая дополнительная информация, если она полезна, помогает объекту Наблюдателя не рыскать вокруг в поисках, что же изменилось, и не запрашивать дополнительную информацию, если она необходима.

"Наблюдаемый объект", которые определяет, когда и как выполнить обновления, будет называться Наблюдаемый ( Observable).

Наблюдаемый имеет флаг, чтобы указывать было ли изменение. В простейшем дизайне не будет флага. Если что-то происходит, все будут уведомлены. Флаг позволяет вам подождать и уведомлять Наблюдателя только если вы решите, что настало время. Однако заметьте, что управление флагом состояния является защищенным (protected), так что только наследник может решить, когда назначать изменения, а не конечный пользователь результирующего производного класса Наблюдателя.

Большинство работы выполняется в методе notifyObservers( ). Если флаг изменений (changed) не установлен, этот метод ничего не делает. В противном случае он сначала сбрасывает флаг изменений, чтобы повторный вызов метода notifyObservers( ) не тратил время по напрасну. Это делается прежде уведомления наблюдателей на тот случай, если вызов метода update( ) сделает нечто, что вновь станет причиной изменения в Наблюдаемом объекте. Затем происходит перебор множества и вызов члена-функции update( ) для каждого наблюдателя.

На первый взгляд может показаться, что вы можете использовать обычный Наблюдаемый объект для управления обновлениями. Но это не работает. Для получения эффекта вы должны наследовать от Наследуемого и где-то в коде вашего производного класса вызвать метод setChanged( ). Это член-функция, которая устанавливает флаг "изменений", который говорит о том, что при вызове метода notifyObservers( ) все наблюдатели будут, фактически, уведомлены. В каком месте вам вызывать setChanged( ), зависит от логики вашей программы.

Наблюдение за цветами

Ниже приведен шаблон наблюдателя:

//: observer:ObservedFlower.java
// Демонстрация шаблона "наблюдателя".
package observer;

import java.util.*;

import junit.framework.*;

class Flower {
  
private boolean isOpen;
  
private OpenNotifier oNotify = new OpenNotifier();
  
private CloseNotifier cNotify = new CloseNotifier();
  
  
public Flower() {
     
isOpen = false;
  
}
  
  
public void open() { // Открывает лепестки
     
isOpen = true;
      oNotify.notifyObservers
();
      cNotify.open
();
  
}
  
  
public void close() { // Закрывает лепестки
     
isOpen = false;
      cNotify.notifyObservers
();
      oNotify.close
();
  
}
  
  
public Observable opening() {
     
return oNotify;
  
}
  
  
public Observable closing() {
     
return cNotify;
  
}
  
  
private class OpenNotifier extends Observable {
     
private boolean alreadyOpen = false;
     
     
public void notifyObservers() {
        
if (isOpen && !alreadyOpen) {
           
setChanged();
           
super.notifyObservers();
            alreadyOpen =
true;
        
}
      }
     
     
public void close() {
        
alreadyOpen = false;
     
}
   }
  
  
private class CloseNotifier extends Observable {
     
private boolean alreadyClosed = false;
     
     
public void notifyObservers() {
        
if (!isOpen && !alreadyClosed) {
           
setChanged();
           
super.notifyObservers();
            alreadyClosed =
true;
        
}
      }
     
     
public void open() {
        
alreadyClosed = false;
     
}
   }
}

class Bee {
  
private String name;
  
private OpenObserver openObsrv = new OpenObserver();
  
private CloseObserver closeObsrv = new CloseObserver();
  
  
public Bee(String nm) {
     
name = nm;
  
}
  
  
// Внутренний класс для наблюдения открытия:
  
private class OpenObserver implements Observer {
     
public void update(Observable ob, Object a) {
        
System.out.println("Bee " + name + "'s breakfast time!");
     
}
   }
  
  
// Другой внутренний класс для закрытия:
  
private class CloseObserver implements Observer {
     
public void update(Observable ob, Object a) {
        
System.out.println("Bee " + name + "'s bed time!");
     
}
   }
  
  
public Observer openObserver() {
     
return openObsrv;
  
}
  
  
public Observer closeObserver() {
     
return closeObsrv;
  
}
}

class Hummingbird {
  
private String name;
  
private OpenObserver openObsrv = new OpenObserver();
  
private CloseObserver closeObsrv = new CloseObserver();
  
  
public Hummingbird(String nm) {
     
name = nm;
  
}
  
  
private class OpenObserver implements Observer {
     
public void update(Observable ob, Object a) {
        
System.out.println("Hummingbird " + name + "'s breakfast time!");
     
}
   }
  
  
private class CloseObserver implements Observer {
     
public void update(Observable ob, Object a) {
        
System.out.println("Hummingbird " + name + "'s bed time!");
     
}
   }
  
  
public Observer openObserver() {
     
return openObsrv;
  
}
  
  
public Observer closeObserver() {
     
return closeObsrv;
  
}
}

public class ObservedFlower extends TestCase {
  
Flower f = new Flower();
   Bee ba =
new Bee("A"), bb = new Bee("B");
   Hummingbird ha =
new Hummingbird("A"), hb = new Hummingbird("B");
  
  
public void test() {
     
f.opening().addObserver(ha.openObserver());
      f.opening
().addObserver(hb.openObserver());
      f.opening
().addObserver(ba.openObserver());
      f.opening
().addObserver(bb.openObserver());
      f.closing
().addObserver(ha.closeObserver());
      f.closing
().addObserver(hb.closeObserver());
      f.closing
().addObserver(ba.closeObserver());
      f.closing
().addObserver(bb.closeObserver());
     
// Колибри B решает поспать:
     
f.opening().deleteObserver(hb.openObserver());
     
// Изменения для заинтересованный наблюдателей:
     
f.open();
      f.open
(); // Уже открыт, нет изменения.
      // Пчела A не хочет лететь в кровать:
     
f.closing().deleteObserver(ba.closeObserver());
      f.close
();
      f.close
(); // Уже закрыть, нет изменения.
     
f.opening().deleteObservers();
      f.open
();
      f.close
();
  
}
  
  
public static void main(String args[]) {
     
junit.textui.TestRunner.run(ObservedFlower.class);
  
}
}
// /:~


Интересующими нас событиями является то, что Цветок может открываться и закрываться. Поскольку используется идиома внутренних классов, оба этих события могут быть наблюдаемы раздельно. Оба класса OpenNotifier и CloseNotifier наследуются от Observable, так что они имеют доступ к методу setChanged( ) и могут передавать все, что нужно Observable.

Идиома внутренних классов также пригодилась, чтобы определить не один вид Наблюдателя в классах Bee и Hummingbird, так как в обоих этих классах желательно независимое наблюдение за открытием и закрытием Цветка. Обратите внимание, как идиома внутреннего класса сочетается с обеспечением преимуществ наследования (например, возможность доступа к частным (private) данным внешнего класса) без каких-либо ограничений.

В функции main( ) вы можете видеть одну из главных выгод шаблона наблюдателя: возможность изменять поведение во время исполнения при динамической регистрации и отмены регистрации Наблюдателей в Наблюдаемом.

Если вы изучите приведенный выше код, вы увидите, что OpenNotifier и CloseNotifier используют основной интерфейс Observable. Это означает, что вы можете наследовать другие полностью отличающиеся классы Наблюдателя. Только при соединении Наблюдателя с Цветком использует интерфейс Наблюдателя.

Визуальный пример наблюдателей (observers)

Приведенный ниже пример похож на пример ColorBoxes из 14 главы книги Thinking in Java, 2 nd Edition . Прямоугольники помещаются в сетке на экране и каждый из прямоугольников инициализируется случайно выбранным цветом. Кроме того, каждый прямоугольник реализует интерфейс Наблюдателя и регистрируется в Наблюдаемом объекте. Когда вы щелкаете мышкой на прямоугольнике, все остальные прямоугольники уведомляются об изменении, поскольку Наблюдаемый объект автоматически вызывает метод update( ) каждого Наблюдателя. Внутри этого метода прямоугольник проверяет, чтобы узнать, является ли он смежным с тем, на который кликнули, и если это так, он меняет свой цвет в соответствии с цветом прямоугольника, на котором щелкнули.

//: observer:BoxObserver.java
// Демонстрация шаблона Наблюдателя с использованием
// встроенных в Java классов наблюдателей.
package observer;

import javax.swing.*;

import java.awt.*;

import java.awt.event.*;

import java.util.*;

// Вы должны наследовато новый тип от Observable:
class BoxObservable extends Observable {
  
public void notifyObservers(Object b) {
     
// В противном случае он не распространит изменения:
     
setChanged();
     
super.notifyObservers(b);
  
}
}

public class BoxObserver extends JFrame {
  
Observable notifier = new BoxObservable();
  
  
public BoxObserver(int grid) {
     
setTitle("Demonstrates Observer pattern");
      Container cp = getContentPane
();
      cp.setLayout
(new GridLayout(grid, grid));
     
for (int x = 0; x < grid; x++)
        
for (int y = 0; y < grid; y++)
           
cp.add(new OCBox(x, y, notifier));
  
}
  
  
public static void main(String[] args) {
     
int grid = 8;
     
if (args.length > 0)
        
grid = Integer.parseInt(args[0]);
      JFrame f =
new BoxObserver(grid);
      f.setSize
(500, 400);
      f.setVisible
(true);
      f.setDefaultCloseOperation
(EXIT_ON_CLOSE);
  
}
}

class OCBox extends JPanel implements Observer {
  
Observable notifier;
  
int x, y; // Положение в сетке
  
Color cColor = newColor();
  
static final Color[] colors = { Color.BLACK, Color.BLUE, Color.CYAN,
         Color.DARK_GRAY, Color.GRAY, Color.GREEN, Color.LIGHT_GRAY,
         Color.MAGENTA, Color.ORANGE, Color.PINK, Color.RED, Color.WHITE,
         Color.YELLOW
};
  
static Random rand = new Random();
  
  
static final Color newColor() {
     
return colors[rand.nextInt(colors.length)];
  
}
  
  
OCBox(int x, int y, Observable notifier) {
     
this.x = x;
     
this.y = y;
      notifier.addObserver
(this);
     
this.notifier = notifier;
      addMouseListener
(new ML());
  
}
  
  
public void paintComponent(Graphics g) {
     
super.paintComponent(g);
      g.setColor
(cColor);
      Dimension s = getSize
();
      g.fillRect
(0, 0, s.width, s.height);
  
}
  
  
class ML extends MouseAdapter {
     
public void mousePressed(MouseEvent e) {
        
notifier.notifyObservers(OCBox.this);
     
}
   }
  
  
public void update(Observable o, Object arg) {
     
OCBox clicked = (OCBox) arg;
     
if (nextTo(clicked)) {
        
cColor = clicked.cColor;
         repaint
();
     
}
   }
  
  
private final boolean nextTo(OCBox b) {
     
return Math.abs(x - b.x) <= 1 && Math.abs(y - b.y) <= 1;
  
}
}
// /:~

 

Когда вы впервые посмотрите на документацию для Observable, вы будете слегка смущены, поскольку может показаться, что вы можете использовать обычный объект Observable для управления обновлениями. Но это не работает. Попробуйте внутри BoxObserver создать объект Observable вместо объекта BoxObservable и посмотрите, что произойдет: ничего. Чтобы получить эффект, вы должны наследовать от класса Observable и где-то в коде вашего наследуемого класса вызывать метод setChanged( ). Этот метод устанавливает флаг "изменения", который говорит о том, что при вызове метода notifyObservers( ) все наблюдатели должны, фактически, быть уведомлены. В приведенном выше примере метод setChanged( ) просто вызывается внутри метода notifyObservers( ), но вы можете использовать любой критерий, который хотите, чтобы решить, когда вызывать setChanged( ).

BoxObserver содержит единственный объект Observable, который называется notifier, и каждый раз, когда создается объект OCBox, он привязывается к notifier. В OCBox, когда бы вы ни кликнули мышкой, вызывается метод notifyObservers( ), передавая объект, на котором был щелчок в качестве аргумента, так что все прямоугольники получают сообщения (в своем методе update( )) и знают, на каком прямоугольнике произошел щелчок, и могут решить, нужно ли меняться или нет. Используя комбинацию кода в notifyObservers( ) и в методе update( ) вы можете работать и с более сложными схемами.

Может показаться, что способ уведомления наблюдателей должен быть заморожен во время компиляции в методе notifyObservers( ). Однако, если вы взглянете более пристально на приведенный выше код, вы увидите, что есть только одно место в коде BoxObserver или OCBox, когда вы сознательно работаете с объектом BoxObservable. Это точка создания объекта Observable - во всех остальных местах используется базовый интерфейсObservable. Это значит, что вы можете наследовать другой класс от Observable и переключить на него во время выполнения программы, если вы захотите изменить поведение при уведомлении.

Посредник (Mediator)

Уборка мусора под ковриком, чем это отличается от MVC?

MVC имеет различные модели и виды. Посредник может быть чем угодно. MVC - это разновидность посредника.

Упражнения

  1. Создайте минимальный дизайн Наблюдатель-Наблюдаемый из двух классов. Просто создайте голый минимум из двух классов, затем продемонстрируйте ваш дизайн, создав одного наблюдаемого и много наблюдателей, и заставьте Наблюдаемого обновить Наблюдателей.
  2. Создайте минимальную систему Наблюдателя, используя java.util.Timer внутри вашего Наблюдаемого для генерации событий, о которых сообщается Наблюдателям. Создайте несколько различных наблюдателей, используя анонимный внутренний класс, зарегистрируйте их в Наблюдателе и покажите, что они вызываются, когда возникает событие, генерируемое Таймером.
  3. Измените BoxObserver.java таким образом, чтобы превратить ее в простую игру. Если любой из квадратов, окружающих тот, на котором кликнули, является частью пятна одного цвета, то все квадраты этого пятна меняют свой цвет на тот цвет, который вы щелкнули. Вы можете конфигурировать игру так, чтобы превратить ее в соревнование между игроками или следить за количеством щелчков, которые сделает единственный игрок, чтобы закрасить все поле одним цветом. Вы можете также захотеть ограничить цвета игроков первым выбранным цветом.