實例解析觀察者模式及其在Java設(shè)計模式開發(fā)中的運用
一、觀察者模式(Observer)的定義:
觀察者模式又稱為訂閱—發(fā)布模式,在此模式中,一個目標(biāo)對象管理所有相依于它的觀察者對象,并且在它本身的狀態(tài)改變時主動發(fā)出通知。這通常透過呼叫各觀察者所提供的方法來實現(xiàn)。此種模式通常被用來事件處理系統(tǒng)。
1、觀察者模式的一般結(jié)構(gòu)
首先看下觀察者模式的類圖描述:

觀察者模式的角色如下:
Subject(抽象主題接口):定義了主題類中對觀察者列表的一系列操作, 包括增加,刪除, 通知等。
Concrete Subject(具體主題類):
Observer(抽象觀察者接口):定義了觀察者對主題類更新狀態(tài)接受操作。
ConcreteObserver(具體觀察者類):實現(xiàn)觀察者接口更新主題類通知等邏輯。
從這個類圖可以看出, 主題類中維護了一個實現(xiàn)觀察者接口的類列表, 主題類通過這個列表來對觀察者進行一系列的增刪改操作。觀察者類也可以主動調(diào)用update方法來了解獲取主題類的狀態(tài)更新信息。
以上的類圖所描述的只是基本的觀察者模式的思想, 有很多不足。比如作為觀察者也可以主動訂閱某類主題等。下面的例子將進行一些改動, 以便適用具體的業(yè)務(wù)邏輯。
2、觀察者模式示例
我們構(gòu)建一個觀察者和主題類, 觀察者可以主動訂閱主題或者取消主題。主題類統(tǒng)一被一個主題管理者所管理。下面給出類圖:

