В сегодняшнем посте я собираюсь обсудить шаблон наблюдателя. Это один из наиболее широко используемых шаблонов в приложениях 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