javascript設計模式——職責鏈模式

来源:http://www.cnblogs.com/xiaohuochai/archive/2017/12/16/8040195.html
-Advertisement-
Play Games

[1]電商訂單 [2]職責鏈模式重構 [3]AOP [4]文件上傳 ...


前面的話

  職責鏈模式的定義是使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係,將這些對象連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個對象處理它為止。職責鏈模式的名字非常形象,一系列可能會處理請求的對象被連接成一條鏈,請求在這些對象之間依次傳遞,直到遇到一個可以處理它的對象,把這些對象稱為鏈中的節點。本文將詳細介紹職責鏈模式

 

電商訂單

  職責鏈模式的例子在現實中並不難找到,以下就是兩個常見的跟職責鏈模式有關的場景

  如果早高峰能順利擠上公交車的話,那麼估計這一天都會過得很開心。因為公交車上人實在太多了,經常上車後卻找不到售票員在哪,所以只好把兩塊錢硬幣往前面遞。除非運氣夠好,站在前面的第一個人就是售票員,否則,硬幣通常要在N個人手上傳遞,才能最終到達售票員的手裡

  中學時代的期末考試,如果平時不太老實,考試時就會被安排在第一個位置。遇到不會答的題目,就把題目編號寫在小紙條上往後傳遞,坐在後面的同學如果也不會答,他就會把這張小紙條繼續遞給他後面的人

  從這兩個例子中,很容易找到職責鏈模式的最大優點:請求發送者只需要知道鏈中的第一個節點,從而弱化了發送者和一組接收者之間的強聯繫。如果不使用職責鏈模式,那麼在公交車上,就得先搞清楚誰是售票員,才能把硬幣遞給他。同樣,在期末考試中,也許就要先瞭解同學中有哪些可以解答這道題

  假設負責一個售賣手機的電商網站,經過分別交納500元定金和200元定金的兩輪預定後(訂單已在此時生成),現在已經到了正式購買的階段。公司針對支付過定金的用戶有一定的優惠政策。在正式購買後,已經支付過500元定金的用戶會收到100元的商城優惠券,200元定金的用戶可以收到50元的優惠券,而之前沒有支付定金的用戶只能進入普通購買模式,也就是沒有優惠券,且在庫存有限的情況下不一定保證能買到

  訂單頁面是PHP吐出的模板,在頁面載入之初,PHP會傳遞給頁面幾個欄位

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

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

  3、stock:表示當前用於普通購買的手機庫存數量,已經支付過500元或者200元定金的用戶不受此限制

  下麵把這個流程寫成代碼:

var order = function( orderType, pay, stock ){
    if ( orderType === 1 ){ // 500 元定金購買模式
        if ( pay === true ){ // 已支付定金
            console.log( '500 元定金預購, 得到100 優惠券' );
        }else{ // 未支付定金,降級到普通購買模式
            if ( stock > 0 ){ // 用於普通購買的手機還有庫存
                console.log( '普通購買, 無優惠券' );

            }else{
                console.log( '手機庫存不足' );
            }
        }
    }
    else if ( orderType === 2 ){ // 200 元定金購買模式
        if ( pay === true ){
            console.log( '200 元定金預購, 得到50 優惠券' );
        }else{
            if ( stock > 0 ){
                console.log( '普通購買, 無優惠券' );
            }else{
                console.log( '手機庫存不足' );
            }
        }
    }
    else if ( orderType === 3 ){
        if ( stock > 0 ){
            console.log( '普通購買, 無優惠券' );
        }else{
            console.log( '手機庫存不足' );
        }
    }
};
order( 1 , true, 500); // 輸出: 500 元定金預購, 得到100 優惠券

  雖然得到了意料中的運行結果,但這遠遠算不上一段值得誇獎的代碼。order函數不僅巨大到難以閱讀,而且需要經常進行修改。雖然目前項目能正常運行,但接下來的維護工作無疑是個夢魘

 