Subject:
public interface Subject {
//注冊一個observer
public void register(Observer observer);
//移除一個observer
public void remove(Observer observer);
//通知所有觀察者
public void notifyObservers();
//獲取主題類要發(fā)布的消息
public String getMessage();
}
ConcerteSubject:
public class MySubject implements Subject {
private List<Observer> observers;
private boolean changed;
private String message;
//對象鎖, 用于同步更新觀察者列表
private final Object mutex = new Object();
public MySubject() {
observers = new ArrayList<Observer>();
changed = false;
}
@Override
public void register(Observer observer) {
if (observer == null)
throw new NullPointerException();
//保證不重復(fù)
if (!observers.contains(observer))
observers.add(observer);
}
@Override
public void remove(Observer observer) {
observers.remove(observer);
}
@Override
public void notifyObservers() {
// temp list
List<Observer> tempObservers = null;
synchronized (mutex) {
if (!changed)
return;
tempObservers = new ArrayList<>(this.observers);
this.changed = false;
}
for(Observer obj : tempObservers) {
obj.update();
}
}
//主題類發(fā)布新消息
public void makeChanged(String message) {
System.out.println("The Subject make a change: " + message);
this.message = message;
this.changed = true;
notifyObservers();
}
@Override
public String getMessage() {
return this.message;
}
}
ConcerteSubject做出更新時, 就通知列表中的所有觀察者, 并且調(diào)用觀察者update方法以實現(xiàn)接受通知后的邏輯。這里注意notifyObservers中的同步塊。在多線程的情況下, 為了避免主題類發(fā)布通知時, 其他線程對觀察者列表的增刪操作, 同步塊中用一個臨時List來獲取當(dāng)前的觀察者列表。
SubjectManagement:主題類管理器
public class SubjectManagement {
//一個記錄 名字——主題類 的Map
private Map<String, Subject> subjectList = new HashMap<String, Subject>();
public void addSubject(String name, Subject subject) {
subjectList.put(name, subject);
}
public void addSubject(Subject subject) {
subjectList.put(subject.getClass().getName(), subject);
}
public Subject getSubject(String subjectName) {
return subjectList.get(subjectName);
}
public void removeSubject(String name, Subject subject) {
}
public void removeSubject(Subject subject) {
}
//singleton
private SubjectManagement() {}
public static SubjectManagement getInstance() {
return SubjectManagementInstance.instance;
}
private static class SubjectManagementInstance {
static final SubjectManagement instance = new SubjectManagement();
}
}
主題類管理器的作用就是在觀察者訂閱某個主題時, 獲取此主題的實例對象。
Observer:
public interface Observer {
public void update();
public void setSubject(Subject subject);
}
ConcerteObserver:
public class MyObserver implements Observer {
private Subject subject;
// get the notify message from Concentrate Subject
@Override
public void update() {
String message = subject.getMessage();
System.out.println("From Subject " + subject.getClass().getName()
+ " message: " + message);
}
@Override
public void setSubject(Subject subject) {
this.subject = subject;
}
// subcirbe some Subject
public void subscribe(String subjectName) {
SubjectManagement.getInstance().getSubject(subjectName).register(this);
}
// cancel subcribe
public void cancelSubcribe(String subjectName) {
SubjectManagement.getInstance().getSubject(subjectName).remove(this);
}
}
測試:我們將主題類和觀察者抽象成寫者和讀者
public class ObserverTest {
private static MySubject writer;
@BeforeClass
public static void setUpBeforeClass() throws Exception {
writer = new MySubject();
//添加一個名為Linus的作家
SubjectManagement.getInstance().addSubject("Linus",writer);
}
@Test
public void test() {
//定義幾個讀者
MyObserver reader1 = new MyObserver();
MyObserver reader2 = new MyObserver();
MyObserver reader3 = new MyObserver();
reader1.setSubject(writer);
reader2.setSubject(writer);
reader3.setSubject(writer);
reader1.subscribe("Linus");
reader2.subscribe("Linus");
reader3.subscribe("Linus");
writer.makeChanged("I have a new Changed");
reader1.update();
}
}
以上就是觀察者模式的小示例??梢钥闯雒總€主題類都要維護一個相應(yīng)的觀察者列表, 這里可以根據(jù)具體主題的抽象層次進一步抽象, 將這種聚集放到一個抽象類中去實現(xiàn), 來共同維護一個列表, 當(dāng)然具體操作要看實際的業(yè)務(wù)邏輯。
二、Servlet中的Listener
再說Servlet中的Listener之前, 先說說觀察者模式的另一種形態(tài)——事件驅(qū)動模型。與上面提到的觀察者模式的主題角色一樣, 事件驅(qū)動模型包括事件源, 具體事件, 監(jiān)聽器, 具體監(jiān)聽器。
Servlet中的Listener就是典型的事件驅(qū)動模型。
JDK中有一套事件驅(qū)動的類, 包括一個統(tǒng)一的監(jiān)聽器接口和一個統(tǒng)一的事件源, 源碼如下:
/**
* A tagging interface that all event listener interfaces must extend.
* @since JDK1.1
*/
public interface EventListener {
}
這是一個標(biāo)志接口, JDK規(guī)定所有監(jiān)聽器必須繼承這個接口。
public class EventObject implements java.io.Serializable {
private static final long serialVersionUID = 5516075349620653480L;
/**
* The object on which the Event initially occurred.
*/
protected transient Object source;
/**
* Constructs a prototypical Event.
*
* @param source The object on which the Event initially occurred.
* @exception IllegalArgumentException if source is null.
*/
public EventObject(Object source) {
if (source == null)
throw new IllegalArgumentException("null source");
this.source = source;
}
/**
* The object on which the Event initially occurred.
*
* @return The object on which the Event initially occurred.
*/
public Object getSource() {
return source;
}
/**
* Returns a String representation of this EventObject.
*
* @return A a String representation of this EventObject.
*/
public String toString() {
return getClass().getName() + "[source=" + source + "]";
}
}
EvenObject是JDK給我們規(guī)定的一個統(tǒng)一的事件源。EvenObject類中定義了一個事件源以及獲取事件源的get方法。
下面就分析一下Servlet Listener的運行流程。
1、Servlet Listener的組成
目前, Servlet中存在6種兩類事件的監(jiān)聽器接口, 具體如下圖:

具體觸發(fā)情境如下表:

