工作--如何封裝第三方服務?

業務開發中經常會對接某某第三方服務,因此會經常寫一些SDK供服務使用,一種比較好的做法就是使用命令模式封裝第三方服務,命令模式對於調用方來說簡潔明瞭,也正是封裝最根本的目的,便於調用方使用。

命令模式

定義

命令模式是一種行爲型模式,其會把具體的行爲封裝成一個命令Command,接着指定命令接收者Receiver,最後是在Invoker中執行命令。

其UML圖如下:(圖片來自命令模式

執行時序圖如下:

其中角色信息爲

  • Command: 抽象命令類
  • ConcreteCommand: 具體命令類
  • Invoker: 調用者,由使用方調用的執行命令的類。
  • Receiver: 接收者,真正執行命令的地方。通常與Invoker放在一起,邏輯複雜時可以使用這種方式分離依賴。

舉個例子,遙控器如果要用命令該怎麼實現?

首先分清角色,每一個按鈕是一個ConcreteCommand,遙控器本身是InvokerReceiver的角色,也就是負責命令的執行,當按下按鈕時Invoker接收到一個ConcreteCommand,然後根據內部邏輯觸發相應的紅外信號,完成整個鏈路執行。

優點

命令模式的優點主要如下:

  1. 命令模式對於使用方非常友好,使用方只需要關係命令與執行命令的執行器,而不需要關係具體的執行過程,就像遙控器一樣,使用方只需要知道遙控器的位置以及按鈕所產生的作用即可。
  2. 命令模式內部更加靈活,因爲對外的簡單,因此對內就可以很靈活的實現各種邏輯,比如命令的記錄,撤銷,命令隊列等等,這些優化在內部很容易實現,並且不會對外部造成影響。
  3. 命令模式很直觀,當實現新功能時,那麼需要做的事添加一個命令對象,編寫對應的執行邏輯,如果邏輯是一個通用的邏輯,那麼只需要添加完命令對象就實現了這個功能。(這一點在接下來的實戰中有體現)

廣義上來說前後端交互過程也算是一種命令模式,他們的交互是HTTP協議,也就是具體的命令對象,每次在命令中填充不同的參數,服務端會返回對應所需要的內容,而客戶端不需要理會服務端是如何處理的,只需要知道自己可以使用哪些命令(請求參數),這樣理解是不是更能體會到命令模式的本質。

如何封裝?

回到問題本身,如何使用命令模式簡化第三方請求?根據上文命令模式的簡要講解,可以發現命令模式與第三方服務的需求很像,第三方給我們提供接口,我們使用接口完成某一個功能,接口就是遙控器按鈕,第三方就是遙控器本身。

在第三方請求過程中往往有以下幾種角色:

  • Req:請求的參數包裝類,命令模式下的Command
  • Client:與使用者交互的類,其主要功能是控制整個與第三方交互的流程,給出使用者所期望的返回信息,往往全局單例。命令模式下的Invoker與Receiver
  • Resp:對應於請求的響應結果包裝類,命令模式下執行action()後得到的反饋。
  • HttpClient:負責與第三方交互,當然這裏只是類比,第三方提供的WebService等等也都是這個角色。

那麼封裝的目的是讓使用者更方便的使用,那麼從使用者的角度來觀察,使用者往往所期望的是我該填充哪些參數,我該怎麼調用請求,我該怎麼得到想要的返回值,轉換爲代碼可以理解爲以下兩行:

// ....context代表 上下文參數
Req req = new Req(context);
Resp resp = client.execute(req);
// .....業務處理

這就是最終封裝想要得到的結果。

說得再好也不如看代碼來的直接,筆者最近在對接一個雲賬戶結算平臺,因此使用該思路寫了一個示例項目,該示例項目更好的體現了到底該怎麼封裝,歡迎fork研究一下。 https://github.com/mrdear/yunzhanghu-sdk-example

示例項目封裝後,對於使用方只需要以下幾步即可輕鬆使用。

/**
  * 初始化調用者
  */
 private YunzhanghuClient client =
     new DefaultYunzhanghuClient("0123456", "sha256",
         "78f9b4fad3481fbce1df0b30eee58577", "123456788765432112345678", new WebUtils());

 @Test
 public void test() {
   // 構造銀行卡三要素驗證命令
   VerifyBankcardThreeFactorReq req = new VerifyBankcardThreeFactorReq();
   req.setCardNo("");
   req.setIdCard("");
   req.setRealName("");

   // 發送命令,並拿到返回值
   VerifyBankcardThreeFactorResp resp = client.execute(req);

   System.out.println(resp);
 }

其他問題

包結構建議

分包原則就是整個jar的package該如何組織,這裏參考alipay-sdk的分法。其中internal包是你不想被外部使用的一些類定義,比如轉爲此次對接定製的簽名類,定製的Http類等等,因爲Java沒有對應的module作用域,因此放在internal中算是一種約定。

com
└── alipay
    └── api
        ├── domain    // 一些實體類,主要爲request與response服務,構成其內部的屬性
        ├── internal    // 僅供sdk使用的內部工具,不希望外部使用
        ├── request    // 發送請求信息的包裝類
        └── response   // 封裝返回信息的包裝類
        └── commom.java ....      // 一些對外公共的類

下圖是筆者在對接雲賬戶結算平臺時所定義的包結構,可以作爲參考。

提高擴展靈活性建議

對於靈活性的提升需要使用依賴倒置原則,也就是關鍵點需要依賴對應的接口。比如在一個封裝過程中其HttpClient的實現往往就需要暴露出接口,便於使用方針對連接複用,參數調優等等。

舉個例子,筆者在對接第三方平臺時會把Http請求以接口形式暴露出去,如下所示:

public interface YunzhanghuWebClient {
  /**
   * 單純的get請求
   *
   * @return 結果
   */
  String yzh_Get(Map<String, Object> headers, String url, Map<String, Object> queryParam) throws Exception;

  /**
   * 單純的post請求
   *
   * @return 結果
   */
  String yzh_Post(Map<String, Object> headers, String url, Map<String, Object> formParam) throws Exception;

}

然後再封裝一個用於自己業務的WebUtils實現,其中會加入自己的業務邏輯。

public class YzhWebUtils {

  private YunzhanghuWebClient yunzhanghuWebClient;

  public YzhWebUtils(YunzhanghuWebClient yunzhanghuWebClient) {
    this.yunzhanghuWebClient = yunzhanghuWebClient;
  }

  public String doGet(String dealerId, String requestId, String url, Map<String, Object> queryParam) {
    try {
      Map<String, Object> header = new HashMap<>(2);
      header.put("dealer-id", dealerId);
      header.put("request-id", requestId);
      // 調用外部提供的http請求
      return yunzhanghuWebClient.yzh_Get(header, url, queryParam);
    } catch (Exception e) {
      logger.error("yunzhanghu get error, url is {}",url, e);
      return "";
    }
  }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章