1. 程式人生 > >行為類模式:觀察者模式VS責任鏈模式。

行為類模式:觀察者模式VS責任鏈模式。

為什麼要把觀察者模式和責任鏈模式放在一起對比呢?看起來這兩個模式沒有太多的相似性,真沒有嗎?回答是有。我們在觀察者模式中也提到了觸發鏈(也叫做觀察者)的問題,一個具體的角色既可以是觀察者,也可以是被觀察者,這樣就形成了一個觀察者鏈。這與責任鏈模式非常類似,他們都實現了事務的鏈條化處理,比如說在上課的時候你睡著了,打鼾聲音太大,蓋過了老師講課聲音,老師火了,捅到了校長這裡,校長也處理不了,然後告狀給你父母,於是你的魔鬼日子來臨了,這是責任鏈模式,老師、校長、父母都是鏈中的一個具體角色,事件(你睡覺)在鏈中傳遞,最終由一個具體的節點來處理,並將結果反饋給呼叫者(你捱揍了)。那什麼是觸發鏈?你還是在課堂上睡覺,還是打鼾聲音太大,老師火了,但是老師掏出個擴音器來講課,於是你睡不著了,同時其他同學的耳朵遭殃了,這就是觸發鏈,其實老師既是觀察者(相對你)也是被觀察者(相對其他同學),事件從“你睡覺”到老師這裡轉化為“擴音器放大聲音”,這也是一個鏈條結構,但是鏈結構中傳遞的事情改變了。

我們還是從一個具體的例子來說明兩者的區別,DNS協議相信大家都聽說過,只要在“網路設定”中設定一個DNS伺服器地址就可以把我們需要的域名翻譯成IP地址。DNS協議還是比較簡單的,傳遞過去一個域名以及記錄標誌(比如是要A記錄還是要MX記錄),DNS就開始查詢自己的記錄樹,找到後把IP地址反饋給請求者。我們可以在Windows作業系統中瞭解一下NDS解析過程,在DNS視窗下輸入nslookup命令。

DNS伺服器解析IP地址,是怎麼設計的呢?他規定了每個區域的DNS伺服器(Local DNS)只保留自己區域的域名解析,對於不能解析的域名,則提交上級域名解析器解析,最終由一臺位於美國洛杉磯的頂級域名伺服器進行解析,返回結果。很明顯這是一個事務的鏈結構處理,我們使用兩種模式來實現該解析過程。

責任鏈模式實現DNS解析過程

首先我們定義一下業務場景,這裡有三個DNS伺服器:上海DNS伺服器(區域伺服器)、中國頂級DNS伺服器(父伺服器)、全球頂級DNS伺服器。

假設有請求者發觸請求,由上海DNS進行解析,如果能夠解析,則返回結果,若不能解析,則提交給父伺服器(中國頂級DNS)進行解析,若還不能解析,則提交到全球頂級DNS進行解析,若還不能解析呢?那就返回該域名無法解析。

Recorder是一個BO物件,他記錄DNS伺服器解析後的結果,包括域名、IP地址、屬主(即由誰來解析的),除此之外還有getter/setter方法。DnsServer抽象類中的resolve方法是一個基本方法,每個DNS伺服器都必須擁有該方法,他對DNS進行解析,如何解析呢?具體是由echo方法來實現的,每個DNS伺服器獨自實現。我們首先看一下解析記錄Recorder類,如下所示。

public class Recorder {
	// 域名
	private String domain;
	// IP地址
	private String ip;
	// 屬主
	private String owner;

	public String getDomain() {
		return domain;
	}

	public void setDomain(String domain) {
		this.domain = domain;
	}

	public String getIp() {
		return ip;
	}

	public void setIp(String ip) {
		this.ip = ip;
	}

	public String getOwner() {
		return owner;
	}

	public void setOwner(String owner) {
		this.owner = owner;
	}

	@Override
	public String toString() {
		String str = "域名:" + this.domain;
		str = str + "\nIP地址:" + this.ip;
		str = str + "\n解析者:" + this.owner;
		return str;
	}
}

為什麼要覆寫toString方法呢?是為了列印展示的需要,可以直接把Recorder的資訊打印出來。我們再來看抽象域名伺服器,如下所示。

public abstract class DnsServer {
	// 上級DNS是誰
	private DnsServer upperServer;

	/**
	 * 解析域名
	 * 
	 * @param domain
	 * @return
	 */
	public final Recorder resolve(String domain) {
		Recorder recorder = null;
		if (isLocal(domain)) { // 是從本伺服器能解析的域名
			recorder = this.echo(domain);
		} else { // 本伺服器不能解析
			// 提交上級DNS進行解析
			recorder = this.upperServer.resolve(domain);
		}
		return recorder;
	}