2、一個具體的Listener觸發(fā)過程
我們以ServletRequestAttributeListener為例, 來分析一下此處事件驅(qū)動的流程。
首先一個Servlet中, HttpServletRequest調(diào)用setAttrilbute方法時, 實際上是調(diào)用的org.apache.catalina.connector.request#setAttrilbute方法。 我們看下它的源碼:
public void setAttribute(String name, Object value) {
...
//上面的邏輯代碼已省略
// 此處即通知監(jiān)聽者
notifyAttributeAssigned(name, value, oldValue);
}
下面是notifyAttributeAssigned(String name, Object value, Object oldValue)的源碼
private void notifyAttributeAssigned(String name, Object value,
Object oldValue) {
//從容器中獲取webAPP中定義的Listener的實例對象
Object listeners[] = context.getApplicationEventListeners();
if ((listeners == null) || (listeners.length == 0)) {
return;
}
boolean replaced = (oldValue != null);
//創(chuàng)建相關(guān)事件對象
ServletRequestAttributeEvent event = null;
if (replaced) {
event = new ServletRequestAttributeEvent(
context.getServletContext(), getRequest(), name, oldValue);
} else {
event = new ServletRequestAttributeEvent(
context.getServletContext(), getRequest(), name, value);
}
//遍歷所有監(jiān)聽器列表, 找到對應(yīng)事件的監(jiān)聽器
for (int i = 0; i < listeners.length; i++) {
if (!(listeners[i] instanceof ServletRequestAttributeListener)) {
continue;
}
//調(diào)用監(jiān)聽器的方法, 實現(xiàn)監(jiān)聽操作
ServletRequestAttributeListener listener =
(ServletRequestAttributeListener) listeners[i];
try {
if (replaced) {
listener.attributeReplaced(event);
} else {
listener.attributeAdded(event);
}
} catch (Throwable t) {
ExceptionUtils.handleThrowable(t);
context.getLogger().error(sm.getString("coyoteRequest.attributeEvent"), t);
// Error valve will pick this exception up and display it to user
attributes.put(RequestDispatcher.ERROR_EXCEPTION, t);
}
}
}
上面的例子很清楚的看出ServletRequestAttributeListener是如何調(diào)用的。用戶只需要實現(xiàn)監(jiān)聽器接口就行。Servlet中的Listener幾乎涵蓋了Servlet整個生命周期中你感興趣的事件, 靈活運用這些Listenser可以使程序更加靈活。
三、綜合示例
舉個例子,如果你看過TVB的警匪片,你就知道臥底的工作方式。一般一個警察可能有幾個臥底,潛入敵人內(nèi)部,打探消息,臥底完全靠他的領(lǐng)導(dǎo)的指示干活,領(lǐng)導(dǎo)說幾點行動,他必須按照這個時間去執(zhí)行,如果行動時間改變,他也要立馬改變自己配合行動的時間。領(lǐng)導(dǎo)派兩個臥底去打入敵人內(nèi)部,那么領(lǐng)導(dǎo)相當(dāng)于抽象主題,而督察警官張三這個人派了兩個臥底李四和萬王五,張三就相當(dāng)于具體主題,臥底相當(dāng)于抽象觀察者,這兩名臥底是李四和王五就是具體觀察者,派的這個動作相當(dāng)于觀察者在主題的登記。那么這個類圖如下:

