「基於接口而非實現編程」這條原則的英文描述是:「Program to an interface, not an implementation」。咱們理解這條原則的時候,千萬不要一開始就與具體的編程語言掛鉤,侷限在編程語言的「接口」語法中(好比 Java 中的 interface 接口語法)。這條原則最先出現於 1994 年 GoF 的《設計模式》這本書,它先於不少編程語言而誕生(好比 Java 語言),是一條比較抽象、泛化的設計思想。java
這條原則能很是有效地提升代碼質量,之因此這麼說,那是由於,應用這條原則,能夠::編程
上游系統面向接口而非實現編程,不依賴不穩定的實現細節,這樣當實現發生變化的時候,上游系統的代碼基本上不須要作改動,以此來下降耦合性,提升擴展性。設計模式
實際上,「基於接口而非實現編程」這條原則的另外一個表述方式,是「基於抽象而非實現編程」。後者的表述方式其實更能體現這條原則的設計初衷。在軟件開發中,最大的挑戰之一就是需求的不斷變化,這也是考驗代碼設計好壞的一個標準。越抽象、越頂層、越脫離具體某一實現的設計,越能提升代碼的靈活性,越能應對將來的需求變化。好的代碼設計,不只能應對當下的需求,並且在未來需求發生變化的時候,仍然可以在不破壞原有代碼設計的狀況下靈活應對。而抽象就是提升代碼擴展性、靈活性、可維護性最有效的手段之一。架構
假設咱們的系統中有不少涉及圖片處理和存儲的業務邏輯。圖片通過處理以後被上傳到阿里雲上。爲了代碼複用,咱們封裝了圖片存儲相關的代碼邏輯,提供了一個統一的 AliyunImageStore 類,供整個系統來使用。具體的代碼實現以下所示:編程語言
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 類對象。這樣的修改聽起來並不複雜,只是簡單替換而已,對整個代碼的改動並不大。不過,咱們常常說,「細節是魔鬼」。這句話在軟件開發中特別適用。實際上,剛剛的設計實現方式,就隱藏了不少容易出問題的「魔鬼細節」,咱們一塊來看看都有哪些。url
新的 PrivateImageStore 類須要設計實現哪些方法,才能在儘可能最小化代碼修改的狀況下,替換掉 AliyunImageStore 類呢?這就要求咱們必須將 AliyunImageStore 類中所定義的全部 public 方法,在 PrivateImageStore 類中都逐必定義並從新實現一遍。而這樣作就會存在一些問題,我總結了下面兩點。架構設計
首先,AliyunImageStore 類中有些函數命名暴露了實現細節,好比,uploadToAliyun() 和 downloadFromAliyun()。若是開發這個功能的同事沒有接口意識、抽象思惟,那這種暴露實現細節的命名方式就不足爲奇了,畢竟最初咱們只考慮將圖片存儲在阿里雲上。而咱們把這種包含「aliyun」字眼的方法,照抄到 PrivateImageStore 類中,顯然是不合適的。若是咱們在新類中從新命名 uploadToAliyun()、downloadFromAliyun() 這些方法,那就意味着,咱們要修改項目中全部使用到這兩個方法的代碼,代碼修改量可能就會很大。設計
其次,將圖片存儲到阿里雲的流程,跟存儲到私有云的流程,可能並非徹底一致的。好比,阿里雲的圖片上傳和下載的過程當中,須要生產 access token,而私有云不須要 access token。一方面,AliyunImageStore 中定義的 generateAccessToken() 方法不能照抄到 PrivateImageStore 中;另外一方面,咱們在使用 AliyunImageStore 上傳、下載圖片的時候,代碼中用到了 generateAccessToken() 方法,若是要改成私有云的上傳下載流程,這些代碼都須要作調整。
那這兩個問題該如何解決呢?解決這個問題的根本方法就是,在編寫代碼的時候,要聽從「基於接口而非實現編程」的原則,具體來說,咱們須要作到下面這 3 點。
咱們按照這個思路,把代碼重構一下。重構後的代碼以下所示:
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() 方法。
總結一下,咱們在作軟件開發的時候,必定要有抽象意識、封裝意識、接口意識。在定義接口的時候,不要暴露任何實現細節。接口的定義只代表作什麼,而不是怎麼作。並且,在設計接口的時候,咱們要多思考一下,這樣的接口設計是否足夠通用,是否可以作到在替換具體的接口實現的時候,不須要任何接口定義的改動。
「基於接口而非實現編程」,這條原則的另外一個表述方式,是「基於抽象而非實現編程」。後者的表述方式其實更能體現這條原則的設計初衷。咱們在作軟件開發的時候,必定要有抽象意識、封裝意識、接口意識。越抽象、越頂層、越脫離具體某一實現的設計,越能提升代碼的靈活性、擴展性、可維護性。
咱們在定義接口的時候,一方面,命名要足夠通用,不能包含跟具體實現相關的字眼;另外一方面,與特定實現有關的方法不要定義在接口中。
「基於接口而非實現編程」這條原則,不只僅能夠指導很是細節的編程開發,還能指導更加上層的架構設計、系統設計等。好比,服務端與客戶端之間的「接口」設計、類庫的「接口」設計。