設計模式之美 - 52 | 門面模式:如何設計合理的接口粒度以兼顧接口的易用性和通用性?

這系列相關博客,參考 設計模式之美

設計模式之美 - 52 | 門面模式:如何設計合理的接口粒度以兼顧接口的易用性和通用性?

前面我們已經學習了代理模式、橋接模式、裝飾器模式、適配器模式,這 4 種結構型設計模式。今天,我們再來學習一種新的結構型模式:門面模式。門面模式原理和實現都特別簡單,應用場景也比較明確,主要在接口設計方面使用。

如果你平時的工作涉及接口開發,不知道你有沒有遇到關於接口粒度的問題呢?

爲了保證接口的可複用性(或者叫通用性),我們需要將接口儘量設計得細粒度一點,職責單一一點。但是,如果接口的粒度過小,在接口的使用者開發一個業務功能時,就會導致需要調用 n 多細粒度的接口才能完成。調用者肯定會抱怨接口不好用。

相反,如果接口粒度設計得太大,一個接口返回 n 多數據,要做 n 多事情,就會導致接口不夠通用、可複用性不好。接口不可複用,那針對不同的調用者的業務需求,我們就需要開發不同的接口來滿足,這就會導致系統的接口無限膨脹。

那如何來解決接口的可複用性(通用性)和易用性之間的矛盾呢?通過今天對於門面模式的學習,我想你心中會有答案。話不多說,讓我們正式開始今天的學習吧!

門面模式的原理與實現

門面模式,也叫外觀模式,英文全稱是 Facade Design Pattern。在 GoF 的《設計模式》一書中,門面模式是這樣定義的:

Provide a unified interface to a set of interfaces in a subsystem. Facade Pattern
defines a higher-level interface that makes the subsystem easier to use.

翻譯成中文就是:門面模式爲子系統提供一組統一的接口,定義一組高層接口讓子系統更易用。

這個定義很簡潔,我再進一步解釋一下。

假設有一個系統 A,提供了 a、b、c、d 四個接口。系統 B 完成某個業務功能,需要調用A 系統的 a、b、d 接口。利用門面模式,我們提供一個包裹 a、b、d 接口調用的門面接口x,給系統 B 直接使用。

不知道你會不會有這樣的疑問,讓系統 B 直接調用 a、b、d 感覺沒有太大問題呀,爲什麼還要提供一個包裹 a、b、d 的接口 x 呢?關於這個問題,我通過一個具體的例子來解釋一下。

假設我們剛剛提到的系統 A 是一個後端服務器,系統 B 是 App 客戶端。App 客戶端通過後端服務器提供的接口來獲取數據。我們知道,App 和服務器之間是通過移動網絡通信的,網絡通信耗時比較多,爲了提高 App 的響應速度,我們要儘量減少 App 與服務器之間的網絡通信次數。

假設,完成某個業務功能(比如顯示某個頁面信息)需要“依次”調用 a、b、d 三個接口,因自身業務的特點,不支持併發調用這三個接口。

如果我們現在發現 App 客戶端的響應速度比較慢,排查之後發現,是因爲過多的接口調用過多的網絡通信。針對這種情況,我們就可以利用門面模式,讓後端服務器提供一個包裹a、b、d 三個接口調用的接口 x。App 客戶端調用一次接口 x,來獲取到所有想要的數據,將網絡通信的次數從 3 次減少到 1 次,也就提高了 App 的響應速度。

這裏舉的例子只是應用門面模式的其中一個意圖,也就是解決性能問題。實際上,不同的應用場景下,使用門面模式的意圖也不同。接下來,我們就來看一下門面模式的各種應用場景。

門面模式的應用場景舉例

在 GoF 給出的定義中提到,“門面模式讓子系統更加易用”,實際上,它除了解決易用性問題之外,還能解決其他很多方面的問題。關於這一點,我總結羅列了 3 個常用的應用場景,你可以參考一下,舉一反三地借鑑到自己的項目中。

除此之外,我還要強調一下,門面模式定義中的“子系統(subsystem)”也可以有多種理解方式。它既可以是一個完整的系統,也可以是更細粒度的類或者模塊。關於這一點,在下面的講解中也會有體現。

1. 解決易用性問題

