介面卡設計模式在JavaScript中非常有用,在處理跨瀏覽器相容問題、整合多個第三方SDK的呼叫,都可以看到它的身影。
其實在日常開發中,很多時候會不經意間寫出符合某種設計模式的程式碼,畢竟設計模式就是老前輩們總結提煉出來的一些能夠幫助提升開發效率的一些模版,源於日常的開發中。
而介面卡
其實在JavaScript
中應該是比較常見的一種了。
在維基百科中,關於介面卡模式的定義為:
在軟體工程中,介面卡模式是一種軟體設計模式,允許從另一個介面使用現有類的介面。它通常用於使現有的類與其他類一起工作,而無需修改其原始碼。
生活中的例子
在生活中最常見的就是電源插頭的介面卡了,世界各國的插座標準各不相同,如果需要根據各國的標準購買對應的電源插頭那未免太過於浪費錢財,如果說自己帶著插座,把人家牆敲碎,重新接線,也肯定是不現實的。
所以就會有插頭的介面卡,用來將某種插頭轉換成另一種插頭,在插座和你的電源之間做中轉的這個東西,就是介面卡。
在程式碼中的體現
而轉向到程式設計中,我個人是這樣理解的:
將那些你不願意看見的髒程式碼藏起來,你就可以說這是一個介面卡
接入多個第三方SDK
舉個日常開發中的例子,我們在做一個微信公眾號開發,裡邊用到了微信的支付模組,經過長時間的聯調,終於跑通了整個流程,正當你準備開心的打包上線程式碼的時候,得到了一個新需求:
我們需要接入支付寶公眾號的SDK,也要有支付的流程
為了複用程式碼,我們可能會在指令碼中寫下這樣的邏輯:
1 2 3 4 5 6 7 |
if (platform === 'wechat') { wx.pay(config) } else if (platform === 'alipay') { alipay.pay(config) } // 做一些後續的邏輯處理 |
但是一般來說,各廠的SDK所提供的介面呼叫方式都會多多少少有些區別,雖說有些時候文件可能用的是同一份,致敬友商。
所以針對上述的程式碼可能是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
// 並不是真實的引數配置,僅僅舉例使用 const config = { price: 10, goodsId: 1 } // 還有可能返回值的處理方式也不相同 if (platform === 'wechat') { config.appId = 'XXX' config.secretKey = 'XXX' wx.pay(config).then((err, data) => { if (err) // error // success }) } else if (platform === 'alipay') { config.token = 'XXX' alipay.pay(config, data => { // success }, err => { // error }) } |
就目前來說,程式碼介面還算是清晰,只要我們寫好註釋,這也不是一個太糟糕的程式碼。
但是生活總是充滿了意外,我們又接到了需求需要新增QQ的SDK、美團的SDK、小米的SDK,或者某些銀行的SDK。
此時你的程式碼可能是這樣的:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
switch (platform) { case 'wechat': // 微信的處理邏輯 break case 'QQ': // QQ的處理邏輯 break case 'alipay': // 支付寶的處理邏輯 break case 'meituan': // 美團的處理邏輯 break case 'xiaomi': // 小米的處理邏輯 break } |
這已經不是一些註釋能夠彌補的問題了,這樣的程式碼會變得越來越難維護,各種SDK千奇百怪的呼叫方式,如果其他人也要做類似的需求,還需要重新寫一遍這樣的程式碼,那肯定是很浪費資源的一件事兒。
所以為了保證我們業務邏輯的清晰,同時也為了避免後人重複的踩這個坑,我們會將它進行拆分出來作為一個公共的函式來存在:
找到其中某一個SDK的呼叫方式或者一個我們約定好的規則作為基準。
我們來告訴呼叫方,你要怎麼怎麼做,你能怎樣獲取返回資料,然後我們在函式內部進行這些各種骯髒的判斷:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
function pay ({ price, goodsId }) { return new Promise((resolve, reject) => { const config = {} switch (platform) { case 'wechat': // 微信的處理邏輯 config.price = price config.goodsId = goodsId config.appId = 'XXX' config.secretKey = 'XXX' wx.pay(config).then((err, data) => { if (err) return reject(err) resolve(data) }) break case 'QQ': // QQ的處理邏輯 config.price = price * 100 config.gid = goodsId config.appId = 'XXX' config.secretKey = 'XXX' config.success = resolve config.error = reject qq.pay(config) break case 'alipay': // 支付寶的處理邏輯 config.payment = price config.id = goodsId config.token = 'XXX' alipay.pay(config, resolve, reject) break } }) } |
這樣無論我們在什麼環境下,只要我們的介面卡支援,就可以按照我們約定好的通用規則進行呼叫,而具體執行的是什麼SDK,則是介面卡需要關心的事情:
1 2 3 4 5 |
// run anywhere await pay({ price: 10, goodsId: 1 }) |
對於SDK提供方,僅僅需要知道自己所需要的一些引數,然後按照自己的方式進行資料返回。
對於SDK呼叫房,僅僅需要我們約定好的通用的引數,以及按照約定的方式進行監聽回撥處理。
整合多個第三方SDK的任務就交由介面卡來做,然後我們將介面卡的程式碼壓縮,混淆,放在一個看不見的角落裡去,這樣的程式碼邏輯就會變得很清晰了 :)。
介面卡大致就是這樣的作用,有一點一定要明確,介面卡不是銀彈,__那些繁瑣的程式碼始終是存在的,只不過你在寫業務的時候看不到它罷了__,眼不見心不煩。
一些其他的例子
個人覺得,jQuery
中就有很多介面卡的例子,包括最基礎的$('selector').on
,這個不就是一個很明顯的介面卡模式麼?
一步步的進行降級,並且抹平了一些瀏覽器之間的差異,讓我們可以通過簡單的on
來進行在主流瀏覽器中進行事件監聽:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// 一個簡單的虛擬碼示例 function on (target, event, callback) { if (target.addEventListener) { // 標準的監聽事件方式 target.addEventListener(event, callback) } else if (target.attachEvent) { // IE低版本的監聽方式 target.attachEvent(event, callback) } else { // 一些低版本的瀏覽器監聽事件方式 target[`on${event}`] = callback } } |
或者在Node中的這樣的例子更是常見,因為早年是沒有Promise
的,所以大多數的非同步由callback
來完成,且有一個約定好的規則,Error-first callback
:
1 2 3 4 5 6 7 |
const fs = require('fs') fs.readFile('test.txt', (err, data) => { if (err) // 處理異常 // 處理正確結果 }) |
而我們的新功能都採用了async/await
的方式來進行,當我們需要複用一些老專案中的功能時,直接去修改老專案的程式碼肯定是不可行的。
這樣的相容處理需要呼叫方來做,所以為了讓邏輯程式碼看起來不是太混亂,我們可能會將這樣的回撥轉換為Promise
的版本方便我們進行呼叫:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
const fs = require('fs') function readFile (fileName) { return new Promise((resolve, reject) => { fs.readFile(fileName, (err, data) => { if (err) reject(err) resolve(data) }) }) } await readFile('test.txt') |
因為前邊也提到了,這種Error-first callback
是一個約定好的形式,所以我們可以很輕鬆的實現一個通用的介面卡:
1 2 3 4 5 6 7 8 9 |
function promisify(func) { return (...args) => new Promise((resolve, reject) => { func(...args, (err, data) => { if (err) reject(err) resolve(data) }) }) } |
然後在使用前進行對應的轉換就可以用我們預期的方式來執行程式碼:
1 2 3 4 5 |
const fs = require('fs') const readFile = promisify(fs.readFile) await readFile('test.txt') |
在Node8中,官方已經實現了類似這樣的工具函式:util.promisify
小結
個人觀點:所有的設計模式都不是憑空想象出來的,肯定是在開發的過程中,總結提煉出的一些高效的方法,這也就意味著,可能你並不需要在剛開始的時候就去生啃這些各種命名高大上的設計模式。
因為書中所說的場景可能並不全面,也可能針對某些語言,會存在更好的解決辦法,所以生搬硬套可能並不會寫出有靈魂的程式碼 :)
紙上得來終覺淺,絕知此事要躬行。 ———— 《冬夜讀書示子聿》,陸游