溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析

發(fā)布時間:2022-08-10 09:47:50 來源:億速云 閱讀:153 作者:iii 欄目:開發(fā)技術(shù)

本文小編為大家詳細(xì)介紹“JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析”,內(nèi)容詳細(xì),步驟清晰,細(xì)節(jié)處理妥當(dāng),希望這篇“JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析”文章能幫助大家解決疑惑,下面跟著小編的思路慢慢深入,一起來學(xué)習(xí)新知識吧。

職責(zé)鏈模式

職責(zé)鏈模式的定義是:使多個對象都有機(jī)會處理請求,從而避免請求的發(fā)送者和接收者之間的耦合關(guān)系,將這些對象連成一條鏈,并沿著這條鏈傳遞該請求,直到有一個對象處理它為止。

職責(zé)鏈模式的名字非常形象,一系列可能會處理請求的對象被連接成一條鏈,請求在這些對象之間依次傳遞,直到遇到一個可以處理它的對象,我們把這些對象稱為鏈中的節(jié)點(diǎn),如下圖所示。

JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析

1. 現(xiàn)實(shí)中的職責(zé)鏈模式

職責(zé)鏈模式的例子在現(xiàn)實(shí)中并不難找到,以下就是常見的跟職責(zé)鏈模式有關(guān)的場景。

如果早高峰能順利擠上公交車的話,那么估計(jì)這一天都會過得很開心。因?yàn)楣卉嚿先藢?shí)在太多了,經(jīng)常上車后卻找不到售票員在哪,所以只好把兩塊錢硬幣往前面遞。除非你運(yùn)氣夠好,站在你前面的第一個人就是售票員,否則,你的硬幣通常要在 N 個人手上傳遞,才能最終到達(dá)售票員的手里。

從這個例子中,我們很容易找到職責(zé)鏈模式的最大優(yōu)點(diǎn):請求發(fā)送者只需要知道鏈中的第一個節(jié)點(diǎn),從而弱化了發(fā)送者和一組接收者之間的強(qiáng)聯(lián)系。如果不使用職責(zé)鏈模式,那么在公交車上,我就得先搞清楚誰是售票員,才能把硬幣遞給他。

2. 實(shí)際開發(fā)中的職責(zé)鏈模式

假設(shè)我們負(fù)責(zé)一個售賣手機(jī)的電商網(wǎng)站,經(jīng)過分別交納 500 元定金和 200 元定金的兩輪預(yù)定后(訂單已在此時生成),現(xiàn)在已經(jīng)到了正式購買的階段。

公司針對支付過定金的用戶有一定的優(yōu)惠政策。在正式購買后,已經(jīng)支付過 500 元定金的用戶會收到 100 元的商城優(yōu)惠券,200 元定金的用戶可以收到 50 元的優(yōu)惠券,而之前沒有支付定金的用戶只能進(jìn)入普通購買模式,也就是沒有優(yōu)惠券,且在庫存有限的情況下不一定保證能買到。

我們的訂單頁面是 Node 吐出的模板,在頁面加載之初,Node 會傳遞給頁面幾個字段。

  • orderType:表示訂單類型(定金用戶或者普通購買用戶),code 的值為 1 的時候是 500 元定金用戶,為 2 的時候是 200 元定金用戶,為 3 的時候是普通購買用戶。

  • pay:表示用戶是否已經(jīng)支付定金,值為 true 或者 false, 雖然用戶已經(jīng)下過 500 元定金的訂單,但如果他一直沒有支付定金,現(xiàn)在只能降級進(jìn)入普通購買模式。

  • stock:表示當(dāng)前用于普通購買的手機(jī)庫存數(shù)量,已經(jīng)支付過 500 元或者 200 元定金的用戶不受此限制。

下面我們把這個流程寫成代碼:

const order = function (orderType, pay, stock) {
	if (orderType === 1) { // 500 元定金購買模式
		if (pay === true) { // 已支付定金
			console.log('500 元定金預(yù)購, 得到 100 優(yōu)惠券');
		} else { // 未支付定金,降級到普通購買模式
			if (stock > 0) { // 用于普通購買的手機(jī)還有庫存
				console.log('普通購買, 無優(yōu)惠券');
			} else {
				console.log('手機(jī)庫存不足');
			}
		}
	} else if (orderType === 2) { // 200 元定金購買模式
		if (pay === true) {
			console.log('200 元定金預(yù)購, 得到 50 優(yōu)惠券');
		} else {
			if (stock > 0) {
				console.log('普通購買, 無優(yōu)惠券');
			} else {
				console.log('手機(jī)庫存不足');
			}
		}
	} else if (orderType === 3) {
		if (stock > 0) {
			console.log('普通購買, 無優(yōu)惠券');
		} else {
			console.log('手機(jī)庫存不足');
		}
	}
};
order(1, true, 500); // 輸出: 500 元定金預(yù)購, 得到 100 優(yōu)惠券

雖然我們得到了意料中的運(yùn)行結(jié)果,但這遠(yuǎn)遠(yuǎn)算不上一段值得夸獎的代碼。order 函數(shù)不僅巨大到難以閱讀,而且需要經(jīng)常進(jìn)行修改。雖然目前項(xiàng)目能正常運(yùn)行,但接下來的維護(hù)工作無疑是個夢魘。恐怕只有最“新手”的程序員才會寫出這樣的代碼。

3. 用職責(zé)鏈模式重構(gòu)代碼

現(xiàn)在我們采用職責(zé)鏈模式重構(gòu)這段代碼,先把 500 元訂單、200 元訂單以及普通購買分成 3 個函數(shù)。

接下來把 orderType、paystock 這 3 個字段當(dāng)作參數(shù)傳遞給 500 元訂單函數(shù),如果該函數(shù)不符合處理?xiàng)l件,則把這個請求傳遞給后面的 200 元訂單函數(shù),如果 200 元訂單函數(shù)依然不能處理該請求,則繼續(xù)傳遞請求給普通購買函數(shù),代碼如下:

// 500 元訂單
const order500 = function (orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log('500 元定金預(yù)購, 得到 100 優(yōu)惠券');
	} else {
		order200(orderType, pay, stock); // 將請求傳遞給 200 元訂單
	}
};
// 200 元訂單
const order200 = function (orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log('200 元定金預(yù)購, 得到 50 優(yōu)惠券');
	} else {
		orderNormal(orderType, pay, stock); // 將請求傳遞給普通訂單
	}
};
// 普通購買訂單
const orderNormal = function (orderType, pay, stock) {
	if (stock > 0) {
		console.log('普通購買, 無優(yōu)惠券');
	} else {
		console.log('手機(jī)庫存不足');
	}
};
// 測試結(jié)果:
order500(1, true, 500); // 輸出:500 元定金預(yù)購, 得到 100 優(yōu)惠券
order500(1, false, 500); // 輸出:普通購買, 無優(yōu)惠券
order500(2, true, 500); // 輸出:200 元定金預(yù)購, 得到 500 優(yōu)惠券
order500(3, false, 500); // 輸出:普通購買, 無優(yōu)惠券
order500(3, false, 0); // 輸出:手機(jī)庫存不足

可以看到,執(zhí)行結(jié)果和前面那個巨大的 order 函數(shù)完全一樣,但是代碼的結(jié)構(gòu)已經(jīng)清晰了很多,我們把一個大函數(shù)拆分了 3 個小函數(shù),去掉了許多嵌套的條件分支語句。

目前已經(jīng)有了不小的進(jìn)步,但我們不會滿足于此,雖然已經(jīng)把大函數(shù)拆分成了互不影響的 3 個小函數(shù),但可以看到,請求在鏈條傳遞中的順序非常僵硬,傳遞請求的代碼被耦合在了業(yè)務(wù)函數(shù)之中:

const order500 = function (orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log('500 元定金預(yù)購, 得到 100 優(yōu)惠券');
	} else {
		order200(orderType, pay, stock); // 將請求傳遞給 200 元訂單
		// order200 和 order500 耦合在一起
	}
};