職責鏈模式重構

  現在我們職責鏈模式重構這段代碼,先把500元訂單、200元訂單以及普通購買分成3個函數。接下來把orderType、pay、stock這3個欄位當作參數傳遞給500元訂單函數,如果該函數不符合處理條件,則把這個請求傳遞給後面的200元訂單函數,如果200元訂單函數依然不能處理該請求,則繼續傳遞請求給普通購買函數,代碼如下:

var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金預購, 得到100 優惠券' );
    }else{
        order200( orderType, pay, stock ); // 將請求傳遞給200 元訂單
    }
};
// 200 元訂單
var order200 = function( orderType, pay, stock ){
    if ( orderType === 2 && pay === true ){
        console.log( '200 元定金預購, 得到50 優惠券' );
    }else{
        orderNormal( orderType, pay, stock ); // 將請求傳遞給普通訂單
    }
};
// 普通購買訂單
var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){
        console.log( '普通購買, 無優惠券' );
    }else{
        console.log( '手機庫存不足' );
    }
};

// 測試結果:
order500( 1 , true, 500); // 輸出:500 元定金預購, 得到100 優惠券
order500( 1, false, 500 ); // 輸出:普通購買, 無優惠券
order500( 2, true, 500 ); // 輸出:200 元定金預購, 得到500 優惠券
order500( 3, false, 500 ); // 輸出:普通購買, 無優惠券
order500( 3, false, 0 ); // 輸出:手機庫存不足

  可以看到,執行結果和前面那個巨大的order函數完全一樣,但是代碼的結構已經清晰了很多,把一個大函數拆分了3個小函數,去掉了許多嵌套的條件分支語句

  雖然已經把大函數拆分成了互不影響的3個小函數,但可以看到,請求在鏈條傳遞中的順序非常僵硬,傳遞請求的代碼被耦合在了業務函數之中:

var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金預購, 得到100 優惠券' );
    }else{
        order200( orderType, pay, stock ); // 將請求傳遞給200 元訂單
    }
};

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

【靈活可拆分的職責鏈節點】

  下麵採用一種更靈活的方式,來改進上面的職責鏈模式,目標是讓鏈中的各個節點可以靈活拆分和重組

  首先需要改寫一下分別表示3種購買模式的節點函數,約定如果某個節點不能處理請求,則返回一個特定的字元串'nextSuccessor'來表示該請求需要繼續往後面傳遞:

var order500 = function( orderType, pay, stock ){
    if ( orderType === 1 && pay === true ){
        console.log( '500 元定金預購,得到100 優惠券' );
    }else{
        return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往後面傳遞
    }
};

var order200 = function( orderType, pay, stock ){
    if ( orderType === 2 && pay === true ){
        console.log( '200 元定金預購,得到50 優惠券' );
    }else{
        return 'nextSuccessor'; // 我不知道下一個節點是誰,反正把請求往後面傳遞
    }
};

var orderNormal = function( orderType, pay, stock ){
    if ( stock > 0 ){
        console.log( '普通購買,無優惠券' );
    }else{
        console.log( '手機庫存不足' );
    }
};

  接下來需要把函數包裝進職責鏈節點,定義一個構造函數Chain,在new Chain的時候傳遞的參數即為需要被包裝的函數,同時它還擁有一個實例屬性this.successor,表示在鏈中的下一個節點。此外Chain的prototype中還有兩個函數,它們的作用如下所示:

//Chain.prototype.setNextSuccessor 指定在鏈中的下一個節點
//Chain.prototype.passRequest 傳遞請求給某個節點
var Chain = function( fn ){
    this.fn = fn;
    this.successor = null;
};

Chain.prototype.setNextSuccessor = function( successor ){
    return this.successor = successor;
};

