一、背景
基本上每一個需要對接支付公司的項目都有這樣一個煩惱:不同的支付公司給到你的支付費率是不一樣的,微信支付寶收的費率是0.6%(不知道后面有沒有降低),A支付公司費率的是0.5%,B支付公司費率是0.48%。。。此外還有活動等
大部分公司一開始只對接一家或兩家支付公司,后面的可能會由於一些原因呢,慢慢的對接多幾家支付公司,降低一下成本,提高收益。從代碼的角度上看,一開始的支付代碼可能是這樣的,eg:
//支付數據 $pay = [ 'money' => 10.00, 'xx' => 'xx' ]; if (微信支付) { $wechat = new WeChat(); $result = $weichat->pay($pay); } else if (支付寶支付) { $alipay = new Alipay(); $result = $alipay->pay($pay); } if ($result) { //支付成功 } else { //支付失敗 } ...
這樣寫代碼呢,如果項目從頭到尾只會對接一家或者兩家支付公司的話,且支付的接口只有一兩個,理論上不會有太大的問題。
但是如果多接了一家支付公司的話,基本上會在這個基礎上進行修改,比如加多一個 if 判斷等,eg:
//支付數據 $pay = [ 'money' => 10.00, 'xx' => 'xx' ]; if (微信支付) { if (微信官方支付) { $wechat = new WeChat(); $result = $weichat->pay($pay); } else if (A微信支付) { $aWechat = new AWeChat(); $result = $aWeichat->pay($pay); } else if (B微信支付) { $bWechat = new BWeChat(); $result = $bWeichat->pay($pay); } } else if (支付寶支付) { if (支付寶官方支付) { $alipay = new Alipay(); $result = $alipay->pay($pay); } else if (A支付寶支付) { $aAlipay = new AAlipay(); $result = $aAlipay->pay($pay); } else if (B支付寶支付) { $bAlipay = new BAlipay(); $result = $bAlipay->pay($pay); } } if ($result) { //支付成功 } else { //支付失敗 } ...
紅色部分為改動的部分,這樣的話,每多對接一家支付公司,就多得一個判斷,而且每一個涉及到支付的接口都得改動。這樣造成的后果就是代碼越來越難看,當接口需要改動的時候,任何涉及到支付的接口都要進行修改,維護成本高,容錯率低。
這種情況下,往往一開始沒有什么特別大的問題,當后面用戶多起來的時候,老板想賺更多錢對接更多的支付公司的時候(盈利模式說白了就是中間商賺差價),問題就慢慢出現了。
下面的篇幅筆者將從聚合支付的角度來分析如何優化上述這種支付代碼,以及簡單介紹下用到設計模式。
二、聚合支付
前段時間,筆者有幸接觸到一個支付改造的項目,一開始呢,項目只對接了微信和支付寶的官方支付,代碼結構比較簡單,支付的接口也只有一個。
后面由於項目的發展,開始對接很多的第三方支付,其中有一個版本需要對接三四個支付方,而且當時用到支付的業務又不少,支付接口有十來個,如果按照以前的方式搞的話,基本上每一個支付接口都要改動,工作量巨大,還要算上支付配置那一塊,這里基本上可以算是噩夢了,此外還要處理統計類的業務,比如統計某個商家某段時間某個支付渠道的收入情況等等,別忘了還要處理退款和查詢業務。
小結一下,每多一個支付渠道,改動的地方包括:支付接口、支付配置、退款、統計業務。
多一個支付渠道就要改動這么多地方,如果是在之前的代碼上加多個 if 進行處理的話,工作量大不說,基本上是復制粘貼,沒有任何技術含量,就是簡單的邏輯判斷,調用接口,處理返回結果,然后就沒有了。如果思想只停留在這,那非常危險!必須給自己找點苗頭,這樣才有搞頭,完成任務的同時又提高自己的技術水平,而不是重復同樣的事情。
結論已經很清晰了,必須對原有的支付進行改造,這里簡單介紹下聚合支付,啥是聚合支付呢,說白了就是一個項目接入了多個支付渠道,而且能夠使用任意一個渠道進行支付、退款等操作,而且任何渠道之間沒有任何關系,彼此不會互相干擾。
到這里呢,我們來簡單梳理一下聚合支付的業務:
-
需要對接多個支付渠道
-
所有的支付能夠兼容任意渠道
-
所有的退款能夠兼容任何渠道
-
任何渠道都能需要獨立進行配置
-
任何渠道都有統計功能
-
渠道之間能夠無縫進行切換(比如某個渠道奔潰了,能夠切換到其他渠道)
如果想滿足上面的功能,又不影響原有的業務的情況下,就需要將原有的支付模塊獨立抽離開來,單獨作為一個服務,也就是聚合支付,凡是項目里面的任何支付、退款、查詢、統計等都要通過聚合支付來處理。
然后,要怎么設計呢?考慮到由於涉及到多個支付渠道,首先工廠模式跑不了,一個支付渠道可以看成一個工廠;此外單例模式也要用到,支付的配置是固定的,每必要重復 new 創建;還要適配器模式,由於不同的支付渠道使用的參數或者返回結果都可能不一樣,適配器就派上用場了;此外還有策略模式,比如你要根據什么依據創建支付渠道進行支付。
下面的篇幅主要結合支付方面的業務來簡單介紹這幾種設計模式,以及它們的有點和部分偽代碼實現
三、工廠模式
工廠模式:這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。在工廠模式中,我們在創建對象時不會對客戶端暴露創建邏輯,並且是通過使用一個共同的接口來指向新創建的對象。
好處:
-
一個調用者想創建一個對象,只要知道其名稱就可以了
-
擴展性高,如果想增加一個產品,只要擴展一個工廠類就可以
-
屏蔽產品的具體實現,調用者只關心產品的接口
為什么用工廠模式呢?由於支付渠道很多,而且不同的支付渠道其實是有共性的,比如:支付、回調、查詢、退款、退款查詢等。把這些共同的東西抽出來當成一個 IPayChannel 接口,任何支付渠道都需要實現這個接口。
接着上面的聚合支付,使用工廠模式可以將所有的支付渠道抽出一個模型出來,把它們的共同點全部封裝成一個接口,不同的支付渠道都需要實現這個接口。eg:
說明:unifiedOrder是統一下單入口、parsePayNotify是處理回調的、orderQuery訂單查詢、closeOrder訂單關閉、refund退款、refundQuery退款查詢
下面看一下改造后的代碼結構:
//接口 interface IPayChannel { public function unifiedOrder(params); public function parsePayNotify(params); public function orderQuery(params); public function closeOrder(params); public function refund(params); public function refundQuery(params); public function facepayAuthinfo(params); } //AChannel class AChannel implements IPayChannel { public function unifiedOrder(params) { ...do things } public function parsePayNotify(params) { ...do things } public function orderQuery(params) { ...do things } public function closeOrder(params) { ...do things } public function refund(params) { ...do things } public function refundQuery(params) { ...do things } public function facepayAuthinfo(params) { ...do things } }
如果你想使用 AChannel 進行支付的話,就直接創建一個對象,調對應的方法即可,不同通道的操作也是如此。
四、單例模式
單例模式:這種類型的設計模式屬於創建型模式,它提供了一種創建對象的最佳方式。這種模式涉及到一個單一的類,該類負責創建自己的對象,同時確保只有單個對象被創建。這個類提供了一種訪問其唯一的對象的方式,可以直接訪問,不需要實例化該類的對象。
好處:
-
在內存里只有一個實例,減少了內存的開銷,尤其是頻繁的創建和銷毀實例
-
避免對資源的多重占用
對接過支付的人都知道,調用任何一個接口都需要用特定的支付配置,比如公鑰、私鑰、計算簽名的key、請求接口、回調驗簽的key等,這種配置類型的參數,我們可以抽出來當成一個單例類,避免每一次支付都頻繁創建和銷毀,減少內存開支。
比如可以把某個支付渠道 A 的配置參數抽出來,當成一個 AChannelConfig,eg:
下面看下改造后的代碼,eg:
class AChannelConfig { private static $instance = null; private $setting = []; private function __construct(){} private function __clone(){} public static function getInstance() { if (self::$instance == null) { self::$instance = new self(); } return self::$instance; } public function set($index, $value) { $this->setting[$index] = $value; } public function get($index) { return $this->setting[$index]; } }
支付配置單例類是在創建工廠的時候順帶創建的,一種渠道只需創建一個單例類,由於構造函數是私有的,單例類是無法通過 new 來創建的,一定程度上減少了資源的開銷。
五、適配器模式
適配器模式:作為兩個不兼容的接口之間的橋梁。這種類型的設計模式屬於結構型模式,它結合了兩個獨立接口的功能。這種模式涉及到一個單一的類,該類負責加入獨立的或不兼容的接口功能。
好處:
-
可以讓任何兩個沒有關聯的類一起運行
-
提高了類的復用
-
增加了類的透明度
-
靈活性好
當你對接的支付渠道多了之后,你會發現,不同的公司的請求參數和返回參數都是不一樣的,這種情況下,你就得需要一個適配器,把它們的數據格式進行適配,轉化成你自己的格式,后面不管你對接多少個渠道,對你項目來說,只需要處理適配器返回的數據格式就行了,不需要管第三方返回的格式;支付也是類型,你只管把參數傳給適配器,由適配器逆向適配即可。
創建一個 PayChannelAdapter,來對支付參數以及返回結果進行適配,這個跟單例類一樣需要結合工廠類進行處理,eg:
改造后的代碼如下,eg:
class PayChannelAdapter { public function pay(params) { ...do things } public function refund(params) { ...do things } public function close(params) { ...do things } public function query(params) { ...do things } }
說白了,適配器主要是把返回結果很請求參數進行統一而已,比如A渠道返回的支付金額是amount,B渠道返回的是money,有了適配器之后,你可以將這些統一成amount,這樣一來,不管對接什么支付渠道,你僅需要處理適配器返回的結果即可。
至於支付和退款,這個就有點意思,用的是反向適配器,比如我們想支付,傳了一組固定的支付參數,適配器會根據你不同的支付渠道生成對應的參數,再調指定的支付渠道。
六、策略模式
策略模式:一個類的行為或其算法可以在運行時更改。這種類型的設計模式屬於行為型模式。在策略模式中,我們創建表示各種策略的對象和一個行為隨着策略對象改變而改變的 context 對象。策略對象改變 context 對象的執行算法。
好處:
-
算法可以自由切換
-
避免使用多重條件判斷
-
擴展性良好
為什么使用策略模式呢?先看下我們的需求,其中有一點說,渠道之間能夠無縫切換,就是為了避免某個渠道突然出問題不能用了,為了不影響商家正常營業,只能臨時幫商家切換到備用的渠道,盡可能減少商家的損失。這種情況主要是對接的支付公司不是特別靠譜導致的,想想也是,規模達到一定程度的公司,費率也都差不多。一般只有新的渠道為了搶占市場份額才會推出低費率,吸引更多的使用者來使用。
前面說了工廠模式 ,不同的支付渠道對應一個工廠,現在問題來了,要怎么創建工廠,誰來創建工廠,這就得用到策略模式了。
創建一個 PayChannelStrategy 類,用來創建對應的支付通道,結合工廠模式效果更佳,eg:
其中,支付的時候根據一些賬號之類的數據判斷商家用的默認支付渠道是什么,根據這個依據創建不同工廠並返回;查詢的時候則根據已有的訂單ID,查詢訂單下單時用的哪個渠道,返回對應的工廠類。
改造后的代碼結構如下,eg:
class PayChannelStrategy { public function createPayChannel($params) { 這里根據一些特定的依據創建並返回工廠類 } public function createPayChannelByOrderId($params) { 這里根據一些特定的依據創建並返回工廠類 } }
有了策略模式,我們只需要根據商家的信息以及要支付的數據,就可以輕輕松松拿到對應的工廠類,再調用對應的方法完成支付、查詢、退款等操作。如同你去某個地方旅行一樣,坐汽車、坐高鐵、坐飛機等都是一種策略,每個商家也對應一種支付策略,不同的策略之間往往是獨立的,不會相互影響。
七、優缺點
上述的聚合支付設計,用到了:工廠模式、單例模式、適配器模式、策略模式
對於使用者來說,你僅僅只需要拿到商家的一些數據,用 PayChannelStrategy 創建支付通道,拿到對應的通道類后,就可以進行你的支付、查詢、退款等操作,最后根據返回的結果進行判斷就行了。
從代碼的角度上看,原本亂七八糟的代碼現在被划分為幾塊,一個是通道塊(工廠類),一個是配置塊(單例類),一個是適配器塊,以及策略塊,如下圖:
先說下優點:
-
代碼結構清晰,不同的類處理不同的業務
-
易於擴展,新增一個渠道so easy
-
屏蔽了具體的實現,只需要關心接口即可
-
靈活性高,算法可以自由切換,避免多重判斷
-
兼容性高
當然也是有缺點的:
-
由於使用了工廠模式,每多一個渠道就要新增一個文件,當工廠多了就不是什么好事了
-
適配器過多的使用也會造成一定的復雜性,一個類盡量少用或者使用一個適配器
-
策略類多了,也會有膨脹的問題
八、總結
在我們平時的開發過程中,不僅要避免重復性的工作,也不能一味為了做需求而敲代碼,要學會思考,盡可能結合我們學到的數據結構、算法、設計模式,畢竟這些解決方案是眾多軟件開發人員經過相當長的一段時間的試驗和錯誤總結出來的。不僅能在一定程度上提高我們的開發效率,也能夠讓我們鞏固基礎知識,也能提高團隊的效率,一個項目往往不是一個人開發的,我們寫代碼的同時也要關注團隊開發效率上的問題。
此外呢,很多的設計模式在實際的開發過程中不一定是單一使用的,而是綜合使用的。
不過也要注意一些問題,算法、數據結構、設計模式等從根據實際業務出發,不能盲目使用,也不是用的越多越好,對你的業務有幫助才是最好的解決辦法。使用得當的話,會使代碼干凈整潔易於維護,減少大量重復的判斷和使用,讓代碼更加易於維護和拓展。
參考:設計模式