”基于接口而非實現(xiàn)編程“這個原則非常重要扔仓,是一種非常有效的提高代碼質(zhì)量的手段咖耘,在平時的開發(fā)中特別經(jīng)常被用到鲤看。
如何解讀原則中的“接口”二字义桂?
理解這條接口的原則慷吊,關(guān)鍵在與理解“接口”兩個字。從本質(zhì)上看堰酿,“接口”就是一組協(xié)議和“約定”疾宏,是功能提供者提供給使用者的一個功能列表〈ゴ矗“接口”在不同的應用場景下會有不同的解讀坎藐,比如服務端與客戶端之間的“接口”,類庫提供的“接口”哼绑,甚至是一組通信的協(xié)議都可以叫作“接口”岩馍。剛剛對“接口”的理解,都比較偏上層抖韩、偏抽象蛀恩,與實際的寫代碼離得有點遠。如果落實到具體的編碼茂浮,“基于接口而非實現(xiàn)編程”這條原則中的“接口”双谆,可以理解為編程語言中的接口或者抽象類。
這條原則能非常有效地提高代碼質(zhì)量席揽,之所以這么說佃乘,那是因為,應用這條原則驹尼,可以將接口和實現(xiàn)相分離趣避,封裝不穩(wěn)定的實現(xiàn),暴露穩(wěn)定的接口新翎。上游系統(tǒng)面向接口而非實現(xiàn)編程程帕,不依賴不穩(wěn)定的實現(xiàn)細節(jié)住练,這樣當實現(xiàn)發(fā)生變化的時候,上游系統(tǒng)的代碼基本上不需要做改動愁拭,以此來降低耦合性讲逛,提高擴展性。
越抽象岭埠、越頂層盏混、越脫離具體某一實現(xiàn)的設計,越能提高代碼的靈活性惜论,越能應對未來的需求變化许赃。好的代碼設計,不僅能應對當下的需求馆类,而且在將來需求發(fā)生變化的時候混聊,仍然能夠在不破壞原有代碼設計的情況下靈活應對。而抽象就是提高代碼擴展性乾巧、靈活性句喜、可維護性最有效的手段之一。
如何將這條原則應用到實戰(zhàn)中沟于?
對于這條原則咳胃,我們結(jié)合一個具體的實戰(zhàn)案例來進一步講解一下。
假設我們的系統(tǒng)中有很多涉及圖片處理和存儲的業(yè)務邏輯旷太。圖片經(jīng)過處理之后被上傳到阿里云上展懈。為了代碼復用,我們封裝了圖片存儲相關(guān)的代碼邏輯泳秀,提供了一個統(tǒng)一的 AliyunImageStore 類标沪,供整個系統(tǒng)來使用榄攀。具體的代碼實現(xiàn)如下所示:
public class AliyunImageStore {
//...省略屬性嗜傅、構(gòu)造函數(shù)等...
public void createBucketIfNotExisting(String bucketName) {
// ...創(chuàng)建bucket代碼邏輯...
// ...失敗會拋出異常..
}
public String generateAccessToken() {
// ...根據(jù)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";
//...省略其他無關(guān)代碼...
public void process() {
Image image = ...; //處理圖片,并封裝為Image對象
AliyunImageStore imageStore = new AliyunImageStore(/*省略參數(shù)*/);
imageStore.createBucketIfNotExisting(BUCKET_NAME);
String accessToken = imageStore.generateAccessToken();
imagestore.uploadToAliyun(image, BUCKET_NAME, accessToken);
}
}
整個上傳流程包含三個步驟:創(chuàng)建 bucket(你可以簡單理解為存儲目錄)檩赢、生成 access token 訪問憑證吕嘀、攜帶 access token 上傳圖片到指定的 bucket 中。代碼實現(xiàn)非常簡單贞瞒,類中的幾個方法定義得都很干凈偶房,用起來也很清晰,乍看起來沒有太大問題军浆,完全能滿足我們將圖片存儲在阿里云的業(yè)務需求棕洋。
不過,軟件開發(fā)中唯一不變的就是變化乒融。過了一段時間后掰盘,我們自建了私有云摄悯,不再將圖片存儲到阿里云了,而是將圖片存儲到自建私有云上愧捕。為了滿足這樣一個需求的變化奢驯,我們該如何修改代碼呢?
新的 PrivateImageStore 類需要設計實現(xiàn)哪些方法次绘,才能在盡量最小化代碼修改的情況下瘪阁,替換掉 AliyunImageStore 類呢?這就要求我們必須將 AliyunImageStore 類中所定義的所有 public 方法邮偎,在 PrivateImageStore 類中都逐一定義并重新實現(xiàn)一遍管跺。而這樣做就會存在一些問題,總結(jié)了下面兩點:
1钢猛、首先:AliyunImageStore 類中有些函數(shù)命名暴露了實現(xiàn)細節(jié)伙菜。
2、其次:將圖片存儲到阿里云的流程命迈,跟存儲到私有云的流程贩绕,可能并不是完全一致的。
那這兩個問題該如何解決呢壶愤?解決這個問題的根本方法就是淑倾,在編寫代碼的時候,要遵從“基于接口而非實現(xiàn)編程”的原則征椒,具體來講娇哆,我們需要做到下面這 3 點:
1.函數(shù)的命名不能暴露任何實現(xiàn)細節(jié)。比如勃救,前面提到的 uploadToAliyun() 就不符合要求碍讨,應該改為去掉 aliyun 這樣的字眼,改為更加抽象的命名方式蒙秒,比如:upload()勃黍。
2.封裝具體的實現(xiàn)細節(jié)。比如晕讲,跟阿里云相關(guān)的特殊上傳(或下載)流程不應該暴露給調(diào)用者覆获。我們對上傳(或下載)流程進行封裝,對外提供一個包裹所有上傳(或下載)細節(jié)的方法瓢省,給調(diào)用者使用弄息。為實現(xiàn)類定義抽象的接口。
3.具體的實現(xiàn)類都依賴統(tǒng)一的接口定義勤婚,遵從一致的上傳功能協(xié)議摹量。使用者依賴接口,而不是具體的實現(xiàn)類來編程。
總結(jié)一下:
我們在做軟件開發(fā)的時候缨称,一定要有抽象意識废亭、封裝意識、接口意識具钥。在定義接口的時候豆村,不要暴露任何實現(xiàn)細節(jié)。接口的定義只表明做什么骂删,而不是怎么做掌动。而且,在設計接口的時候宁玫,我們要多思考一下粗恢,這樣的接口設計是否足夠通用,是否能夠做到在替換具體的接口實現(xiàn)的時候欧瘪,不需要任何接口定義的改動眷射。
是否需要為每個類定義接口?
前面我們也提到佛掖,這條原則的設計初衷是妖碉,將接口和實現(xiàn)相分離,封裝不穩(wěn)定的實現(xiàn)芥被,暴露穩(wěn)定的接口欧宜。上游系統(tǒng)面向接口而非實現(xiàn)編程,不依賴不穩(wěn)定的實現(xiàn)細節(jié)拴魄,這樣當實現(xiàn)發(fā)生變化的時候冗茸,上游系統(tǒng)的代碼基本上不需要做改動,以此來降低代碼間的耦合性匹中,提高代碼的擴展性夏漱。從這個設計初衷上來看,如果在我們的業(yè)務場景中顶捷,某個功能只有一種實現(xiàn)方式挂绰,未來也不可能被其他實現(xiàn)方式替換,那我們就沒有必要為其設計接口焊切,也沒有必要基于接口編程扮授,直接使用實現(xiàn)類就可以了芳室。除此之外专肪,越是不穩(wěn)定的系統(tǒng),我們越是要在代碼的擴展性堪侯、維護性上下功夫嚎尤。相反,如果某個系統(tǒng)特別穩(wěn)定伍宦,在開發(fā)完之后芽死,基本上不需要做維護乏梁,那我們就沒有必要為其擴展性,投入不必要的開發(fā)時間关贵。
重點回顧
1.“基于接口而非實現(xiàn)編程”遇骑,這條原則的另一個表述方式,是“基于抽象而非實現(xiàn)編程”揖曾。后者的表述方式其實更能體現(xiàn)這條原則的設計初衷落萎。我們在做軟件開發(fā)的時候,一定要有抽象意識炭剪、封裝意識练链、接口意識。越抽象奴拦、越頂層媒鼓、越脫離具體某一實現(xiàn)的設計,越能提高代碼的靈活性错妖、擴展性绿鸣、可維護性。
- 我們在定義接口的時候暂氯,一方面枚驻,命名要足夠通用,不能包含跟具體實現(xiàn)相關(guān)的字眼株旷;另一方面再登,與特定實現(xiàn)有關(guān)的方法不要定義在接口中。
3.“基于接口而非實現(xiàn)編程”這條原則晾剖,不僅僅可以指導非常細節(jié)的編程開發(fā)锉矢,還能指導更加上層的架構(gòu)設計、系統(tǒng)設計等齿尽。比如沽损,服務端與客戶端之間的“接口”設計、類庫的“接口”設計循头。