【Golang】interface接口設計原則

interface接口

interface 是GO語言的基礎特性之一。可以理解爲一種類型的規範或者約定。它跟java,C# 不太一樣,不需要顯示說明實現了某個接口,它沒有繼承或子類或“implements”關鍵字,只是通過約定的形式,隱式的實現interface 中的方法即可。因此,Golang 中的 interface 讓編碼更靈活、易擴展。

如何理解go 語言中的interface ?只需記住以下三點即可。

  1. interface是方法聲明的集合
  2. 任何類型的對象實現了在interface接口中聲明的全部方法,則表明該類型實現了接口。
  3. interface可以作爲一種數據類型,實現了該接口的任何對象都可以給對應的接口類型變量賦值。
  1. interface可以被任意對象實現,一個類型/對象也可以實現多個interface.
  2. 方法不能重載,如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()方法,打印的效果不同,那麼就是體現出了多態的特性。

面向對象中的開閉原則

平鋪式的模塊設計

那麼作爲 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()支付等。那麼如果這個業務員模塊只有這幾個方法還好,但是隨着我們的程序寫的越來越複雜,銀行業務員可能就要增加方法,會導致業務員模塊越來越臃腫。
使用Golang的interface接口設計原則
這樣的設計會導致,當我們去給Banker添加新的業務的時候,會直接修改原有的Banker代碼,那麼Banker模塊的功能會越來越多,出現問題的機率也就越來越大,假如此時Banker已經有99個業務了,現在我們要添加第100個業務,可能由於一次的不小心,導致之前99個業務也一起崩潰,因爲所有的業務都在一個Banker類裏,他們的耦合度太高,Banker的職責也不夠單一,代碼的維護成本隨着業務的複雜正比成倍增大

開閉設計原則

那麼,如果我們擁有接口, interface這個東西,那麼我們就可以抽象一層出來,製作一個抽象的Banker模塊,然後提供一個抽象的方法。分別根據這個抽象模塊,去實現支付Banker(實現支付方法),轉賬Banker(實現轉賬方法)
如下:
使用Golang的interface接口設計原則
那麼依然可以搞定程序的需求。然後,當我們想要給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{})
}

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

接口的意義

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

面向對象中的依賴倒轉原則

耦合度極高的模塊設計

使用Golang的interface接口設計原則

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\color{red}{interface} 接口層的代碼,顯然最後我們的兩個業務,\color{red}{張三開奔馳, 李四開寶馬},程序中也都實現了。但是這種設計的問題就在於,小規模沒什麼問題,但是一旦程序需要擴展,比如我現在要增加一個\color{red}{豐田汽車} 或者 \color{red}{司機王五}, 那麼模塊和模塊的依賴關係將成指數級遞增,想蜘蛛網一樣越來越難維護和捋順。

面向抽象層的依賴倒轉設計

使用Golang的interface接口設計原則
  如上圖所示,如果我們在設計一個系統的時候,將模塊分爲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)
}

依賴倒轉小案例

模擬組裝2臺電腦

  • 抽象層
    有顯卡Card 方法display,有內存Memory 方法storage,有處理器CPU 方法calculate

  • 實現層
    有 Intel因特爾公司 、產品有(顯卡、內存、CPU),有 Kingston 公司, 產品有(內存3),有 NVIDIA 公司, 產品有(顯卡)

  • 邏輯層
    組裝一臺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()
}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章