Golang 設計模式之適配器模式

1 適配器模式

本期繼續和大家探討 Golang 設計模式這一主題. 今天,我們來聊一聊設計模式中的 "適配器模式".

適配器模式的作用是能夠實現兩個不兼容或弱兼容接口之間的適配橋接作用,該設計模式中會涉及到如下幾個核心角色:

下面舉個直觀點的例子來說,我們作爲用戶(client)現在手中持有一個兩孔的插頭,需要匹配的目標是一個兩孔的插座(target),但是現狀是我們只找到了三孔的插座(adaptee),於是我們通過在三孔插座上插上一個實現三孔轉兩孔的適配器(adapter),最終實現了兩孔插頭與三孔插座之間的適配使用.

2 代碼實現

第 1 章聊完概念,讓大家對適配器模式用了一個大致的影響,下面我們結合類圖和代碼,來和大家一起探討適配器模式的實踐案例,從而進一步加深對該種設計模式的理解.

我把適配器模式分爲常規適配模式和 interface 適配模式兩種類型,下面逐一探討.

2.1 常規適配模式

下面給出一個適配器模式相對通用的類圖.

在上述類圖中,包含如下關鍵點:

下面我們給出一個具體的場景,進一步加深印象:

理完了這個場景以及對應的類圖關係,下面我們 show 一下代碼:

其中手機充電器對應爲 PhoneCharger interface,存在的核心方法爲 Output5V:

type PhoneCharger interface {
    Output5V()
}

具體的手機充電器型號包括華爲手機充電器和小米手機充電器:

type HuaWeiCharger struct {
}


func NewHuaWeiCharger() *HuaWeiCharger {
    return &HuaWeiCharger{}
}


func (h *HuaWeiCharger) Output5V() {
    fmt.Println("華爲手機充電器輸出 5V 電壓...")
}


type XiaoMiCharger struct {
}


func NewXiaoMiCharger() *XiaoMiCharger {
    return &XiaoMiCharger{}
}


func (x *XiaoMiCharger) Output5V() {
    fmt.Println("小米手機充電器輸出 5V 電壓...")
}

另外還有一種蘋果筆記本充電器,輸出的電壓是 28V:

type MacBookCharger struct {
}


func NewMacBookCharger() *MacBookCharger {
    return &MacBookCharger{}
}


func (m *MacBookCharger) Output28V() {
    fmt.Println("蘋果筆記本充電器輸出 28V 電壓...")
}

蘋果筆記本輸出的電壓是 28V,這個手機無法直接承受,因此我們創建出一個手機充電器的適配器類,在適配器類的 Output5V 方法中,會調用蘋果筆記本輸出電壓的能力,並將其適配轉換成 5V 輸出:

type MacBookChargerAdapter struct {
    core *MacBookCharger
}


func NewMacBookChargerAdapter(m *MacBookCharger) *MacBookChargerAdapter {
    return &MacBookChargerAdapter{
        core: m,
    }
}


func (m *MacBookChargerAdapter) Output5V() {
    m.core.Output28V()
    fmt.Println("適配器將輸出電壓調整爲 5V...")
}

定義完手機充電器之後,下面進行手機的類型聲明,包括一個 Phone interface 以及一種具體的手機實現類 HuaWeiPhone. Phone 具備一個用於充電的 Charge 方法,其中使用到手機充電器 PhoneCharger 的 Output5V 方法,執行手機的充電操作.

type Phone interface {
    Charge(phoneCharger PhoneCharger)
}


type HuaWeiPhone struct {
}


func NewHuaWeiPhone() Phone {
    return &HuaWeiPhone{}
}


func (h *HuaWeiPhone) Charge(phoneCharger PhoneCharger) {
    fmt.Println("華爲手機準備開始充電...")
    phoneCharger.Output5V()
}

下面是給出這一場景下的使用示例,首先是對華爲手機充電器的使用,然後使用經由適配器轉換後的蘋果筆記本充電器進行充電:

func Test_adapter(t *testing.T) {
    // 創建一個華爲手機實例
    huaWeiPhone := NewHuaWeiPhone()


    // 使用華爲手機充電器進行充電
    HuaWeiCharger := NewHuaWeiCharger()
    huaWeiPhone.Charge(HuaWeiCharger)


    // 使用適配器轉換後的 macbook 充電器進行充電
    macBookCharger := NewMacBookCharger()
    macBookChargerAdapter := NewMacBookChargerAdapter(macBookCharger)
    huaWeiPhone.Charge(macBookChargerAdapter)
}

上述測試代碼對應的輸出結果如下:

華爲手機準備開始充電...
華爲手機充電器輸出 5V 電壓...


華爲手機準備開始充電...
蘋果筆記本充電器輸出 28V 電壓...
適配器將輸出電壓調整爲 5V...

2.2 interface 適配模式

在 golang 中,class 對 interface 的實現採用的隱式實現的方式,即我們在定義具體類型時,不需要顯式聲明對 interface 的 implement 操作,只需要實現了 interface 的所有方法,就自動會被編譯器識別爲 interface 的一種實現.

下面給出對應的示例:

type MyInterface interface {
    MethodA()
    MethodB()
}


type MyClass struct{}


func NewMyClass() *MyClass {
    return &MyClass{}
}


// MyClass 實現了 MyInterface 聲明的所有方法
func (m *MyClass) MethodA() {}


func (m *MyClass) MethodB() {}


func Test_implement(t *testing.T) {
    // 獲取 myClass 的類型
    myClassTyp := reflect.TypeOf(NewMyClass())
    // 獲取 myInterface 的類型
    myInterTyp := reflect.TypeOf((*MyInterface)(nil)).Elem()
    // 判斷是否具有實現關係
    t.Log(myClassTyp.Implements(myInterTyp))
}

對應的輸出結果爲:

    true

正是基於 golang 中這種隱式實現的特性,使得 interface 本身也具備了適配器的功能.

2.1 小節中,我們所探討的一類常規適配器模式,指的是被適配對象 adaptee 中缺少了一部分目標 target 的核心能力,需要由適配器 adapter 完成這部分能力的適配補齊.

而在接下來的 2.2 小節中,我們聊的是另一種場景:adaptee 不僅具備 target 的全部能力,還聚合了一部分 target 本身不關心的能力. 因此倘若我們直接把 adaptee 當作 target 使用,這部分不相干能力也會被暴露出來,最終對 target 的使用方造成困惑.

這對於這種問題,我們可以通過對 interface 的合適定義使其充當適配器的角色,來規避這類因邊界不清晰導致功能泄漏的代碼規範問題.

下面我們對於 golang 中 interface 的使用進行一輪梳理:

2.2.1 interface 建立接口規範

首先,interface 最常用的一種使用模式,是抽象出了同一類型下多種角色的共性,將其聲明成一個接口規範的形式,最終所有實現類 class 都需要實現 interface 的所有方法,或者反過來說,具體 class 本身對應於 interface 類型中的一種具體角色,它就理所應當具備 interface 所抽象出來的核心能力,否則,就只能說明 interface 的抽象程度並不合理.

舉個具體的例子,我們定義出了手機的 interface——phone,其中抽象出每種手機都需要具備的能力,包括撥打電話的 Call 方法、充電 Charge 方法、發送短信 SendMessage 方法等...

我們基於品牌維度聲明出 Phone 的具體實現類,包括華爲手機 HuaWeiPhone、小米手機 XiaoMiPhone、OPPO 手機 OPPOPhone、VIVO 手機 VIVOPhone 等,每種實現類 class 都需要實現好 Phone interface 中定義好的 Call、Charge、SendMessage 等方法.

這個場景對應的類圖結構如下:

2.2.2 通過 interface 隱藏實現細節

另一種 interface 的使用場景,在模塊間進行類的傳輸時,爲了保護具體的實現類隱藏其中的實現細節,轉而使用抽象 interface 的形式進行傳遞. 同時這種基於 interface 進行傳遞參數的方式,也基於了使用方一定的靈活度,可以通過注入 interface 不同實現 class 的方式,賦予方法更高的靈活度. 這正是我們在編程設計模式中所推崇的面向接口編程而非面向實現編程的思路體現.