	/**
	 * 指向上級DNS
	 * 
	 * @param upperServer
	 */
	public void setUpperServer(DnsServer upperServer) {
		this.upperServer = upperServer;
	}

	/**
	 * 每個DNS都有一個數據處理區(ZONE),檢查域名是否在本區中
	 * 
	 * @param domain
	 * @return
	 */
	protected abstract boolean isLocal(String domain);

	/**
	 * 每個DNS伺服器都必須實現解析任務
	 * 
	 * @param domain
	 * @return
	 */
	protected Recorder echo(String domain) {
		Recorder recorder = new Recorder();
		// 獲得IP地址
		recorder.setIp(getIpAddress());
		recorder.setDomain(domain);
		return recorder;
	}

	/**
	 * 隨機產生一個IP地址,工具類
	 * 
	 * @return
	 */
	private String getIpAddress() {
		Random random = new Random();
		String address = random.nextInt(255) + "." + random.nextInt(255) + "."
				+ random.nextInt(255) + "." + random.nextInt(255);
		return address;
	}
}

在該類中有一個方法——getIpAddress方法——沒有在類圖中展現出來,他用於實現隨機生成IP地址,這是我們為模擬DNS解析場景而建立的一個虛擬方法,在實際的應用中是不可能出現的。抽象DNS伺服器編寫完成,我們再來看具體的DNS伺服器,先看上海的DNS伺服器,如下所示。

public class SHDnsServer extends DnsServer {

	@Override
	protected Recorder echo(String domain) {
		Recorder recorder = super.echo(domain);
		recorder.setOwner("上海DNS伺服器");
		return recorder;
	}

	@Override
	protected boolean isLocal(String domain) {
		return domain.endsWith(".sh.cn");
	}

}

為什麼覆寫echo方法?各具體的DNS伺服器實現自己的解析過程,屬於個性化處理,他代表的是每個DNS伺服器的不同處理邏輯。還要注意一下,我們在這裡做了一個簡化處理,所有以“.sh.cn”結尾的域名都由上海DNS伺服器解析。其他的中國頂級DNS和全球頂級DNS實現過程類似,如下所示。

public class ChinaTopDnsServer extends DnsServer {
	@Override
	protected Recorder echo(String domain) {
		Recorder recorder = super.echo(domain);
		recorder.setOwner("中國頂級DNS伺服器");
		return recorder;
	}

	@Override
	protected boolean isLocal(String domain) {
		return domain.endsWith(".cn");
	}

}
public class TopDnsServer extends DnsServer {
	@Override
	protected Recorder echo(String domain) {
		Recorder recorder = super.echo(domain);
		recorder.setOwner("全球頂級DNS伺服器");
		return recorder;
	}

	@Override
	protected boolean isLocal(String domain) {
		// 所有的域名最終的解析地點
		return true;
	}

}

所有的DNS伺服器都準備好了,下面我們寫一個客戶端來模擬一下IP地址是怎麼解析的,如下所示。

public class Client {
	public static void main(String[] args) throws Exception {
		// 上海域名伺服器
		DnsServer sh = new SHDnsServer();
		// 中國頂級域名伺服器
		DnsServer china = new ChinaTopDnsServer();
		// 全球頂級域名伺服器
		DnsServer top = new TopDnsServer();
		// 定義查詢路徑
		china.setUpperServer(top);
		sh.setUpperServer(china);
		// 解析域名
		System.out.println("=====域名解析模擬器=====");
		while (true) {
			System.out.println("\n請輸入域名(輸入N退出):");
			String domain = (new BufferedReader(
					new InputStreamReader(System.in))).readLine();
			if ("n".equalsIgnoreCase(domain)) {
				return;
			}
			Recorder recorder = sh.resolve(domain);
			System.out.println("----DNS伺服器解析結果----");
			System.out.println(recorder);
		}
	}
}

這個模擬過程看起來很完整,他完全就是責任鏈模式的一個具體應用,把一個請求放置到鏈中的首節點,然後由鏈中的某個節點進行解析並將結果反饋給呼叫者。但是,我可以負責任的告訴你:這個解析過程是有缺陷的,什麼缺陷?後面會說明。

觸發鏈模式實現DNS解析過程

上面說到使用責任鏈模式模擬DNS解析過程是有缺陷的,究竟有什麼缺陷?我們來做一個實現,在dos視窗下輸入nslookup命令,然後輸入多個域名,注意觀察返回值有哪些資料是相同的。可以看出,解析者都相同,都是由同一個DNS伺服器解析的,準確的說都是由本機配置的DNS伺服器做的解析。這與我們上面的模擬過程是不相同的,看看我們模擬的過程,對請求者來說,“.sh.cn”是由區域DNS解析的,“.com”卻是由全球頂級DNS解析的,與真實的過程不相同,這是怎麼回事呢?