Chain.prototype.passRequest = function(){

    var ret = this.fn.apply( this, arguments );
    if ( ret === 'nextSuccessor' ){
        return this.successor && this.successor.passRequest.apply( this.successor, arguments );
    }
    return ret;
};

  現在把3個訂單函數分別包裝成職責鏈的節點:

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

  然後指定節點在職責鏈中的順序:

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

  最後把請求傳遞給第一個節點:

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

  通過改進,可以自由靈活地增加、移除和修改鏈中的節點順序,假如某天網站運營人員又想出了支持300元定金購買,那就在該鏈中增加一個節點即可:

varorder300=function(){
  //具體實現略
};

chainOrder300=newChain(order300);
chainOrder500.setNextSuccessor(chainOrder300);
chainOrder300.setNextSuccessor(chainOrder200);

【非同步的職責鏈】

  上面的職責鏈模式中,讓每個節點函數同步返回一個特定的值"nextSuccessor",來表示是否把請求傳遞給下一個節點。而在現實開發中,經常會遇到一些非同步的問題,比如要在節點函數中發起一個ajax非同步請求,非同步請求返回的結果才能決定是否繼續在職責鏈中passRequet

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

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

  下麵是一個非同步職責鏈的例子:

var fn1 = new Chain(function(){ 
  console.log( 1 );
  return 'nextSuccessor';
});

var fn2 = new Chain(function(){ 
  console.log( 2 );
  var self = this;
  setTimeout(function(){ 
    self.next();
  }, 1000 );
});

var fn3 = new Chain(function(){ 
  console.log( 3 );
});

fn1.setNextSuccessor( fn2 ).setNextSuccessor( fn3 ); 
fn1.passRequest();

  現在得到了一個特殊的鏈條,請求在鏈中的節點里傳遞,但節點有權利決定什麼時候把請求交給下一個節點。可以想象,非同步的職責鏈加上命令模式,可以很方便地創建一個非同步ajax隊列庫

【優缺點】

  職責鏈模式的最大優點就是解耦了請求發送者和N個接收者之間的複雜關係,由於不知道鏈中的哪個節點可以處理髮出的請求,所以只需把請求傳遞給第一個節點即可

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

  其次,使用了職責鏈模式之後,鏈中的節點對象可以靈活地拆分重組。增加或者刪除一個節點,或者改變節點在鏈中的位置都是輕而易舉的事情

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

  拿代碼來證明這一點,假設某一天網站中支付過定金的訂單已經全部結束購買流程,在接下來的時間里只需要處理普通購買訂單,所以可以直接把請求交給普通購買訂單節點:

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

  如果運用得當,職責鏈模式可以很好地幫助我們組織代碼,但這種模式也並非沒有弊端,首先不能保證某個請求一定會被鏈中的節點處理。比如在期末考試的例子中,小紙條上的題目也許沒有任何一個同學知道如何解答,此時的請求就得不到答覆,而是徑直從鏈尾離開,或者拋出一個錯誤異常。在這種情況下,可以在鏈尾增加一個保底的接受者節點來處理這種即將離開鏈尾的請求

  另外,職責鏈模式使得程式中多了一些節點對象,可能在某一次的請求傳遞過程中,大部分節點並沒有起到實質性的作用,它們的作用僅僅是讓請求傳遞下去,從性能方面考慮,要避免過長的職責鏈帶來的性能損耗

 

AOP

  在之前的職責鏈實現中,利用了一個Chain類來把普通函數包裝成職責鏈的節點。其實利用javascript的函數式特性,有一種更加方便的方法來創建職責鏈

  下麵改寫一下Function.prototype.after函數,使得第一個函數返回'nextSuccessor'時,將請求繼續傳遞給下一個函數,無論是返回字元串'nextSuccessor'或者false都只是一個約定,當然在這裡也可以讓函數返回false表示傳遞請求,選擇'nextSuccessor'字元串是因為它看起來更能表達我們的目的,代碼如下:

Function.prototype.after = function( fn ){
    var self = this;
    return function(){
        var ret = self.apply( this, arguments );
        if ( ret === 'nextSuccessor' ){
            return fn.apply( this, arguments );
        }
        return ret;
    }
};

var order = order500yuan.after( order200yuan ).after( orderNormal );
order( 1, true, 500 ); // 輸出:500 元定金預購,得到100 優惠券
order( 2, true, 500 ); // 輸出:200 元定金預購,得到50 優惠券
order( 1, false, 500 ); // 輸出:普通購買,無優惠券

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

 

文件上傳

  迭代器模式中,有一個獲取文件上傳對象的例子:當時創建了一個迭代器來迭代獲取合適的文件上傳對象,其實用職責鏈模式可以更簡單,完全不用創建這個多餘的迭代器,完整代碼如下:

var getActiveUploadObj = function(){ 
  try{
    return new ActiveXObject("TXFTNActiveX.FTNUpload");    // IE 上傳控制項
  }catch(e){
    return 'nextSuccessor' ;
  }
};

var getFlashUploadObj = function(){ 
  if ( supportFlash() ){
    var str = '<object type="application/x-shockwave-flash"></object>'; 
    return $( str ).appendTo( $('body') );
  }
  return 'nextSuccessor' ;
};
 

var getFormUpladObj = function(){
  return $( '<form><input name="file" type="file"/></form>' ).appendTo( $('body') );
};

var getUploadObj = getActiveUploadObj.after( getFlashUploadObj ).after( getFormUpladObj ); 
console.log( getUploadObj() );

  在javascript開發中,職責鏈模式是最容易被忽視的模式之一。實際上只要運用得當,職責鏈模式可以很好地幫助我們管理代碼,降低發起請求的對象和處理請求的對象之間的耦合性。職責鏈中的節點數量和順序是可以自由變化的,可以在運行時決定鏈中包含哪些節點

  無論是作用域鏈、原型鏈,還是DOM節點中的事件冒泡,都能從中找到職責鏈模式的影子。職責鏈模式還可以和組合模式結合在一起,用來連接部件和父部件,或是提高組合對象的效率

 


您的分享是我們最大的動力!

-Advertisement-
Play Games
更多相關文章
  • 第一步:安裝從網上下載文件的wget命令 第二步:下載mysql的repo源 第三步:安裝mysql-community-release-el7-5.noarch.rpm包 第四步:查看下 會獲得兩個mysql的yum repo源:/etc/yum.repos.d/mysql-community.r ...
  • [20171213]john破解oracle口令.txt--//跟別人討論的oracle破解問題,我曾經提過不要使用6位字元以下的密碼,其實不管那種系統低於6位口令非常容易破解.--//而且oracle預設還保證舊口令模式在sys.user$文件中,破解這個更容易.我僅僅寫一些例子:1.環境:SYS ...
  • MySQL雙主+keeplived安裝部署說明 一、環境介紹 1.1、規劃 序號 類別 版本 主機名 IP 埠 備註 1 OS CentOS release 6.9 (Final) (minimal) my1 172.16.210.180 8306 172.16.210.183 2 mysql m ...
  • 用xmapp中mysql 資料庫實現.sql文件上傳已經實現資料庫的增刪改查。 ...
  • 使用GIT前請閱讀(有git基礎可略過) git指引 :http://www.bootcss.com/p/git-guide/ 一、 下載git http://git-scm.com/download/ 二、 安裝 全部預設配置即可。 三、 配置 運行Git Bash (一) 配置用戶信息: 輸入以 ...
  • 前天開源了框架:開源:Sagit.Framework For IOS 開發框架,所以註定要追補一套開發教程了,所以開始第一篇入門了... ...
  • 先上一個能用的js代碼: 後臺獲取不到值的解決方案: 1、保證前臺能獲取到輸入框的值。 2、url,data的key-value書寫正確。 3、註意HTTP請求的Header,headers類型: 預設:application/x-www-form-urlencoded 上傳文件時:multipar ...
  • 查看本地git配置信息 git config --global -e 查看自己科學上網的代理地址和埠信息 為git添加代理 git config --global http.proxy https://127.0.0.1:51643 git config --global https.proxy ...