首先我們給出具體的實現案例:我們需要設計一個課程服務模塊,其中包含了一系列課程的學習,包括編程課程、體育課程、音樂課程等等,涉獵的範圍非常廣泛,能夠做到讓使用方結合自身的興趣找到合適的課程資源.

在實現時,我們預定好一個 CourseService interface,其中聲明瞭一系列課程對應的方法;在此之上,我們定義了一個實現 class——courseServiceImpl,統一實現出上述的所有課程方法.

對應的類圖以及實現代碼如下所示:

type CourseService interface {
    // 一系列編程課程
    LearnGolang()
    LearnJAVA()
    LearnC()
    // ...


    // 一系列體育課程
    LearnBasketball()
    LearnFootball()
    LearnSki()
    // ...


    // 一系列音樂課程
    LearnPiano()
    LearnHarmonica()
    LearnGuita()
    // ...
}


type courseServiceImpl struct {
}


func NewCourseService() CourseService {
    return &courseServiceImpl{}
}


func (c *courseServiceImpl) LearnGolang() {
    fmt.Println("learn go...")
}


func (c *courseServiceImpl) LearnJAVA() {
    fmt.Println("learn java...")
}


func (c *courseServiceImpl) LearnC() {
    fmt.Println("learn c...")
}


func (c *courseServiceImpl) LearnBasketball() {
    fmt.Println("learn basketball...")
}


func (c *courseServiceImpl) LearnFootball() {
    fmt.Println("learn football...")
}


func (c *courseServiceImpl) LearnSki() {
    fmt.Println("learn ski...")
}


func (c *courseServiceImpl) LearnPiano() {
    fmt.Println("learn piano...")
}


func (c *courseServiceImpl) LearnHarmonica() {
    fmt.Println("learn harmonica...")
}


func (c *courseServiceImpl) LearnGuita() {
    fmt.Println("learn guita...")
}

上面這種實現方式中,interface 和實現 class 是緊密綁定的,遵循還是類似於 JAVA 中顯式實現的代碼風格,然而這種風格在 golang 中是並不值得推崇的,根本原因就在於 Golang 隱式實現與 JAVA 顯式實現的差異.

這種實現方式的缺陷在於:

產生這個問題的根本原因在於,構造 interface 的工作不應該由模塊的實現方來做. 定義 interface 本質上是一個對類型的邊界和職責進行抽象的過程,作爲實現方的角色,它永遠無法做到未卜先知地站在未來使用方的視角,來幫助使用方做出” 如何使用這個模塊 “的定義和決策.

就舉上面給出的這個 CourseService 的示例而言,課程服務的使用方有可能只是使用 CourseService 進行編程課程的學習,那麼此時 CourseService 中有關於音樂和體育部分的課程內容對於使用方來說就是無關甚至累贅的一部分信息. 再進一步舉個例子,使用方有可能只希望通過 CourseService 進行 Golang 課程的學習,那麼此時編程課程中的 JAVA 課程和 C 課程對於使用方來說也屬於是無須關心的一部分信息.

爲了解決這個實現方與使用方之間視角衝突的問題,最終 Golang 官方給出了對應的解決方案:interface 的定義應該由模塊的使用方而非實現方來進行定義. 只有這樣,使用方纔能根據自己的視角,對模塊進行最合適或者說最貼合自己使用需求的抽象定義.

關於這一點內容,可以參見 Golang 官方在 CodeReview 評論中給出一部分有關於 interface 用法的介紹內容:

內容鏈接:http://github.com/golang/go/wiki/CodeReviewComments#interface

看到這裏,大家腦海中可能有一些朦朧的概念,但是認知可能還是不夠清晰. 我們就趁熱打鐵,下面就結合適配器模式的主題,針對 CourseService 的場景給出 Golang 所推崇的 interface 的設計架構,以此來展示 interface 所其到的適配器功能.

