二學“面向接口編程”

面向接口編程——遵循契約辦事

雖然之前學過【策略模式】,知道要面向接口,而不是面向實現,那麼真的有get到精髓嗎?

接口的本質是什麼?——代碼世界裏的契約

接口裏的一個個方法,就是契約的條款

OOP中和接口非常相似的就是抽象類,這經常被大家拿來比較,雖然我之前也寫過了,但我還是想再說一遍

接口和抽象類——它們其實分工明確

首先它們相似在哪?——沒有具體實現邏輯

那差在哪呢?我們先看例子先:

// 抽象類
abstract class Door {
    abstract void open();
    abstract void close();
}
abstract class Fish {
    abstract void swim();
    abstract void eat();
}

// 接口
interface IFlyable {
    void fly();
}
interface IComparer {
    int compare(Object a,Object b);
}

有點感覺了吧?抽象類重在它的類名本身,而接口更強調的是它裏面的一個個方法,而名字只是輔助。

我們接着看:

interface IFlyable {
    void fly();
}
abstract class Animal {}
class Bird extends Animal implements IFlyable {}
class Plane implements IFlyable {}

懂了吧???接口是很輕量級的,附着於主體類上的一個像裝備一樣輔助的東西。

而抽象類則直接說明了,你是誰誰誰的一員,是單繼承,但接口你可以多實現呀

  • 接口針對行爲抽象,更強調“你能幹啥”(動態)
  • 抽象類針對數據抽象,更強調“你是個啥”(靜態)

(C++和Python中,接口和抽象類是合二爲一的概念,都是抽象類,我們這邊主要是說OOP純度高的,以Java爲例)

但是,抽象類命名也有對行爲的抽象啊,那爲什麼還要多抽出一個接口?

  • 數據、行爲分開了定義,更方便理解
  • 繼承的時候,太重了,難免把不必要的也放進來

那接口怎麼用呢?往下看。

接口的應用

先簽約,後對接

該場景主要是爲了有相互依賴調用關係的雙(多)方能同時開發,開工前需要保證模塊間能夠順利對接

案例背景:

【數據源模塊】——>【數據庫模塊】【中間件模塊】【日誌模塊】

(數據處理完之後,會根據業務需要寫到三個模塊內)

最早只能咋樣,數據源模塊的開發者對着另外三個模塊的開發者吼:“你們都給我寫個void writeResult(String data)的方法,實現具體邏輯,方法名別錯了!我就只管傳數據!“這顯然不合適啊,那咋辦,四組開發者定個協議咯,而這個協議,就是接口。這可是一大步飛躍啊,約定好的方法名,從word文檔到了代碼裏,不實現接口,編譯器還不讓你過,難道不香嗎???

(其實這是【觀察者模式】,數據源模塊是”被觀察者“,其他模塊是”觀察者“,Container是中介,以Container爲核心,實現解耦的消息通知系統)

  • 先在大家都能引用的Common模塊裏新增接口
interface IWriteResult {
    void writeResult(String data); 
}
  • 【數據源模塊】裏有個observers數組,用來裝所有註冊過的觀察者(註冊方式:通過配置文件,反射掃描,反射查找,創建實例)
// 表示實現了這個接口的對象
IWriteResult[] observers;
  • 解析完一條數據後
void sendResult(String data){
    for(IWriteResult observer:observers){
        observer.writeResult(data);
    }
}
  • 然後每個目標模塊有自己完成writeResult()的業務邏輯,至此,我們就完成各個模塊的對接啦

接口的貫穿從始至終,其價值體現在項目開發的過程中

專注抽象,脫離具體、

這個場景就不是多人同時開發了,而是架構師設計框架性代碼時,採用接口專攻抽象的主體邏輯

我們就拿數組的排序來說:

// 給用戶制定標準,用戶自定義一個排序標準
Array.sort(IComparer comparer);

// 接口定義
interface IComparer {
    int compare(Object a,Object b);
}

// 內部實現
Array sort(IComparer comparer){
	int length = this.elements.length;
	int m = length;
    for(int i=0;i<length;i++){
        m = m - 1;
        for(int j=0;j<m;j++){
            // 最關鍵的抽象判斷
            if(comparer(this.elements[j],this.elements[j+1])>0)
                swap(this.elements[j],this.elements[j+1]);
        }
    }
    return this.elements;
}

這就是面向抽象編程,而接口就是實現這一方式的關鍵

我們用戶就根據需要實現接口:(比方這裏是年齡)

class PersonComparer implements IComparer {
    public int compare(Person a,Person b){
        return a.age > b.age;
    }
}

說白了,IComparer這個接口,就是爲了擺脫細節實現,把這一part留給用戶

解開耦合,破除纏繞

這個場景主要用於不應該依賴其他底層模塊的封裝,一旦底層模塊有調用上層模塊的需求,可以藉助接口抽象化

先來看個案例:

在父窗口parentWindow裏嵌入一個子UI控件childControl,子控件有幾個功能:

  • Reload菜單

  • Add菜單

  • Save菜單

// 雙向引用
class ChildControl {
    ParentWindow parentWindow;
    void reloadClicked(){
        this.parentWindow.Reload();
    }

    void addClicked(){
        this.parentWindow.Add();
    }

    void saveClicked(){
        this.parentWindow.Save();
    }
}

class ParentWindow {
    void Reload(){}
    void Add(){}
    void Save(){}
}

父窗口和子控件莫名其妙耦合了,而且子控件還成不了輪子,很傷的,咋辦?接口!

interface IDataOperation {
    void Reload();
    void Add();
    void Save();
}

子控件裏我們只關心這個接口:(其實也就是把這三個操作具體實現的任務丟出去,我不管誰來整,反正讓我鼠標敲下去有對應的正確反饋就好)

class ChildControl {
    // 讓父窗口去實現這個接口
    IDataOperation dataOperation;
    void reloadClicked() {
        if(dataOperation != null)
            dataOperation.Reload();
    }
    // 其他兩個省略
}

//  父窗口中實現這個接口
class ParentWindow implements IDataOperation{
    // 接受子控件的接口實現任務
    ChildControl childControl;
    void Reload(){
        this.dataArr = getDataFromDB();
    }
    // 其他兩個省略
    // 交付任務
    this.childControl.dataOperation = this;
}

經常用於相互引用的耦合,我中有你,你中有我的場景

原來子控件的模塊,還需要考慮父窗口怎麼處理,有哪些方法;現在相反,由子控件來整個接口丟出去,讓父窗口來實現

這樣的接口具有特殊性,爲了滿足業務特定需求的,實現模塊間解耦

同樣的應用,可以參考model層和controller層,model層會調用controller層的方法(事件通知),但它並不知道controller層的信息,一樣也是爲了解耦。

總結

接口的語法是簡單,但是難在用法太花太多了!!!

接口和函數指針

有人覺得很像,有人覺得是兩個東西,仁者見仁吧

親兄弟

以C爲例:

int (*calculate)(int,int) = add;
int sum = (*calculate)(100,200);

函數指針也有個特點:自己不做決定,放權給別人來做

函數指針能做的,接口也能做(就是累點);除了OOP特性的東西,函數指針也能做接口的東西,好似兩個親兄弟

接口的優勢

接口的業務承載能力明顯比函數指針強,而函數指針更強調函數結構的定義

  • 優勢一:函數指針不是OOP的產物,而接口是OOP的高級抽象

    • 接口裏可以寫很多函數,而函數指針不行
    • 接口間還能接口繼承接口,而函數指針不行
    • 我們可以利用反射查詢多少類實現了某個接口,而函數指針不行
  • 優勢二:接口是個很好的“協議”

    • 語法上,接口清晰易懂,溝通成本低;函數指針反之
    • 可以有多個函數,函數指針只能一一對應
    • 實現該接口的模塊必須實現細節,否則過不了編譯;函數指針只有在動態運行時纔可能會報錯

函數指針的優勢

  • 優勢一:函數指針比接口還輕量

    • 函數指針短小精悍,動態賦值;而接口則需要在某個類上才能發揮作用。比如說加減乘除,函數指針只要一個指針和四個函數就可以了;而接口則需要寫一個接口和加減乘除四個類,明顯臃腫了很多(函數指針似乎更適合小場面)
  • 優勢二:接口可能會造成空函數

    • 接口要求所有方法都要實現,萬一我有一些不需要呢?
    • 函數指針一一對應,細粒度的匹配,不會造成浪費
  • 優勢三:接口比函數指針更加笨重,函數指針適合業務語境很少的環境

假如有個接口A和一個接口B,A中有9個方法要實現,B中只有1個,但它們的地位卻看似相等?B中的那個方法而且很可能只是主體類的一個邊邊角角小嘍囉,沒必要大動干戈,在主體類中去掉B接口,用函數指針不香嗎?

而可惜的是Java沒有,而C#是有的,但是…Java有Lambda表達式

Lambda表達式

爲了模仿函數指針,JDK1.8增加了函數式編程新特性——Lambda表達式

  • Java8之前,我們都是用匿名內部類來模仿函數指針
  • Java8之後,我們用lambda表達式
    • 要求1:接口只有一個方法
    • 要求2:如果要簡化的話,該方法只能有一行代碼
interface ILambda {
    void out();
}

public class Test {
    public static void main(String[] args) {
        // 匿名內部類
        ILambda i1 = new ILambda(){
            @Override
            public void out() {
                System.out.println("i1");
            }          
        };
        i1.out();
        // Lambda表達式
        ILambda i2 = ()->{
            System.out.println("i2");
        };
        i2.out();
        // 簡化(如果只有一行)
        ILambda i3 = ()->System.out.println("i3");
        i3.out();
    }
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章