門面模式可以用來封裝系統的底層實現,隱藏系統的複雜性,提供一組更加簡單易用、更高層的接口。比如,Linux 系統調用函數就可以看作一種“門面”。它是 Linux 操作系統暴露給開發者的一組“特殊”的編程接口,它封裝了底層更基礎的 Linux 內核調用。再比如,Linux 的 Shell 命令,實際上也可以看作一種門面模式的應用。它繼續封裝系統調用,提供更加友好、簡單的命令,讓我們可以直接通過執行命令來跟操作系統交互。

我們前面也多次講過,設計原則、思想、模式很多都是相通的,是同一個道理不同角度的表述。實際上,從隱藏實現複雜性,提供更易用接口這個意圖來看,門面模式有點類似之前講到的迪米特法則(最少知識原則)和接口隔離原則:兩個有交互的系統,只暴露有限的必要的接口。除此之外,門面模式還有點類似之前提到封裝、抽象的設計思想,提供更抽象的接口,封裝底層實現細節。

2. 解決性能問題

關於利用門面模式解決性能問題這一點,剛剛我們已經講過了。我們通過將多個接口調用替換爲一個門面接口調用,減少網絡通信成本,提高 App 客戶端的響應速度。所以,關於這點,我就不再舉例說明了。我們來討論一下這樣一個問題:從代碼實現的角度來看,該如何組織門面接口和非門面接口?

如果門面接口不多,我們完全可以將它跟非門面接口放到一塊,也不需要特殊標記,當作普通接口來用即可。如果門面接口很多,我們可以在已有的接口之上,再重新抽象出一層,專門放置門面接口,從類、包的命名上跟原來的接口層做區分。如果門面接口特別多,並且很多都是跨多個子系統的,我們可以將門面接口放到一個新的子系統中。

3. 解決分佈式事務問題

關於利用門面模式來解決分佈式事務問題,我們通過一個例子來解釋一下。

在一個金融系統中,有兩個業務領域模型,用戶和錢包。這兩個業務領域模型都對外暴露了一系列接口,比如用戶的增刪改查接口、錢包的增刪改查接口。假設有這樣一個業務場景:在用戶註冊的時候,我們不僅會創建用戶(在數據庫 User 表中),還會給用戶創建一個錢包(在數據庫的 Wallet 表中)。

對於這樣一個簡單的業務需求,我們可以通過依次調用用戶的創建接口和錢包的創建接口來完成。但是,用戶註冊需要支持事務,也就是說,創建用戶和錢包的兩個操作,要麼都成功,要麼都失敗,不能一個成功、一個失敗。

要支持兩個接口調用在一個事務中執行,是比較難實現的,這涉及分佈式事務問題。雖然我們可以通過引入分佈式事務框架或者事後補償的機制來解決,但代碼實現都比較複雜。而最簡單的解決方案是,利用數據庫事務或者 Spring 框架提供的事務(如果是 Java 語言的話),在一個事務中,執行創建用戶和創建錢包這兩個 SQL 操作。這就要求兩個 SQL 操作要在一個接口中完成,所以,我們可以借鑑門面模式的思想,再設計一個包裹這兩個操作的新接口,讓新接口在一個事務中執行兩個 SQL 操作。

重點回顧

好了,今天的內容到此就講完了。我們來一塊總結回顧一下,你需要重點掌握的內容。

我們知道,類、模塊、系統之間的“通信”,一般都是通過接口調用來完成的。接口設計的好壞,直接影響到類、模塊、系統是否好用。所以,我們要多花點心思在接口設計上。我經常說,完成接口設計,就相當於完成了一半的開發任務。只要接口設計得好,那代碼就差不到哪裏去。

接口粒度設計得太大,太小都不好。太大會導致接口不可複用,太小會導致接口不易用。在實際的開發中,接口的可複用性和易用性需要“微妙”的權衡。針對這個問題,我的一個基本的處理原則是,儘量保持接口的可複用性,但針對特殊情況,允許提供冗餘的門面接口,來提供更易用的接口。

門面模式除了解決接口易用性問題之外,我們今天還講到了其他 2 個應用場景,用它來解決性能問題和分佈式事務問題。

課堂討論

  1. 適配器模式和門面模式的共同點是,將不好用的接口適配成好用的接口。你可以試着總結一下它們的區別嗎?
  2. 在你過往的項目開發中,有沒有遇到過不合理的接口需求?又或者,有沒有遇到過非常難用的接口?可以留言“吐槽”一下。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章