Статьи

Образец Наблюдателя

В сегодняшнем посте я собираюсь обсудить шаблон наблюдателя. Это один из наиболее широко используемых шаблонов в приложениях J2EE. Я говорю это из-за внутренней простоты этого шаблона и того факта, что очень возможно, что вы уже реализуете этот шаблон, даже не осознавая этого! Это то, чего нельзя сказать о других шаблонах, таких как Visitor или Factory.

Как я всегда делаю, я собираюсь разработать использование этого паттерна в вымышленной игровой ситуации.

Давайте представим, что мы играем в игру ужасов. Да, на этот раз я выбираю игру ужасов вместо хорошего старого FPS!

Во-первых, давайте начнем с несколько формального введения в шаблон.

Как следует из названия, шаблон Observer в основном используется, чтобы связать объект A с группой других объектов, которые наблюдают любые изменения в свойствах объекта A, и выполняют некоторые очень важные, потрясающие задачи, основанные на изменениях, внесенных в Свойства объекта А.

Ну, это как другой способ говорить о событии — слушатель событий, модель программирования.

В паттерне Observer есть три основных проблемы.

1) наблюдаемый объект — наблюдаемый объект.
2) Объекты, которые наблюдают наблюдаемый объект — объекты Наблюдателя.
3) Способ связать и вызвать объект Observable с помощью Observers — функция Delegate.

Чтобы быть кратким — объекты-наблюдатели наблюдают наблюдаемые объекты.

Это 5 О в одном предложении!

Там два стиль реализации шаблона Observer — толкающая модель и Модель Толкай . Эти две модели отличаются тем, как устроены наблюдатели и функция делегата. Наблюдаемый объект остается тем же самым. Позже я расскажу обе эти модели, а также расскажу о плюсах и минусах обеих техник.

Однако теперь пришло время поиграть в некоторые игры.

Главным героем нашей игры ужасов является очень смелый игрок по имени ScoobyDoo. Итак, мы создаем базовый класс проигрывателя с тремя переменными экземпляра: name, xCoordinate, yCoordinate. Вот определение класса.

