Javascript-設計模式_裝飾者模式
在js函式開發中,想要為現有函式新增與現有功能無關的新功能時,按普通思路肯定是在現有函式中新增新功能的程式碼。這並不能說錯,但因為函式中的這兩塊程式碼其實並無關聯,後期維護成本會明顯增大,也會造成函式臃腫
比較好的辦法就是採用裝飾器模式。在保持現有函式及其內部程式碼實現不變的前提下,將新功能函式分離開來,然後將其通過與現有函式包裝起來一起執行
裝飾者模式
描述
裝飾者模式是一種為函式或類增添特性的技術,它可以讓我們在不修改原來物件的基礎上,為其增添新的能力和行為。它本質上也是一個函式(在javascipt中,類也只是函式的語法糖),裝飾者模式將一個物件嵌入另一個物件之中,實際上相當於這個物件被另一個物件包裝起來,形成一條包裝鏈
繼承的缺陷
"封裝,繼承,多型"是面向物件的三大特點,開發過程中,我們都不希望某些類一開始就特別龐大,一次性包含太多職責,這時就不得不去面對三大特性的權衡
在傳統的面嚮物件語言中,給物件新增功能常常使用繼承的方式,但是繼承的方式並不靈活,還會帶來許多問題:
-
會導致超類和子類之間存在強耦合性,當超類改變時,子類也會隨之改變
-
繼承這種功能複用方式通常被稱為“白箱複用“,“白箱”是相對可見性而言的,在繼承方式中,超類的內部細節是對子類可見的,繼承常常被認為破壞了封裝性
-
在完成一些功能複用的同時,有可能創建出大量的子類,使子類的數量呈爆炸性增長。比如現在有4種型號的相機,我們為每種相機都定義了一個單獨的類。現在要給每種相機都裝上鏡頭、濾鏡和掛帶這3種配件。如果使用繼承的方式來給每種相機建立子類,則需要4×3=12個子類
但是如果把鏡頭、濾鏡和掛帶這些物件動態組合到相機上面,則只需要額外增加3個類
這種給物件動態地增加職責的方式稱為裝飾者(decorator)模式。裝飾者模式能夠在不改變物件自身的基礎上,在程式執行期間給物件動態地新增職責。跟繼承相比,裝飾者是一種更輕便靈活的做法,這是一種“即用即付”的方式,比如天冷了就多穿一件外套,需要飛行時就在頭上插一支竹蜻蜓
一系列問題
不改動原函式的情況下,給該函式新增些額外的功能
儲存原引用
window.onload = function() { console.log('onload') }; var _onload = window.onload || function() {} window.onload = function() { _onload() console.log('_onload') }
-
必須維護中間變數
-
可能遇到this被劫持問題 在window.onload的例子中沒有這個煩惱,是因為呼叫普通函式_onload時,this也指向window,跟呼叫window.onload時一樣
this被劫持
const _getElementById = document.getElementById document.getElementById = function(id) { console.log('_getElementById') return _getElementById(id) } return _getElementById(id) // 報錯“Uncaught TypeError: Illegal invocation”
面向物件的裝飾者
一個打飛機遊戲,飛機一開始火力是普通子彈,升2級後是導彈,3級後是核導彈,用裝飾者的寫法是:
class Plane{ fire(){ console.log('傳送普通子彈') } } class Missibe{ constructor(plane) { this.plane = plane } fire(){ this.plane.fire() console.log('傳送導彈') } } class Atom{ constructor(plane) { this.plane = plane } fire(){ this.plane.fire() console.log('傳送核導彈') } } const plane = new Plane() const missibePlane = new Missibe(plane) const atom = new Atom(missibePlane) atom.fire()
AOP裝飾函式
Aop又叫面向切面程式設計,其中“通知”是切面的具體實現,分為before(前置通知)、after(後置通知)、around(環繞通知),在js中AOP是一個被嚴重忽視的技術點
/** * 讓新新增的函式在原函式之前執行(前置裝飾) */ Function.prototype.before = function(beforefn) { var _self = this return function() { // 新函式接收的引數會被原封不動的傳入原函式 beforefn.apply(this, arguments) return _self.apply(this, arguments) } } /** * 讓新新增的函式在原函式之後執行(後置裝飾) */ Function.prototype.after = function(afterfn) { const _self = this return function() { const ret = _self.apply(this, arguments) afterfn.apply(this, arguments) return ret } } document.getElementById = document.getElementById.before(function() { console.log('getElementById') })
通過Function.prototype.apply來動態傳入正確的this,保證了函式在被裝飾之後,this不會被劫持
用AOP裝飾函式的技巧在實際開發中非常有用。不論是業務程式碼的編寫,還是在框架層面,我們都可以把行為依照職責分成粒度更細的函式,隨後通過裝飾把它們合併到一起,這有助於我們編寫一個鬆耦合和高複用性的系統
-
用aop動態改變函式的引數
假設在使用一串模板方法,在非同步請求前,你給before傳入一段函式
let fnAjax=function(param){ console.log(arguments) console.log(param) } fnAjax=fnAjax.before(function(param){ param.username='djtao' }) fnAjax({password:123456}) // Object {password: 123456, username: "djtao"}
可以用它來為你的ajax請求預處理引數。比如帶個token什麼的
-
外掛式的表單校驗
假設點選提交時都要校驗,如果把validate寫在submit處理函式中,這段程式碼沒有任何可複用性,可以改寫一下before
Function.prototype.before = function(fn) { const _this = this return function() { if(fn.apply(this,arguments) === false) return return _this.apply(this, arguments) } } const validate = function(params) { const {username, password} = params if(username === '') { alert('使用者名稱不得為空') return false } if(password === '') { alert('密碼不得為空') return false } } login = login.before(validate)
在這段程式碼中,校驗輸入和提交表單的程式碼完全分離開來,它們不再有任何耦合關係,formSubmit = formSubmit.before(validata)這句程式碼,如同把校驗規則動態接在formSubmit函式之前,validata成為一個即插即用的函式,它甚至可以被寫成配置檔案的形式,這有利於我們分開維護這兩個函式。再利用策略模式稍加改造,我們就可以把這些校驗規則都寫成外掛的形式,用在不同的專案當中
-
統計資料上報
現有一個按鈕,功能已經開發完成
const btnFn = () => { //... } document.querySelector('#btn', btnFn)
這時需要加多一個點選統計功能,如果直接在btnFn繼續寫統計,是不合適的。這是兩個層面的功能,卻被耦合在一個函式裡,這時可以考慮aop分離
-
const notePoint = ()=> { // 統計邏輯 } btnFn = btnFn.after(notePoint)
和代理模式的區別
裝飾者模式和代理模式的結構看起來非常相像,這兩種模式都描述了怎樣為物件提供一定程度上的間接引用,它們的實現部分都保留了對另外一個物件的引用,並且向那個物件傳送請求
代理模式和裝飾者模式最重要的區別在於它們的意圖和設計目的。代理模式的目的是,當直接訪問本體不方便或者不符合需要時,為這個本體提供一個替代者。本體定義了關鍵功能,而代理提供或拒絕對它的訪問,或者在訪問本體之前做一些額外的事情。裝飾者模式的作用就是為物件動態加入行為
換句話說,代理模式強調一種關係(Proxy與它的實體之間的關係),這種關係在一開始就可以被確定。而裝飾者模式用於一開始不能確定物件的全部功能時,代理模式通常只有一層代理本體的引用,而裝飾者模式經常會形成一條長長的裝飾鏈
在虛擬代理實現圖片預載入的例子中,本體負責設定img節點的src,代理則提供了預載入的功能,這看起來也是“加入行為”的一種方式,但這種加入行為的方式和裝飾者模式的偏重點是不一樣的。裝飾者模式是實實在在的為物件增加新的職責和行為,而代理做的事情還是跟本體一樣,最終都是設定src。但代理可以加入一些“聰明”的功能,比如在圖片真正載入好之前,先使用一張佔位的loading圖片反饋給客戶
小結
使用裝飾者模式可以讓我們為原有的類和函式增添新的功能,並且不會修改原有的程式碼或者改變其呼叫方式,因此不會對原有的系統帶來副作用,也不用擔心原來系統會因為它而失靈或者不相容