實證與虛無,抽象和具象,Go lang1.18入門精煉教程,由白丁入鴻儒,Go lang介面(interface)的使用EP08

劉悅的技術部落格 發表於 2022-11-23
Go

看到介面這兩個字,我們一定會聯想到面向介面程式設計。說白了就是介面指定執行物件的具體行為,也就是介面表示讓執行物件具體應該做什麼,所以,普遍意義上講,介面是抽象的,而實際執行行為,則是具象的。

介面(interface)的定義

在Go lang中,介面是一組方法簽名,當型別為介面中的所有方法提供定義時,它被稱為實現介面。和麵向介面的思想非常類似,介面指定了型別應該具有的方法,型別決定了到底該怎麼實現這些方法:

/* 定義介面 */  
type interface_name interface {  
   method_name1 [return_type]  
   method_name2 [return_type]  
   method_name3 [return_type]  
   ...  
   method_namen [return_type]  
}  
  
/* 定義結構體 */  
type struct_name struct {  
   /* variables */  
}  
  
/* 實現介面方法 */  
func (struct_name_variable struct_name) method_name1() [return_type] {  
   /* 方法實現 */  
}  
...  
func (struct_name_variable struct_name) method_namen() [return_type] {  
   /* 方法實現*/  
}

具體實現方式:

package main  
  
import (  
    "fmt"  
)  
  
type Phone interface {  
    call()  
}  
  
type Android struct {  
}  
  
func (android Android) call() {  
    fmt.Println("I am Android")  
}  
  
type Ios struct {  
}  
  
func (ios Ios) call() {  
    fmt.Println("I am Ios")  
}  
  
func main() {  
    var phone Phone  
  
    phone = new(Android)  
    phone.call()  
  
    phone = new(Ios)  
    phone.call()  
  
}

程式返回:

I am Android  
I am Ios

是的,現在我們可以結構體、函式、以及介面三箭齊發了,這裡首先定義好手機介面,並且指定call()方法,意思是我在抽象層面擁有一個手機,手機應該具有打電話的功能。

隨後分別定義結構體和函式(也是方法),分別具現化的實現介面的指定行為,精神上大家是一樣的,但肉體上,一個是安卓,另一個則是蘋果。

Go lang中,介面可以被任意的物件實現,同樣地,一個物件也可以實現任意多個介面,任意的型別都實現了空介面(interface{}),也就是包含0個method的interface。

誠然,如果單獨使用結構體,我們也可以,實現類似多型的結構:



package main  
  
import "fmt"  
  
type Human struct {  
    name  string  
    age   int  
    phone string  
}  
type Student struct {  
    Human  //匿名欄位  
    school string  
    loan   float32  
}  
type Employee struct {  
    Human   //匿名欄位  
    company string  
    money   float32  
} //Human實現Sayhi方法  
func (h Human) SayHi() {  
    fmt.Printf("Hi, I am %s you can call me on %s\n", h.name, h.phone)  
} //Human實現Sing方法  
func (h Human) Sing(lyrics string) {  
    fmt.Println("。。。。。。。。", lyrics)  
} //Employee重寫Human的SayHi方法  
func (e Employee) SayHi() {  
    fmt.Printf("Hi, I am %s, I work at %s. Call me on %s\n", e.name,  
        e.company, e.phone) //Yes you can split into 2 lines here.  
}

可以單獨為結構體定義方法,但如果介面參與邏輯:

type Men interface {  
    SayHi()  
    Sing(lyrics string)  
}  
  

func main() {  
    mike := Student{Human{"Mike", 10, "1"}, "MIT", 0.00}  
    paul := Student{Human{"Paul", 20, "2"}, "Harvard", 100}  
    sam := Employee{Human{"Sam", 30, "3"}, "Golang Inc.", 1000}  
    Tom := Employee{Human{"Tom", 40, "4"}, "Things Ltd.", 5000}  
    //定義Men型別的變數i  
    var i Men  
    //i能儲存Student  
    i = mike  
    fmt.Println("This is Mike, a Student:")  
    i.SayHi()  
    i.Sing("song")  
    //i也能儲存Employee  
    i = Tom  
    fmt.Println("This is Tom, an Employee:")  
    i.SayHi()  
    i.Sing("song")  
    //定義了slice Men  
    fmt.Println("Let's use a slice of Men and see what happens")  
    x := make([]Men, 3)  
    //T這三個都是不同型別的元素,但是他們實現了同一個介面  
    x[0], x[1], x[2] = paul, sam, mike  
    for _, value := range x {  
        value.SayHi()  
    }  
}

程式返回:



This is Mike, a Student:  
Hi, I am Mike you can call me on 1  
。。。。。。。。 song  
This is Tom, an Employee:  
Hi, I am Tom, I work at Things Ltd.. Call me on 4  
。。。。。。。。 song  
Let's use a slice of Men and see what happens  
Hi, I am Paul you can call me on 2  
Hi, I am Sam, I work at Golang Inc.. Call me on 3  
Hi, I am Mike you can call me on 1

