重學 Java 設計模式:實戰橋接模式(多支付渠道「微信、支付寶」與多支付模式「刷臉、指紋」場景)

小傅哥發表於2020-06-05

image

作者:小傅哥
部落格:https://bugstack.cn - 編寫系列原創專題文章

沉澱、分享、成長,讓自己和他人都能有所收穫!?

一、前言

為什麼你的程式碼那麼多ifelse

同類的業務、同樣的功能,怎麼就你能寫出來那麼多ifelse。很多時候一些剛剛從校園進入企業的萌新,或者一部分從小公司跳槽到大企業的程式設計師,初次承接業務需求的時候,往往編碼還不成熟,經常一杆到底的寫需求。初次實現確實很快,但是後期維護和擴充套件就十分痛苦。因為一段程式碼的可讀性閱讀他後期的維護成本也就越高。

設計模式是可以幫助你改善程式碼

很多時候你寫出來的ifelse都是沒有考慮使用設計模式優化,就像;同類服務的不同介面適配包裝、同類物料不同組合的建造、多種獎品組合的營銷工廠等等。它們都可以讓你程式碼中原本使用if判斷的地方,變成一組組類和麵向物件的實現過程。

怎麼把設計模式和實際開發結合起來

多從實際場景思考,只找到程式碼優化的最佳點,不要可以想著設計模式的使用。就像你最開始看設計模式適合,因為沒有真實的場景模擬案例,都是一些畫圓形、方形,對新人或者理解能力還不到的夥伴來說很不友好。所以即使學了半天 ,但實際使用還是摸不著頭腦。

二、開發環境

  1. JDK 1.8
  2. Idea + Maven
  3. 涉及工程三個,可以通過關注公眾號bugstack蟲洞棧,回覆原始碼下載獲取(開啟獲取的連結,找到序號18)
工程 描述
itstack-demo-design-7-01 使用一坨程式碼實現業務需求
itstack-demo-design-7-02 通過設計模式優化改造程式碼,產生對比性從而學習

三、橋接模式介紹

橋接模式,圖片來自 refactoringguru.cn

橋接模式的主要作用就是通過將抽象部分與實現部分分離,把多種可匹配的使用進行組合。說白了核心實現也就是在A類中含有B類介面,通過建構函式傳遞B類的實現,這個B類就是設計的

那麼這樣的橋接模式,在我們平常的開發中有哪些場景

JDBC多種驅動程式的實現、同品牌型別的桌上型電腦和筆記本平板、業務實現中的多類介面同組過濾服務等。這些場景都比較適合使用橋接模式進行實現,因為在一些組合中如果有如果每一個類都實現不同的服務可能會出現笛卡爾積,而使用橋接模式就可以非常簡單。

四、案例場景模擬

場景模擬;多種支付和模式

隨著市場的競爭在支付服務行業出現了微信和支付寶還包括一些其他支付服務,但是對於商家來說並不希望改變使用者習慣。就像如果我的地攤只能使用微信或者只能使用支付寶付款,那麼就會讓我顧客傷心,雞蛋灌餅也賣不動了。

在這個時候就出現了第三方平臺,把市面上綜合佔據市場90%以上的支付服務都集中到自己平臺中,再把這樣的平臺提供給店鋪、超市、地攤使用,同時支援人臉、掃描、密碼多種方式。

我們這個案例就模擬一個這樣的第三方平臺來承接各個支付能力,同時使用自家的人臉讓使用者支付起來更加容易。那麼這裡就出現了多支付多模式的融合使用,如果給每一個支付都實現一次不同的模式,即使是繼承類也需要開發好多。而且隨著後面接入了更多的支付服務或者支付方式,就會呈爆炸似的擴充套件。

所以你現在可以思考一下這樣的場景該如何實現?

五、用一坨坨程式碼實現

產品經理說老闆要的需求,要儘快上,kpi你看著弄!

既然你逼我那就別怪我無情,還沒有我一個類寫不完的需求!反正寫完就完事了,拿完績效也要走了天天逼著寫需求,程式碼越來越亂心疼後面的兄弟3秒。

1. 工程結構

itstack-demo-design-7-01
└── src
    └── main
        └── java
            └── org.itstack.demo.design
                └── PayController.java
  • 只有一個類裡面都是ifelse,這個類實現了支付和模式的全部功能。

2. 程式碼實現

public class PayController {

    private Logger logger = LoggerFactory.getLogger(PayController.class);