這依然是違反開放—封閉原則的,如果有天我們要增加 300 元預(yù)訂或者去掉 200 元預(yù)訂,意味著就必須改動這些業(yè)務(wù)函數(shù)內(nèi)部。就像一根環(huán)環(huán)相扣打了死結(jié)的鏈條,如果要增加、拆除或者移動一個節(jié)點(diǎn),就必須得先砸爛這根鏈條。

4. 靈活可拆分的職責(zé)鏈節(jié)點(diǎn)

本節(jié)我們采用一種更靈活的方式,來改進(jìn)上面的職責(zé)鏈模式,目標(biāo)是讓鏈中的各個節(jié)點(diǎn)可以靈活拆分和重組。

首先需要改寫一下分別表示 3 種購買模式的節(jié)點(diǎn)函數(shù),我們約定,如果某個節(jié)點(diǎn)不能處理請求,則返回一個特定的字符串 'nextSuccessor'來表示該請求需要繼續(xù)往后面?zhèn)鬟f:

const order500 = function (orderType, pay, stock) {
	if (orderType === 1 && pay === true) {
		console.log('500 元定金預(yù)購,得到 100 優(yōu)惠券');
	} else {
		return 'nextSuccessor'; // 我不知道下一個節(jié)點(diǎn)是誰,反正把請求往后面?zhèn)鬟f
	}
};
const order200 = function (orderType, pay, stock) {
	if (orderType === 2 && pay === true) {
		console.log('200 元定金預(yù)購,得到 50 優(yōu)惠券');
	} else {
		return 'nextSuccessor'; // 我不知道下一個節(jié)點(diǎn)是誰,反正把請求往后面?zhèn)鬟f
	}
};
const orderNormal = function (orderType, pay, stock) {
	if (stock > 0) {
		console.log('普通購買,無優(yōu)惠券');
	} else {
		console.log('手機(jī)庫存不足');
	}
};

接下來需要把函數(shù)包裝進(jìn)職責(zé)鏈節(jié)點(diǎn),我們定義一個構(gòu)造函數(shù) Chain,在 new Chain 的時候傳 遞的參數(shù)即為需要被包裝的函數(shù),同時它還擁有一個實(shí)例屬性 this.successor,表示在鏈中的下 一個節(jié)點(diǎn)。

此外 Chainprototype 中還有兩個函數(shù),它們的作用如下所示:

// Chain.prototype.setNextSuccessor 指定在鏈中的下一個節(jié)點(diǎn)
// Chain.prototype.passRequest 傳遞請求給某個節(jié)點(diǎn)
const Chain = function (fn) {
	this.fn = fn;
	this.successor = null;
};
Chain.prototype.setNextSuccessor = function (successor) {
	return this.successor = successor;
};
Chain.prototype.passRequest = function () {
	const ret = this.fn.apply(this, arguments);
	if (ret === 'nextSuccessor') {
		return this.successor && this.successor.passRequest.apply(this.successor, arguments);
	}
	return ret;
};

現(xiàn)在我們把 3 個訂單函數(shù)分別包裝成職責(zé)鏈的節(jié)點(diǎn):

const chainOrder500 = new Chain(order500);
const chainOrder200 = new Chain(order200);
const chainOrderNormal = new Chain(orderNormal);

然后指定節(jié)點(diǎn)在職責(zé)鏈中的順序:

chainOrder500.setNextSuccessor(chainOrder200);
chainOrder200.setNextSuccessor(chainOrderNormal);

最后把請求傳遞給第一個節(jié)點(diǎn):

chainOrder500.passRequest(1, true, 500); // 輸出:500 元定金預(yù)購,得到 100 優(yōu)惠券
chainOrder500.passRequest(2, true, 500); // 輸出:200 元定金預(yù)購,得到 50 優(yōu)惠券
chainOrder500.passRequest(3, true, 500); // 輸出:普通購買,無優(yōu)惠券
chainOrder500.passRequest(1, false, 0); // 輸出:手機(jī)庫存不足

