寫在前面
- 書籍介紹:本書在尊重《設計模式》原意的同時,針對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.小結
- 狀態模式也許是被大家低估的模式之一
- 實際上,通過狀態模式重構代碼之后,很多雜亂無章的代碼會變得清晰
第十七章 配接器模式
寫在前面
- 配接器模式的作用是解決兩個軟體物體間的介面不兼容的問題
- 在程式開發中有許多這樣的場景:當我們試圖呼叫模塊或者物件的某個介面時,卻發現這個介面的格式并不符合目前的需求
- 兩種解決辦法,第一種是修改原來的介面實作,第二種辦法是創建一個配接器,將原介面轉換為客戶希望的另一個介面,客戶只需要和配接器打交道
17.1.現實中的配接器
- 幾個現實生活中的配接器模式:1.港式插頭轉換器;2.電源配接器;3.USB轉介面
17.2.配接器模式的應用
- 配接器模式是一種“亡羊補牢”的模式,沒有人會在程式的設計之初就使用它
17.3.小結
- 配接器模式是一種相對簡單的模式
- 有一些模式跟配接器模式的結構非常相似,比如裝飾者模式、代理模式和外觀模式
- 配接器模式主要用來解決兩個已有介面之間不匹配的問題,它不考慮這些介面是怎樣實作的,也不考慮它們將來可能會如何演化
- 裝飾者模式和代理模式也不會改變原有物件的介面,但裝飾者模式的作用是為了給物件增加功能
- 外觀模式的作用倒是和配接器模式比較相似,有人把外觀模式看成一組物件的配接器,但外觀模式最顯著的特點是定義了一個新的介面
第十八章 單一職責原則
- 前輩總結的這些設計原則通常指的是單一職責原則、里氏替換原則、依賴倒置原則、介面隔離原則、合成復用原則和最少知識原則
寫在前面
- 單一職責原則(SRP)的職責被定義為“引起變化的原因”
- SRP原則體現為:一個物件(方法)只做一件事情
18.1.設計模式中的SRP原則
- SRP原則在很多設計模式中都有著廣泛的運用,例如代理模式、迭代器模式、單例模式和裝飾者模式
18.2.何時應該分離職責
- SRP原則是所有原則中最簡單也是最難正確運用的原則之一
- 一方面,如果隨著需求的變化,有兩個職責總是同時變化,那就不必分離他們
- 另一方面,職責的變化軸線僅當它們確定會發生變化時才具有意義
18.4.SRP原則的優缺點
- SRP原則的優點時降低單個類或者物件的復雜度,按照職責把物件分解成更小的粒度,這有助于代碼的復用,也有利于進行單元測驗
- SRP原則的一些缺點,最明顯的是會增加撰寫代碼的復雜度
第十九章 最少知識原則
- 最少知識原則(LKP)說的是一個軟體物體應當盡可能少地與其他物體發生相互作用,
- 這里的軟體物體是一個廣義的概念,不僅包括物件,還包括系統、類、模塊、函式、變數等
19.1.減少物件之間的聯系
- 最少知識原則要求我們在設計程式時,應當盡量減少物件之間的互動
19.2.設計模式中的最少知識原則
- 最少知識原則在設計模式中體現得最多的地方是中介者模式和外觀模式
- 1.中介者模式(通過增加一個中介者物件,讓所有的相關物件都通過中介者物件來通信,而不是互相參考)
- 2.外觀模式(外觀模式的作用是對客戶屏蔽一組子系統的復雜性,外觀模式對客戶提供一個簡單易用的高層介面,高層介面會把客戶的請求轉發給子系統來完成具體的功能實作;外觀模式的作用主要有兩點(為一組子系統提供一個簡單便利的訪問入口;隔離客戶與復雜子系統之間的聯系,客戶不用去了解子系統的細節))
19.3.封裝在最少知識原則中的體現
- 封裝在很大程度上表達的是資料的隱藏,一個模塊或者物件可以將內部的資料或者實作細節隱藏起來,只暴露必要的介面API供外界訪問
- 最少知識原則也叫做迪米特法則(Law of Demeter,LoD)
第二十章 開放-封閉原則
寫在前面
- 在面向物件的程式設計中,開放-封閉原則(OCP)是最重要的一條原則
- 開放-封閉原則定義如下:軟體物體(類、模塊、函式)等應該是可以擴展的,但是不可修改
20.1.擴展window.onload函式
- 通過增加代碼,而不是修改代碼的方式,來給window.onload函式添加新的功能
- 通過動態裝飾函式的方式,我們完全不用理會從前window.onload函式的內部實作,無論它的實作優雅或是丑陋
20.2.開放和封閉
- 引出開放-封閉原則的思想-當需要改變一個程式的功能或者給這個程式增加新功能的時候,可以使用增加代碼的方式,但是不允許改動程式的源代碼
20.3.用物件的多型性消除條件分支
- 利用多型的思想,我們把程式中不變的部分隔離出來(動物都會叫),然后把可變的部分封裝起來(不同型別的動物發出不同的叫聲),這樣一來程式就具有了可擴展性
20.4.找出變化的地方
- 我們還是能找到一些讓程式盡量遵守開放-封閉原則的規律,最明顯的就是找出程式中將要發生變化的地方,然后把變化封裝起來
- 通過封裝變化的方式,可以把系統中穩定不變的部分和容易變化的部分隔離出來
- 除了利用物件的多型性之外,還有其他方式可以幫助我們撰寫遵守開放-封閉原則的代碼:1.放置掛鉤(hook);2.使用回呼函式
20.5.設計模式中的開放-封閉原則
- 幾乎所有的設計模式都是遵守開放-封閉原則的,我們見到的好設計,通常都經得起開放-封閉原則的考驗
20.6.開放-封閉原則的相對性
- 實際上,讓程式保持完全封閉是不容易做到的
- 而且讓程式符合開放-封閉原則的代價是引入更多的抽象層次,更多的抽象有可能會增大代碼的復雜度
第二十一章 介面和面向介面編程
談到介面時通常涉及以下幾種含義
- 通過主動暴露的介面來通信,可以隱藏軟體系統內部的作業細節,這也是我們最熟悉的第一種介面含義
- 第二種介面是一些語言提供的關鍵字,比如Java的interface,interface關鍵字可以產生一個完全抽象的類
- 第三種介面即是我們談論的“面向介面編程”中的介面,介面的含義在這里體現得更為抽象
21.1.回到Java的抽象類
- 靜態型別語言通常設計為可以“向上轉型”,當給一個類變數賦值時,這個變數的型別既可以使用這個類本身,也可以使用這個類的超類
- 從程序上來看,“面向介面編程”其實是“面向超型別編程”
21.2.interface
- 雖然很多人在實際使用中刻意區分抽象類和interface,但使用interface實際上也是繼承的一種方式,叫做介面繼承
21.3.JavaScript語言是否需要抽象類和interface
- 抽象類和interface的作用主要都是以下兩點(1.通過向上轉型來隱藏物件的真正型別,以表現物件的多型性;2.約定類與類之間的一些契約行為)
- 很少人在JavaScript開發中去關心物件的真正型別
- 因為不需要進行向上轉型,介面在JavaScript中的最大作用就退化到了檢查代碼的規范性
21.4.用鴨子型別進行介面檢查
- 鴨子型別是動態語言面向物件設計中德一個重要概念,利用鴨子型別的思想,不必借助超型別的幫助,就能在動態型別語言中輕松地實作本章提到的設計原則:面向介面編程,而不是面向實作編程
21.5.用TypeScript撰寫基于interface的命令模式
- Typescript是微軟開發的一種編程語言,是JavaScript的一個超集,Typescript代碼最侄訓被編譯成原生的JavaScript代碼執行,通過Typescript,我們可以使用靜態語言的方式來撰寫JavaScript程式
第二十二章 代碼重構
- 模式和重構之間有著一種與生俱來的關系,從某種角度來看,設計模式的目的即使為許多重構行為提供目標
22.1.提煉函式
- 這是一種很常見的優化作業,這樣做的好處主要有以下幾點:1.避免出現超大函式;2.獨立出來的函式有助于代碼復用;3.獨立出來的函式更容易被覆寫;4.獨立出來的函式如果擁有一個良好的命名,本身就起到注釋的作用;
22.2.合并重復的條件片段
- 條件分支陳述句內部散布了一些重復的代碼,那么就有必要進行合并去重作業
22.3.把條件分支陳述句提煉成函式
- 復雜的條件分支陳述句是導致程式難以閱讀和理解的重要原因,而且容易導致一個龐大的函式
22.4.合理使用回圈
- 合理利用回圈不僅可以完成同樣的功能,還可以使代碼量更少
22.5.提前讓函式退出代替嵌套條件分支
- 關于“函式只有一個出口”,往往會有一些不同的看法
- 有一個常見的技巧,即在面對一個嵌套的if分支時,我們可以把外層if運算式進行反轉
22.6.傳遞物件引數代替過長的引數串列
- 有時候一個函式可能接受多個引數,而引數的數量越多,函式就越難以理解和使用
22.7盡量減少引數數量
- 在實際開發中,向函式傳遞引數不可避免,但我們應該盡量減少函式接收的引數數量
22.8.少用三目運算子
- 三目運算子性能高,代碼量少,這理由很難站住腳
- 相比損失的代碼可讀性和可維護性,三目運算子節省的代碼量也可以忽略不計
寫在后面
- pdf書籍、筆記思維導圖、隨書代碼打包下載地址:https://pan.baidu.com/s/17QLgFO2zcTBJNKCJz05Lsg(提取碼:j29j)
- 紙質書京東購買地址:https://u.jd.com/wlo6gr(推薦購買紙質書來學習)
轉載請註明出處,本文鏈接:https://www.uj5u.com/qiye/90152.html
標籤:JavaScript
上一篇:順序跳躍顯示陣列中的值
