javascript設計模式源碼
㈠ 設計模式(五)行為型模式
在上一篇結構型模式中,我們以功能為基本單位,研究了一些設計模式,用於實現功能轉換、功能組合、功能封裝等目的。
我們知道,面向對象編程有兩個核心元素:對象、對象間通信協作。從面向對象的角度看,任何系統和功能,都是由一個個對象,相互分工合作實現的。推而廣之,很多系統也都是這樣組織和運行的。
本章的設計模式,列舉了通用場景下常用功能機制的經典實現方法,講解了經典實現中是如何高效組織對象、控制對象協作交互的,具有很好的參考價值。
示例:https://www.runoob.com/design-pattern/chain-of-responsibility-pattern.html
責任鏈模式,就是把程序處理對象前後排列起來,形成一條處理線。處理線上需要被處理的信息,在處理線上向下傳遞,任何一個節點都可以隨時中斷傳遞。
GUI系統中的事件傳遞機制(在javascript中叫做事件冒泡),是責任鏈模式最典型的應用之一。
當某一事件發生時,最頂層GUI對象會首先收到事件,但是它先不處理,而是依次交給命中的子GUI對象處理。當子GUI對象返回為False時,表示事件未被接收,此時父GUI對象才真正對發生的事件進行業務處理。
可以看出,事件傳遞機制,是一種增強版的責任鏈模式,它的節點處理權,經歷了向下和向上的雙向傳遞過程。
總結:當項目中一個數據對象,需要被多個處理對象進行處理時,可以將處理對象鏈接起來,然後把數據對象傳遞給頭節點,隨著處理的進行,數據對象的處理權會在處理鏈中流動,從而完成整個處理過程。
責任鏈模式結構適用於需求固定的場景,用於實現簡單高效的處理機制。假如需求不斷變化,而且功能很復雜,那麼用責任鏈模式很可能就無法勝任了,需要採用新的高復雜度的設計。例如,如果想要數據對象在所有處理對象中根據狀態來實現跳轉,可以選擇使用狀態機等其他方案來實現。
示例:https://www.runoob.com/design-pattern/command-pattern.html
想要實現撤銷、重做、事務等功能,可以使用此設計模式。通常在編輯器、資料庫中有此類功能需求。
命令也就是請求,或者叫調用。命令模式要求將請求參數和請求相關的方法封裝在一起。
請求對象中封裝了實現「撤銷」、「重做」、「事務」功能所需要的所有信息,實現了關聯信息的高內聚,所以可以實現我們想要的功能。
例如,可以在請求對象中保存修改之前的值、修改之後的值。利用修改之前的值,可以實現「撤銷」功能;利用修改之後的值,可以實現「重做」功能。如果將所有請求對象都記錄下來,並按照先後順序排列起來,形成「撤銷重做」堆棧,這樣就可以實現連續的「撤銷」、「重做」。「事務」則是「撤銷」與「重做」的結合體,正常執行流程等同於「重做」,發生錯誤需要回滾,等同於「撤銷」。
如果不採用這種方式,會導致實現這些功能的信息,分散在源碼中多個地方,或者已經丟失,沒有保存,就無法實現「撤銷」、「重做」、「事務」功能。
同時,實現請求參數高內聚,也可以很方便地將它們保存到磁碟上,保存到文件的過程叫做「序列化」,從文件中讀取的過程叫「反序列化」。這里的序列指的就是二進制流。
Qt中與命令模式相關的部分是:Undo Framework,裡面有示例項目,不熟悉的同學可以抽點時間看一看。
示例:https://www.runoob.com/design-pattern/interpreter-pattern.html
顧名思義,解釋器模式是用來實現解釋器的。 解釋器是這樣一個程序:解釋器以符合語法的文本為輸入,解釋輸入內容,完成一定的計算功能。文本可以在程序運行時動態載入,動態解釋、動態執行。
實現簡單的解釋器:命令行程序,如ping命令、cd命令等; 實現復雜的解釋器:腳本語言解釋器,如python,lua,javascript;計算器。
我們知道,在GUI圖形用戶界面被發明之前,人類和程序之間的交互是通過敲命令行實現的,缺點是使用難度較大,門檻較高。 在GUI發明以後,交互更加友好,電腦更加易於使用了,所以也更加普及了。
但是GUI交互的缺點在於,不夠靈活,對參數的控制粒度不夠細致。例如,現在大多數開發者都使用集成開發環境來開發軟體,一般情況下都使用默認參數,比較方便。但是如果你想要更改某些編譯選項,可能還是需要直接修改底層的編譯命令。命令相對於GUI元素更加靈活,過於靈活的地方用GUI比較難於實現,例如組合、遞歸、跳轉等等。在這些場景下,使用解釋器是非常合適的。但是通常情況下,這個模式並不常用。
示例:https://www.runoob.com/design-pattern/iterator-pattern.html
在需要多次遍歷同一個數據集合的時候,為了少些一些for,或者想要把遍歷過程封裝起來,降低耦合,就可以使用迭代器模式。這個模式非常常用。
迭代器就是一個專門用來遍歷數組的類。它只需要實現兩個介面:hasNext()、next()。 hasNext()介面用於控制循環何時停止;next()介面用於取出當前位置的數據元素,並將遍歷指針指向下一個元素。 當然,構造迭代器對象的時候,需要將數據集合傳遞給迭代器,讓迭代器知道要遍歷哪些數據。
原本需要用for循環來遍歷的代碼,現在通過封裝,提取出了「遍歷」這一功能所需要的必要信息,定義了兩個介面,把不必要暴露的信息封裝在了迭代器中,妥妥的實現了解耦。
示例:https://www.runoob.com/design-pattern/mediator-pattern.html
中介者模式是指,在原本直接通信的對象之間,添加一個通信中間層,使對象間通信變為間接通信,降低對象間的耦合。
此模式和代理模式基本思想上是一致的。二者的區別是:代理模式是通過加一個中間層,來實現兩個原本很難交互的功能主體,實現順暢交互;中介者模式是為了降低對象間通信時的耦合而提出的,為的是提高代碼的可維護性。
比較大的項目中會用到,一般存在於某些框架中。因為大的項目中對象繁多,通信也比較復雜,適合使用中介者模式。
在大的項目中,一般會有一個全局的通信管理器,任何對象都可以使用通信管理器提供的介面,將自己注冊為某一個具有唯一ID消息的發送者和接收者。這樣發送者只需要發送消息,不需要管誰來接收,不需要擁有發送者的實例指針,發出消息後,已注冊的接收者都會收到消息。接收者不需要管信號是誰發的,即不需要擁有發送者的實例指針。
所以,中介者模式也可以叫「通信中介模式」。
示例:https://www.runoob.com/design-pattern/memento-pattern.html
這個模式和狀態存檔功能是綁定在一起的。為了在程序中實現狀態存檔功能,可以使用備忘錄模式。
原例子中有三個類,個人覺得沒有必要,這里我們簡化成兩個類,即備忘錄模式中有兩個類:狀態對象類和狀態對象管理類。 狀態對象類是狀態欄位是集合,並提供了存取介面;狀態對象管理類負責組織和保存狀態對象。當然實際實現中可以根據需求增加類,配合使用,完成狀態保存恢復。
當一個對象會影響到其他多個對象時,即當對象間存在一對多關系時,使用觀察者模式。 一般應用於單向通知的場景,如GUI中滑鼠事件、按鍵事件、窗口事件通知。使用Qt中的信號槽機制可以實現此模式。
「一」是指發生變化的那個對象,「多」是指需要獲取此變化通知的對象組。其中,變化消息是單向地由「一」到「多」傳遞的。如果不是單向的或者對象間不是一對多的關系,更加復雜,就需要重新思考其他對象間通信模型。
如果不使用此模式,可能會導致觀察者不能動態增加或刪除;可能會造成發送者的業務代碼和接收者的響應代碼混在一起,耦合嚴重。
使用此模式,需要為觀察者設計一個基類,並設計一個接收通知的介面,所有觀察者需要實現通知介面;所有觀察者指針可以保存在隊列中,實現動態增刪。
狀態模式用於實現狀態機。 如果一個程序功能中存在某些狀態,在一定情況下,這些狀態可以互相轉換,並且在轉換前後需要作出對應的操作,這種情況下使用狀態機來實現就非常合適。
如果不使用狀態機(狀態模式),一般的實現方法是使用一連串的if-else,或者使用長長的switch-case來實現。這樣做的缺點,一方面狀態判斷不夠高效,另一方面是業務代碼集中在一塊,不好維護。
使用狀態機,每個狀態都是一個類,相關的業務代碼分布到各自的狀態類中,能夠實現不同的狀態及與狀態相關的業務代碼解耦。同時某個狀態和下一個狀態是關聯好的,在狀態切換時,效率更高,不需要執行長長的判斷。
Qt中已實現狀態機框架,The State Machine Framework,在此框架下,我們可以更加專注於業務實現,而不是狀態機本身的技術細節。
示例:https://www.runoob.com/design-pattern/null-object-pattern.html
使用基類保存子類對象通常有兩種做法:
第一種方法用指針是基本方法,但是指針用起來要非常小心,要考慮內存釋放的問題。此時空對象就可以用空指針表示。 第二種方法用基類對象保存子類對象,這種方法使用起來相對省心,不用與指針打交道,使用者不用直接管理內存。例如Qt中的Qt XML C++ Classes類的設計就是採用這種方式設計的。這種情況下,因為不使用指針,就需要使用空對象來代替空指針。
可以仿造Qt XML中的類進行設計。一般需要提供isNull()介面,對象類型轉換介面等。
策略模式和橋接模式類似,用於實現功能切換與組合。二者區別在於,策略模式專注於一個功能的不同實現方式;橋接模式專注於多個功能之間的組合。
將功能抽象成單獨的類,功能切換只需要切換不同的功能子類即可,同一個功能需要實現同一個功能介面。
示例:https://www.runoob.com/design-pattern/template-pattern.html
模板模式應該是我們最熟悉的。 這里的模板就是介面類,介面類定義了使用者和功能提供者之間交互的函數列表。子類負責功能的具體實現。
示例:https://www.runoob.com/design-pattern/visitor-pattern.html
訪問者模式用於將數據結構與數據操作相分離。
訪問者模式和迭代器模式類似。迭代器模式一般用來遍歷數組,所以沒有把for封裝起來。而訪問者模式可以遍歷一切類型的數據結構,具體的遍歷過程被封裝在接收者內部。同時,對每一個遍歷得到的數組元素的操作,被封裝在訪問者內部。每一種對元素不同的操作,都需要新建一個訪問者類。
接收者需要實現accept()介面,訪問者需要實現visit()介面。
每種設計模式都有使用場景,都有優點和缺點。隨著需求的改變,任何一種設計模式可能都將不再適用。
㈡ JavaScript設計模式之策略模式
什麼是設計模式?為什麼需要學習設計模式?
學習設計模式的目的是:為了代碼可重用性、讓代碼更容易被他人理解、保證代碼可靠性。設計模式使代碼編寫真正工程化;設計模式是軟體工程的基石脈絡,如同大廈的結構一樣。
經典的設計模式有23種,但並不是每一種設計模式都被頻繁使用。在這里,介紹最常用和最實用的幾種設計模式,本文先來介紹策略模式(StrategyPattern)。
策略模式是一種行為設計模式,定義一系列演算法,將每一個演算法封裝起來,並讓它們可以相互替換。策略模式讓演算法獨立於使用它的客戶而變化,也稱為政策模式(Policy)。
假如正在開發一個在線商城的項目,每個產品都有原價,稱之為originalPrice。但實際上並非所有產品都以原價出售,可能會推出允許以折扣價出售商品的促銷活動。
商家可以在後台為產品設置不同的狀態,然後實際售價將根據產品狀態和原價動態調整。
具體規則如下:
部分產品已預售:為鼓勵客戶預訂,將在原價基礎上享受20%的折扣。
部分產品處於正常促銷階段:如果原價低於或等於100,則以10%的折扣出售;如果原價高於100,則減10元。
有些產品沒有任何促銷活動:它們屬於default狀態,並以原價出售。
這時需要寫一個獲取商品價格的函數getPrice,應該怎麼寫呢?
functiongetPrice(originalPrice,status){//...//返回價格;}事實上,面對這樣的問題,如果不考慮任何設計模式,最直觀的寫法可能if-else多次條件判斷語句來計算價格。
有三種狀態,可以快速編寫如下代碼:
functiongetPrice(originalPrice,status){if(status==="pre-sale"){returnoriginalPrice*0.8;}if(status==="promotion"){if(origialPrice<=100){returnorigialPrice*0.9;}else{returnoriginalPrice-20;}}if(status==="default"){returnoriginalPrice;}}有三個條件,上面的代碼寫了三個if語句,這是非常直觀的代碼,但是這段代碼組織上不好。
首先,它違反了單一職責原則(Singleresponsibilityprinciple,規定每個類或者函數都應該有一個單一的功能,並且該功能應該由這個類或者函數完全封裝起來)。函數getPrice做了太多的事情,這個函數不易閱讀,也容易出現bug。如果一個條件出現bug,整個函數就會崩潰。同時,這樣的代碼也不容易調試。
並且這段代碼很難應對變化的需求,這時就需要考慮設計模式,其往往會在業務邏輯發生變化時展現出它的魅力。
假設業務擴大了,現在還有另一個折扣促銷:黑色星期五。折扣規則如下:
價格低於或等於100元的產品以20%的折扣出售。
價格高於100元但低於200元的產品將減少20元。
價格高於或等於200元的產品將減少20元。
這個時候該怎麼擴展getPrice?函數呢?
看起來必須在getPrice函數中添加一個條件語句:
functiongetPrice(originalPrice,status){if(status==="pre-sale"){returnoriginalPrice*0.8;}if(status==="promotion"){if(origialPrice<=100){returnorigialPrice*0.9;}else{returnoriginalPrice-20;}}//黑色星期五規則if(status==="black-friday"){if(origialPrice>=100&&originalPrice<200){returnorigialPrice-20;}elseif(originalPrice>=200){returnoriginalPrice-50;}else{returnoriginalPrice*0.8;}}if(status==="default"){returnoriginalPrice;}}每當增加或減少折扣時,都需要更改函數。這種做法違反了開閉原則(對擴展開放,對修改關閉)。修改已有的功能很容易出現新的錯誤,而且還會使得getPrice越來越臃腫。
那麼如何優化這段代碼呢?
首先,可以拆分這個函數getPrice?以減少臃腫。
/***預售商品價格規則*@param{*}origialPrice*@returns*/functionpreSalePrice(origialPrice){returnoriginalPrice*0.8;}/***促銷商品價格規則*@param{*}origialPrice*@returns*/functionpromotionPrice(origialPrice){if(origialPrice<=100){returnorigialPrice*0.9;}else{returnoriginalPrice-20;}}/***黑色星期五促銷規則*@param{*}origialPrice*@returns*/functionblackFridayPrice(origialPrice){if(origialPrice>=100&&originalPrice<200){returnorigialPrice-20;}elseif(originalPrice>=200){returnoriginalPrice-50;}else{returnoriginalPrice*0.8;}}/***默認商品價格*@param{*}origialPrice*@returns*/functiondefaultPrice(origialPrice){returnorigialPrice;}functiongetPrice(originalPrice,status){if(status==="pre-sale"){returnpreSalePrice(originalPrice);}if(status==="promotion"){returnpromotionPrice(originalPrice);}if(status==="black-friday"){returnblackFridayPrice(originalPrice);}if(status==="default"){returndefaultPrice(originalPrice);}}經過這次修改,雖然代碼行數增加了,但是可讀性有了明顯的提升。getPrice函數顯然沒有那麼臃腫,寫單元測試也比較方便。
但是上面的改動並沒有解決根本的問題:代碼還是充滿了if-else,而且當增加或者減少折扣規則的時候,仍然需要修改getPrice。
其實使用這些if-else的目的就是為了對應狀態和折扣策略。
從圖中可以發現,這個邏輯本質上是一種映射關系:產品狀態與折扣策略的映射關系。
可以使用映射而不是冗長的if-else來存儲映射,按照這個思路可以構造一個價格策略的映射關系(策略名稱與其處理函數之間的映射),如下:
constpriceStrategies={"pre-sale":preSalePrice,promotion:promotionPrice,"black-friday":blackFridayPrice,default:defaultPrice,};將狀態與折扣策略結合起來,價格函數就可以優化成如下:
functiongetPrice(originalPrice,status){returnpriceStrategies[status](originalPrice);}這時候如果需要加減折扣策略,不需要修改函數,只需要修改價格策略映射關系priceStrategies
之前的代碼邏輯如下:
優化後的代碼邏輯如下:
以上的優化策略就是使用了設計模式之策略模式,在實際的項目開發過程中還是比較實用。
在什麼情況下可以考慮使用策略模式呢?如果函數具有以下特徵:
判斷條件很多
各個判斷條件下的代碼相互獨立
然後可以將每個判斷條件下的代碼封裝成一個獨立的函數,然後建立判斷條件和具體策略的映射關系。
原文:https://juejin.cn/post/7113451841001619463