通過改進(jìn),我們可以自由靈活地增加、移除和修改鏈中的節(jié)點(diǎn)順序,假如某天網(wǎng)站運(yùn)營人員又想出了支持 300 元定金購買,那我們就在該鏈中增加一個節(jié)點(diǎn)即可:

const order300 = function () {
	// 具體實(shí)現(xiàn)略 
};
const chainOrder300 = new Chain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

對于程序員來說,我們總是喜歡去改動那些相對容易改動的地方,就像改動框架的配置文件遠(yuǎn)比改動框架的源代碼簡單得多。在這里完全不用理會原來的訂單函數(shù)代碼,我們要做的只是增加一個節(jié)點(diǎn),然后重新設(shè)置鏈中相關(guān)節(jié)點(diǎn)的順序。

5. 異步的職責(zé)鏈

在上一節(jié)的職責(zé)鏈模式中,我們讓每個節(jié)點(diǎn)函數(shù)同步返回一個特定的值"nextSuccessor",來表示是否把請求傳遞給下一個節(jié)點(diǎn)。而在現(xiàn)實(shí)開發(fā)中,我們經(jīng)常會遇到一些異步的問題,比如我們要在節(jié)點(diǎn)函數(shù)中發(fā)起一個 ajax異步請求,異步請求返回的結(jié)果才能決定是否繼續(xù)在職責(zé)鏈中 passRequest。

這時候讓節(jié)點(diǎn)函數(shù)同步返回"nextSuccessor"已經(jīng)沒有意義了,所以要給 Chain 類再增加一個原型方法 Chain.prototype.next,表示手動傳遞請求給職責(zé)鏈中的下一個節(jié)點(diǎn):

Chain.prototype.next = function () {
	return this.successor && this.successor.passRequest.apply(this.successor, arguments);
};

來看一個異步職責(zé)鏈的例子:

const fn1 = new Chain(function () {
	console.log(1);
	return 'nextSuccessor';
});
const fn2 = new Chain(function () {
	console.log(2);
	setTimeout(() => {
		this.next();
	}, 1000);
});
const fn3 = new Chain(function () {
	console.log(3);
});
fn1.setNextSuccessor(fn2).setNextSuccessor(fn3);
fn1.passRequest();

現(xiàn)在我們得到了一個特殊的鏈條,請求在鏈中的節(jié)點(diǎn)里傳遞,但節(jié)點(diǎn)有權(quán)利決定什么時候把請求交給下一個節(jié)點(diǎn)??梢韵胂?,異步的職責(zé)鏈加上命令模式(把 ajax 請求封裝成命令對象),我們可以很方便地創(chuàng)建一個異步 ajax 隊(duì)列庫。

6. 職責(zé)鏈模式的優(yōu)缺點(diǎn)

前面已經(jīng)說過,職責(zé)鏈模式的最大優(yōu)點(diǎn)就是解耦了請求發(fā)送者和 N 個接收者之間的復(fù)雜關(guān)系,由于不知道鏈中的哪個節(jié)點(diǎn)可以處理你發(fā)出的請求,所以你只需把請求傳遞給第一個節(jié)點(diǎn)即可,如下圖所示。

JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析

用職責(zé)鏈模式改進(jìn)后:

JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析

在手機(jī)商城的例子中,本來我們要被迫維護(hù)一個充斥著條件分支語句的巨大的函數(shù),在例子里的購買過程中只打印了一條 log 語句。其實(shí)在現(xiàn)實(shí)開發(fā)中,這里要做更多事情,比如根據(jù)訂單種類彈出不同的浮層提示、渲染不同的 UI 節(jié)點(diǎn)、組合不同的參數(shù)發(fā)送給不同的 cgi 等。用了職責(zé)鏈模式之后,每種訂單都有各自的處理函數(shù)而互不影響。

其次,使用了職責(zé)鏈模式之后,鏈中的節(jié)點(diǎn)對象可以靈活地拆分重組。增加或者刪除一個節(jié)點(diǎn),或者改變節(jié)點(diǎn)在鏈中的位置都是輕而易舉的事情。這一點(diǎn)我們也已經(jīng)看到,在上面的例子中,增加一種訂單完全不需要改動其他訂單函數(shù)中的代碼。