肯定地說,採用責任鏈模式模擬DNS解析過程是不完美的,或者說是有缺陷的,怎麼來修復這個缺陷呢?實際上,例如本機請求查詢一個www.abcdefg.com的域名,上海DNS伺服器解析不到這個域名,於是提交到中國頂級DNS伺服器,如果中國頂級DNS伺服器有該域名的記錄,則找到該記錄,反饋到上海DNS伺服器,上海DNS伺服器做兩件事務處理:一是響應請求者,二是儲存該記錄,以備其他請求者再次查詢,這類似於資料快取。

整個場景我們已經清晰,想想看,我們把請求者看成是被觀察者,他的行為或屬性變更通知了觀察者——上海DNS,上海DNS又作為被觀察者出現了自己不能處理的行為(行為改變),通知了中國頂級DNS,依此類推,這是不是一個非常標準的觸發鏈?而且還必須是同步的觸發,非同步觸發已經在該場景中失去了意義。他的類圖與責任鏈模式很相似,僅僅多了一個Observable父類和Observer介面,但是在實現上這兩種模式有非常大的差異。我們先來解釋一下抽象DnsServer的作用。

  • 標識宣告

表示所有的DNS伺服器都具備雙重身份:既是觀察者也是被觀察者,這很重要,他宣告所有的伺服器都具有相同的身份標誌,具有該標誌後就可以在鏈中隨意移動,而無需固定在鏈中的某個位置(這也是鏈的一個重要特性)。

  • 業務抽象

方法setUpperServer的作用是設定父DNS,也就是設定自己的觀察者,update方法不僅僅是一個事件的處理者,也同時是事件的觸發者。

我們來看程式碼,首先是最簡單的,Recorder類與責任鏈模式中的記錄相同,這裡不再贅述。那我們就先看看該模式的核心抽象DnsServer,如下所示。

public abstract class DnsServer extends Observable implements Observer {
	/**
	 * 處理請求,也就是接收到事件後的處理
	 */
	@Override
	public void update(Observable o, Object arg) {
		Recorder recorder = (Recorder) arg;
		// 如果本機能解析
		if (this.isLocal(recorder)) {
			recorder.setIp(this.getIpAddress());
		} else { // 本機不能解析,則提交到上級DNS
			this.responseFromUpperServer(recorder);
		}
		// 簽名
		this.sign(recorder);
	}

	/**
	 * 作為被觀察者,允許增加觀察者,這裡上級DNS一般只有一個
	 * 
	 * @param dnsServer
	 */
	public void setUpperServer(DnsServer dnsServer) {
		// 先清空,然後再增加
		super.deleteObservers();
		super.addObserver(dnsServer);
	}

	/**
	 * 向父DNS請求解析,也就是通知觀察者
	 * 
	 * @param recorder
	 */
	private void responseFromUpperServer(Recorder recorder) {
		super.setChanged();
		super.notifyObservers(recorder);
	}

	/**
	 * 每個DNS伺服器簽上自己的名字
	 * 
	 * @param recorder
	 */
	protected abstract void sign(Recorder recorder);

	/**
	 * 每個DNS伺服器都必須定義自己的處理級別
	 * 
	 * @param recorder
	 * @return
	 */
	protected abstract boolean isLocal(Recorder recorder);

	/**
	 * 隨機產生一個IP地址,工具類
	 * 
	 * @return
	 */
	private String getIpAddress() {
		Random random = new Random();
		String address = random.nextInt(255) + "." + random.nextInt(255) + "."
				+ random.nextInt(255) + "." + random.nextInt(255);
		return address;
	}
}

注意看一下responseFromUpperServer方法,他只允許設定一個觀察者,因為一般的DNS伺服器都只有一個上級DNS伺服器。sign方法是簽名,這個記錄是由誰解析出來的,就由各個實現類獨自來實現。三個DnsServer的實現類都比較簡單,如下所示。

public class SHDnsServer extends DnsServer {

	@Override
	protected void sign(Recorder recorder) {
		recorder.setOwner("上海DNS伺服器");
	}

	@Override
	protected boolean isLocal(Recorder recorder) {
		return recorder.getDomain().endsWith(".sh.cn");
	}

}
public class ChinaTopDnsServer extends DnsServer {

	@Override
	protected void sign(Recorder recorder) {
		recorder.setOwner("中國頂級DNS伺服器");
	}

	@Override
	protected boolean isLocal(Recorder recorder) {
		return recorder.getDomain().endsWith(".cn");
	}

}
public class TopDnsServer extends DnsServer {

