設計模式(二)帶你搞懂觀察者模式

老李:小張啊,最近忙嘛呢?下班就跑
小張:昨天買了彩票,今天去看下自己是否財務自由了
老李:官網註冊個賬號,坐等中獎號碼通知不香嗎?
小張我就是不想加班…,嗯確實香,老李你先把刀放下
老李:哪天心灰意冷了,就註銷掉,別辜負了工作給你的熱情
小張:…

觀察者模式

關於觀察者模式的定義,我就直接引用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);
	}
}

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章