針對於 CourseService 的場景問題,我們進行如下改造:

這樣實現下來,對應的類圖結構如下所示:

接下來我們做一下代碼展示:

在實現方這一側,直接將 CourseService 定義爲具體的類型,不再額外通過 interface 進行包裝:

type CourseService struct {
}


func NewCourseService() *CourseService {
    return &CourseService{}
}


func (c *CourseService) LearnGolang() {
    fmt.Println("learn go...")
}


func (c *CourseService) LearnJAVA() {
    fmt.Println("learn java...")
}


func (c *CourseService) LearnC() {
    fmt.Println("learn c...")
}


func (c *CourseService) LearnBasketball() {
    fmt.Println("learn basketball...")
}


func (c *CourseService) LearnFootball() {
    fmt.Println("learn football...")
}


func (c *CourseService) LearnSki() {
    fmt.Println("learn ski...")
}


func (c *CourseService) LearnPiano() {
    fmt.Println("learn piano...")
}


func (c *CourseService) LearnHarmonica() {
    fmt.Println("learn harmonica...")
}


func (c *CourseService) LearnGuita() {
    fmt.Println("learn guita...")
}

對於使用方來說,需要根據自身對於 CourseService 的適用範圍,完成 interface 的定義. 這個定義過程即明確了自身對於 CourseService 職責定位,也實現了對無關方法的消音屏蔽.

在這個過程中,interface 就扮演了適配器的角色,對 class 的範圍起到適配和收斂的作爲,使其在使用方手中,有一個更加恰到好處的定位和空間.

下面是對 interface 的定義,這裏僅僅給出了編程課程、體育課程和音樂課程的三類定義,在實際使用場景中,對於 interface 的粒度還可以有更加靈活自由的控制,完全取決於使用方的使用訴求.

type CSCourseProxy interface {
    LearnGolang()
    LearnJAVA()
    LearnC()
}


type PECourseProxy interface {
    LearnBasketball()
    LearnFootball()
    LearnSki()
}


type MusicCourseProxy interface {
    LearnPiano()
    LearnHarmonica()
    LearnSki()
}

比如,使用方如果只需要使用到 CourseService 中有關於 Golang 課程的資源,則可以進一步細化,將 interface 定義爲 GolangCourseProxy 的維度:

type GolangCourseProxy interface{
    LearnGolang()
}

於是,這個使用方在使用過程中,可以通過 GolangCourseProxy 類型對 CourseService 實例進行介紹,這樣該實例的職責是非常單一且清晰的.

func Test_golangCourseProxy(t *testing.T) {
    var proxy GolangCourseProxy = NewCourseService()
    proxy.LearnGolang()
}

再比如,使用方倘若需要針對 interface 進行 mock 打樁,那麼在做 mock 實現類聲明時,成本也是很低的,只需要實現自身聚焦的 LearnGolang 這一個方法即可.

type mockGolangCourseProxy struct {
}


func NewMockGolangCourseProxy() *mockGolangCourseProxy {
    return &mockGolangCourseProxy{}
}


func (m *mockGolangCourseProxy) LearnGolang() {
    fmt.Println("mock learn go...")
}


func Test_mockGolangCourseProxy(t *testing.T) {
    var mockProxy GolangCourseProxy = NewMockGolangCourseProxy()
    mockProxy.LearnGolang()
}

有關 Golang interface 這部分內容,大家如果覺得看完本文理解認知還不夠清晰,可以觀看一下我之前發表在 bililibili 上的相關講解視頻:

https://www.bilibili.com/video/BV14M411H7J9/?p=7

golang 單測心得分享——小徐先生 1212

3 總結

本期和大家探討了設計模式中的適配器模式. 適配器模式的作用是能夠實現兩個不兼容或者弱兼容接口之間的適配橋接作用,按照我的個人理解可以細分爲常規適配模式和 interface 適配模式兩種類型,本文中結合概念介紹、類圖和代碼展示的方式向大家一一作了展示.

本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源https://mp.weixin.qq.com/s/g1xKCmRMqxP9DmtW208A6w