- 一文帶你搞懂Java動態代理
- 幾分鐘帶你搞懂策略模式
- 幾分鐘帶你搞懂觀察者模式
- 一文徹底搞明白工廠和抽象工廠
- 一文搞明白裝飾者模式
- 最全單例模式
- 幾段代碼搞明白命令模式
- 幾段代碼搞明白適配器模式
- 一看就懂的外觀模式
- 一看就懂的模版方法模式
- 幾段代碼搞懂迭代器模式
- 一文搞懂明白狀態模式
老李:小張啊,最近忙嘛呢?下班就跑
小張:昨天買了彩票,今天去看下自己是否財務自由了
老李:官網註冊個賬號,坐等中獎號碼通知不香嗎?
小張:我就是不想加班…,嗯確實香,老李你先把刀放下
老李:哪天心灰意冷了,就註銷掉,別辜負了工作給你的熱情
小張:…
觀察者模式
關於觀察者模式的定義,我就直接引用HeadFirst書中的描述了:觀察者模式定義了對象之間的一對多依賴,這樣一來,當一個對象改變狀態時,它的所有依賴者都會收到通知並自動更新。我們通常把有狀態的對象稱爲主題,收到主題通知的依賴者對象稱爲觀察者。主題和觀察者定義了一對多的關係,觀察者依賴於主題,只要主題狀態一有變化,觀察者就會被通知。類圖見下:
我們來用程序語音來描述下,以彩票官網爲例,彩民可以自由的向其註冊或取消註冊,當中獎號碼更新後,即官網此狀態改變後,每個註冊過的彩民都會收到官網傳來的通知。這裏的官網就相當於我們所說的主題,彩民相當於我們的觀察者,我們可先創建一個主題接口類:
/**
* 這是主題類。
*
* <p>用戶只要向其註冊,主題狀態改變後,
* 就可以收到官網發送來的彩票信息。
*/
public interface ISubject {
// 給彩民用戶提供的註冊和移除方法
void registerObserver(IObserver o);
void removeObserver(IObserver o);
// 給用戶發送“彩票信息變化通知”
void notifyLottery();
}
爲什麼要使用接口,而不是直接使用具體的主題類,因爲不想主題與觀察者過分耦合,要努力使對象之間的互相依賴降到最低,這樣才能夠應付變化,建立有彈性的OO系統。 這是一個彩民接口即觀察者接口,這個接口只有一個updateLottery(Lottery lottery)方法,當主題的狀態改變時它就會被調用。
/**
* 觀察者接口類。
*
* <p>所有的觀察者都必須實現該接口,關於觀察者的一切
* 主題只知道觀察者實現了當前接口即IObserver
* 主題不需要知道觀察者的具體類是誰、做了些什麼或其他任何細節
* 這就使主題和觀察者之間的依賴程度非常低。
*/
public interface IObserver {
// 當知道彩票信息更新後的處理方法
void updateLottery(Lottery lottery);
}
這是一個具體的主題類,一個具體的主題總是實現主題接口,除了註冊和取消註冊方法之外,具體主題還實現了notifyLottery()方法,此方法用於在狀態改變時更新所有當前觀察者,即彩票信息改變時,將彩票的當前信息通知給彩民。
/**
* 具體的主題類
*/
public class LotteryData implements ISubject{
// 持有彩民(觀察者)的類
private ArrayList<IObserver> list = new ArrayList<>();
// 彩票信息類
private Lottery lottery;
@Override
public void registerObserver(IObserver o) {
list.add(o);
}
@Override
public void removeObserver(IObserver o) {
int index = list.indexOf(o);
if(index != -1){
list.remove(index);
}
}
@Override
public void notifyLottery() {
for(IObserver o : list){
o.updateLottery(lottery);
}
}
/**
* 智能彩票機開始搖號。
*
* <p>這裏模擬5s爲1天的情況,每5s彩票狀態改變一次。
*/
public void beginWork() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
notifyInfo();
}
}, 0, 5000);
}
private void notifyInfo() {
if(lottery == null)
lottery = new Lottery();
// 添加日期
lottery.setDate(new Date());
// 添加中獎數字,這裏測試只有五位數了
lottery.setWinningCount(new Random().nextInt(90000)+10000);
// 彩票狀態改變,通知自己的所有依賴者進行更新
notifyLottery();
}
}
當彩票狀態改變時,我們將Lottery數據直接推(push)給了觀察者,但是有的觀察者可能只需要一點點數據(如只需要獲獎數字不需要時間),並不想被強迫的收到所有數據。這時我們可以考慮讓觀察者自己從主題中拉(pull)數據,主題只需要提供公開的get方法即可。這是彩票的實體類,包括彩票的所屬日期和當前中獎號碼,可以根據需要隨意增添。
/**
* 彩票信息類
*/
public class Lottery {
// 彩票的日期
private Date date;
// 彩票的獲獎數字
private int winningCount;
public Date getDate() {
return date;
}
public void setDate(Date date) {
this.date = date;
}
public int getWinningCount() {
return winningCount;
}
public void setWinningCount(int winningCount) {
this.winningCount = winningCount;
}
}
這是具體的觀察者彩民1號,觀察者必須實現IObserver接口和註冊具體主題,以便接收更新。
/**
* 具體的觀察者
*/
public class LotteryBuyerOne implements IObserver{
public LotteryBuyerOne(ISubject s) {
// 註冊
s.registerObserver(this);
}
@Override
public void updateLottery(Lottery lottery) {
System.out.println("我是彩民1號 彩票日期:"+lottery.getDate()+" 中獎號碼爲:"+lottery.getWinningCount());
}
}
根據需要我們可以隨意添加觀察者,因爲觀察者和主題之間是松耦合的,所以我們改變觀察者或者主題其中一方,並不會影響另一方。我們來測試一下這個設計吧。
public class ObserverPatternTest {
public static void main(String[] args) {
// 聲明一個主題
final LotteryData subject = new LotteryData();
// 註冊彩民用戶
final LotteryBuyerOne loOne = new LotteryBuyerOne(subject);
subject.beginWork();
final Timer timer = new Timer();
// 6s後彩民1號,因爲總中不了獎失去了興趣,取消註冊了
timer.schedule(new TimerTask() {
@Override
public void run() {
subject.removeObserver(loOne);
timer.cancel();
}
}, 6000);
}
}
三、內置觀察者模式
除了我們自己實現一整套觀察者模式,java還提供了內置的觀察者模式。java.util包(package)內包含最基本的Observer接口和Observable類,這和我們的Observer接口和Subject接口很相似。同樣的場景我們用內置觀察者模式看下:
這是一個具體的主題類,因爲Observable是個具體類而不是接口,所以在擴展性上不是很靈活,限制了Observable的複用潛力。
/**
* 具體的主題
*/
public class LotteryData extends Observable{
// 彩票信息類
private Lottery lottery;
/**
* 這裏模擬每5s彩票狀態改變一次
*/
public void beginWork() {
Timer timer = new Timer();
timer.schedule(new TimerTask() {
@Override
public void run() {
notifyInfo();
}
}, 0, 5000);
}
public void notifyInfo() {
if(lottery == null)
lottery = new Lottery();
// 添加日期
lottery.setDate(new Date());
// 添加中獎數字,這裏測試只有五位數了
lottery.setWinningCount(new Random().nextInt(90000)+10000);
// 彩票狀態改變,通知自己的所有依賴者進行更新
updata();
}
/**
* 提供了
*/
public Date getDate() {
return lottery.getDate();
}
public int getWinningCount() {
return lottery.getWinningCount();
}
private void updata() {
setChanged(); // 改變狀態
notifyObservers(this); // 通知觀察者
}
}
Observable爲我們提供了notifyObservers()方法和notifyObservers(Object arg)方法,所以如果你想推(push)數據給觀察者,直接可以把數據對象傳遞給一個參數的更新方法,而如果你想讓觀察者拉(pull)數據,只需要調用無參數更新方法,同時提供公開的get方法即可。這是具體的觀察者彩民1號
/**
* 具體的觀察者1號。
* 通過向官網註冊,當彩票狀態發生改變獲得通知。
*/
public class LotteryBuyerOne implements Observer{
public LotteryBuyerOne(Observable observable) {
observable.addObserver(this);
}
@Override
public void update(Observable o, Object arg) {
// 當彩票狀態改變的時候,彩民需要獲得通知更新
if(o != null && o instanceof LotteryData){
LotteryData lotteryData = (LotteryData)o;
System.out.println("我是彩民1號->彩票日期:"
+ lotteryData.getDate() + ", 中獎號碼爲:"
+ lotteryData.getWinningCount());
}
}
}
來測試一下這個設計吧。需要注意的是,內置的觀察者模式,通知的次序不同於我們註冊的次序,所以當我們對於通知順序有要求的時候,不能使用內置的觀察者模式。
public class BuiltInObserverPatternTest {
public static void main(String[] args) {
// 聲明一個具體主題
final LotteryData lotteryData = new LotteryData();
// 聲明觀察者1號並註冊
final LotteryBuyerOne lBuyerOne = new LotteryBuyerOne(lotteryData);
lotteryData.beginWork();
final Timer timer = new Timer();
// 6s後彩民1號,因爲總中不了獎失去了興趣,取消註冊了
timer.schedule(new TimerTask() {
@Override
public void run() {
lotteryData.deleteObserver(lBuyerOne);
timer.cancel();
}
}, 6000);
}
}