从抽象谈起(二):观察者模式与回调

观察者模式又叫发布订阅模式,有订阅者和发布者;发布者可以包含了多个订阅者订阅的事件,一旦发布者执行,会执行所有的订阅者订阅的事件。我觉得这么讲还是很迷糊。其实就是说“发布者”是一段上层代码,他知道他所需要执行的过程中会发生一些事情,而这些事情具体逻辑自己又不知道,就算知道所有的逻辑,要用条件分支判断执行,这总归的是不好的,所以才有了这个模式。这是一个非常棒的模式。他使得发布者的代码保持不变。而订阅者的事件可以散步在他们自己的代码中。

我们实际应用中最常见的就是页面中的按钮点击事件。当我们双击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的代码也不想改怎么办呢?后面再说吧。

posted @ 2013-06-30 16:32  君之蘭  阅读(2709)  评论(2编辑  收藏  举报