public class Player {

private String name;
private Integer xCooridinate;
private Integer yCooridinate;

public Integer getxCooridinate() {
return xCooridinate;
}

public Integer getyCooridinate() {
return yCooridinate;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void moveForward(Integer steps){
xCooridinate+=steps;
}

public void moveBackward(Integer steps){
xCooridinate-=steps;
}

public void jump(Integer height){
yCooridinate+=height;
}

public void fallTo(Integer height){
yCooridinate=height;
}
}

Я также добавил функцию, которая позволяет игроку двигаться вперед и прыгать (то есть двигаться вверх) и наоборот. Они будут использоваться для управления свойствами xCoordinate и yCoordinate нашего проигрывателя.

Я не думаю, что игру можно сделать страшной без правильного освещения и звуковых эффектов. Но что делает игру пугающей, так это правильное время действия этих эффектов.

Предположим, сцена, где ScoobyDoo входит в пустую комнату. На данный момент огни яркие и блестящие. XCoordinate и yCoordinate игрока устанавливаются в 0, когда игрок входит в комнату.

Теперь мы хотим, чтобы, когда ScoobyDoo находился прямо в середине комнаты, то есть примерно в 10 шагах от комнаты, огни внезапно начали мигать.

И не только это, но мы также хотим, чтобы начался страшный звук в случае, если ScoobDoo пытается действовать умно и пытается вскочить, выбегая из комнаты.

Да, это может звучать так, как будто я так злюсь, пытаясь напугать дерьмо этой собакой, но эй, я не виноват. Могу поспорить, что эта собака тоже испугалась бы звука своего пердуна

Хорошо, вернемся к работе.

Нам нужен способ наблюдать координаты ScoobyDoo в комнате и предпринимать соответствующие действия, когда его координаты пересекают определенный порог.

Итак, здесь наблюдаемым объектом является Player.

Прежде чем мы начнем, нам нужно определить несколько интерфейсов, которые будут использоваться для общего использования и обработки Observers и Observables.

Давайте сначала посмотрим на определение этого интерфейса.

public interface Observable {
public void notifyObservers();
public void addObserver(Observer o);
public void removeObserver(Observer o);
}
public interface Observer {
public void update(Object o);
}

Давайте создадим Обозреватели.

public class SoundEffectController implements Observer{

private boolean isSoundPlaying = false;

public void playSound(){
System.out.println("Play an eerie sound");
isSoundPlaying = true;
}

public void update(Object o) {
if(o instanceof Integer){
Integer yCoordiante = (Integer)o;
if(yCoordiante>0 && isSoundPlaying== false){
playSound();
}
}
}

}

public class LightEffectController  implements Observer{

private boolean isLightFlickering = false;

public void flickerLights(){
System.out.println("Flicker the lights");
isLightFlickering = true;
}

public void update(Object o) {
if(o instanceof Integer){
Integer xCoordiante = (Integer)o;
if(xCoordiante>10 && isLightFlickering==false){
flickerLights();
}
}
}

}

Как видите, есть два наблюдателя. Контроллер SoundEffectController и LightEffectController.

SoundEffectController — это класс, который управляет звуковыми эффектами комнаты. Контроллер световых эффектов — это класс, который управляет световыми эффектами в комнате.

Теперь нам нужен способ связать этих наблюдателей с интересующими их событиями — с изменением xCoordinate и yCoordinate проигрывателя. Это где делегат функция входит в картину.

Основная цель функций делегата — передать управление всем заинтересованным наблюдателям при наступлении любого события в наблюдаемом объекте. Это основная функция в шаблоне наблюдателя. Именно эта функция позволяет вам иметь 2 варианта шаблона Observer. Сначала мы увидим модель Push модели наблюдателя.

Функция делегата определена в наблюдаемом объекте. Чтобы заставить делегатскую функцию делать полезные вещи, методы, вызывающие изменение свойств объекта, должны вызывать делегатскую функцию. Кроме того, класс Observable должен поддерживать список наблюдателей.

В модели Push функция делегата перебирает каждого из этих наблюдателей и передает соответствующую информацию Наблюдателю. Следующий код демонстрирует новую реализацию класса player с использованием модели push.

А теперь вот новое определение класса Player, которое теперь реализует интерфейс Observable.

public class Player implements Observable{

private String name;
private Integer xCooridinate;
private Integer yCooridinate;

private List<observer> observers ;

public Player(String name) {
this.name = name;
this.observers=new ArrayList();
}


public Integer getxCooridinate() {
return xCooridinate;
}

public Integer getyCooridinate() {
return yCooridinate;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public void moveForward(Integer steps){
xCooridinate+=steps;
notifyObservers();
}

public void moveBackward(Integer steps){
xCooridinate-=steps;
notifyObservers();
}

public void jump(Integer height){
yCooridinate+=height;
notifyObservers();
}

public void fallTo(Integer height){
yCooridinate=height;
notifyObservers();
}

//This is the core function that delegates the change in the state of the
//Observable object to the observers
public void notifyObservers() {
for(Observer o:observers){
if(o instanceof SoundEffectController){
o.update(yCooridinate);
}
else{
if(o instanceof LightEffectController){
o.update(xCooridinate);
}
}
}
}

public void addObserver(Observer o) {
this.observers.add(o);
}

public void removeObserver(Observer o) {
this.observers.remove(o);
}
}
</observer>

Вот основной класс.

public class Main {
public static void main(String[] args){
Player myDog = new Player("ScoobyDoo");

myDog.addObserver(new SoundEffectController());
myDog.addObserver(new LightEffectController());

myDog.moveForward(2);
System.out.println("Nothing yet.");
myDog.moveForward(3);
System.out.println("Nothing yet.");

myDog.moveForward(7);

myDog.jump(3);

}
}

Запустив основную программу, вы получите следующий вывод.

Nothing yet.
Nothing yet.
Flicker the lights
Play an eerie sound

Как видно из приведенного выше кода, ясно видно, что функция делегата — notifyObservers () вызывает конкретную функцию в Observer, чтобы уведомить ее об изменениях в состоянии. При внимательном рассмотрении вы заметите, что функция делегата не так гибка, как хотелось бы. Это связано с тем, что делегат должен знать о данных, которые каждый наблюдатель заинтересован в прослушивании, что делает функцию делегата более сложной, чем она должна быть. По мере того, как время идет, и все больше наблюдателей хотят получать уведомления об изменениях в объекте Observable, функция делегата становится более запутанной, поскольку ей необходимо знать требования каждого типа наблюдателей. Например, может быть ‘n’ количество наблюдателей, заинтересованных в изменении координаты x игрока, в то время как может быть ‘m’количество наблюдателей, которые могут быть заинтересованы в изменении координаты «у» игрока. Также может быть «l» количество наблюдателей, которые могут быть заинтересованы в координатах x и y игрока. Проблема, безусловно, становится все труднее решить, так как разнообразие и количество наблюдателей увеличивается со временем.

Здесь на помощь приходит модель тяги. В модели извлечения функция делегата отправляет сам объект Observable наблюдателям. Затем ответственность за извлечение информации, в которой он заинтересован, становится обязанностью наблюдателя из наблюдаемого объекта. В нашем случае это можно реализовать следующим образом.

Измените код в notifyObservers () в классе Player на следующий

public void notifyObservers() {
for(Observer o:observers){
o.update(this);
}
}

Измените функцию обновления в SoundEffectControler на следующую

public void update(Object o) {
if(o instanceof Player){
Player p = (Player)o;
Integer yCoordiante = p.getyCooridinate();
if(yCoordiante>0 && isSoundPlaying== false){
playSound();
}
}
}

Измените функцию обновления в LightEffectController на следующую

public void update(Object o) {
if(o instanceof Player){
Player p = (Player)o;
Integer xCoordiante = p.getxCooridinate();
if(xCoordiante>10 && isLightFlickering==false){
flickerLights();
}
}
}

Запустив основную программу, вы получите следующий вывод.

Nothing yet.
Nothing yet.
Flicker the lights
Play an eerie sound

Как видно из приведенного выше кода, сложность теперь перешла от функции делегата к классам Observer. В настоящее время ответственность Обозревателя заключается в том, чтобы извлечь информацию, а также выполнить приведение типов для идентификации объекта, с которым она уведомляется.

Когда вы решите реализовать шаблон наблюдателя, вы должны иметь в виду несколько вещей. Push-модель отлично работает в тех областях, где у вас есть несколько компонентов, которые создают систему, и некоторые из этих компонентов действуют как наблюдатели для множества различных типов наблюдаемых объектов. В этом случае вы бы предпочли предоставить наблюдателю только определенную часть информации вместо всего наблюдаемого объекта. Это значительно увеличит разделение компонентов Observer от компонентов Observable, что даст вам возможность использовать одного и того же наблюдателя для нескольких объектов.

Модель pull отлично работает, если ваш объект Observable уже на месте и вы создаете компоненты вокруг этого объекта Observable. Если существует много наблюдателей, и каждый наблюдатель зависит от различных комбинаций свойств наблюдаемого объекта. В этом случае, поскольку компоненты разрабатываются с учетом только определенного класса Observable, перенос сложности на классы Observer представляется хорошей идеей. Каждый наблюдатель будет извлекать соответствующую информацию из наблюдаемой и выполнять необходимые действия.

Как и в случае с любым шаблоном проектирования, способ его реализации зависит от множества факторов. Понимание того, как конкретная реализация повлияет на ваше решение, должно быть ключевым вопросом при выборе конкретной реализации.

Удачного программирования ?

От http://mycodefixes.blogspot.com/2010/07/observer-pattern.html