利用javaAPI來實現(xiàn),代碼描述如下:
package observer;
import java.util.List;
import java.util.Observable;
import java.util.Observer;
/**
*描述:警察張三
*/
public class Police extends Observable {
private String time ;
public Police(List<Observer> list) {
super();
for (Observer o:list) {
addObserver(o);
}
}
public void change(String time){
this.time = time;
setChanged();
notifyObservers(this.time);
}
}
package observer;
import java.util.Observable;
import java.util.Observer;
/**
*描述:臥底A
*/
public class UndercoverA implements Observer {
private String time;
@Override
public void update(Observable o, Object arg) {
time = (String) arg;
System.out.println("臥底A接到消息,行動時間為:"+time);
}
}
package observer;
import java.util.Observable;
import java.util.Observer;
/**
*描述:臥底B
*/
public class UndercoverB implements Observer {
private String time;
@Override
public void update(Observable o, Object arg) {
time = (String) arg;
System.out.println("臥底B接到消息,行動時間為:"+time);
}
}
package observer;
import java.util.ArrayList;
import java.util.List;
import java.util.Observer;
/**
*描述:測試
*/
public class Client {
/**
* @param args
*/
public static void main(String[] args) {
UndercoverA o1 = new UndercoverA();
UndercoverB o2 = new UndercoverB();
List<Observer> list = new ArrayList<>();
list.add(o1);
list.add(o2);
Police subject = new Police(list);
subject.change("02:25");
System.out.println("===========由于消息敗露,行動時間提前=========");
subject.change("01:05");
}
}
測試運行結(jié)果:
臥底B接到消息,行動時間為:02:25 臥底A接到消息,行動時間為:02:25 ===========由于消息敗露,行動時間提前========= 臥底B接到消息,行動時間為:01:05 臥底A接到消息,行動時間為:01:05
四、總結(jié)
觀察者模式定義了對象之間一對多的關(guān)系, 當(dāng)一個對象(被觀察者)的狀態(tài)改變時, 依賴它的對象都會收到通知??梢詰?yīng)用到發(fā)布——訂閱, 變化——更新這種業(yè)務(wù)場景中。
觀察者和被觀察者之間用松耦合的方式, 被觀察者不知道觀察者的細(xì)節(jié), 只知道觀察者實現(xiàn)了接口。
事件驅(qū)動模型更加靈活,但也是付出了系統(tǒng)的復(fù)雜性作為代價的,因為我們要為每一個事件源定制一個監(jiān)聽器以及事件,這會增加系統(tǒng)的負(fù)擔(dān)。
觀察者模式的核心是先分清角色、定位好觀察者和被觀察者、他們是多對一的關(guān)系。實現(xiàn)的關(guān)鍵是要建立觀察者和被觀察者之間的聯(lián)系、比如在被觀察者類中有個集合是用于存放觀察者的、當(dāng)被檢測的東西發(fā)生改變的時候就要通知所有觀察者。在觀察者的構(gòu)造方法中將被觀察者傳入、同時將本身注冊到被觀察者擁有的觀察者名單中、即observers這個list中。
1.觀察者模式優(yōu)點:
(1)抽象主題只依賴于抽象觀察者
(2)觀察者模式支持廣播通信
(3)觀察者模式使信息產(chǎn)生層和響應(yīng)層分離
2.觀察者模式缺點:
(1)如一個主題被大量觀察者注冊,則通知所有觀察者會花費較高代價
(2)如果某些觀察者的響應(yīng)方法被阻塞,整個通知過程即被阻塞,其它觀察者不能及時被通知
- Java通俗易懂系列設(shè)計模式之觀察者模式
- Java設(shè)計模式之觀察者模式原理與用法詳解
- JAVA中常用的設(shè)計模式:單例模式,工廠模式,觀察者模式
- Java設(shè)計模式—觀察者模式詳解
- 23種設(shè)計模式(13)java觀察者模式
- Java設(shè)計模式之觀察者模式_動力節(jié)點Java學(xué)院整理
- Java經(jīng)典設(shè)計模式之觀察者模式原理與用法詳解
- java設(shè)計模式之觀察者模式學(xué)習(xí)
- java設(shè)計模式之觀察者模式
- Java設(shè)計模式開發(fā)中使用觀察者模式的實例教程
- Java設(shè)計模式之觀察者模式(Observer模式)
相關(guān)文章
IntelliJ IDEA2023中運行Spring Boot找不到VM options進
這篇文章主要介紹了IntelliJ IDEA2023中運行Spring Boot找不到VM options進行端口的修改的問題解決,本文通過圖文并茂的形式給大家介紹的非常詳細(xì),需要的朋友可以參考下2023-11-11
Java畢業(yè)設(shè)計實戰(zhàn)之財務(wù)預(yù)算管理系統(tǒng)的實現(xiàn)
這是一個使用了java+SSM+Jsp+Mysql+Layui+Maven開發(fā)的財務(wù)預(yù)算管理系統(tǒng),是一個畢業(yè)設(shè)計的實戰(zhàn)練習(xí),具有財務(wù)預(yù)算管理該有的所有功能,感興趣的朋友快來看看吧2022-02-02
解決@CachePut設(shè)置的key值無法與@CacheValue的值匹配問題
這篇文章主要介紹了解決@CachePut設(shè)置的key的值無法與@CacheValue的值匹配問題,具有很好的參考價值,希望對大家有所幫助。如有錯誤或未考慮完全的地方,望不吝賜教2021-12-12

