面向介面程式設計原理與實踐
阿新 • • 發佈:2020-06-03
## 面向介面程式設計原理
“基於介面而非實現程式設計”這條原則的英文描述是:“Program to an interface, not an implementation”。我們理解這條原則的時候,千萬不要一開始就與具體的程式語言掛鉤,侷限在程式語言的“介面”語法中(比如 Java 中的 interface 介面語法)。這條原則最早出現於 1994 年 GoF 的《設計模式》這本書,它先於很多程式語言而誕生(比如 Java 語言),是一條比較抽象、泛化的設計思想。
這條原則能非常有效地提高程式碼質量,之所以這麼說,那是因為,應用這條原則,可以::
- **將介面和實現相分離**
- **封裝不穩定的實現**
- **暴露穩定的介面**
上游系統面向介面而非實現程式設計,不依賴不穩定的實現細節,這樣當實現發生變化的時候,上游系統的程式碼基本上不需要做改動,以此來**降低耦合性,提高擴充套件性**。
實際上,“基於介面而非實現程式設計”這條原則的另一個表述方式,是“基於抽象而非實現程式設計”。後者的表述方式其實更能體現這條原則的設計初衷。在軟體開發中,最大的挑戰之一就是需求的不斷變化,這也是考驗程式碼設計好壞的一個標準。越抽象、越頂層、越脫離具體某一實現的設計,越能提高程式碼的靈活性,越能應對未來的需求變化。好的程式碼設計,不僅能應對當下的需求,而且在將來需求發生變化的時候,仍然能夠在不破壞原有程式碼設計的情況下靈活應對。**而抽象就是提高程式碼擴充套件性、靈活性、可維護性最有效的手段之一**。
## 面向介面程式設計實踐
假設我們的系統中有很多涉及圖片處理和儲存的業務邏輯。圖片經過處理之後被上傳到阿里雲上。為了程式碼複用,我們封裝了圖片儲存相關的程式碼邏輯,提供了一個統一的 AliyunImageStore 類,供整個系統來使用。具體的程式碼實現如下所示:
```java
public class AliyunImageStore {
//...省略屬性、建構函式等...
public void createBucketIfNotExisting(String bucketName) {
// ...建立bucket程式碼邏輯...
// ...失敗會丟擲異常..
}
public String generateAccessToken() {
// ...根據accesskey/secrectkey等生成access token
}
public String uploadToAliyun(Image image, String bucketName, String accessToken) {
//...上傳圖片到阿里雲...
//...返回圖片儲存在阿里雲上的地址(url)...
}
public Image downloadFromAliyun(String url, String accessToken) {
//...從阿里雲下載圖片...
}
}
// AliyunImageStore類的使用舉例
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
//...省略其他無關程式碼...
public void process() {
Image image = ...; //處理圖片,並封裝為Image物件
AliyunImageStore imageStore = new AliyunImageStore(/*省略引數*/);
imageStore.createBucketIfNotExisting(BUCKET_NAME);
String accessToken = imageStore.generateAccessToken();
imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
}
}
```
整個上傳流程包含三個步驟:建立 bucket(你可以簡單理解為儲存目錄)、生成 access token 訪問憑證、攜帶 access token 上傳圖片到指定的 bucket 中。程式碼實現非常簡單,類中的幾個方法定義得都很乾淨,用起來也很清晰,乍看起來沒有太大問題,完全能滿足我們將圖片儲存在阿里雲的業務需求。
不過,軟體開發中唯一不變的就是變化。過了一段時間後,我們自建了私有云,不再將圖片儲存到阿里雲了,而是將圖片儲存到自建私有云上。為了滿足這樣一個需求的變化,我們該如何修改程式碼呢?
我們需要重新設計實現一個儲存圖片到私有云的 PrivateImageStore 類,並用它替換掉專案中所有的 AliyunImageStore 類物件。這樣的修改聽起來並不複雜,只是簡單替換而已,對整個程式碼的改動並不大。不過,我們經常說,“細節是魔鬼”。這句話在軟體開發中特別適用。實際上,剛剛的設計實現方式,就隱藏了很多容易出問題的“魔鬼細節”,我們一塊來看看都有哪些。
新的 PrivateImageStore 類需要設計實現哪些方法,才能在儘量最小化程式碼修改的情況下,替換掉 AliyunImageStore 類呢?這就要求我們必須將 AliyunImageStore 類中所定義的所有 public 方法,在 PrivateImageStore 類中都逐一定義並重新實現一遍。而這樣做就會存在一些問題,我總結了下面兩點。
首先,AliyunImageStore 類中有些函式命名暴露了實現細節,比如,uploadToAliyun() 和 downloadFromAliyun()。如果開發這個功能的同事沒有介面意識、抽象思維,那這種暴露實現細節的命名方式就不足為奇了,畢竟最初我們只考慮將圖片儲存在阿里雲上。而我們把這種包含“aliyun”字眼的方法,照抄到 PrivateImageStore 類中,顯然是不合適的。如果我們在新類中重新命名 uploadToAliyun()、downloadFromAliyun() 這些方法,那就意味著,我們要修改專案中所有使用到這兩個方法的程式碼,程式碼修改量可能就會很大。
其次,將圖片儲存到阿里雲的流程,跟儲存到私有云的流程,可能並不是完全一致的。比如,阿里雲的圖片上傳和下載的過程中,需要生產 access token,而私有云不需要 access token。一方面,AliyunImageStore 中定義的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另一方面,我們在使用 AliyunImageStore 上傳、下載圖片的時候,程式碼中用到了 generateAccessToken() 方法,如果要改為私有云的上傳下載流程,這些程式碼都需要做調整。
那這兩個問題該如何解決呢?解決這個問題的根本方法就是,在編寫程式碼的時候,要遵從“基於介面而非實現程式設計”的原則,具體來講,我們需要做到下面這 3 點。
1. 函式的命名不能暴露任何實現細節。比如,前面提到的 uploadToAliyun() 就不符合要求,應該改為去掉 aliyun 這樣的字眼,改為更加抽象的命名方式,比如:upload()。
1. 封裝具體的實現細節。比如,跟阿里雲相關的特殊上傳(或下載)流程不應該暴露給呼叫者。我們對上傳(或下載)流程進行封裝,對外提供一個包裹所有上傳(或下載)細節的方法,給呼叫者使用。
1. 為實現類定義抽象的介面。具體的實現類都依賴統一的介面定義,遵從一致的上傳功能協議。使用者依賴介面,而不是具體的實現類來程式設計。
我們按照這個思路,把程式碼重構一下。重構後的程式碼如下所示:
```java
public interface ImageStore {
String upload(Image image, String bucketName);
Image download(String url);
}
public class AliyunImageStore implements ImageStore {
//...省略屬性、建構函式等...
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
String accessToken = generateAccessToken();
//...上傳圖片到阿里雲...
//...返回圖片在阿里雲上的地址(url)...
}
public Image download(String url) {
String accessToken = generateAccessToken();
//...從阿里雲下載圖片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...建立bucket...
// ...失敗會丟擲異常..
}
private String generateAccessToken() {
// ...根據accesskey/secrectkey等生成access token
}
}
// 上傳下載流程改變:私有云不需要支援access token
public class PrivateImageStore implements ImageStore {
public String upload(Image image, String bucketName) {
createBucketIfNotExisting(bucketName);
//...上傳圖片到私有云...
//...返回圖片的url...
}
public Image download(String url) {
//...從私有云下載圖片...
}
private void createBucketIfNotExisting(String bucketName) {
// ...建立bucket...
// ...失敗會丟擲異常..
}
}
// ImageStore的使用舉例
public class ImageProcessingJob {
private static final String BUCKET_NAME = "ai_images_bucket";
//...省略其他無關程式碼...
public void process() {
Image image = ...;//處理圖片,並封裝為Image物件
ImageStore imageStore = new PrivateImageStore(...);
imagestore.upload(image, BUCKET_NAME);
}
}
```
除此之外,很多人在定義介面的時候,希望通過實現類來反推介面的定義。先把實現類寫好,然後看實現類中有哪些方法,照抄到介面定義中。如果按照這種思考方式,就有可能導致介面定義不夠抽象,依賴具體的實現。這樣的介面設計就沒有意義了。不過,如果你覺得這種思考方式更加順暢,那也沒問題,只是將實現類的方法搬移到介面定義中的時候,要有選擇性的搬移,不要將跟具體實現相關的方法搬移到介面中,比如 AliyunImageStore 中的 generateAccessToken() 方法。
總結一下,我們在做軟體開發的時候,一定要有抽象意識、封裝意識、介面意識。**在定義介面的時候,不要暴露任何實現細節。介面的定義只表明做什麼,而不是怎麼做**。而且,在設計介面的時候,我們要多思考一下,這樣的介面設計是否足夠通用,是否能夠做到在替換具體的介面實現的時候,不需要任何介面定義的改動。
## 面向介面程式設計總結
1. “基於介面而非實現程式設計”,這條原則的另一個表述方式,是“基於抽象而非實現程式設計”。後者的表述方式其實更能體現這條原則的設計初衷。我們在做軟體開發的時候,一定要有抽象意識、封裝意識、介面意識。越抽象、越頂層、越脫離具體某一實現的設計,越能提高程式碼的靈活性、擴充套件性、可維護性。
1. 我們在定義介面的時候,一方面,命名要足夠通用,不能包含跟具體實現相關的字眼;另一方面,與特定實現有關的方法不要定義在介面中。
1. “基於介面而非實現程式設計”這條原則,不僅僅可以指導非常細節的程式設計開發,還能指導更加上層的架構設計、系統設計等。比如,服務端與客戶端之間的“介面”設計、類庫的“介面”