iT邦幫忙

第 11 屆 iT 邦幫忙鐵人賽

DAY 8
0

今天要來聊聊的是大家生活中很常見的 Adapter 配接器模式,請大家先看 VCR,噢,是定義。

將類別的介面轉換成外界所預期的另一種介面,讓原先囿於介面不相容問題而無法協力合作的類別能夠兜在一起用。

(取自 物件導向設計模式−可再利用物件導向軟體之要素

畫個重點,「轉換成外界所預期的另一種介面」、「介面不相容問題」、「能夠兜在一起」。

舊音響再利用

家裏有一台還算不錯的音響,有 Bass 喇叭和兩個喇叭,之前聽音樂或者是看電影都有拿來用,一個小型家庭劇院的感覺,恩,挺享受的。

不過前陣子忙碌,音響好一陣子沒用,而且麻煩的是,現在在用的電腦只剩下 USB Type-C 插槽,而我的音響是 3.5mm 的接口。囧,這該如何是好⋯⋯

好險,有種產品叫 Type-C 轉 3.5mm 轉接線,我的音響又復活了!
什麼?!你說直接找 藍芽接收器 來用以後更方便?!

情境與解決方法

偶爾我們會遇到像上面這個故事的情境,手邊有兩種物品,我們需要讓它們可以兜在一起運作,然而偏偏這兩樣物品沒有可以直接組合在一起的接口。為了解決這個問題,我們可以選擇,

  1. 重新買一個可以組合在一起的,但是貴,而且原本的物品就被遺留在旁邊了。
  2. 找一個轉換器把其中一樣物品的接口轉換成另一個物品可以組合的接口,好處呢,比較省錢,而且原本的東西可以繼續使用。

我們也很容易在開發和維護軟體的過程中遇到像是這種情境,特別是當我們持續的增加功能、調整商業邏輯時,我們也許需要重新設計商業邏輯層以及使用新的介面,但是先前所開發好的類別只是介面不同而已;或者是其他同事已經實作了同樣功能的類別,但是提供的公開介面卻跟我們預期的一樣。

這個時候,我們可以利用 Adapter Pattern 來減少額外的開發以及盡可能倚賴已經實作、測試過且穩定的類別。(如果已經存在的類別沒測試,那⋯⋯可能先考慮幫他加一下喔~)

所以,到底為什麼 Adapter Pattern 可以幫助我們解決這種介面不同的問題呢?這個模式的核心運作原理主要是

  1. Adapter 實作了一個外部程式會依賴的介面,因此外部程式可以直接使用而不會有介面的問題。
  2. Adapter 在接收到外部程式傳遞的訊息時,將該訊息轉換成原有類別所能理解的樣式和順序。

Adapter Pattern 的兩種設計

Object Adapter Pattern 物件配接器

07_adapter_01

物件配接器的設計是使用組合物件的原則(Object Composition Principle)來達到目的, Adapter 與主程式透過共通的介面來溝通,而 Adapter 負責將傳遞進來的訊息轉換成被包裝物件所需要的格式,並調用合適的介面。

Class Adapter Pattern 類別配接器

07_adapter_02

類別配接器的設計是透過繼承(Inheritance)來達成想要的效果,與物件配接器很類似,只是這邊的 Adapter 是原有類別的一種子類別。這種方式個人覺得滿特別的,但只有部分支援多重繼承(Multiple Inheritance)的程式語言例如 Python 以及 C++ 才能使用。

Show me the code

我們用 Rust 來示範。

// 原有的類比音訊耳機,與新的數位音訊播放介面並不相容
struct JackHeadphone;
impl JackHeadphone {
  fn analog_playback(&self, _analog_signal: &str) {
    println!("Analog playback");
  }
}

// 新的數位音訊耳機播放介面
trait DigitalAudioPlayback {
  fn playback(&self, _digital_signal: &str);
}

// 新的數位音訊耳機播放裝置,是一種藍芽耳機。
struct BluetoothHeadphone;
impl DigitalAudioPlayback for BluetoothHeadphone {
  fn playback(&self, _digital_signal: &str) {
    println!("Playback digital signal through Bluetooth!");
  }
}

// 藍芽訊號轉類比訊號配接器,用以讓舊有的類比音訊裝置能與新系統相容。
struct BluetoothToJackAdapter {
  jack_headphone: JackHeadphone,
}

// 在 BluetoothToJackAdapter 中實作數位訊號轉類比訊號的轉換功能。
impl BluetoothToJackAdapter {
  fn digital_to_analog(_digital_signal: &str) -> String {
    println!("converting");
    return "Converted signal".to_string()
  } 
}

// 讓 BluetoothToJackAdapter 能回應新的 playback 介面,並調用 JackHeadphone 的播放。
impl DigitalAudioPlayback for BluetoothToJackAdapter {
  fn playback(&self, digital_signal: &str) {
    let analog_signal = &BluetoothToJackAdapter::digital_to_analog(digital_signal);
    self.jack_headphone.analog_playback(analog_signal);
  }
}

// 新的數位音訊播放設備
fn playback_digital_audio<T: DigitalAudioPlayback>(speaker: &T) {
    speaker.playback("Start sending digital signal");
}

fn main() {
  // 新的數位音訊設備可以直接被使用
  let bluetooth_headphone = BluetoothHeadphone {};
  playback_digital_audio(&bluetooth_headphone);

  // 透過 Adapter Pattern 讓原有的類比音訊設備可以相容於新的介面
  let jack_headphone = JackHeadphone {};
  let adapted_digital_speaker = BluetoothToJackAdapter {
    jack_headphone: jack_headphone
  };
  playback_digital_audio(&adapted_digital_speaker);
}

Adapter 的優缺點

優點

  1. 符合單一職責原則(Single Responsibility Principle),資料轉換的職責跟商業邏輯的部分是分開的。
  2. 符合開放封閉原則(Open-Close Principle),Adapter 讓原有的類別去適應了新的主程式,但並沒有改變原有的類別封裝好的行為。

缺點

  1. 整個軟體的程式碼複雜度會因為加入新的介面和類別而增加。

與其他設計模式比較

Bridge Pattern

定義 - 將實作體系與抽象體系分離開來,讓兩者能各自更動各自演進。
Bridge 通常是在一開始設計的時候就把程式設計成適合協作的樣子,Adapter 則比較常使用在讓不相容的類別能夠與現有的軟體協作。

Decorator Pattern

Adapter 是透過加入新的介面讓原有的類別能夠與新的物件協作,而 Decorator 是在不改變介面的約束下,擴增了原有物件的行為。

總結

在維護現有產品的時候,難免會遇到需要調整類別介面的情況,與其重寫整個類別或者是重頭實作一個新的類別,在可以接受的程式碼複雜度下,可以考慮看看是不是能夠透過 Adapter Pattern 來達成目標。

小試身手

想想看,下面的圖片中,橡皮筋是不是 Adapter?

橡皮筋小技巧-滑牙
取自KKNews

參考資料

Adapter
Dive into Design Pattern - Adapter

作者:Yenting


上一篇
[Design Pattern] Observer 觀察者模式
下一篇
[Design Pattern] Template 模板模式
系列文
什麼?又是/不只是 Design Patterns!?32

尚未有邦友留言

立即登入留言