使用Golang的interface介面設計原則

Aceld發表於2019-05-21

1 interface介面

  interface 是GO語言的基礎特性之一。可以理解為一種型別的規範或者約定。它跟java,C# 不太一樣,不需要顯示說明實現了某個介面,它沒有繼承或子類或“implements”關鍵字,只是通過約定的形式,隱式的實現interface 中的方法即可。因此,Golang 中的 interface 讓編碼更靈活、易擴充套件。 如何理解go 語言中的interface ? 只需記住以下三點即可:

(1) interface 是方法宣告的集合 (2) 任何型別的物件實現了在interface 介面中宣告的全部方法,則表明該型別實現了該介面。 (3) interface 可以作為一種資料型別,實現了該介面的任何物件都可以給對應的介面型別變數賦值。

注意:  a. interface 可以被任意物件實現,一個型別/物件也可以實現多個 interface  b. 方法不能過載,如 eat(), eat(s string) 不能同時存在

package main

import "fmt"

type Phone interface {
    call()
}

type NokiaPhone struct {
}

func (nokiaPhone NokiaPhone) call() {
    fmt.Println("I am Nokia, I can call you!")
}

type ApplePhone struct {
}

func (iPhone ApplePhone) call() {
    fmt.Println("I am Apple Phone, I can call you!")
}

func main() {
    var phone Phone
    phone = new(NokiaPhone)
    phone.call()

    phone = new(ApplePhone)
    phone.call()
}

上述中體現了interface介面的語法,在main函式中,也體現了多型的特性。 同樣一個phone的抽象介面,分別指向不同的實體物件,呼叫的call()方法,列印的效果不同,那麼就是體現出了多型的特性。

2 物件導向中的開閉原則

2.1 平鋪式的模組設計

那麼作為interface資料型別,他存在的意義在哪呢? 實際上是為了滿足一些物件導向的程式設計思想。我們知道,軟體設計的最高目標就是高內聚,低耦合。那麼其中有一個設計原則叫開閉原則。什麼是開閉原則呢,接下來我們看一個例子:

package main

import "fmt"

//我們要寫一個類,Banker銀行業務員
type Banker struct {
}

//存款業務
func (this *Banker) Save() {
    fmt.Println( "進行了 存款業務...")
}

//轉賬業務
func (this *Banker) Transfer() {
    fmt.Println( "進行了 轉賬業務...")
}

//支付業務
func (this *Banker) Pay() {
    fmt.Println( "進行了 支付業務...")
}

func main() {
    banker := &Banker{}

    banker.Save()
    banker.Transfer()
    banker.Pay()
}

程式碼很簡單,就是一個銀行業務員,他可能擁有很多的業務,比如Save()存款、Transfer()轉賬、Pay()支付等。那麼如果這個業務員模組只有這幾個方法還好,但是隨著我們的程式寫的越來越複雜,銀行業務員可能就要增加方法,會導致業務員模組越來越臃腫。

Easy搞定Golang設計模式.001.png

這樣的設計會導致,當我們去給Banker新增新的業務的時候,會直接修改原有的Banker程式碼,那麼Banker模組的功能會越來越多,出現問題的機率也就越來越大,假如此時Banker已經有99個業務了,現在我們要新增第100個業務,可能由於一次的不小心,導致之前99個業務也一起崩潰,因為所有的業務都在一個Banker類裡,他們的耦合度太高,Banker的職責也不夠單一,程式碼的維護成本隨著業務的複雜正比成倍增大。

2.2 開閉原則設計

那麼,如果我們擁有介面, interface這個東西,那麼我們就可以抽象一層出來,製作一個抽象的Banker模組,然後提供一個抽象的方法。 分別根據這個抽象模組,去實現支付Banker(實現支付方法),轉賬Banker(實現轉賬方法) 如下: Easy搞定Golang設計模式.002.png

那麼依然可以搞定程式的需求。 然後,當我們想要給Banker新增額外功能的時候,之前我們是直接修改Banker的內容,現在我們可以單獨定義一個股票Banker(實現股票方法),到這個系統中。 而且股票Banker的實現成功或者失敗都不會影響之前的穩定系統,他很單一,而且獨立。

所以以上,當我們給一個系統新增一個功能的時候,不是通過修改程式碼,而是通過增添程式碼來完成,那麼就是開閉原則的核心思想了。所以要想滿足上面的要求,是一定需要interface來提供一層抽象的介面的。

golang程式碼實現如下:

package main

import "fmt"

//抽象的銀行業務員
type AbstractBanker interface{
    DoBusi()    //抽象的處理業務介面
}

//存款的業務員
type SaveBanker struct {
    //AbstractBanker
}

func (sb *SaveBanker) DoBusi() {
    fmt.Println("進行了存款")
}