    public boolean doPay(String uId, String tradeId, BigDecimal amount, int channelType, int modeType) {
        // 微信支付
        if (1 == channelType) {
            logger.info("模擬微信渠道支付劃賬開始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            if (1 == modeType) {
                logger.info("密碼支付,風控校驗環境安全");
            } else if (2 == modeType) {
                logger.info("人臉支付,風控校驗臉部識別");
            } else if (3 == modeType) {
                logger.info("指紋支付,風控校驗指紋資訊");
            }
        }
        // 支付寶支付
        else if (2 == channelType) {
            logger.info("模擬支付寶渠道支付劃賬開始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            if (1 == modeType) {
                logger.info("密碼支付,風控校驗環境安全");
            } else if (2 == modeType) {
                logger.info("人臉支付,風控校驗臉部識別");
            } else if (3 == modeType) {
                logger.info("指紋支付,風控校驗指紋資訊");
            }
        }
        return true;
    }

}
  • 上面的類提供了一個支付服務功能,通過提供的必要欄位;使用者ID交易ID金額渠道模式,來控制支付方式。
  • 以上的ifelse應該是最差的一種寫法,即使寫ifelse也是可以優化的方式去寫的。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_pay() {
    PayController pay = new PayController();
    System.out.println("\r\n模擬測試場景;微信支付、人臉方式。");
    pay.doPay("weixin_1092033111", "100000109893", new BigDecimal(100), 1, 2);
    
    System.out.println("\r\n模擬測試場景;支付寶支付、指紋方式。");
    pay.doPay("jlu19dlxo111","100000109894",new BigDecimal(100), 2, 3);
}
  • 以上分別測試了兩種不同的支付型別和支付模式;微信人臉支付、支付寶指紋支付

3.2 測試結果

模擬測試場景;微信支付、人臉方式。
23:05:59.152 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付劃賬開始。uId:weixin_1092033111 tradeId:100000109893 amount:100
23:05:59.155 [main] INFO  o.i.demo.design.pay.mode.PayCypher - 人臉支付,風控校驗臉部識別
23:05:59.155 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付風控校驗。uId:weixin_1092033111 tradeId:100000109893 security:true
23:05:59.155 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付劃賬成功。uId:weixin_1092033111 tradeId:100000109893 amount:100

模擬測試場景;支付寶支付、指紋方式。
23:05:59.156 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付劃賬開始。uId:jlu19dlxo111 tradeId:100000109894 amount:100
23:05:59.156 [main] INFO  o.i.demo.design.pay.mode.PayCypher - 指紋支付,風控校驗指紋資訊
23:05:59.156 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付風控校驗。uId:jlu19dlxo111 tradeId:100000109894 security:true
23:05:59.156 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付劃賬成功。uId:jlu19dlxo111 tradeId:100000109894 amount:100

Process finished with exit code 0
  • 從測試結果看已經滿足了我們的不同支付型別和支付模式的組合,但是這樣的程式碼在後面的維護以及擴充套件都會變得非常複雜。

六、橋接模式重構程式碼

接下來使用橋接模式來進行程式碼優化,也算是一次很小的重構。

從上面的ifelse方式實現來看,這是兩種不同型別的相互組合。那麼就可以把支付方式支付模式進行分離通過抽象類依賴實現類的方式進行橋接,通過這樣的拆分後支付與模式其實是可以單獨使用的,當需要組合時候只需要把模式傳遞給支付即可。

橋接模式的關鍵是選擇的橋接點拆分,是否可以找到這樣類似的相互組合,如果沒有就不必要非得使用橋接模式。

1. 工程結構

itstack-demo-design-7-02
└── src
    ├── main
    │   └── java
    │       └── org.itstack.demo.design.pay
    │           ├── channel
    │           │   ├── Pay.java
    │           │   ├── WxPay.java
    │           │   └── ZfbPay.java
    │           └── mode
    │               ├── IPayMode.java
    │               ├── PayCypher.java
    │               ├── PayFaceMode.java
    │               └── PayFingerprintMode.java
    └── test
         └── java
             └── org.itstack.demo.design.test
                 └── ApiTest.java

橋接模式模型結構

橋接模式模型結構

  • 左側Pay是一個抽象類,往下是它的兩個支付型別實現;微信支付、支付寶支付。
  • 右側IPayMode是一個介面,往下是它的兩個支付模型;刷臉支付、指紋支付。
  • 那麼,支付型別 × 支付模型 = 就可以得到相應的組合。

2. 程式碼實現

2.1 支付型別橋接抽象類

public abstract class Pay {

    protected Logger logger = LoggerFactory.getLogger(Pay.class);

    protected IPayMode payMode;

    public Pay(IPayMode payMode) {
        this.payMode = payMode;
    }

    public abstract String transfer(String uId, String tradeId, BigDecimal amount);

}
  • 在這個類中定義了支付方式的需要實現的劃賬介面:transfer,以及橋接介面;IPayMode,並在建構函式中使用者方自行選擇支付方式。
  • 如果沒有接觸過此類實現,可以重點關注 IPayMode payMode,這部分是橋接的核心。

2.2 兩個支付型別的實現

微信支付

public class WxPay extends Pay {

    public WxPay(IPayMode payMode) {
        super(payMode);
    }

