從抽象談起(二):觀察者模式與回調


觀察者模式又叫發布訂閱模式,有訂閱者和發布者;發布者可以包含了多個訂閱者訂閱的事件,一旦發布者執行,會執行所有的訂閱者訂閱的事件。我覺得這么講還是很迷糊。其實就是說“發布者”是一段上層代碼,他知道他所需要執行的過程中會發生一些事情,而這些事情具體邏輯自己又不知道,就算知道所有的邏輯,要用條件分支判斷執行,這總歸的是不好的,所以才有了這個模式。這是一個非常棒的模式。他使得發布者的代碼保持不變。而訂閱者的事件可以散步在他們自己的代碼中。

我們實際應用中最常見的就是頁面中的按鈕點擊事件。當我們雙擊webform中的按鈕后會自動生成一個btn_OnClick的方法,然后在里面編寫一些邏輯,同時也生成了btn.Click+= new EventHandler(btn_OnClick)代碼(只是2.0之后這個代碼就被隱藏起來了),這就是給按鈕btn(訂閱者)訂閱了一個事件。這些邏輯理當屬於按鈕所在的頁面,而不是需要執行這個方法的代碼中。

當按鈕點擊之后,會觸發頁面的提交,webform框架可以獲取是哪個按鈕被點擊過,然后執行btn.Click(),就可以執行我們具體的邏輯了。 設想如果不用這個模式,按鈕的Click方法是不是要寫很多switch來判斷是哪個按鈕,然后調用該有的邏輯。

那么假如說我們不用.NET的這套事件機制,該如何漂亮的抽象出Click的代碼呢?

其實只要相當上一節的策略模式,我們只要給Click接受一個IClickEvent接口,然后button類再包含一組IClickEvent的成員,就可以遍歷這些成員執行了。訂閱的代碼就變成了button.AddEvent(new xxClickEvent());即可。

在.net中,我們沒必要用IClickEvent接口的形式,因為我們有委托這個方法代理(或者叫方法指針),他可以說是一個只具有一個方法的接口,而.net中的事件本身也是委托。只是事件形式的委托是封閉的,不可在外部直接賦值操作,只能訂閱和刪除訂閱。

綜上來看,觀察者模式不過是一個處理未知方法的模式,他漂亮的把具體邏輯分散到他該屬於地方。

在web前端的Javascript中,這種情況就更為普遍。比如我們用jQuery時,給一個按鈕增加一個onClick方法,只需要$(“#btn”).click(function(){})即可。瀏覽器會知道具體的哪個按鈕被點擊,甚至我們隨便點擊頁面的一個地方,都會被瀏覽器截獲,假如我們有相應的OnClick方法,他會執行調用,並傳值給我們當前鼠標的位置等。 在我們發起一個ajax請求時,會有一個參數是callback方法,在判斷完XmlHttpRequest的readyState == 4后調用,每個ajax的請求完后的callback都不一致。所以說他也是觀察者模式。我們說這種叫做回調模式是不是更好?所謂的“回”就是使用之前的代碼,而不是當前的代碼。

所以說委托或者回調(方法指針)也是一種抽象,在不具備這種能力的語言里可用接口來代替。重復上節的話,抽象提取變化事物的共性,不管是面向對象還是過程式還是函數式,都離不開抽象的思想。

說了這么多,不知道表達清楚沒有,我們來講一個實際應用不依賴於框架的。

比如我們發布一篇文章,常用邏輯就是保存文章。如果哪天來了新的需求,比如說跟某某公司合作,發表完文章之后需要給用戶增加一些獎勵。又過了幾天又來一個新的需求,所最近抓的緊,需要對文章審核,一旦有違禁的關鍵詞不允許發布。這兩個邏輯之前都不存在。我們是不是要修改代碼呢?這兩個需求都是臨時的。假如修改的保存邏輯,回頭合作取消和風頭過后又要取消掉。這太不合適了。我們應讓代碼的修改的范圍縮到最小。

如果我們在保存邏輯前后增加事件,比如PreSave和EndSave,然后對應增加相關的訂閱代碼,就不需要修改Save的邏輯。但是調用Save的代碼依然需要增加兩個方法和訂閱代碼。

public class ArticleController : Controller
{
	public ActionResult Save(Article article)
	{
		var articleManager = new ArticleManager();
		articleManager.PreSaveEvent +=  x => 
		{
			var shouldBlock = BlockWords.Filter(article);
			if(shouldBlock)
			{
				throw new SecretExcepetion("對不起,您的文章中包含違禁關鍵詞,請檢查");
				//封殺用戶
			}
		};
		
		articleManager.EndSaveEvent += x=>
		{
			//獎勵用戶
		}
		
		articleManager.Save(article);
	}
}

public class ArticleManager
{
	public event Action<Article> PreSaveEvent;
	public event Action<Article> EndSaveEvent;
	
	public void Save(Article article)
	{
		if(PreSaveEvent!=null)
		{
			PreSaveEvent(article);
		}
		var db = new ArticleRepository();
		db.Save(article);
		if(EndSaveEvent!=null)
		{
			EndSaveEvent(article);
		}
	}
}

public class ArticleRepository
{
	public void Save(Article article)
	{
		database.Save(article);
	}
}

雖然上面的用的是事件的方式,如果用委托作為Save的參數也可以,只是作為參數對以后的重構可能會帶來麻煩。

這樣只需要修改Controller的代碼就能改變這些需求,如果不連Controller的代碼也不想改怎么辦呢?后面再說吧。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM