若是說繼承是面向對象程序設計中承前啓後的特質,那麼接口就是海納百川的體現了。它們都是對數據和行爲的抽象,都是對性質和關係的歸納。只不過前者是縱向角度,然後者是橫向角度罷了。今天呢,我想從設計+語法角度說一說我感覺到的面向接口編程,從而初探設計與實現分離的模式。java
(本文所使用的面嚮對象語言爲java,相關代碼都是java代碼)算法
繼承的思想很容易理解,提取幾類相近數據中的公共部分爲基類,各個獨立部分在基類的基礎上作本身專屬的延伸。接口是抽象歸納輸入和輸出,而具體的實現交由具體實現接口的類來完成,從而達到同樣的接口不同的實現方式,使得管理統一化,實現多樣化。編程
概念扯了那麼多,仍是先上個例子吧,以課程中的出租車調度項目爲例。數組
該項目是模擬出租車運行,地圖爲 的正方形網格圖,每一個點的四個鄰接點不必定都連通,但保證整個圖是連通的,共有100輛出租車運行。
任意兩個結點之間有道路或者無道路。
出租車未接單時爲隨機遊走,即隨機向可行方向之一運動一步。接單以後選擇最短路徑運行。
看到這個版本一的需求,我當時的第一想法是什麼呢?出租車的行爲可歸納成兩種模式,隨機遊走和最短距離尋路,這兩種行爲都是要基於圖數據的,那麼就開個鄰接矩陣存儲圖,連通爲1不連通爲0,而後去作相應的實現便可。這樣聽起來彷佛沒什麼問題,徹底是基本操做嘛。可是,看到我說版本一,相信聰明的人必定猜到還有後續的版本。是的,變化的需求是程序設計者最大的敵人。版本二的需求改動以下:安全
新增道路打開關閉功能,連通的路能夠被關閉,關閉以後也能夠選擇再次打開,道路的狀態變成了三種,普通的出租車沒法經過關閉後的道路。新增VIP出租車,VIP出租車能夠經過被關閉的道路。
關閉道路?嗯…面對這樣的需求改動,以大一時的蠢習慣,那就開個flag數組,對於全部的連通邊初始化爲1,關閉道路就把對應的flag置爲0,每次訪問圖的同時訪問flag數組,想法是很美好的,但若是需求又變了呢,道路的狀態再次增長了呢,總不可能繼續開更多的flag吧。因此,應該先定義好各類狀態對應的值,經過一個鄰接矩陣來存儲對應的狀態值,使用一種數據結構來管理。爲簡化說明咱們就設置關閉道路代號爲2。網絡
數據存儲解決以後,就要作相應的邏輯處理了,兩種出租車,對於圖中的道路有不一樣的訪問權限,那是否是應該每一個出租車寫一個最短路徑搜索呢?又或者是給最短路搜索方法新傳入一個出租車類型參數,根據類型參數的不一樣選擇不一樣的分支去執行。這個時候,就輪到接口出場了。咱們來細細梳理邏輯,兩種出租車都是要搜索最短路徑,所使用的算法是相同的,惟一的不一樣點在於兩種出租車對於「連通」的判斷邏輯不一樣,其餘的代碼部分應該都是可複用的。被C語言腐蝕的我第一時間想到了什麼——函數指針,若是是使用C語言的話,咱們須要爲兩種出租車定義各自的連通性判斷函數,而後經過一個函數指針傳入最短路徑搜索函數(相似stdlib.h中的qsort函數同樣)。那麼在java中有殊途同歸之妙的就是使用接口來實現了,這正好符合面向接口編程的目的——實現不一樣,接口內容相同。因此咱們應該對於每種類型的出租車實現專屬的連通性判斷接口,在任何須要訪問圖的時候傳入該接口便可。下面附上代碼:數據結構
版本一:ide
// 普通出租車 if(inRange(u)&&graph[v][u]==1){ do something } // VIP出租車 if(inRange(u)&&graph[v][u]==1||graph[v][u]==2){ do something }
版本二:函數
if(inRange(u)&&inter.isConnected(v,u)){ do something }
試想你的代碼中有多處須要判斷連通性,你是選擇一處一處寫「graph[v][u]==XXX」,仍是選擇使用接口來管理呢?全部須要使用的地方使用同樣的模式,代碼可讀性高,複用性好。需求改變修改代碼時僅需修改或新增接口實現便可,不用在文件中各處修補,維護起來也方便。一樣將具體的實現邏輯做爲保存在類中,外部只能調用沒法修改,提升了安全性。學習
聽到這裏確定有人會想:明白了明白了趕忙代碼走起。不過先別急,在最基本的接口實現語法以外,還有一種更加高級的寫法——動態接口。
基本的接口實現是在類中實現重寫接口的具體實現,而後將其做爲該類的實例化對象的方法使用,說到這裏聰明的你必定發現了:這樣的作法傳參數的時候仍是必須將對象傳進去,咱們的目的是僅僅使用這一個方法,可是卻不得不將整個對象傳進去,這又擴大了對象的共享範圍,難道就不能像C語言同樣只是傳個方法進去嗎?答案是確定的,那就是動態接口。具體的代碼以下:
// 接口定義 public interface TaxiInterface { boolean isConnected(int x,int y); } // 接口在類中的實現 public TaxiInterface setTaxiInterface(){ return new TaxiInterface() { @Override public boolean isConnected(int x, int y) { int temp; temp=map.getGraphInfo(x,y); return temp==MapHelper.getOpen()||temp==MapHelper.getSamePoint(); } }; }
什麼?在方法裏重寫方法。是的你沒有看錯,隨時隨處重寫,哪裏有需求,哪裏就有接口的實現,很是的靈活。語法提煉一下,就是在新建接口對象的時候重寫其實現內容。對於咱們的問題,咱們對於每一個出租車類定義一個接口類型成員變量,而後經過set方法定義具體內容。在傳遞的時候使用相應的get方法,只是將此接口變量傳遞出去。外部的方法只能使用接口中定義的內容,關於該類的其餘全部內容都無權訪問。這種寫法既方便快捷,又保證了數據的隱私性和安全性。不過提醒一點,在沒有熟練掌握前不要亂用哦。
如今咱們跳躍到下一個問題。假如說如今你有成噸的類,都要實現某一個接口,而其中不少類對於接口中某個方法的實現是相同的,僅有少數不一樣。可是要修改的類太多了,按照傳統的路子,你得實現一個,而後不停的人肉ctrl+c,這種事光是想一下就以爲痛苦,程序猿明明是最擅長偷懶的人啊!不要擔憂,在Java 8 以後,接口擁有了default和static方法,拯救了這個問題。
咱們都知道接口中定義的抽象方法都是自帶public abstract屬性的,可是在方法聲明最前面加上default關鍵字,就能夠在接口中完成此方法的缺省實現,其餘實現該接口的類均可以通用該方法,有特殊需求類的單獨重寫就能夠,調用時直接經過方法名調用便可。舉個例子,Iterable.java源碼中的forEach遍歷方法就是這樣實現的,提供了一個通用的迭代方法。
default void forEach(Consumer<? super T> action) { Objects.requireNonNull(action); for (T t : this) { action.accept(t); } }
P.S. 有時間能夠多讀讀相關類庫源碼。我讀了部分TensorFlow源碼和java類庫源碼發現本身相關能力都有很大提升。
話說回來,那static又能幹什麼呢,這個就很相似類中的static修飾的方法,即不須要實現接口(implement XXX),使用接口名.方法名便可調用。
注意:一個接口中能夠有多個default和static修飾的方法,可是一旦使用這兩個關鍵字該方法就必須實現。
在初學OOP的時候,很使人苦惱的一點就是對象的傳遞,每一個類負責本身的數據,各個類實例化的對象之間又要共享數據傳遞信息,可是將整個對象傳來傳去的話又會形成數據隱私的暴露,說不定還會產生奇奇怪怪的錯誤,很難追溯緣由。那麼藉由以前使用接口傳遞連通性判斷方法的思路,咱們能不能變傳入對象爲傳入接口呢?
傳入對象,就可使用對象全部public的數據和方法(一個package的話固然default也能夠,不過一個package這麼反工程的事情可幹不得)。既然有可使用的可能性那麼就有了各類錯誤和安全問題的可能性,設計的初衷是交給它幾個方法的使用權,實際上卻搞成了一鍵root?可能有人會想開發時保證不亂調用方法便可,可是潛在的危險始終存在,咱們最好仍是將全部問題扼殺在搖籃裏。
若是咱們對於每一個類想傳遞的方法(信息交流內容)定義專門的接口,將接口做爲參數傳遞進去,則就是另外一番景象。因爲接口對象只能使用接口中定義的方法,至關於咱們已經定義好了條條框框,接收者只能使用規定的內容,配合每一個方法中的規約定義和異常檢測,這樣就將危險的可能性降到了零。同時,將一個接口做爲類之間的交流通道,信息傳遞必須按照接口定義的規則來,這是否是一瞬間感受有點像操做系統中的系統調用syscall或是網絡中的通訊協議?這一點很好的符合了「封閉-開放原則」,即對修改封閉,對擴展開放。任何類沒法修改傳遞信息的方式,而每一個類自身能夠任意的進行擴展,只要不影響傳遞信息的相關方法想怎麼擴展怎麼擴展,兩邊互不關心對方的發展,只要知足傳遞信息接口的要求便可。
面向接口編程說究竟是將設計和實現分離,這是其核心。同時,這裏的「接口」並非單單指java中的interface或是其餘語言的相似語法,這是一種思想,先規約設計,再具體實現。
設計規約(JSF)
以前的三次做業我並無出現JSF問題,多是因爲主要是使用天然語言書寫表意比較完整,那麼對於一樣的內容,如何使用邏輯語言達到完備的表達效果同時又十分簡潔呢,我以爲一個辦法是經過閱讀好的寫法來學習,下面上幾個例子:
1.
private synchronized int selectTaxi(){ /** * @REQUIRES: None * @MODIFIES: None * @EFFECTS: \exist taxi in response;taxi has the highest credit;select taxi; * if taxi.num>1;select the shortest current distance to passenger one; * if not \exist taxi in response, return -1; * @THREAD_EFFECTS: \locked() */ }
該方法是從response隊列中選擇出信用最高的出租車,若是有多輛車信用相同選擇到乘客距離最近的一輛,返回其對應的索引值,若是隊列爲空返回-1.(其實應該拋出異常更好,這是出租車代碼中最古老的部分了還沒來得及重構)。能夠看到我以前的寫法主要使用了天然語言輔以部分邏輯語言,那麼改進版以下:
private synchronized int selectTaxi(){ /** * @REQUIRES: None * @MODIFIES: None * @EFFECTS: (response.size == 0) ==> \result = -1;
* (response.size > 0) ==> ((\result = index) ==>
* (selected_taxi.index == index) && (\all taxi response.contain(taxi);taxi.credit <= selected_taxi.credit;) &&
* (\all taxi taxi.credit == selected_taxi.credit; taxi.distance >= selected_taxi.distance;))
* @THREAD_EFFECTS: \locked() */ }
2.
public boolean runPermission(Point src, Point now, Point dst){ /** * @REQUIRES: src.inRange && now.inRange && dst.inRange && src is neighbour of now && now is neighbour of dst; * @MODIFIES: None; * @EFFECTS: \result = whether the current light state permits taxi passing through; */ }
該方法的做用是在路口判斷是否能夠直接通行或是等待紅綠燈,初始版是標準的「白話文」,那麼改進版以下:
public boolean runPermission(Point src, Point now, Point dst){ /** * @REQUIRES: traffic.state in {0,1,2} && graph.contain(src) && graph.contain(now) && graph.contain(dst) && traffic.locate == now
* \exist edge in edges;edge.begin == src && edge.end == now &&
* \exist edge in edges;edge.begin == now && edge.end == dst; * @MODIFIES: None; * @EFFECTS: (\result == true) ==> trace.contain(src,now,dst) && trace.runDirection obey traffic.state;
* (\result == false) ==> trace.contain(src,now,dst) && trace.runDirection disobey traffic.state; */ }
首先,對於邏輯語言JSF的書寫,不要從主觀角度去描述行爲,誰作了什麼誰擁有什麼,而是要從客觀出發,描述客觀對象的性質和狀態,相似於數學定義的方法,狀態A就能對應到反饋A1,狀態B就能對應到反饋B1。在書寫格式角度正確以後,則應該着重注意邏輯的嚴密性,單單的A==>B是很弱的,這僅僅描述了事物的一部分。完整來看,應該是A==>B,B==>A,!A==>!B,!B==>!A四個環節的關係,固然通常爲了簡化僅使用前兩個,可是咱們考慮問題就應該多想一點,要作到正確條件必定致使正確結果,不正確條件必定致使不正確結果,要使整個規約定義是完備的,這樣才能使設計毫無漏洞。
規約定義配合以前說的面向接口思想,將設計和實現分離開來,用接口來設計功能,用規約定義來規範每一個接口和方法的內容,保證每次運行使用給定的正確的方法,每一個方法的執行符合規格定義的內容,對於符合前置條件的輸入進行對應的後置條件處理,對不符合的作相應的異常檢查和處理。當作完這些設計工做,完成了規約層的事,這時候再開始實現層的工做就會事半功倍!這樣,才叫程序設計。