寫在前面 書籍介紹:本書在尊重《設計模式》原意的同時,針對JavaScript語言特性全面介紹了更適合JavaScript程式員的了16個常用的設計模式,講解了JavaScript面向對象和函數式編程方面的基礎知識,介紹了面向對象的設計原則及其在設計模式中的體現,還分享了面向對象編程技巧和日常開發中 ...
寫在前面
- 書籍介紹:本書在尊重《設計模式》原意的同時,針對JavaScript語言特性全面介紹了更適合JavaScript程式員的了16個常用的設計模式,講解了JavaScript面向對象和函數式編程方面的基礎知識,介紹了面向對象的設計原則及其在設計模式中的體現,還分享了面向對象編程技巧和日常開發中的代碼重構。本書將教會你如何把經典的設計模式應用到JavaScript語言中,編寫出優美高效、結構化和可維護的代碼。
- 我的簡評:這本書主要圍繞JavaScript中的一些設計模式和設計原則,每種模式的講解都帶有生活實例,恰當貼切,比較容易懂。不過,在此特意建議一下,有一定的編程經驗和項目經歷後再讀設計模式方面的書。
- !!文末有pdf書籍、筆記思維導圖、隨書代碼打包下載地址,需要請自取!閱讀「書籍精讀系列」所有筆記,請移步:推薦收藏-JavaScript書籍精讀筆記系列導航
第一章 面向對象的JavaScript
- JavaScript沒有提供傳統面向對象語言中的類式繼承,而是通過原型委托的方式來實現對象與對象之間的繼承。JavaScript也沒有在語言層面提供對抽象類和介面的支持
1.1.動態類型語言和鴨子類型
- 靜態類型語言在編譯時便確定變數的類型,而動態類型語言的變數類型要到程式運行的時候,待變數被賦予某個值之後,才會具有某種類型
- 鴨子類型的通俗說法是:如果它走起路來像鴨子,叫起來也是鴨子,那麼它就是鴨子
- 鴨子類型指導我們只關註對象的行為,而不關註對象本身,也就是關註HAS-A,而不是IS-A
- 在動態類型語言的面向對象設計中,鴨子類型的概念至關重要。利用鴨子類型的思想,我們不必藉助超類型的幫助,就能輕鬆地在動態類型語言中實現一個原則:面向介面編程,而不是面向實現編程
1.2.多態
- 多態的實際含義是:同一操作作用於不同的對象上面,可以產生不同的解釋和不同的執行結果。換句話說,給不同的對象發送同一個消息的時候,這些對象會根據這個消息分別給出不同的反饋
- 一段”多態“的JavaScript代碼:多態背後的思想是將“做什麼”和“誰去做以及怎麼去做”分離開,也就是將“不變的事物”與“可能改變的事物”分離開來
- 類型檢查和多態:靜態類型的面向對象語言通常被設計為可以向上轉型:當給一個類變數賦值時,這個變數的類型既可以使用這個類本身,也可以使用這個類的超類
- JavaScript的多態:多態的思想實際上是把“做什麼”和“誰去做”分離開來,要實現這一點,歸根結底先要消除類型之間的耦合關係;在JavaScript中,並不需要諸如向上轉型之類的技術取得多態的結果;
- 多態在面向對象程式設計中的作用:多態最根本的作用就是通過把過程化的條件分支語句轉換為對象的多態性,從而消除這些條件分支語句
- 設計模式與多態:GoF所著的《設計模式》,完全是從面向對象設計的角度出發的,通過對封裝、繼承、多態組合等技術的反覆使用,提煉出一些可重覆使用的面向對象設計技巧
1.3.封裝
- 封裝的目的是將信息隱藏
- 封裝數據:但JavaScript並沒有提供對private、protected、public這些關鍵字的支持,我們只能依賴變數的作用域來實現封裝特性,而且只能模擬出public和private這兩種封裝性
- 封裝實現:封裝的目的是將信息隱藏。封裝應該被視為“任何形式的封裝”,也就是說,封裝不僅僅是隱藏數據,還包括隱藏實現細節、設計細節以及隱藏對象的類型等;封裝使得對象之間的耦合變鬆散,對象之間只通過暴露的API介面來通信;
- 封裝類型:封裝類型是靜態類型語言中一種重要的封裝方式。一般而言,封裝類型是通過抽象類和介面類進行的;JavaScript本身也是一門類型模糊的語言。在封裝類型方面,JavaScript沒有能力,也沒有必要做得更多;
- 封裝變化:《設計模式》一書中共歸納總結了23種設計模式。從意圖上區分,這23種設計模式分別被劃分為創建型模式、結構型模式和行為型模式
1.4.原型模式和基於原型繼承的JavaScript對象
- 原型模式不單是一種設計模式,也被稱為一種編程範性
- 使用克隆的原型模式:原型模式的實現關鍵,是語言本身是否提供了clone方法。ECMAScript5提供了Object.create方法,可以用來克隆對象
- 克隆是創建對象的手段:但原型模式的真正目的並非在於需要得到一個一模一樣的對象,而是提供了一種便捷的方式去創建某個類型的對象,克隆只是創建這個對象的過程和手段
- 體驗Io語言:在JavaScript語言中不存在類的概念,對象也並非從類中創建出來的,所有的JavaScript對象都是從某個對象上克隆而來的;JavaScript基於原型的面向對象系統參考了Self語言和Smalltalk語言;
- 原型編程範性的一些規則:Io語言和JavaScript語言一樣,基於原型鏈的委托機制就是原型鏈繼承的本質;原型編程中的一個重要特性,即當對象無法響應某個請求時,會把該請求委托給它自己的原型;原型編程範型至少包括以下基本規則(所有的數據都是對象;要得到一個對象,不是通過實例化類,而是找到一個對象作為原型並克隆它;對象會記住它的原型;如果對象無法響應某個請求,它會把這個請求委托給它自己的原型)
- JavaScript中的原型繼承:事實上,JavaScript中的根對象是Object.prototype對象。Object.prototype對象是一個空的對象;JavaScript的函數既可以作為普通函數被調用,也可以作為構造器被調用。當使用new運算符來調用函數時,此時的函數就是一個構造器。用new運算符來創建對象的過程,實際上也只是先克隆Object.prototype對象,再進行一些其他額外操作的過程;就JavaScript的真正實現來說,其實並不能說對象有原型,而只能說對象的構造器有原型。對於“對象把請求委托給它自己的原型”這句話,更好的說法是對象把請求委托給它的構造器的原型;雖然JavaScript的對象最初都是由Object.prototype對象克隆而來的,但對象構造器的原型並不僅限於Object.prototype上,而是可以動態指向其他對象;留意一點,原型鏈並不是無限長的;
- 原型模式是一種設計模式,也是一種編程泛型,它構成了JavaScript這門語言的根本
第二章 this、call、apply
2.1.this
- 跟別的語言大相徑庭的是,JavaScript的this總是指向一個對象,而具體指向哪個對象是在運行時基於函數的執行環境動態綁定的,而非函數被聲明時的環境
- this的指向大致可以分為以下4種(作為對象的方法調用;作為普通函數的調用;構造器調用;Function.prototype.call或Function.prototype.apply調用);
- 1.作為對象的方法調用:this指向該對象
- 2.作為普通函數調用:this指向全局對象;在ECMAScript5的strict模式下,這種情況下的this已經被規定為不會指向全局對象,而是undefined;
- 3.構造器調用:當用new運算符調用函數時,該函數總會返回一個對象,通常情況下,構造器里的this就指向返回的這個對象
- 4.Function.prototype.call或Function.prototype.apply調用(可以動態的改變傳入函數的this;call和apply方法能很好的體現JavaScript的函數式語言特性,在JavaScript中,幾乎每一次編寫函數式語言風格的代碼,都離不開call和apply)
- 丟失的this:當用另一個變數getName2來引用obj.getName,並且調用getName2時,此時是普通函數調用方式,this是指向全局window的,所以程式的執行結果是undefined
2.2.call和apply
- ECMA3給Function的原型定義了兩個方法,它們是Function.prototype.call和Function.prototype.apply
- call和apply的區別:區別僅在於傳入參數形式的不同;apply接受兩個參數,第一個參數指定了函數體內this對象的指向,第二個參數為一個帶下標的集合;call傳入的參數數量不固定,第一個參數也是代表函數體內的this指向,從第二個參數開始往後,每個參數被依次傳入函數;當調用一個函數時,JavaScript的解釋器並不會計較形參和實參在數量、類型以及順序上的區別,JavaScript的參數在內部就是用一個數組表示的。從這個意義上說,apply比call的使用率更高,我們不必關心具體有多少參數被傳入函數;
- call和apply的用途:1.改變this指向(最常見的用途);2.Function.prototype.bind(大部分高級瀏覽器都實現了內置的Function.prototype.bind,用來指定函數內部的this指向);3.借用其他對象的方法;
第三章 閉包和高階函數
- 函數式語言的鼻祖是LISP,JavaScript在設計之初參考了LISP兩大方言之一的Scheme,引入了Lambda表達式、閉包、高階函數等特性
3.1.閉包
- 閉包的形成與變數的作用域以及變數的生存周期密切相關
- 變數的作用域:指變數的有效範圍
- 變數的生存周期:全局變數的生成周期是永久的,除非主動銷毀它;在函數內用var關鍵字聲明的局部變數,當退出函數時,這些局部變數就失去了價值,他們會隨著函數調用的結束而被銷毀;
- 閉包的更多作用:1.封裝變數(閉包可以幫助把一些不需要暴露在全局的變數封裝成“私有變數”);2.延長局部變數的壽命;
- 閉包和麵向對象設計:在JavaScript語言的祖先Scheme語言中,甚至都沒有提供面向對象的原生設計,但可以使用閉包來實現一個完整的面向對象系統
- 用閉包實現命令模式:命令模式的意圖是把請求封裝為對象,從而分離請求的發起者和請求的接收者之間的耦合關係
- 閉包與記憶體管理:一種慫人聽聞的說法是閉包會造成記憶體泄露,所以要儘量減少閉包的使用;局部變數本來應該在函數退出的時候被解除引用,但如果局部變數被封閉在閉包形成的環境中,那麼這個局部變數就能一直生存下去;在基於引用技術策略的垃圾回收機制中,如果兩個對象之間形成了迴圈引用,那麼這兩個對象都無法被回收,但迴圈引用造成的記憶體泄露在本質上也不是閉包造成的;
3.2.高階函數
- 高階函數是指至少滿足下列條件之一的函數:1.函數作為參數傳遞;2.函數作為返回值輸出;3.高階函數實現AOP;4.高階函數的其他應用;
- 1.函數作為參數傳遞:其中一個重要應用場景就是常見的回調函數;Array.prototype.sort接受一個函數作為參數,這個函數裡面封裝了數組元素的排序規則;
- 2.函數作為返回值輸出:函數當作返回值輸出的應用場景也許更多,也更能體現函數式編程的巧妙;1.判斷數據的類型,isType函數;2.getSingle,單例模式的例子;
- 3.高階函數實現AOP:AOP(面向切麵編程)的主要作用是把一些跟核心業務邏輯模塊無關的功能抽離出來,這些跟業務邏輯無關的功能包括日誌統計、安全控制、異常處理等;在Java語言中,可以通過反射和動態代理機制來實現AOP技術。而在JavaScript這種動態語言中,AOP的實現更加簡單,這是JavaScript與生俱來的能力;使用AOP的方式來給函數添加職責,也是JavaScript語言中一種非常特別和巧妙的裝飾者模式實現;
- 4.高階函數的其他應用:1.currying,函數柯里化;2.uncurrying;3.函數節流;4.分時函數;5.惰性載入函數;
第四章 單例模式
- 單例模式的定義是:保證一個類僅有一個實例,並提供一個訪問它的全局訪問點
- 單例模式是一種常用的模式,有一些對象我們往往只需要一個,比如線程池、全局緩存、瀏覽器中的window對象等
4.1.實現單例模式
- 要實現一個標準的單例模式並不複雜,無非是用一個變數來標誌當前是否已經為某個類創建過對象,如果是,則在下一次獲取該類的實例時,直接返回之前創建的對象
4.2.透明的單例模式
- 實現一個“透明”的單例類,用戶從這個類中創建對象的時候,可以像使用其他任何普通類一樣
4.3.用代理實現單例模式
- 把負責管理單例的代碼移除出去
4.4.JavaScript中的單例模式
- Douglas Crockford多次把全局變數稱為JavaScript中最糟糕的特性
- 在對JavaScript的創造者Brendan Eich的訪談中,他本人也承認全局變數是設計上的失誤,是在沒有足夠的時間思考一些東西的情況下導致的結果
- 以下幾種方式可以相對降低全局變數帶來的命名污染
- 1.使用命名空間:適當的使用命名空間,並不會杜絕全局變數,但可以減少全局變數的數量
- 2.使用閉包封裝私有變數:把一些變數封裝在閉包的內部,只暴露一些介面跟外界通信
4.5.惰性單例
- 惰性單例指的是在需要的時候才創建對象實例
- 以WebQQ的登錄浮窗為例:可以用一個變數來判斷是否已經創建過登錄浮窗
4.6.通用的惰性單例
- 上一節還有如下問題:違反單一職責原則的,創建對象和管理單例的邏輯都放在createLoginLayer對象內部;如果下次需要創建頁面中唯一的iframe、script等用來跨域請求數據,必須照抄一遍代碼;
- 把如何管理單例的邏輯從原來的代碼中抽離出來
4.7.小結
- 單例模式是一種簡單但非常實用的模式,特別是惰性單例技術,在合適的時候才創建對象,並且只創建唯一的一個
- 更奇妙的是,創建對象和管理單例的職責被分佈在兩個不同的方法,這兩個方法組合起來才具有單例模式的威力
第五章 策略模式
- 策略模式的定義是:定義一系列的演算法,把它們一個個封裝起來,並且使它們可以相互替換
5.1.使用策略模式計算獎金
- 以年終獎的計算為例:1.最初if判斷的代碼實現;2.使用組合函數重構代碼;3.使用策略模式重構代碼;
- 策略模式指的是定義一系列的演算法,把它們一個個封裝起來。將不變的部分和變化的部分分隔開始每個設計模式的主題,策略模式也不例外,策略模式的目的就是將演算法的使用與演算法的實現分離開來
- 一個基於策略模式的程式至少由兩部分組成。第一個部分是一組策略類,策略類封裝了具體的演算法,並負責具體的計算過程。第二個部分是環境類Context,Context接受客戶的請求,隨後把請求委托給某一個策略類
5.2.JavaScript版本的策略模式
- 實際上在JavaScript語言中,函數也是對象,所以更簡單和直接的做法是把strategy直接定義為函數
5.3.多態在策略模式中的體現
5.4.使用策略模式實現緩動動畫
- 實現動畫效果的原理:動畫片是把一些差距不大的原畫以較快ide幀數播放,來達到視覺上的動畫效果;JavaScript中,可以通過連續改變元素的某個CSS屬性,比如left、top、background-position來實現動畫效果;
- 思路和一些準備工作:需要提前記錄一些有用的信息
- 讓小球運動起來:
Animate.prototype.start = function(propertyName, endPos, duration, easing){}
5.5.更廣義的“演算法”
- 從定義上看,策略模式就是用來封裝演算法的。但如果把策略模式僅僅用來封裝演算法,未免有一點大材小用
- 在實際開發中,我們通常會把演算法的含義擴散開來,是策略模式也可以用來封裝一系列的“業務規則”
5.6.表單校驗
- 表單校驗的第一個版本:多個if判斷
- 用策略模式重構表單校驗:先創建了一個validator對象,然後通過validator.add方法,往validator對象中添加一些校驗規則
- 給某個文本輸入框添加多種檢驗規則
5.7.策略模式的優缺點
- 策略模式是一種常用且有效的設計模式,本章提供了計算獎金、緩動動畫、表單校驗這Sanger例子來加深對策略模式的理解
- 總結策略模式的一些優點:策略模式利用組合、委托和多態等技術和思想,可以有效避免多重條件選擇語句;策略模式提供了對開放-封閉原則的完美支持,將演算法封裝在獨立的strategy中,使得它們易於切換、易於理解、易於擴展;策略模式中的演算法也可以復用在系統的其他地方,從而避免許多重覆的複製粘貼模式;在策略模式中利用組合和委托來讓Context擁有執行演算法的能力,這yeshiva繼承的一種更輕便的替代方案;
5.8.一等函數對象與策略模式
- 實際上在JavaScript這種將函數作為一等對象的語言里,策略模式已經融入到了語言本身當中,我們經常用高階函數來封裝不同的行為,並且把它傳遞到另一個函數中
5.9.小結
- 在JavaScript語言的策略模式中,策略類往往被函數所代替,這是策略模式就成為一種“隱形”的模式
第六章 代理模式
- 代理模式是為一個對象提供一個代用品或占位符,以便控制對它的訪問
- 代理模式的關鍵是:當客戶不方便直接訪問一個對象那個或者不滿足需要的時候,提供一個替身對象來控制對這個對象的訪問,客戶實際上訪問的是替身對象。替身對象對請求做出一些處理之後,再把請求轉交給本體對象
6.1.第一個例子-小明追MM的故事
- 讓小明和MM共同的朋友代為送花
6.2.保護代理和虛擬代理
- 保護代理用於控制不同許可權的對象對目標對象的訪問,但在JavaScript並不容易實現保護代理,因為我們無法判斷誰訪問了某個對象
- 虛擬代理是最常用的一種代理模式
6.3.虛擬代理實現圖片預載入
- 圖片預載入,常用的做法是先用一張loading圖片占位,然後用非同步的方式載入圖片,等圖片載入好了再把它填充到img節點
6.4.代理的意義
- 實際上我們需要的只是給img節點設置src,預載入圖片只是一個錦上添花的功能
- 代理的作用在這裡體現出來,代理負責預載入圖片,預載入的操作完成之後,把請求重新交給本體MyImage
6.5.代理和本體介面的一致性
- 其中關鍵是代理對象和本體都對外提供了setSrc方法,在客戶看來,代理對象和本體是一致的,代理接手請求的過程對於用戶來說是透明的,用戶並不清楚代理和本體的區別
- 在Java等語言中,代理和本體都需要顯式地實現同一個介面,一方面介面保證了它們會擁有同樣的方法,另一方面,面向介面編程迎合依賴倒置原則,通過介面進行向上轉型,從而避開編譯器的類型檢查,代理和本體將來可以被替換使用
- 在JavaScript這種動態類型語言中,我們有時通過鴨子類型來檢測代理和本體是否都實現了setSrc方法,另外大多數時候甚至乾脆不做檢測,全部依賴程式員的自覺性,這對於程式的健壯性是有影響的
6.6.虛擬代理合併HTTP請求
- 文件同步的功能
- 解決方案是,可以通過一個代理函數來收集一段時間之內的請求,最後一次性發送給伺服器
6.7.虛擬代理在惰性載入中的應用
- miniConsole.js開源項目,希望在按下F12來主動喚出控制台的時候進行載入
6.8.緩存代理
- 緩存代理可以為一些開銷大的運算結果提供暫時的存儲,在下次運算時,如果傳遞進來的參數跟之前一致,則可以直接返回前面存儲的運算結果
- 緩存代理的例子-計算乘積
- 緩存代理用於ajax非同步請求數據
- 常見的分頁的需求,同一頁的數據理論上只需要去後臺拉取一次,這些已經拉取到的數據在某個地方被緩存之後,下次再請求同一頁的時候,便可以直接使用之前的數據
6.9.用高階函數動態創建代理
- 通過傳入高階函數這種更加靈活的方式,可以為各種計算方法創建緩存代理
6.10其他代理模式
- 代理模式的變體種類非常多:防火牆代理;遠程代理;保護代理;智能引用代理;寫時複製代理;
6.11.小結
- 代理模式包括許多小分類,在JavaScript開發中最常見的是虛擬代理和緩存代理
- 我們在編寫業務代碼的時候,往往不需要去預先猜測是否需要使用代理模式,當真正發現不方便直接訪問某個對象的時候,再編寫代理也不遲
第七章 迭代器模式
- 迭代器模式 是指提供一種方法順序訪問一個聚合對象中的各個元素,而又不需要暴露該對象的內部表示
7.1.jQuery中的迭代器
- 迭代器模式無非就是迴圈訪問聚合對象中的各個元素
7.2.實現自己的迭代器
7.3.內部迭代器和外部迭代器
- 迭代器可以分為內部迭代器和外部迭代器,它們有各自的適用場景
- 在一些沒有閉包的語言中,內部迭代器本身的實現也相當複雜
- 外部迭代器必須顯式的請求迭代下一個元素
- 外部迭代器雖然調用方式相對複雜,但它的實用面更廣,也能滿足更多變的需求
7.5.倒序迭代器
- 迭代器模式提供了迴圈訪問一個聚合對象中每個元素的方法,但它沒有規定我們以順序、倒序還是中序遍歷聚合對象
7.6.中止迭代器
- jQuery的each函數約定如果回調函數的執行結果返回false,則提前終止迴圈
7.7.迭代器模式的應用舉例
- 根據不同的瀏覽器獲取相應的上傳組件對象
7.8.小結
- 迭代器模式是一種相對簡單的模式,簡單到很多時候我們都不認為它是一種設計模式
第八章 發佈訂閱模式
- 發佈-訂閱模式又叫觀察者模式,它定義對象間的一種一對多的依賴關係,當一個對象的狀態發生改變時,所有依賴於它的對象都將得到通知
- 在JavaScript開發中,我們一般用事件模型來替代傳統的發佈-訂閱模式
8.1.現實中的發佈-訂閱模式
- 售樓的例子:購房者的電話號碼都被記在售樓處的花名冊上,新樓盤推出的時候,售樓MM會翻開花名冊,遍歷上面的電話號碼,依次發送一條簡訊來通知他們
8.2.發佈-訂閱模式的作用
- 發佈-訂閱模式可以廣泛應用於非同步編程中,這是一種替代傳遞迴調函數的方案
- 發佈-訂閱模式可以取代對象之間硬編碼的通知機制,一個對象不用再顯式的調用另外一個對象的某個介面
8.3.DOM事件
- 實際上,只要我們曾經在DOM節點上面綁定過事件函數,那我們就曾經使用過發佈-訂閱模式
- 還可以隨意增加或者刪除訂閱者,增加任何訂閱者都不會影響發佈者代碼的編寫
8.4.自定義事件
- 如何一步步實現發佈-訂閱模式:首先要指定好誰充當發佈者(比如售樓處);然後給發佈者添加一個緩存列表,用於存放回調函數以便通知訂閱者(售樓處的花名冊);最後發佈消息的時候,發佈者會遍歷這個緩存列表,依次觸發裡面的訂閱者回調函數(遍歷花名冊,挨個發簡訊);
8.5.發佈-訂閱模式的通用實現
- 把發佈-訂閱的功能提取出來,放在一個單獨的對象內
8.7.真實的例子-網站登錄
- 網站里有header頭部、nav導航、消息列表、購物車等模塊。這些模塊有一個共同的前提條件,就是必須先用Ajax非同步請求獲取用戶的登錄信息
- 更重要的一點是,我們不知道除了header頭部、nav導航、消息列表、購物車之外,將來還有哪些模塊需要使用這些用戶信息
- 用發佈-訂閱模式重寫後,對用戶信息感興趣的業務模塊將自行訂閱登錄成功的消息事件。當登錄成功時,登錄模塊只需要發佈登錄成功的消息,而業務方接受到消息之後,就會開始進行各自的業務處理,登錄模塊並不關心業務究竟要做什麼,也不想去瞭解它們的內部細節
8.8.全局的發佈-訂閱對象
- 買房子未必要親自去售樓處,我們只要把訂閱的請求交給中介公司,而各大房產公司也只需要通過中介公司來發佈房子消息。這樣一來,我們不用關心消息是來自哪個房產公司,我們在意的是能否順利收到消息
- 發佈-訂閱模式可以用一個全局的Event對象來實現,訂閱者不需要瞭解消息來自哪個發佈者,發佈者也不知道消息會推送給哪些訂閱者,Event作為一個類似”中介者“的角色,把訂閱者和發佈者聯繫起來
8.9.模塊間通信
- 要留意一個問題,模塊之間如果用了太多的全局發佈-訂閱模式來通信,那麼模塊與模塊之間的聯繫就被隱藏到了後面。最終搞不清楚消息來自哪個模塊,或者消息會流向哪些模塊,這會給我們的維護帶來一些麻煩,也許某個模塊的作用就是暴露一些介面給其他模塊調用
8.10.必須先訂閱再發佈嗎
- 在某些情況下,我們需要先將這條消息保存下來,等到有對象來訂閱它的時候,再重新把消息發佈給訂閱者。就如同QQ中的離線消息一樣,離線消息被保存在伺服器中,接收人下次登錄上線之後,可以重新收到這條消息
- 為了滿足這個需求,我們要建立一個存放離線事件的堆棧,當事件發佈的時候,如果此時還沒有訂閱者來訂閱這個事件,我們暫時把發佈事件的動作包裹在一個函數里,這些包裝函數將被存入堆棧中,等到終於有對象來訂閱此事件的時候,我們將遍歷堆棧並且依次執行這些包裝函數,也就是重新發佈裡面的事件
8.12.JavaScript實現發佈-訂閱模式的便利性
- 在Java中實現一個自己的發佈-訂閱模式,通常會把訂閱者對象自身當成引用傳入發佈者對象中,同時訂閱者對象還需提供一個名為諸如update的方法,供發佈者對象在適合的時候調用
- 而在JavaScript中,我們用註冊回調函數的形式來代替傳統的發佈-訂閱模式,顯得更加優雅和簡單
- 在JavaScript中,我們無需去選擇使用推模型還是拉模型。推模型是指事件發生時,發佈者一次性把所有更改的狀態和數據都推送給訂閱者。拉模型不同的地方是,發佈者僅僅通知訂閱者事件已經發生了,此外發佈者需提供一些公開的介面供訂閱者來主動拉去數據
8.13.小結
- 發佈-訂閱模式的優點非常明顯,一是時間上的解耦,而是對象之間的解耦
- 應用非常廣泛,既可以用在非同步編程中,也可以幫助我們完成更松耦合的代碼編寫
- 缺點:創建訂閱者本身要消耗一定的時間和記憶體,而且當你訂閱一個消息後,也許此消息最後都未發生,但這個訂閱者會始終存在於記憶體中。另外,發佈訂閱模式雖然可以弱化對象之間的聯繫,但如果過度使用的話,對象和對象之間的必要聯繫也將被深埋在背後,會導致程式難以跟蹤維護和理解
第九章 命令模式
9.1.命令模式的用途
- 命令模式是最簡單和優雅的模式之一,命令模式中的命令(command)指的是一個執行某些特定事情的指令
- 命令模式最常見的應用場景是:有時候需要向某些對象發送請求,但是並不知道請求的接收者是誰,也不知道被請求的操作是什麼。此時希望用一種松耦合的方式來設計程式,使得請求發送者和請求接收者能夠消除彼此之間的耦合關係
9.2.命令模式的例子-菜單程式
- 在這裡運用命令模式的理由:點擊了按鈕之後,必須向某些負責具體行為的對象發送請求,這些對象就是請求的接收者
- 設計模式的主題總是把不變的事務和變化的事物分離開來,命令模式也不例外
9.3.JavaScript中的命令模式
- 在面向對象設計中,命令模式的接收者被當成command對象的屬性保存起來,同時約定執行命令的操作command.execute方法
9.4.撤銷命令
- 命令模式的作用不僅是封裝運算塊,而且可以很方便地給命令對象增加撤銷操作
- 撤銷是命令模式里一個非常有用的功能,試想一下開發一個圍棋程式的時候,我們把每一步棋子的變化都封裝成命令,則可以輕而易舉的實現毀棋功能
9.6.命令隊列
- 命令對象的生命周期跟初始請求發生的時間無關,command對象的execute方法可以在程式運行的任何時刻執行,即使點擊按鈕的請求早已發生,但我們的命令對象仍然是有生命的
9.7.巨集命令
- 巨集命令是命令模式與組合模式的聯用產物
9.9.小結
- 跟許多其他語言不同,JavaScript可以用高階函數非常方便地實現命令模式
- 命令模式在JavaScript語言中是一種隱形的模式
第十章 組合模式
- 組合模式就是用小的子對象來構建更大的對象,而這些小的子對象本身也許是由更小的”孫對象“構成的
10.1.回顧巨集命令
- 巨集命令對象包含了一組具體的子命令對象,不管是巨集命令對象,還是子命令對象,都有一個execute方法負責執行命令
- 在macroCommand的execute方法里,並不執行真正的操作,而是遍歷它所包含的葉對象,把真正的execute請求委托給這些葉對象
10.2.組合模式的用途
- 組合模式將對象組合成樹形結構,以表示”部分-整體“的層次結構。除了用來表示樹形結構之外,組合模式的另一個好吃是通過對象的多態性表現,使得用戶對單個對象和組合對象的使用具有一致性
10.3.請求在樹中傳遞的過程
- 在組合模式中,請求在樹中傳遞的過程總是遵循一種邏輯
- 作為客戶,只需要關心樹最頂層的組合對象,客戶只需要請求這個組合對象,請求便會沿著樹往下傳遞,依次到達所有的葉對象
10.4.更強大的巨集命令
- 基本對象可以被組合成更複雜的組合對象,組合對象又可以被組合,這樣不斷遞歸下去,這棵樹的結構可以支持任意多的複雜度
10.5.抽象類在組合模式中的作用
- 組合模式最大的優點在於可以一致地對待組合對象和基本對象。客戶不需要知道當前處理的巨集命令還是普通命令,只要它是一個命令,並且有execute方法,這個命令就可以被添加到樹中
- 在JavaScript這種動態類型語言中,對象的多態性是與生俱來的,也沒有編譯器去檢查變數的類型,所以我們通常不會去模擬一個”怪異“的抽象類,在JavaScript中實現組合模式的難點在於要保證組合對象和葉對象擁有同樣的方法,這通常需要用鴨子類型的思想對它們進行介面檢查
- 在JavaScript中實現組合模式,看起來缺乏一些嚴謹性,我們的代碼算不上安全,但能更快速和自由地開發,這既是JavaScript的缺點,也是它的優點
10.6.透明性帶來的安全問題
- 組合模式的透明性使得發起請求的客戶不用去顧忌樹中組合對象和葉對象的區別,但它們在本質上有區別的
10.7.組合模式的例子-掃描文件夾
- 文件夾和文件之間的關係,非常適合用組合模式來描述。文件夾里既可以包含文件,又可以包含其他文件夾,最終可能組合成一棵樹
- 在添加一批文件的操作過程中,客戶不用分辨它們到底是文件還是文件夾。新增加的文件和文件夾能夠很容易地添加到原來的樹結構中,和樹里已有的對象一起工作
10.8.一些值得註意的地方
- 1.組合模式不是父子關係:組合模式是一種HAS-A(聚合)的關係,而不是IS-A
- 2.對葉對象操作的一致性:組合模式除了要求組合對象和葉對象擁有相同的介面之外,還有一個必要條件,就是對一組葉對象的操作必須具有一致性
- 3.雙向映射關係
- 4.用職責鏈模式提高組合模式性能
10.10.何時使用組合模式
- 適用於以下兩種情況:表示對象的部分-整體層次結構;客戶希望統一對待樹中的所有對象;
10.11.小結
- 組合模式可以讓我們使用樹形方式創建對象的結構。我們可以把相同的操作應用在組合對象和單個對象上
- 組合模式並不是完美的,它可能會產生一個這樣的系統:系統中的每個對象看起來都與其他對象差不多/它們的區別隻有在運行的時候才會顯現出來,這會使代碼難以理解
第十一章 模板方法模式
- 一種基於繼承的設計模式
11.1.模板方法模式的定義和組成
- 模板方法模式是一種只需使用繼承就可以實現的非常簡單的模式
- 模板方法模式由兩部分結構組成,第一部分是抽象父類,第二部分是具體的實現子類。
- 通常在抽象父類中封裝了子類的演算法框架,包括實現一些公共方法以及封裝子類中所有方法的執行順序。子類通過繼承這個抽象類,也繼承了整個演算法結構,並且可以選擇重寫父類的方法
11.2.第一個例子-Coffee or Tea
- 先泡一杯咖啡
- 泡一壺茶
- 3.分離出共同點:都能整理為下麵四步:1.把水煮沸;2.用沸水沖泡飲料;3.把飲料倒進杯子;4.加調料
- 4.創建Coffee子類和Tea子類:Beverage.prototype.init被稱為模板方法的原因是,該方法中封裝了子類的演算法框架,它作為一個演算法的模板,指導子類以何種順序去執行哪些方法
11.3.抽象類
- 首先要說明的是,模板方法是一種嚴重依賴抽象類的設計模式
- 抽象類的作用
- 抽象方法和具體方法
- 用Java實現Coffee or Tea的例子
- JavaScript沒有抽象類的缺點和解決方案:JavaScript並沒有從語法層面提供對抽象類的支持。抽象類的第一個作用是隱藏對象的具體類型,由於JavaScript是一門“類型模糊”的語言,所以隱藏對象的類型在JavaScript中並不重要;另一方面,當我們在JavaScript中使用原型繼承來模擬傳統的類式繼承時,並沒有編譯器幫助我們進行任何形式的檢查,我們也沒有辦法保證子類會重寫父類中的“抽象方法”;在Java中編譯器會保證子類會重寫父類中德抽象方法,但在JavaScript中卻沒有進行這些檢查工作;兩種變通的解決方案(第一種方案是用鴨子類型來模擬介面檢查,以便確保子類中確實重寫了父類的方法;第2種方案是讓Beverage.prototype.brew等方法直接拋出一個異常)
11.4.模板方法模式的使用場景
- 模板方法模式常被架構師用於搭建項目的框架,架構師定好了框架的骨架,程式員繼承框架的結構之後,負責往裡面填空
11.5.鉤子方法
- 鉤子方法(hook)可以用來解決這個問題(讓子類不受某個步驟的約束),放置鉤子是隔離變化的一種常見手段。我們在父類中容易變化的地方放置鉤子,鉤子可以有一個預設的實現,究竟要不要”掛鉤“,這由子類自行決定。鉤子方法的返回結果決定了模板方法後面部分的執行步驟,也就是程式接下來的走向,這樣一來,程式就擁有了變化的可能
11.6.好萊塢原則
- 好萊塢原則:不要來找我,我會給你打電話
- 在這一原則的指導下,我們允許底層組件將自己掛鉤到高層組件中,而高層組件會決定什麼時候,以何種方式去使用這些底層組件,高層組件對待底層組件的方式,跟演藝公司對待新人演員一樣,都是“別調用我們,我們會調用你”
- 當我們用模板方法模式編寫一個程式時,就意味著子類放棄對自己的控制權,而是改用父類通知子類,哪些方法應該在什麼時候被調用
- 好萊塢原則還常常應用於其他模式和場景,例如發佈-訂閱模式和回調函數
11.7.真的需要”繼承“嗎
- 模板方法模式是為數不多的基於繼承的設計模式,但JavaScript語言實際上沒有提供真正的類式繼承,繼承是通過對象與對象之間的委托來實現的
11.8.小結
- 模板方法模式是一種典型的通過封裝變化提高系統擴展性的設計模式
- 在傳統的面向對象語言中,一個運用了模板方法模式的程式中,子類的方法種類和執行順序都是不變的,所以我們把這部分邏輯抽象到父類的模板方法裡面
- 而子類的方法具體怎麼實現則是可變的,於是我們把這部分變化的邏輯封裝到子類中。通過增加新的子類,我們便能給系統增加新的功能,並不需要改動抽象父類以及其他子類,這也符合開放-封閉原則
第十二章 享元模式
寫在前面
- 享元(flyweight)模式是一種用於性能優化的模式
- 享元模式的核心是運用共用技術來有效支持大量細粒度的對象
- 如果系統中因為創建了大量類似的對象而導致記憶體占用過高,享元模式就非常有用了
12.1.初識享元模式
- 50種男士內衣和50種女士內衣穿在塑料模特上拍成廣告照片的例子
12.2.內部狀態與外部狀態
- 享元模式要求將對象的屬性劃分為內部狀態與外部狀態(狀態在這裡通常指屬性)
- 享元模式的目標是儘量減少共用對象的數量
- 關於如何劃分內部狀態和外部狀態的幾條經驗:內部狀態存儲於對象內部;內部狀態可以被一些對象共用;內部狀態獨立於具體的場景,通常不會改變;外部狀態取決於具體的場景,並根據場景而變化,外部狀態不能被共用;
- 享元模式是一種用時間換空間的優化模式
- 通常來講,內部狀態有多少種組合,系統中便最多存在多少個對象
- 使用享元模式的關鍵時如何區別內部狀態和外部狀態
- 可以被對象共用的屬性通常被劃分為內部狀態,如同不管什麼樣式的衣服,都可以按照性別不同,穿在同一個男模特或者女模特身上,模特的性別就可以作為內部狀態儲存在共用對象的內部
- 外部狀態取決於具體的場景,並根據場景而變化,就像例子中每件衣服都是不同的,它們不能被一些對象共用,因此只能被劃分為外部狀態
12.3.享元模式的通用結構
12.4.文件上傳的例子
- 在微雲上傳模塊的開發中,就曾經藉助享元模式提升了程式的性能
- 1.對象爆炸:支持同時選擇2000個文件,每一個文件都對應著一個JavaScript上傳對象的創建;支持好幾種上傳方式,比如瀏覽器插件、Flash和表單上傳等;
- 2.享元模式重構文件上傳:upload對象必須依賴uploadType屬性才能工作,這是因為插件上傳、Flash上傳、表單上傳的實際工作原理有很大的區別,它們各自調用的介面也是完全不一樣的,必須在對象創建之初就明確它是什麼類型的插件,才可以在程式的運行過程中,讓它們分別調用各自的start、pause、cancel、del等方法
- 3.剝離外部狀態:明確了uploadType作為內部狀態之後,再把其他的外部狀態從構造函數中抽離出來,Upload構造函數中只保留uploadType參數
- 4.工廠進行對象實例化
- 5.管理器封裝外部狀態
12.5.享元模式的實用性
- 一般來說,以下情況發生時便可以使用享元模式
- 1.一個程式中使用了大量的相似對象
- 2.由於使用了大量對象,造成很大的記憶體開銷
- 3.對象的大多數狀態都可以變為外部狀態
- 4.剝離出對象的外部狀態之後,可以用相對較少的共用對象取代大量對象
12.6.再談內部狀態和外部狀態
- 有多少種內部狀態的組合,系統中便最多存在多少個共用對象,而外部狀態儲存在共用對象的外部,在必要時被傳入共用對象來組裝成一個完整的對象
- 1.沒有內部狀態的享元:管理器部分的代碼不需要改動,還是負責剝離和組裝外部狀態。可以看到,當對象沒有內部狀態的時候,生產共用對象的工廠實際上變成了一個單例工廠
- 2.沒有外部狀態的享元:享元模式的關鍵時區別內部狀態和外部狀態。享元模式的過程是剝離外部狀態,並把外部狀態保存在其他地方,在合適的時刻再把外部狀態組成進共用對象
12.7.對象池
- 對象池技術的應用非常廣泛,HTTP連接池和資料庫連接池都是其代表應用
- 1.對象池實現
- 2.通用對象池實現
- 對象池是另外一種性能優化方案,它跟享元模式有一些相似之處,但沒有分離內部狀態和外部狀態這個過程
12.8.小結
- 享元模式是為瞭解決性能問題而生的模式
- 在一個存在大量相似對象的系統中,享元模式可以很好的解決大量對象帶來的性能問題
第十三章 職責鏈模式
- 職責鏈模式的定義是:使多個對象都有機會處理請求,從而避免請求的發送者和接收者之間的耦合關係,將這些對象連成一條鏈,並沿著這條鏈傳遞該請求,直到有一個對象處理它為止
13.1.現實中的職責鏈模式
- 公交車上遞硬幣的例子
- 職責鏈模式的最大優點:請求發送者只需要知道鏈中的第一個節點,從而弱化了發送者和一組接收者之間的強聯繫
13.2.實際開發中的職責鏈模式
- if..else再套if..else
13.3.用職責鏈模式重構代碼
- 去掉嵌套的條件分支語句,拆分成多個小函數
13.5.非同步的職責鏈
- 遇到非同步的問題,比如要在節點函數中發起一個ajax非同步請求,非同步請求返回的結果才能決定是否繼續在職責鏈中passRequest
- 非同步的職責鏈加上命令模式(把ajax請求封裝成命令對象),可以很方便的創建一個非同步ajax隊列庫
13.6.職責鏈模式的優缺點
- 職責鏈模式的最大優點就是解耦了請求發送者和N個接收者之間的複雜關係,由於不知道鏈中的哪個節點可以處理你發出的請求,所以你只需把請求傳遞給第一個節點即可
- 其次,使用了職責鏈模式之後,鏈中的節點對象可以靈活地拆分重組。增加或者刪除一個節點,或者改變節點在鏈中的位置都是輕而易舉的事情
- 還有一個優點,那就是可以手動指定起始節點,請求並不是非得從鏈中的第一個節點開始傳遞
- 一個弊端,首先我們不能保證某個請求一定會被鏈中德節點處理
- 另外,職責鏈模式使得程式中多了一些節點對象,可能在某一次的請求傳遞過程中,大部分節點並沒有起到實質性的作用,它們的作用僅僅是讓請求傳遞下去,從性能方面考慮,要避免過長的職責鏈帶來的性能損耗
13.7.用AOP實現職責鏈
- 利用JavaScript的函數式特性,有一種更加方便的方法來創建職責鏈
- 改寫一下之前的Function.prototype.after函數,使得第一個函數返回'nextSuccessor'時,將請求繼續傳遞給下一個函數
13.8.用職責鏈模式獲取文件上傳對象
- 之前創建了一個迭代器來迭代獲取合適的文件上傳對象,其實用職責鏈模式可以更簡單,完全不用創建這個多餘的迭代器
13.9.小結
- 在JavaScript開發中,職責鏈模式是最容易被忽視的模式之一
- 職責鏈模式可以很好的幫助我們管理代碼,降低發起請求的對象和處理請求時的對象之間的耦合性。職責鏈中的節點數量和順序是可以自由變化的,我們可以在運行時決定鏈中包含哪些節點
- 無論是作用域鏈、原型鏈,還是DOM節點中的事件冒泡,我們都能從中找到職責鏈模式的影子
- 職責鏈還可以和組合模式結合在一起,用來連接部件和父部件,或是提高組合對象的效率
第十四章 中介者模式
寫在前面
- 面向對象設計鼓勵將行為分佈到各個對象中,把對象劃分成更小的粒度,有助於增強對象的可復用性,但由於這些細粒度對象之間的聯繫激增,又有可能會反過來降低它們的可復用性
- 中介者模式的作用就是解除對象與對象之間的緊耦合關係
- 增加一個中介者對象後,所有的相關對象都通過中介者對象來通信,而不是互相引用,所以當一個對象發生改變時,只需要通知中介者對象即可。中介者使各對象之間耦合鬆散,而且可以獨立的改變它們之間的交互。中介者模式使網狀的多對多關係變成了相對簡單的一對多關係
14.1.現實中的中介者
- 1.機場指揮塔
14.2.中介者模式的例子--泡泡堂游戲
- 1.為游戲增加隊伍:需要在每個玩家死亡的時候,都遍歷其他隊友的生存狀況,如果隊友全部死亡,則這局游戲失敗,同時敵人隊伍的所有玩家都取得勝利
- 2.玩家增多帶來的困擾:可以隨意地為游戲增加玩家或者隊伍,但問題是,每個玩家和其他玩家都是緊緊耦合在一起的
- 3.用中介者模式改造泡泡堂:playerDirector開放一個對外暴露的介面receiveMessage,負責接收player對象發送的消息,而player對象發送消息的時候,總是把自身this作為參數發送給playerDirector,以便playerDirector識別消息來自於哪個玩家對象;除了中介者本身,沒有一個玩家知道其他任何玩家的存在,玩家與玩家之間的耦合關係已經完全解除,某個玩家的任何操作都不需要通知其他玩家,而只需要給中介者發送一個消息,中介者處理完消息之後會把處理結果反饋給其他的玩家對象;
14.3.中介者的例子--購買商品
- 1.開始編寫代碼
- 2.對象之間的聯繫
- 3.可能遇到的困難
- 4.引入中介者
14.4.小結
- 中介者模式是迎合迪米特法則的一種實現。迪米特法則也叫最少知識原則,是指一個對象應該儘可能少地瞭解另外的對象(類似不和陌生人說話)
- 在中介者模式里,對象之間幾乎不知道彼此的存在,它們只能通過中介者對象來互相影響對方
- 中介者模式使各個對象之間得以解耦,以中介者和對象之間的一對多關係取代了對象之間的網狀多對多關係。各個對象只需關註自身功能的實現,對象之間的交互關係交給了中介者對象來實現和維護
- 最大的缺點是系統中會新增一個中介者對象,因為對象之間交互的複雜性,轉移成了中介者對象的複雜性,使得中介者對象經常是巨大的。中介者對象自身往往就是一個難以維護的對象
- 一般來說,如果對象之間的複雜耦合確實導致調用和維護出現了困難,而且這些耦合度隨項目的變化呈指數增長曲線,那我們就可以考慮用中介者模式來重構代碼
第十五章 裝飾者模式
寫在前面
- 在程式開發中,許多時候都並不希望某個類天生就非常龐大,一次性包含許多職責
- 裝飾者模式可以動態地給某個對象添加一些額外的職責,而不會影響從這個類中派生的其他對象
- 裝飾者模式能夠在不改變對象自身的基礎上,在程式運行期間給對象動態地添加職責
15.1.模擬傳統面向對象語言的裝飾者模式
- 這種給對象動態增加職責的方式,並沒有真正地改動對象自身,而是將對象放入另一個對象之中,這些對象以一條鏈的方式進行引用,形成一個聚合對象
15.2.裝飾者也是包裝器
- GoF原想把裝飾者(decorator)模式稱為包裝器(wrapper)模式
- 從功能上而言,decorator能很好地描述這個模式,但從結構上看,wrapper的說法更加貼切。裝飾者模式將一個對象嵌入另一個對象之中,實際上相當於這個對象被另一個對象包裝起來,形成一條包裝鏈。請求隨著這條鏈依次傳遞到所有的對象,每個對象都有處理這條請求的機會
15.3.回到JavaScript的裝飾者
15.4.裝飾函數
- 在JavaScript中可以很方便地給某個對象擴展屬性和方法,但卻很難在不改動某個函數源代碼的情況下,給該函數添加一些額外的功能。在代碼的運行期間,我們很難切入某個函數的執行環境
- 現在需要一個辦法,在不改變函數源代碼的情況下,能給函數增加功能,這正是開放-封閉原則給我們指出的光明道路
- 一種答案,通過保存原引用的方式就可以改寫某個函數
15.5.用AOP裝飾函數
- 首先給出Function.prototype.before方法和Function.prototype.after方法
- 把當前的this保存起來,這個this指向原函數,然後返回一個“代理”函數,這個“代理”函數只是結構上像代理而已,並不承擔代理的職責(比如控制對象的訪問等)。它的工作是把請求分別轉發給新添加的函數和原函數,且負責保證它們的執行順序,讓新添加的函數在原函數之前運行(前置裝飾),這樣就實現了動態裝飾的效果
- Function.prototype.after的原理跟Function.prototype.before一模一樣,唯一不同的地方在於讓新添加的函數在原函數執行之後再執行
15.6.AOP的應用實例
- 不論是業務代碼的編寫,還是框架層面,我們都可以把行為依照職責分成粒度更細的函數,隨後通過裝飾把它們合併到一起,這有助於我們編寫一個松耦合和高復用性的系統
- 1.數據統計上報:分離業務代碼和數據統計代碼,無論在什麼語言中,都是AOP的經典應用之一
- 2.用AOP動態改變函數的參數:解決CSRF攻擊最簡單的一個辦法就是在HTTP請求中帶上一個Token參數
- 3.插件式的表單驗證:分離校驗輸入和提交Ajax請求的代碼,把校驗輸入的邏輯放到validata函數中,並且約定當validata函數返回false的時候,表示校驗未通過
- 這種裝飾方式疊加了函數的作用域,如果裝飾的鏈條過長,性能上也會受到一些影響
15.7.裝飾者模式和代理模式
- 這兩種模式都描述了怎樣為對象提供一定程度上的間接引用,它們的實現部分都保留了對另外一個對象的引用,並且向那個對象發送請求
- 代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情
- 裝飾者模式的作用就是為對象動態加入行為
- 代理模式通常只有一層代理-本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈
第十六章 狀態模式
- 狀態模式的關鍵是區分事務內部的狀態,事務內部狀態的改變往往會帶來事物的行為改變
16.1.初識狀態模式
- 電燈燈光切換的例子
- 通常我們談到封裝,一般都會優先封裝對象的行為,而不是對象的狀態。但在狀態模式中剛好相反,狀態模式的關鍵是把事物每種狀態都封裝成單獨的類,跟此種狀態有關的行為都被封裝在這個類的內部
- 使用狀態模式的好處很明顯,它可以使一種狀態和它對應的行為之間的關係局部化,這些行為被分散和封裝在各自對應的狀態類之中,便於閱讀和管理代碼
- 另外,狀態之間的切換都被分佈在狀態類內部,這使得我們無需編寫過多的if、else條件分支語言來控制狀態之間的轉換
16.2.狀態模式的定義
- GoF中對狀態模式的定義:允許一個對象在其內部狀態改變時改變它的行為,對象看起來似乎修改了它的類
- 第一部分的意思是將狀態封裝成獨立的類,並將請求委托給當前的狀態對象,當對象的內部狀態改變時,會帶來不同的行為變化
- 第二部分是從客戶的角度來看,我們使用的對象,在不同的狀態下具有截然不同的行為,這個對象看起來是從不同的類中實例化而來的,實際上這是使用了委托的效果
16.4.缺少抽象類的變通方式
- 在Java中,所有的狀態必須繼承自一個State抽象父類,當然如果沒有共同的功能值得放入抽象父類中,也可以選擇實現State介面
16.5.另一個狀態模式示例--文件上傳
- 文件上傳程式中有掃描、正在上傳、暫停、上傳成功、上傳失敗這幾種狀態,音樂播放器可以分為載入中、正在播放、暫停、播放完畢這幾種狀態
- 1.更複雜的切換條件
- 2.一些準備工作
- 3.開始編寫代碼
- 4.狀態模式重構文件上傳
16.6.狀態模式的優缺點
- 狀態模式的優點如下:1、狀態模式定義了狀態與行為之間的關係,並將它們封裝在一個類里。通過增加新的狀態類,很容易增加新的狀態和轉換;2、避免Context無限膨脹,狀態切換的邏輯被分佈在狀態類中,也去掉了Context中原本過多的條件分支;3、用對象代替字元串來記錄當前狀態,使得狀態的切換更加一目瞭然;4、Context中的請求動作和狀態類中封裝的行為可以非常容易地獨立變化而互不影響;
- 狀態模式的缺點:1、會在系統中定義許多狀態類,編寫20個狀態類是一項枯燥乏味的工作,而且系統中會因此而增加不少對象;2、由於邏輯分散在狀態類中,雖然避開了不受歡迎的條件分支語句,但也造成了邏輯分散的問題,我們無法在一個地方就看出整個狀態機的邏輯;
16.7.狀態模式中的性能優化點
- 一些比較大的優化點:有兩種選擇來管理state對象的創建和銷毀。第一種是僅當state對象被需要時才創建並隨後銷毀,另一種是一開始就創建好所有的狀態對象,並且始終不銷毀它們;為每個Context對象都創建了一組state對象,實際上這些state對象之間是可以共用的,各Context對象可以共用一個state對象;
16.8.狀態模式和策略模式的關係
- 狀態模式和策略模式向一對雙胞胎,它們都封裝了一系列的演算法或者行為,它們的類圖看起來幾乎一模一樣,但在意圖上有很大不同
- 策略模式和狀態模式的相同點是,它們都有一個上下文、一些策略或者狀態類,上下文把請求委托給這些類來執行
- 它們之間的區別是策略模式中的各個策略類之間是平等又平行的,它們之間沒有任何聯繫,所以客戶必須熟知這些策略類的作用,以便客戶可以隨時主動切換演算法;而在狀態模式中,狀態和狀態對應的行為是早已被封裝好的,狀態之間的切換也早被規定完成,“改變行為”這件事發生在狀態模式內部
16.9.JavaScript版本的狀態機
- 狀態模式是狀態機的實現之一,但在JavaScript這種“無類”語言中,沒有規定讓狀態對象一定要從類中創建而來。另外一點,JavaScript可以非常方便地使用委托技術,並不需要事先讓一個對象持有另一個對象
16.10.表驅動的有限狀態機
- 另外一種實現狀態機的方法,核心是基於表驅動的。可以在表中很清楚的看到下一個狀態是由當前狀態和行為共同決定的。這樣一來,我們就可以在表中查找狀態,而不必定義很多條件分支
16.11.實際項目中的其他狀態機
- 在實際開發中,很多場景都可以用狀態機來模擬,比如一個下拉菜單在hover動作下有顯示、懸浮、隱藏等狀態;一次TCP請求有建立連接、監聽、關閉等狀態;一個格鬥游戲中人物有攻擊、防禦、跳躍、跌倒等狀態
16.12.小結
- 狀態模式也