Scala 與設計模式(五):Adapter 介面卡模式

ScalaCool發表於2019-03-03

本文由 Prefert 發表在 ScalaCool 團隊部落格。

不管你是不是果粉,肯定對 iphone X 都有所耳聞。最近的「掉漆門」和「人臉識別被破解」更是將其推到了風口浪尖上。


Scala 與設計模式(五):Adapter 介面卡模式

但是對於我而言,最難以忍受的還是耳機介面被取消這一改變(自 Iphone 7 開始),你可以想象這樣一幅畫面:當你開開心心地和小夥伴開著語音吃(song)著(kuai)雞(di)或者是多人一起上(song)分時——你的電量見底,為了不影響隊友(shou)的遊戲體驗,肯定得充電玩下去。

這時你得面對一個難題:只有一個孔,插耳機還是插電源!?(在沒有藍芽耳機的前提下)


![](https://user-gold-cdn.xitu.io/2017/11/20/15fd89a0ab870701?w=960&h=540&f=jpeg&s=41550)

(侵刪)

由於生活經常會欺騙我們,以及各種環境因素,所以不是每個人都選擇藍芽耳機(貧窮使我理智)。

是否存在別的解決方法呢?還好有轉接線這樣的好東西


![](https://user-gold-cdn.xitu.io/2017/11/20/15fd89a0ac243914?w=430&h=430&f=jpeg&s=10931)

(侵刪)

在程式設計中,我們也會遇上類似的問題:

  1. 當你想使用一個已經存在的類,而它的介面不符合你的需求;
  2. 你想建立一個可以複用的類,該類可以與其他不相關的類或不可預見的類協同工作;

本文會通過 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 來擴充套件新需求 因為包含關係所以不能擴充套件
其他 繼承被適配類,所以相對靜態 包含被適配類,所以相對靈活

優點

總的來說,介面卡模式主要有以下幾個優點:

  1. 將目標類和適配者類解耦,通過引入一個介面卡類來重用現有的適配者類,無須修改原有結構。
  2. 增加了類的透明性和複用性,將具體的業務實現過程封裝在適配者類中,對於客戶端類而言是透明的,而且提高了適配者的複用性,同一個適配者類可以在多個不同的系統中複用。
  3. 靈活性和擴充套件性都非常好,通過使用配置檔案,可以很方便地更換介面卡,也可以在不修改原有程式碼的基礎上增加新的介面卡類,完全符合「開閉原則」。

看完 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 有如下特點:

  1. 與物件介面卡一樣靈活
  2. 與「類介面卡相比」,沒有對特定被適配類的依賴
  3. 只適用於不需要動態改變被適配類的情況

原始碼連結
如有錯誤和講述不恰當的地方還請指出,不勝感激!

Scala 與設計模式(五):Adapter 介面卡模式

相關文章