    public String transfer(String uId, String tradeId, BigDecimal amount) {
        logger.info("模擬微信渠道支付劃賬開始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        boolean security = payMode.security(uId);
        logger.info("模擬微信渠道支付風控校驗。uId:{} tradeId:{} security:{}", uId, tradeId, security);
        if (!security) {
            logger.info("模擬微信渠道支付劃賬攔截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            return "0001";
        }
        logger.info("模擬微信渠道支付劃賬成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        return "0000";
    }

}

支付寶支付

public class ZfbPay extends Pay {

    public ZfbPay(IPayMode payMode) {
        super(payMode);
    }

    public String transfer(String uId, String tradeId, BigDecimal amount) {
        logger.info("模擬支付寶渠道支付劃賬開始。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        boolean security = payMode.security(uId);
        logger.info("模擬支付寶渠道支付風控校驗。uId:{} tradeId:{} security:{}", uId, tradeId, security);
        if (!security) {
            logger.info("模擬支付寶渠道支付劃賬攔截。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
            return "0001";
        }
        logger.info("模擬支付寶渠道支付劃賬成功。uId:{} tradeId:{} amount:{}", uId, tradeId, amount);
        return "0000";
    }

}
  • 這裡分別模擬了呼叫第三方的兩個支付渠道;微信、支付寶,當然作為支付綜合平臺可能不只是接了這兩個渠道,還會有其很跟多渠道。
  • 另外可以看到在支付的時候分別都呼叫了風控的介面進行驗證,也就是不同模式的支付(刷臉指紋),都需要過指定的風控,才能保證支付安全。

2.3 定義支付模式介面

public interface IPayMode {

    boolean security(String uId);

}
  • 任何一個支付模式;刷臉、指紋、密碼,都會過不同程度的安全風控,這裡定義一個安全校驗介面。

2.4 三種支付模式風控(刷臉、指紋、密碼)

刷臉

public class PayFaceMode implements IPayMode{

    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("人臉支付,風控校驗臉部識別");
        return true;
    }

}

指紋

public class PayFingerprintMode implements IPayMode{

    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("指紋支付,風控校驗指紋資訊");
        return true;
    }

}

密碼

public class PayCypher implements IPayMode{

    protected Logger logger = LoggerFactory.getLogger(PayCypher.class);

    public boolean security(String uId) {
        logger.info("密碼支付,風控校驗環境安全");
        return true;
    }

}
  • 在這裡實現了三種支付模式(刷臉、指紋、密碼)的風控校驗,在使用者選擇不同支付型別的時候,則會進行相應的風控攔截以此保障支付安全。

3. 測試驗證

3.1 編寫測試類

@Test
public void test_pay() {
    System.out.println("\r\n模擬測試場景;微信支付、人臉方式。");
    Pay wxPay = new WxPay(new PayFaceMode());
    wxPay.transfer("weixin_1092033111", "100000109893", new BigDecimal(100));

    System.out.println("\r\n模擬測試場景;支付寶支付、指紋方式。");
    Pay zfbPay = new ZfbPay(new PayFingerprintMode());
    zfbPay.transfer("jlu19dlxo111","100000109894",new BigDecimal(100));
}
  • 與上面的ifelse實現方式相比,這裡的呼叫方式變得整潔、乾淨、易使用;new WxPay(new PayFaceMode())new ZfbPay(new PayFingerprintMode())
  • 外部的使用介面的使用者不需要關心具體的實現,只按需選擇使用即可。

3.2 測試結果

模擬測試場景;微信支付、人臉方式。
23:14:40.911 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付劃賬開始。uId:weixin_1092033111 tradeId:100000109893 amount:100
23:14:40.914 [main] INFO  o.i.demo.design.pay.mode.PayCypher - 人臉支付,風控校驗臉部識別
23:14:40.914 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付風控校驗。uId:weixin_1092033111 tradeId:100000109893 security:true
23:14:40.915 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬微信渠道支付劃賬成功。uId:weixin_1092033111 tradeId:100000109893 amount:100

模擬測試場景;支付寶支付、指紋方式。
23:14:40.915 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付劃賬開始。uId:jlu19dlxo111 tradeId:100000109894 amount:100
23:14:40.915 [main] INFO  o.i.demo.design.pay.mode.PayCypher - 指紋支付,風控校驗指紋資訊
23:14:40.915 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付風控校驗。uId:jlu19dlxo111 tradeId:100000109894 security:true
23:14:40.915 [main] INFO  o.i.demo.design.pay.channel.Pay - 模擬支付寶渠道支付劃賬成功。uId:jlu19dlxo111 tradeId:100000109894 amount:100

Process finished with exit code 0
  • 從測試結果看內容是一樣的,但是整體的實現方式有了很大的變化。所以有時候不能只看結果,也要看看過程

七、總結

  • 通過模擬微信與支付寶兩個支付渠道在不同的支付模式下,刷臉指紋密碼,的組合從而體現了橋接模式的在這類場景中的合理運用。簡化了程式碼的開發,給後續的需求迭代增加了很好的擴充套件性。
  • 從橋接模式的實現形式來看滿足了單一職責和開閉原則,讓每一部分內容都很清晰易於維護和擴充,但如果我們是實現的高內聚的程式碼,那麼就會很複雜。所以在選擇重構程式碼的時候,需要考慮好整體的設計,否則選不到合理的設計模式,將會讓程式碼變得難以開發。
  • 任何一種設計模式的選擇和使用都應該遵頊符合場景為主,不要刻意使用。而且統一場景因為業務的複雜從而可能需要使用到多種設計模式的組合,才能將程式碼設計的更加合理。但這種經驗需要從實際的專案中學習經驗,並提不斷的運用。

八、推薦閱讀

相關文章