一周排行
    -Advertisement-
    Play Games
  • 移動開發(一):使用.NET MAUI開發第一個安卓APP 對於工作多年的C#程式員來說,近來想嘗試開發一款安卓APP,考慮了很久最終選擇使用.NET MAUI這個微軟官方的框架來嘗試體驗開發安卓APP,畢竟是使用Visual Studio開發工具,使用起來也比較的順手,結合微軟官方的教程進行了安卓 ...
  • 前言 QuestPDF 是一個開源 .NET 庫,用於生成 PDF 文檔。使用了C# Fluent API方式可簡化開發、減少錯誤並提高工作效率。利用它可以輕鬆生成 PDF 報告、發票、導出文件等。 項目介紹 QuestPDF 是一個革命性的開源 .NET 庫,它徹底改變了我們生成 PDF 文檔的方 ...
  • 項目地址 項目後端地址: https://github.com/ZyPLJ/ZYTteeHole 項目前端頁面地址: ZyPLJ/TreeHoleVue (github.com) https://github.com/ZyPLJ/TreeHoleVue 目前項目測試訪問地址: http://tree ...
  • 話不多說,直接開乾 一.下載 1.官方鏈接下載: https://www.microsoft.com/zh-cn/sql-server/sql-server-downloads 2.在下載目錄中找到下麵這個小的安裝包 SQL2022-SSEI-Dev.exe,運行開始下載SQL server; 二. ...
  • 前言 隨著物聯網(IoT)技術的迅猛發展,MQTT(消息隊列遙測傳輸)協議憑藉其輕量級和高效性,已成為眾多物聯網應用的首選通信標準。 MQTTnet 作為一個高性能的 .NET 開源庫,為 .NET 平臺上的 MQTT 客戶端與伺服器開發提供了強大的支持。 本文將全面介紹 MQTTnet 的核心功能 ...
  • Serilog支持多種接收器用於日誌存儲,增強器用於添加屬性,LogContext管理動態屬性,支持多種輸出格式包括純文本、JSON及ExpressionTemplate。還提供了自定義格式化選項,適用於不同需求。 ...
  • 目錄簡介獲取 HTML 文檔解析 HTML 文檔測試參考文章 簡介 動態內容網站使用 JavaScript 腳本動態檢索和渲染數據,爬取信息時需要模擬瀏覽器行為,否則獲取到的源碼基本是空的。 本文使用的爬取步驟如下: 使用 Selenium 獲取渲染後的 HTML 文檔 使用 HtmlAgility ...
  • 1.前言 什麼是熱更新 游戲或者軟體更新時,無需重新下載客戶端進行安裝,而是在應用程式啟動的情況下,在內部進行資源或者代碼更新 Unity目前常用熱更新解決方案 HybridCLR,Xlua,ILRuntime等 Unity目前常用資源管理解決方案 AssetBundles,Addressable, ...
  • 本文章主要是在C# ASP.NET Core Web API框架實現向手機發送驗證碼簡訊功能。這裡我選擇是一個互億無線簡訊驗證碼平臺,其實像阿裡雲,騰訊雲上面也可以。 首先我們先去 互億無線 https://www.ihuyi.com/api/sms.html 去註冊一個賬號 註冊完成賬號後,它會送 ...
  • 通過以下方式可以高效,並保證數據同步的可靠性 1.API設計 使用RESTful設計,確保API端點明確,並使用適當的HTTP方法(如POST用於創建,PUT用於更新)。 設計清晰的請求和響應模型,以確保客戶端能夠理解預期格式。 2.數據驗證 在伺服器端進行嚴格的數據驗證,確保接收到的數據符合預期格 ...