//轉賬的業務員
type TransferBanker struct {
    //AbstractBanker
}

func (tb *TransferBanker) DoBusi() {
    fmt.Println("進行了轉賬")
}

//支付的業務員
type PayBanker struct {
    //AbstractBanker
}

func (pb *PayBanker) DoBusi() {
    fmt.Println("進行了支付")
}

func main() {
    //進行存款
    sb := &SaveBanker{}
    sb.DoBusi()

    //進行轉賬
    tb := &TransferBanker{}
    tb.DoBusi()

    //進行支付
    pb := &PayBanker{}
    pb.DoBusi()

}

當然我們也可以根據AbstractBanker設計一個小框架


//實現架構層(基於抽象層進行業務封裝-針對interface介面進行封裝)
func BankerBusiness(banker AbstractBanker) {
    //通過介面來向下呼叫,(多型現象)
    banker.DoBusi()
}

那麼main中可以如下實現業務呼叫:

func main() {
    //進行存款
    BankerBusiness(&SaveBanker{})

    //進行存款
    BankerBusiness(&TransferBanker{})

    //進行存款
    BankerBusiness(&PayBanker{})
}

再看開閉原則定義: 開閉原則:一個軟體實體如類、模組和函式應該對擴充套件開放,對修改關閉。 簡單的說就是在修改需求的時候,應該儘量通過擴充套件來實現變化,而不是通過修改已有程式碼來實現變化。

3 介面的意義

好了,現在interface已經基本瞭解,那麼介面的意義最終在哪裡呢,想必現在你已經有了一個初步的認知,實際上介面的最大的意義就是實現多型的思想,就是我們可以根據interface型別來設計API介面,那麼這種API介面的適應能力不僅能適應當下所實現的全部模組,也適應未來實現的模組來進行呼叫。 呼叫未來可能就是介面的最大意義所在吧,這也是為什麼架構師那麼值錢,因為良好的架構師是可以針對interface設計一套框架,在未來許多年卻依然適用。

4 物件導向中的依賴倒轉原則

4.1 耦合度極高的模組關係設計

混亂的依賴關係.png

package main

import "fmt"

// === > 賓士汽車 <===
type Benz struct {

}

func (this *Benz) Run() {
    fmt.Println("Benz is running...")
}

// === > 寶馬汽車  <===
type BMW struct {

}

func (this *BMW) Run() {
    fmt.Println("BMW is running ...")
}

//===> 司機張三  <===
type Zhang3 struct {
    //...
}

func (zhang3 *Zhang3) DriveBenZ(benz *Benz) {
    fmt.Println("zhang3 Drive Benz")
    benz.Run()
}

func (zhang3 *Zhang3) DriveBMW(bmw *BMW) {
    fmt.Println("zhang3 drive BMW")
    bmw.Run()
}

//===> 司機李四 <===
type Li4 struct {
    //...
}

func (li4 *Li4) DriveBenZ(benz *Benz) {
    fmt.Println("li4 Drive Benz")
    benz.Run()
}

func (li4 *Li4) DriveBMW(bmw *BMW) {
    fmt.Println("li4 drive BMW")
    bmw.Run()
}

func main() {
    //業務1 張3開賓士
    benz := &Benz{}
    zhang3 := &Zhang3{}
    zhang3.DriveBenZ(benz)

    //業務2 李四開寶馬
    bmw := &BMW{}
    li4 := &Li4{}
    li4.DriveBMW(bmw)
}

我們來看上面的程式碼和圖中每個模組之間的依賴關係,實際上並沒有用到任何的interface介面層的程式碼,顯然最後我們的兩個業務 張三開賓士, 李四開寶馬,程式中也都實現了。但是這種設計的問題就在於,小規模沒什麼問題,但是一旦程式需要擴充套件,比如我現在要增加一個豐田汽車 或者 司機王五, 那麼模組和模組的依賴關係將成指數級遞增,想蜘蛛網一樣越來越難維護和捋順。

4.2 面向抽象層依賴倒轉

依賴倒轉設計.png 如上圖所示,如果我們在設計一個系統的時候,將模組分為3個層次,抽象層、實現層、業務邏輯層。那麼,我們首先將抽象層的模組和介面定義出來,這裡就需要了interface介面的設計,然後我們依照抽象層,依次實現每個實現層的模組,在我們寫實現層程式碼的時候,實際上我們只需要參考對應的抽象層實現就好了,實現每個模組,也和其他的實現的模組沒有關係,這樣也符合了上面介紹的開閉原則。這樣實現起來每個模組只依賴物件的介面,而和其他模組沒關係,依賴關係單一。系統容易擴充套件和維護。 我們在指定業務邏輯也是一樣,只需要參考抽象層的介面來業務就好了,抽象層暴露出來的介面就是我們業務層可以使用的方法,然後可以通過多型的線下,介面指標指向哪個實現模組,呼叫了就是具體的實現方法,這樣我們業務邏輯層也是依賴抽象成程式設計。 我們就將這種的設計原則叫做依賴倒轉原則。 來一起看一下修改的程式碼:

package main

import "fmt"

// ===== >   抽象層  < ========
type Car interface {
    Run()
}

type Driver interface {
    Drive(car Car)
}

// ===== >   實現層  < ========
type BenZ struct {
    //...
}

func (benz * BenZ) Run() {
    fmt.Println("Benz is running...")
}

type Bmw struct {
    //...
}

func (bmw * Bmw) Run() {
    fmt.Println("Bmw is running...")
}

type Zhang_3 struct {
    //...
}

func (zhang3 *Zhang_3) Drive(car Car) {
    fmt.Println("Zhang3 drive car")
    car.Run()
}

type Li_4 struct {
    //...
}

func (li4 *Li_4) Drive(car Car) {
    fmt.Println("li4 drive car")
    car.Run()
}

// ===== >   業務邏輯層  < ========
func main() {
    //張3 開 寶馬
    var bmw Car
    bmw = &Bmw{}

    var zhang3 Driver
    zhang3 = &Zhang_3{}

    zhang3.Drive(bmw)

    //李4 開 賓士
    var benz Car
    benz = &BenZ{}

    var li4 Driver
    li4 = &Li_4{}

    li4.Drive(benz)
}

4.3 依賴倒轉小練習

模擬組裝2臺電腦, --- 抽象層 ---有顯示卡Card 方法display,有記憶體Memory 方法storage,有處理器CPU 方法calculate --- 實現層層 ---有 Intel因特爾公司 、產品有(顯示卡、記憶體、CPU),有 Kingston 公司, 產品有(記憶體3),有 NVIDIA 公司, 產品有(顯示卡) --- 邏輯層 ---1. 組裝一臺Intel系列的電腦,並執行,2. 組裝一臺 Intel CPU Kingston記憶體 NVIDIA顯示卡的電腦,並執行

/*
    模擬組裝2臺電腦
    --- 抽象層 ---
    有顯示卡Card  方法display
    有記憶體Memory 方法storage
    有處理器CPU   方法calculate

    --- 實現層層 ---
    有 Intel因特爾公司 、產品有(顯示卡、記憶體、CPU)
    有 Kingston 公司, 產品有(記憶體3)
    有 NVIDIA 公司, 產品有(顯示卡)

    --- 邏輯層 ---
    1. 組裝一臺Intel系列的電腦,並執行
    2. 組裝一臺 Intel CPU  Kingston記憶體 NVIDIA顯示卡的電腦,並執行
*/
package main

import "fmt"

//------  抽象層 -----
type Card interface{
    Display()
}

type Memory interface {
    Storage()
}

type CPU interface {
    Calculate()
}

type Computer struct {
    cpu CPU
    mem Memory
    card Card
}

func NewComputer(cpu CPU, mem Memory, card Card) *Computer{
    return &Computer{
        cpu:cpu,
        mem:mem,
        card:card,
    }
}

func (this *Computer) DoWork() {
    this.cpu.Calculate()
    this.mem.Storage()
    this.card.Display()
}

//------  實現層 -----
//intel
type IntelCPU struct {
    CPU 
}

func (this *IntelCPU) Calculate() {
    fmt.Println("Intel CPU 開始計算了...")
}

type IntelMemory struct {
    Memory
}

func (this *IntelMemory) Storage() {
    fmt.Println("Intel Memory 開始儲存了...")
}

type IntelCard struct {
    Card
}

func (this *IntelCard) Display() {
    fmt.Println("Intel Card 開始顯示了...")
}

//kingston
type KingstonMemory struct {
    Memory
}

func (this *KingstonMemory) Storage() {
    fmt.Println("Kingston memory storage...")
}

//nvidia
type NvidiaCard struct {
    Card
}

func (this *NvidiaCard) Display() {
    fmt.Println("Nvidia card display...")
}

//------  業務邏輯層 -----
func main() {
    //intel系列的電腦
    com1 := NewComputer(&IntelCPU{}, &IntelMemory{}, &IntelCard{})
    com1.DoWork()

    //雜牌子
    com2 := NewComputer(&IntelCPU{}, &KingstonMemory{}, &NvidiaCard{})
    com2.DoWork()
}

關於作者:

作者:Aceld(劉丹冰)

mail: danbing.at@gmail.com">danbing.at@gmail.com

github: https://github.com/aceld

原創書籍gitbook: http://legacy.gitbook.com/@aceld

原創宣告:未經作者允許請勿轉載, 如果轉載請註明出處

相關文章