1. 程式人生 > >設計與實現分離——面向接口編程(OO博客第三彈)

設計與實現分離——面向接口編程(OO博客第三彈)

none @override flag ava 三種 time rri 初學 fault

如果說繼承是面向對象程序設計中承前啟後的特質,那麽接口就是海納百川的體現了。它們都是對數據和行為的抽象,都是對性質和關系的概括。只不過前者是縱向角度,而後者是橫向角度罷了。今天呢,我想從設計+語法角度說一說我感受到的面向接口編程,從而初探設計與實現分離的模式。

(本文所使用的面向對象語言為java,相關代碼都是java代碼)

設計——接口抽象設計

繼承的思想很容易理解,提取幾類相近數據中的公共部分為基類,各個獨立部分在基類的基礎上做自己專屬的延伸。接口是抽象概括輸入和輸出,而具體的實現交由具體實現接口的類來完成,從而達到一樣的接口不一樣的實現方式,使得管理統一化,實現多樣化。

概念扯了那麽多,還是先上個例子吧,以課程中的出租車調度項目為例。

該項目是模擬出租車運行,地圖為 技術分享圖片 的正方形網格圖,每個點的四個鄰接點不一定都連通,但保證整個圖是連通的,共有100輛出租車運行。
任意兩個結點之間有道路或者無道路。
出租車未接單時為隨機遊走,即隨機向可行方向之一運動一步。接單之後選擇最短路徑運行。

看到這個版本一的需求,我當時的第一想法是什麽呢?出租車的行為可概括成兩種模式,隨機遊走和最短距離尋路,這兩種行為都是要基於圖數據的,那麽就開個鄰接矩陣存儲圖,連通為1不連通為0,然後去做相應的實現即可。這樣聽起來似乎沒什麽問題,完全是基本操作嘛。但是,看到我說版本一,相信聰明的人一定猜到還有後續的版本。是的,變化的需求是程序設計者最大的敵人。版本二的需求改動如下:

新增道路打開關閉功能,連通的路可以被關閉,關閉之後也可以選擇再次打開,道路的狀態變成了三種,普通的出租車無法通過關閉後的道路。新增VIP出租車,VIP出租車可以通過被關閉的道路。

關閉道路?嗯…面對這樣的需求改動,以大一時的蠢習慣,那就開個flag數組,對於所有的連通邊初始化為1,關閉道路就把對應的flag置為0,每次訪問圖的同時訪問flag數組,想法是很美好的,但如果需求又變了呢,道路的狀態再次增加了呢,總不可能繼續開更多的flag吧。所以,應該先定義好各種狀態對應的值,通過一個鄰接矩陣來存儲對應的狀態值,使用一種數據結構來管理。為簡化說明我們就設置關閉道路代號為2。

數據存儲解決之後,就要做相應的邏輯處理了,兩種出租車,對於圖中的道路有不同的訪問權限,那是不是應該每個出租車寫一個最短路徑搜索呢?又或者是給最短路搜索方法新傳入一個出租車類型參數,根據類型參數的不同選擇不同的分支去執行。這個時候,就輪到接口出場了。我們來細細梳理邏輯,兩種出租車都是要搜索最短路徑,所使用的算法是相同的,唯一的不同點在於兩種出租車對於“連通”的判斷邏輯不同,其他的代碼部分應該都是可復用的。被C語言腐蝕的我第一時間想到了什麽——函數指針,如果是使用C語言的話,我們需要為兩種出租車定義各自的連通性判斷函數,然後通過一個函數指針傳入最短路徑搜索函數(類似stdlib.h中的qsort函數一樣)。那麽在java中有異曲同工之妙的就是使用接口來實現了,這正好符合面向接口編程的目的——實現不同,接口內容相同。所以我們應該對於每種類型的出租車實現專屬的連通性判斷接口,在任何需要訪問圖的時候傳入該接口即可。下面附上代碼:

版本一:

// 普通出租車
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方法,只是將此接口變量傳遞出去。外部的方法只能使用接口中定義的內容,關於該類的其他所有內容都無權訪問。這種寫法既方便快捷,又保證了數據的隱私性和安全性。不過提醒一點,在沒有熟練掌握前不要亂用哦。  

語法——default和static接口方法

  現在我們跳躍到下一個問題。假如說現在你有成噸的類,都要實現某一個接口,而其中很多類對於接口中某個方法的實現是相同的,僅有少數不同。但是要修改的類太多了,按照傳統的路子,你得實現一個,然後不停的人肉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修飾的方法,但是一旦使用這兩個關鍵字該方法就必須實現。

設計——傳入對象 or 傳入接口

  在初學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四個環節的關系,當然一般為了簡化僅使用前兩個,但是我們考慮問題就應該多想一點,要做到正確條件一定導致正確結果,不正確條件一定導致不正確結果,要使整個規約定義是完備的,這樣才能使設計毫無漏洞。

  規約定義配合之前說的面向接口思想,將設計和實現分離開來,用接口來設計功能,用規約定義來規範每個接口和方法的內容,保證每次運行使用給定的正確的方法,每個方法的執行符合規格定義的內容,對於符合前置條件的輸入進行對應的後置條件處理,對不符合的做相應的異常檢查和處理。當做完這些設計工作,完成了規約層的事,這時候再開始實現層的工作就會事半功倍!這樣,才叫程序設計。

設計與實現分離——面向接口編程(OO博客第三彈)