本文由 Prefert 發表在 ScalaCool 團隊部落格。
不管你是不是果粉,肯定對 iphone X 都有所耳聞。最近的「掉漆門」和「人臉識別被破解」更是將其推到了風口浪尖上。
但是對於我而言,最難以忍受的還是耳機介面被取消這一改變(自 Iphone 7 開始),你可以想象這樣一幅畫面:當你開開心心地和小夥伴開著語音吃(song)著(kuai)雞(di)或者是多人一起上(song)分時——你的電量見底,為了不影響隊友(shou)的遊戲體驗,肯定得充電玩下去。
這時你得面對一個難題:只有一個孔,插耳機還是插電源!?(在沒有藍芽耳機的前提下)
(侵刪)
由於生活經常會欺騙我們,以及各種環境因素,所以不是每個人都選擇藍芽耳機(貧窮使我理智)。
是否存在別的解決方法呢?還好有轉接線這樣的好東西
(侵刪)
在程式設計中,我們也會遇上類似的問題:
- 當你想使用一個已經存在的類,而它的介面不符合你的需求;
- 你想建立一個可以複用的類,該類可以與其他不相關的類或不可預見的類協同工作;
- ...
本文會通過 Adapter Pattern 來探究如何解決這類問題。
本篇文章結構如下:
- adapter pattern 的概念
- 實際問題分解
- Java 例項
- Scala 例項
- 總結
概念
介面卡模式(Adapter Pattern)有時候也稱包裝樣式或者包裝(Wrapper)。定義如下:
將一個類的介面轉接成使用者所期待的。一個適配使得因介面不相容而不能在一起工作的類能在一起工作,做法是將類自己的介面包裹在一個已存在的類中。
它解決了什麼問題
介面卡模式將現有介面轉化為客戶類所期望的介面,實現了對現有類的複用,它是一種使用頻率非常高的設計模式,在軟體開發中得以廣泛應用,在 Spring
等框架、驅動程式設計(如 JDBC
中的資料庫驅動程式)中也使用了介面卡模式。
Java 版本
小 A 是個蘋果控 + 耳機控,之前買了一款很貴的耳機,對其愛不釋手。我們都知道一般耳機介面都是 3.5mm 的。
public interface PhoneJackInterface {
// 傳統的播放音訊
void audioTraditionally();
}
public class PhoneJackConnector implements PhoneJackInterface {
@Override
public void audioTraditionally() {
System.out.println("通過 PhoneJack 播放聲音");
}
}
複製程式碼
iphone 7 之前的 iphone 支援 3.5mm 介面:
public class Iphone {
private PhoneJackInterface phoneJack;
public Iphone(PhoneJackInterface phoneJack) {
this.phoneJack = phoneJack;
}
// Iphone 具備播放聲音的功能
public void play() {
// 通過 3.5mm 介面耳機播放
phoneJack.audioTraditionally();
}
}
複製程式碼
這樣的情況下,小 A 還可以愉快地聽歌:
// test
PhoneJackInterface phoneJack = new PhoneJackConnector();
Iphone iphone6 = new Iphone(phoneJack);
iphone6.play();
// 控制檯輸出 “通過 PhoneJack 播放聲音”
複製程式碼
在 iphone 7 問世後,問題出現了:小 A 發現其不支援 3.5mm 介面 —— 將有線耳機的插口改為了 lightning。
public interface LightningInterface {
void audioWithLightning();
}
public class LightningConnector implements LightningInterface {
@Override
public void audioWithLightning() {
System.out.println("通過 Lightning 播放聲音");
}
}
複製程式碼
一邊是耳機,一邊是手機,這太難以抉擇了。但蘋果怎麼可能沒考慮到這點了,可以通過贈送的耳機轉接器 —— 將傳統的耳機頭轉為 lightning:
public class HeadsetAdapter implements PhoneJackInterface { // 基於傳統耳機介面
LightningInterface lightning; // 相容新介面
/**
* 傳入 lightning 介面
* @param lightning
*/
public HeadsetAdapter(LightningInterface lightning) {
this.lightning = lightning;
}
/**
* 對傳統介面相容
*/
@Override
public void audioTraditionally() {
lightning.audioWithLightning();
}
}
複製程式碼
類介面卡
這樣不夠簡潔,我們可以改一改:
public class HeadsetAdapter extends LightningConnector implements PhoneJackInterface {
@Override
public void audioTraditionally() {
// 傳統介面相容 lightning
super.audioWithLightning();
}
}
複製程式碼
測試:
// test
HeadsetAdapter headsetAdapter = new HeadsetAdapter();
Iphone iphone7 = new Iphone(headsetAdapter);
iphone7.play();
// 控制檯輸出 “通過 Lightning 播放聲音”
複製程式碼
物件介面卡
我們一般將上面的介面卡稱作「類介面卡」,除此之外還有一種 「物件介面卡」,我們可以對介面卡類進行如下修改:
public class ObjectHeadsetAdapter implements PhoneJackInterface { // 基於傳統耳機介面
LightningConnector lightning; // 相容新介面
/**
* 傳入 lightning 介面
* @param lightning
*/
public ObjectHeadsetAdapter(LightningConnector lightning) {
this.lightning = lightning;
}
/**
* 對傳統介面相容
*/
@Override
public void audioTraditionally() {
// 使用委託實現相容
this.lightning.audioWithLightning();
}
}
複製程式碼
測試:
ObjectHeadsetAdapter objectHeadsetAdapter = new ObjectHeadsetAdapter(new LightningConnector());
Iphone iphoneX = new Iphone(objectHeadsetAdapter);
iphoneX.play();
複製程式碼
物件介面卡 vs 類介面卡
通過以上簡單的例子,相信你對介面卡模式有一個大致瞭解了。「類介面卡」與「物件介面卡」的區別概括如下:
- | 類介面卡 | 物件介面卡 |
---|---|---|
建立方式 | 需要通過建立自身建立出一個新的 Adapter | 可以通過已有的 Adapter 物件來轉換介面 |
擴充套件性 | 通過 Override 來擴充套件新需求 | 因為包含關係所以不能擴充套件 |
其他 | 繼承被適配類,所以相對靜態 | 包含被適配類,所以相對靈活 |
優點
總的來說,介面卡模式主要有以下幾個優點:
- 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構。
- 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
- 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合「開閉原則」。
看完 Java 的實現方式,我們再來看看 Scala 是如何實現的。
Scala 版本
在 Scala 中,由於方便的語法糖,我們並不需要像 Java 那樣麻煩,已知傳統介面類(此處省略一些介面)
class PhoneJackConnector {
def audioTraditionally = println("通過 PhoneJack 播放聲音")
}
複製程式碼
如果我們有需要適配的,為其建立一個 trait
即可:
trait Lightning {
def audioWithLightning()
}
複製程式碼
其次再新建一個類,繼承傳統類:
class HeadsetAdapter extends PhoneJackConnector with Lightning {
override def audioTraditionally: Unit = super.audioTraditionally
override def audioWithLightning: Unit = println("通過 Lightning 播放聲音")
}
複製程式碼
你會開心的發現:在這個新的類裡,我們可以對新老方法一起擴充套件——在 Java 中,這是「物件介面卡」和 「類介面卡」比較大的一個劣勢。
測試:
val headsetAdapter = new HeadsetAdapter
headsetAdapter.audioTraditionally
複製程式碼
當然,除了這種方式,Scala 裡還可以通過隱式轉換來實現適配 final
類的介面卡:
final class FinalPhoneJackConector {
def audioTraditionally = println("通過 PhoneJack 播放聲音")
}
object FinalPhoneJackConector {
implicit class ImplictHeadsetAdapter(phoneJackConnector: FinalPhoneJackConector) extends Lightning {
override def audioWithLightning: Unit = println("通過 Lightning 播放聲音")
}
}
複製程式碼
測試:
val headsetAdapter = new HeadsetAdapter
headsetAdapter.audioTraditionally
// 隱式
val light: Lightning = new FinalPhoneJackConector
light.audioWithLightning()
複製程式碼
Hint: 對於不熟悉
implicit
的朋友可以 看一下這裡
總結
光從程式碼量來說,Scala 簡潔比 Java 表現的好太多。
其次,Scala 結合了「類介面卡」和「物件介面卡」所有的優點,並消除了自身問題。與 Java 相比,Scala 有如下特點:
- 與物件介面卡一樣靈活
- 與「類介面卡相比」,沒有對特定被適配類的依賴
- 只適用於不需要動態改變被適配類的情況
原始碼連結 如有錯誤和講述不恰當的地方還請指出,不勝感激!