由此可見,介面的出現,把本來不相關的結構體型別以抽象的形式結合了起來,不同的型別實現內容不同的共性方法。

也就是說,Men介面型別的變數i,那麼i裡面可以存Human、Student或者Employee值,所以i是抽象的,而Human、Student或者Employee就是i的具象化操作。

介面指定函式引數

介面不僅僅可以指定無參方法,也可以指定具體的引數,讓函式接受各種型別的引數:

package main  
  
import "fmt"  
  
type Human interface {  
    Len()  
}  
type Student interface {  
    Human  
}  
  
type Test struct {  
}  
  
func (h *Test) Len() {  
    fmt.Println("10個")  
}  
func main() {  
    var s Student  
    s = new(Test)  
    s.Len()  
}

程式返回:

10個

這裡使用介面巢狀的形式,Human介面定義了Len方法,結構體Test實現了所有的Len介面方法,當結構體s中呼叫Test結構體的時候,s就相當於Python中的繼承,s繼承了Test,因此,s可以不用重寫所有的Human介面中的方法,因為父構造器已經實現了介面。

鴨子型別(ducktyping)

什麼是鴨子型別?當看到一隻鳥走起來像鴨子、游泳起來像鴨子、叫起來也像鴨子,那麼這隻鳥就可以被稱為鴨子。

所謂遠看山有色,近聽水無聲,春去花還在,人來鳥不驚,意象上來講,一個事物究竟是不是某一種型別,取決於它具不具備這個型別的特性,這就是鴨子型別的本質。

所以鴨子型別主要描述事物的外部行為而非內部構造,在物件導向的程式語言中,比如Python中,一個物件有效的語義,不是由繼承自特定的類或實現特定的介面,而是由"當前方法和屬性的集合"決定。

編寫test.py檔案:

class PsyDuck():  
    def gaga(self):  
        print("這是可達鴨")  
  
  
# 使用的物件和方法  
class DoningdDuck():  
    def gaga(self):  
        print("這是唐老鴨")  
  
  
# 被呼叫的函式  
def duckSay(func):  
    return func.gaga()  
  
  
# 限制呼叫方式  
if __name__ != '__main__':  
    print("must __main__")  
  
if __name__ == "__main__":  
  
    # 例項化物件  
    duck = PsyDuck()  
    person = DoningdDuck()  
    # 呼叫函式  
    duckSay(duck)  
    duckSay(person)

程式返回:

這是可達鴨  
這是唐老鴨

所以到底是什麼鴨子不重要,重要的是呼叫了誰的例項。

再來看看go lang的手筆:

package main  
  
import "fmt"  
  
//定義一個鴨子介面  
//Go 介面是一組方法的集合,可以理解為抽象的型別。它提供了一種非侵入式的介面。任何型別,只要實現了該介面中方法集,那麼就屬於這個型別。  
type Duck interface {  
    Gaga()  
}  
  
//假設現在有一個可達鴨型別  
type PsyDuck struct{}  
  
//可達鴨宣告方法-滿足鴨子會嘎嘎叫的特性  
func (pd PsyDuck) Gaga() {  
    fmt.Println("this is PsyDuck")  
}  
  
//假設現在有一個唐老鴨型別  
type DonaldDuck struct{}  
  
//唐老鴨宣告方法-滿足鴨子會嘎嘎叫的特性  
func (dd DonaldDuck) Gaga() {  
    fmt.Println("this is DoningdDuck")  
}  
  
//要呼叫的函式 - 負責執行鴨子能做的事情,注意這裡的引數,有型別限制為Duck介面  
func DuckSay(d Duck) {  
    d.Gaga()  
}  
  
func main() {  
    //提示開始列印  
    fmt.Println("duck typing")  
  
    //例項化物件  
    var pd PsyDuck    //可達鴨型別  
    var dd DonaldDuck //唐老鴨型別  
  
    //呼叫方法  
    DuckSay(pd) //因為可達鴨實現了所有鴨子的函式,所以可以這麼用  
    DuckSay(dd) //因為唐老鴨實現了所有鴨子的函式,所以可以這麼用  
}

程式返回:

duck typing  
this is PsyDuck  
this is DoningdDuck

這裡首先定義抽象的鴨子介面,指定gaga方法,不同的結構體:可達鴨、唐老鴨分別繫結並且實現了鴨子介面的方法,然後宣告一個呼叫函式,在執行的時候,將結構體變數傳遞給呼叫函式,動態地實現了不同型別的方法。

結語

所謂介面(interface)的抽象性,就是從表面看到本質,從片面看到整體,然後抽出那些穩定的、共有的特性。平時我們會考慮程式碼的重用性,元件的複用性,同一個功能對不同場景的複用性,有了複用的能力,就能夠用更少的開發去滿足更多場景的同類需求問題。從而能夠從一個具體的需求,看到一類的需求,看到衍生的相關的需求,甚至再對需求進行分類,看到更高層面的需求。進而才能夠系統性解決同類的需求而不是就事論事點對點解決問題。

所以,總的來說,介面的極致就是抽象,而抽象的極致,則是格局,介面,可以更好的幫我們擴大程式視野的格局。