職責(zé)鏈模式還有一個優(yōu)點(diǎn),那就是可以手動指定起始節(jié)點(diǎn),請求并不是非得從鏈中的第一個節(jié)點(diǎn)開始傳遞。比如在公交車的例子中,如果我明確在我前面的第一個人不是售票員,那我當(dāng)然可以越過他把公交卡遞給他前面的人,這樣可以減少請求在鏈中的傳遞次數(shù),更快地找到合適的請求接受者。這在普通的條件分支語句下是做不到的,我們沒有辦法讓請求越過某一個 if 判斷。

拿代碼來證明這一點(diǎn),假設(shè)某一天網(wǎng)站中支付過定金的訂單已經(jīng)全部結(jié)束購買流程,我們在接下來的時間里只需要處理普通購買訂單,所以我們可以直接把請求交給普通購買訂單節(jié)點(diǎn):

orderNormal.passRequest(1, false, 500); // 普通購買, 無優(yōu)惠券

如果運(yùn)用得當(dāng),職責(zé)鏈模式可以很好地幫助我們組織代碼,但這種模式也并非沒有弊端,首先我們不能保證某個請求一定會被鏈中的節(jié)點(diǎn)處理。此時的請求就得不到答復(fù),而是徑直從鏈尾離開,或者拋出一個錯誤異常。在這種情況下,我們可以在鏈尾增加一個保底的接受者節(jié)點(diǎn)來處理這種即將離開鏈尾的請求。

另外,職責(zé)鏈模式使得程序中多了一些節(jié)點(diǎn)對象,可能在某一次的請求傳遞過程中,大部分節(jié)點(diǎn)并沒有起到實(shí)質(zhì)性的作用,它們的作用僅僅是讓請求傳遞下去,從性能方面考慮,我們要避免過長的職責(zé)鏈帶來的性能損耗。

7. 用 AOP 實(shí)現(xiàn)職責(zé)鏈

在之前的職責(zé)鏈實(shí)現(xiàn)中,我們利用了一個 Chain 類來把普通函數(shù)包裝成職責(zé)鏈的節(jié)點(diǎn)。其實(shí)利用 JavaScript 的函數(shù)式特性,有一種更加方便的方法來創(chuàng)建職責(zé)鏈。

下面我們改寫一下之前的 Function.prototype.after 函數(shù),使得第一個函數(shù)返回'nextSuccessor' 時,將請求繼續(xù)傳遞給下一個函數(shù),無論是返回字符串'nextSuccessor'或者 false 都只是一個約定,當(dāng)然在這里我們也可以讓函數(shù)返回 false 表示傳遞請求,選擇'nextSuccessor'字符串是因?yàn)樗雌饋砀鼙磉_(dá)我們的目的,代碼如下:

Function.prototype.after = function (fn) {
	const self = this;
	return function () {
		const ret = self.apply(this, arguments);
		if (ret === 'nextSuccessor') {
			return fn.apply(this, arguments);
		}
		return ret;
	}
};
const order = order500yuan.after(order200yuan).after(orderNormal);
order(1, true, 500); // 輸出:500 元定金預(yù)購,得到 100 優(yōu)惠券
order(2, true, 500); // 輸出:200 元定金預(yù)購,得到 50 優(yōu)惠券
order(1, false, 500); // 輸出:普通購買,無優(yōu)惠券

用 AOP 來實(shí)現(xiàn)職責(zé)鏈既簡單又巧妙,但這種把函數(shù)疊在一起的方式,同時也疊加了函數(shù)的作用域,如果鏈條太長的話,也會對性能有較大的影響。

讀到這里,這篇“JavaScript設(shè)計(jì)模式之職責(zé)鏈模式實(shí)例分析”文章已經(jīng)介紹完畢,想要掌握這篇文章的知識點(diǎn)還需要大家自己動手實(shí)踐使用過才能領(lǐng)會,如果想了解更多相關(guān)內(nèi)容的文章,歡迎關(guān)注億速云行業(yè)資訊頻道。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI