面向接口編程——遵循契約辦事
雖然之前學過【策略模式】,知道要面向接口,而不是面向實現,那麼真的有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();
}
}