	@Override
	protected void sign(Recorder recorder) {
		recorder.setOwner("全球頂級DNS伺服器");
	}

	@Override
	protected boolean isLocal(Recorder recorder) {
		// 所有的域名最終的解析地點
		return true;
	}

}

我們再建立一個場景類模擬一下DNS解析過程,如下所示。

public class Client {
	public static void main(String[] args) throws Exception {
		// 上海域名伺服器
		DnsServer sh = new SHDnsServer();
		// 中國頂級域名伺服器
		DnsServer china = new ChinaTopDnsServer();
		// 全球頂級域名伺服器
		DnsServer top = new TopDnsServer();
		// 定義查詢路徑
		china.setUpperServer(top);
		sh.setUpperServer(china);
		// 解析域名
		System.out.println("=====域名解析模擬器=====");
		while (true) {
			System.out.println("\n請輸入域名(輸入N退出):");
			String domain = (new BufferedReader(
					new InputStreamReader(System.in))).readLine();
			if ("n".equalsIgnoreCase(domain)) {
				return;
			}
			Recorder recorder = new Recorder();
			recorder.setDomain(domain);
			sh.update(null, recorder);
			System.out.println("---DNS伺服器解析結果---");
			System.out.println(recorder);
		}
	}
}

與責任鏈模式中的場景類很相似。讀者請注意sh.update(null,recorder)這句程式碼,這是我們虛擬了觀察者觸發動作,完整的做法是把場景類作為一個被觀察者,然後設定觀察者為上海DNS伺服器,在進行測試,其結果完全相同。

仔細看一下我們的程式碼邏輯,上下兩個節點之間的關係很微妙,很有意思。

  • 下級節點對上級節點頂禮膜拜

比如我們輸入的這個域名www.xx.com,上海域名伺服器只知道他是由父節點(中國頂級DNS伺服器)解析的,而不知道父節點把該請求轉發給了更上層節點(全球頂級DNS伺服器),也就是說下級節點關注的是上級節點的響應,只要是上級反饋的結果就認為是上級的。www.xxx.com這個域名最終是由最高節點(全球頂級DNS伺服器)解析的,他把解析結果傳遞給第二個節點(中國頂級DNS伺服器)時的簽名為“全球頂級DNS伺服器”,而第二個節點把請求傳遞給首節點(上海DNS伺服器)時的簽名被修改為“中國頂級DNS伺服器”。所有從上級節點反饋的響應都認為是上級節點處理的結果,而不追究到底是不是真的上級節點處理的。

  • 上級節點對下級節點絕對信任

上級節點只對下級節點負責,他不關心下級節點的請求從何而來,只要是下級傳送的請求就認為是下級的。還是以www.xxx.com域名為例,當最高節點(全球頂級DNS伺服器)獲得解析請求時,他認為這個請求是誰的?當然是第二個節點(中國頂級DNS伺服器)的,否則他也不會把結果反饋給他,但是這個請求的源頭卻是首節點(上海DNS伺服器)的。

小結

  • 鏈中的訊息物件不同

從首節點開始到最終的尾節點,兩個鏈中傳遞的訊息物件是不同的。責任鏈模式基本上不改變訊息物件的結構,雖然每個節點都可以參與消費(一般是不參與消費),類似於“雁過拔毛”,但是他的結構不會改變,比如從首節點傳遞進來一個String物件,不會到鏈尾的時候成了int物件,這在責任鏈模式中是不可能的,但是在觸發鏈模式中是允許的,鏈中傳遞的物件可以自由變化,只要上下級節點對傳遞物件瞭解即可,他不要求鏈中的訊息物件不變化,他只要求鏈中相鄰兩個節點的訊息物件固定。

  • 上下節點的關係不同

在責任鏈模式中,上下節點沒有關係,都是接收同樣的物件,所有傳遞的物件都是從鏈受傳遞過來,上一節點是什麼沒有關係,只要按照自己的邏輯處理就成。而觸發鏈模式就不同了,他的上下級關係很親密,下級對上級頂禮膜拜,上級對下級絕對信任,鏈中的任意兩個相鄰節點都是一個牢固的獨立團體。

  • 訊息的分銷渠道不同

在責任鏈模式中,一個訊息從鏈首傳遞進來後,就開始沿著鏈條向鏈尾運動,方向是單一的、固定的;而觸發鏈模式則不同,由於他採用的是觀察者模式,所以有非常大的靈活性,一個訊息傳遞到鏈首後,具體怎麼傳遞是不固定的,可以以廣播方式傳遞,也可以以跳躍式方式傳遞,這取決於處理訊息的邏輯。