行為類模式:觀察者模式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物件,這在責任鏈模式中是不可能的,但是在觸發鏈模式中是允許的,鏈中傳遞的物件可以自由變化,只要上下級節點對傳遞物件瞭解即可,他不要求鏈中的訊息物件不變化,他只要求鏈中相鄰兩個節點的訊息物件固定。
- 上下節點的關係不同
在責任鏈模式中,上下節點沒有關係,都是接收同樣的物件,所有傳遞的物件都是從鏈受傳遞過來,上一節點是什麼沒有關係,只要按照自己的邏輯處理就成。而觸發鏈模式就不同了,他的上下級關係很親密,下級對上級頂禮膜拜,上級對下級絕對信任,鏈中的任意兩個相鄰節點都是一個牢固的獨立團體。
- 訊息的分銷渠道不同
在責任鏈模式中,一個訊息從鏈首傳遞進來後,就開始沿著鏈條向鏈尾運動,方向是單一的、固定的;而觸發鏈模式則不同,由於他採用的是觀察者模式,所以有非常大的靈活性,一個訊息傳遞到鏈首後,具體怎麼傳遞是不固定的,可以以廣播方式傳遞,也可以以跳躍式方式傳遞,這取決於處理訊息的邏輯。