10 Go面向“對(duì)象”:面向接口編程

一、面向?qū)ο蟪跆?/h3>

在軟件開(kāi)發(fā)領(lǐng)域,你應(yīng)該聽(tīng)到過(guò)過(guò)程式編程、面向?qū)ο缶幊?、甚至函?shù)式編程等軟件開(kāi)發(fā)方式。而面向?qū)ο缶幊谈窃诂F(xiàn)今大行其道,JAVA就是面向?qū)ο笳Z(yǔ)言的代表,在JAVA中一切皆對(duì)象,它讓編程中的一切元素、甚至設(shè)計(jì)方式都標(biāo)準(zhǔn)化,這更有利于大型應(yīng)用的編寫(xiě)。

1.什么是面向?qū)ο缶幊蹋?/h4>

面向?qū)ο缶幊?,?jiǎn)稱OOP。在OOP的理念下,任何事物無(wú)論簡(jiǎn)單還是復(fù)雜都可以用對(duì)象表示,每個(gè)對(duì)象都包含屬性和方法,屬性表示對(duì)象是什么?有什么特征?方法表示對(duì)象能做什么?有什么能力?任何應(yīng)用的構(gòu)建都轉(zhuǎn)化成對(duì)象關(guān)系的設(shè)計(jì),這演化成一套標(biāo)準(zhǔn)化的面向?qū)ο笤O(shè)計(jì)模式。

類(lèi)和對(duì)象

要理解OOP,首先要理解類(lèi)和對(duì)象的關(guān)系,類(lèi)是設(shè)計(jì)層面的概念,而對(duì)象則是程序運(yùn)行時(shí)的概念,OOP程序設(shè)計(jì)基于類(lèi)的設(shè)計(jì),類(lèi)在程序運(yùn)行時(shí)實(shí)例化為對(duì)象實(shí)現(xiàn)真正的業(yè)務(wù)邏輯。簡(jiǎn)而言之,所謂類(lèi)可以理解成對(duì)象的模板,你編寫(xiě)一個(gè)類(lèi),在運(yùn)行時(shí)需要實(shí)例化才能在程序調(diào)用棧中傳遞。

屬性和方法

在過(guò)程式編程中,我們熟悉變量和函數(shù),使用這些基本元素我們實(shí)現(xiàn)業(yè)務(wù)邏輯。而屬性和方法是對(duì)象內(nèi)部的特征,咋一看他們很像,其實(shí)本質(zhì)上是將實(shí)現(xiàn)特定功能的函數(shù)和變量封裝成一個(gè)整體,即對(duì)象。一個(gè)對(duì)象包含一系列的屬性和方法專(zhuān)注于實(shí)現(xiàn)某種特定功能。

接口

接口是一系列方法的聲明,是一些方法特征的集合,一個(gè)接口只有方法的特征沒(méi)有方法的實(shí)現(xiàn),因此這些方法可以在不同的地方被不同的類(lèi)實(shí)現(xiàn),而這些實(shí)現(xiàn)可以具有不同的行為(功能),接口是多態(tài)實(shí)現(xiàn)的基礎(chǔ)。

2.面向?qū)ο笕筇匦?/h4>

封裝

將實(shí)現(xiàn)特定功能的屬性和方法抽象封裝成類(lèi),提供public/private/protected訪問(wèn)修飾符,控制外部訪問(wèn)的可見(jiàn)性。

.對(duì)業(yè)務(wù)相近的變量和函數(shù)封裝在類(lèi)/結(jié)構(gòu)體中
.變量=>類(lèi)/結(jié)構(gòu)體的屬性
.函數(shù)=>類(lèi)/結(jié)構(gòu)體的方法
繼承

對(duì)已經(jīng)實(shí)現(xiàn)的類(lèi)或接口提供重用或擴(kuò)展的能力,子類(lèi)可完全繼承父類(lèi)的所有能力。繼承的過(guò)程,就是從一般到特殊的過(guò)程,其過(guò)程可以通過(guò)繼承和組合來(lái)實(shí)現(xiàn)。

.繼承的類(lèi)擁有父類(lèi)/父結(jié)構(gòu)體的全部屬性和方法
.接口繼承則是一行代碼擁有父類(lèi)接口的全部抽象方法
.繼承可以節(jié)約大量重復(fù)代碼
.繼承的目的:
    .提高代碼的復(fù)用度
    .拓展出新的屬性和方法
    .改進(jìn)父類(lèi)/結(jié)構(gòu)體的方法
    .以覆寫(xiě)父類(lèi)/結(jié)構(gòu)體的方法類(lèi)實(shí)現(xiàn)
多態(tài)

多態(tài)是指一個(gè)父類(lèi)/接口可以擁有多種具體的子類(lèi)實(shí)現(xiàn)形態(tài);多態(tài)的好處是可以根據(jù)業(yè)務(wù)需要去方便地調(diào)度子類(lèi)們的共性和個(gè)性

3.面向?qū)ο缶幊痰膬?yōu)缺點(diǎn)

優(yōu)點(diǎn)
  • 高效:面向?qū)ο笤O(shè)計(jì)結(jié)構(gòu)、模塊清晰的應(yīng)用,有利于大型應(yīng)用的開(kāi)發(fā),團(tuán)隊(duì)成員各自維護(hù)局部模塊,降低了成員開(kāi)發(fā)的復(fù)雜性。
  • 易維護(hù):由于高內(nèi)聚低耦合,各個(gè)模塊的維護(hù)都是局部的,這非常方便定位問(wèn)題。
  • 易擴(kuò)展:繼承、封裝、多態(tài)的特性,以及標(biāo)準(zhǔn)化的設(shè)計(jì)模式設(shè)計(jì)出高內(nèi)聚、低耦合的系統(tǒng)結(jié)構(gòu),使得系統(tǒng)更靈活、更容易擴(kuò)展,而且成本較低。
缺點(diǎn)

俗話說(shuō),“如果你手上只有錘子,那么你看什么都是釘子”,OOP把一切都當(dāng)成對(duì)象,但現(xiàn)實(shí)世界是復(fù)雜的,雖然設(shè)計(jì)模式就是解決應(yīng)用中抽象的設(shè)計(jì)問(wèn)題的。這把編碼階段的復(fù)雜性提到設(shè)計(jì)階段。

  • 相對(duì)過(guò)程式編程,面向?qū)ο缶幊痰某绦蚪Y(jié)構(gòu)性能有所下降;
  • 提高系統(tǒng)設(shè)計(jì)的復(fù)雜度;

二、Go的面向“對(duì)象”

了解了面向?qū)ο缶幊痰乃枷耄覀冊(cè)賮?lái)看Go的面向?qū)ο?,?yán)格來(lái)說(shuō),Go并非面向?qū)ο缶幊陶Z(yǔ)言,Go有自己的設(shè)計(jì)理念,面向?qū)ο笾皇且环N軟件開(kāi)發(fā)方法,Go有自己的支持方式。

1.沒(méi)有類(lèi)和對(duì)象,只有類(lèi)型和值

傳統(tǒng)的JAVA類(lèi)和對(duì)象:

//類(lèi)定義
public class Person {
    private String name
    ...
    //構(gòu)造方法
    public Person( String name){
        this.name = name
    }
    //公開(kāi)方法
    public String getName (){
        return this.name
    }
    ...
}

//類(lèi)實(shí)例化成對(duì)象
Person p = new Person("name")
//調(diào)用并打印公開(kāi)方法
System.out.print(p.getName())

Go的類(lèi)型和值:

  • 通過(guò)定義結(jié)構(gòu)體類(lèi)型的方式實(shí)現(xiàn)類(lèi)似類(lèi)的結(jié)構(gòu)
  • 沒(méi)有構(gòu)造方法,直接使用NewXXX()工廠方法
//類(lèi)型定義
type Preson struct {
    name string
}
//類(lèi)型方法
func (p *Person) SetName(name string) {
    p.name = name
}

//類(lèi)型方法
func (p *Person) GetName() string {
    return p.name
}

//工廠方法
func NewPerson(name string) *Person{
    p := new(Person)
    p.SetName(name)
    return p
}

//獲取person類(lèi)型的值指針
p := NewPerson("name")
fmt.Println(p.GetName())

2.聚合和嵌入優(yōu)于繼承

傳統(tǒng)的JAVA繼承:

public class Student extend Person{
    private String school

    //構(gòu)造方法
    public Student(){
        super() //直接使用父類(lèi)的構(gòu)造方法
    }
    
    public void doSomething(){
        //block
    }
    ...
}

Go的聚合和嵌入:

type Preson struct {
    Name string
    age int
}
func (p *Person) SetAge(age int){
    p.age = age
}

//嵌入
type Student1 struct {
    Person //匿名字段為嵌入類(lèi)型
    School string
}

//聚合
type Student2 struct {
    Ps     Person //命名字段為聚合類(lèi)型
    School string
}

func OOPDemo() {
    s1 := new(Student1)
    s1.Name = "fun1" //嵌入的類(lèi)型可直接使用其內(nèi)部屬性,更像繼承
    s1.School = "Social University1"
    s1.SetAge(18) //可以直接使用Person的方法

    s2 := new(Student2)
    s2.Ps.Name = "fun2" //聚合的類(lèi)型需要先訪問(wèn)屬性值名,在訪問(wèn)屬性值內(nèi)部的屬性
    s2.School = "Social University2"
    s1.Ps.SetAge(18) //可以間接使用Person的方法
    
    //OUTPUT:
    //s1: &{{fun1} Social University1}
    //s2: &{{fun2} Social University2}
}


3.自由的結(jié)構(gòu)體屬性類(lèi)型
//接口
type IPerson interface {
    SetName(string)
    GetName() string
}
//自定義函數(shù)
type MyFuncType func(int) int

//大雜燴結(jié)構(gòu)體
type Something struct {
    a   int         //基本數(shù)據(jù)類(lèi)型
    b   []byte      //切片
    p   Person      //結(jié)構(gòu)體
    s   *Student    //指針
    i   IPerson     //接口
    f   MyFuncType  //自定義函數(shù)類(lèi)型
    any interface{} //任意類(lèi)型
}

4.獨(dú)立的方法定義更靈活

Go的類(lèi)型方法在外部任意地方,只要定義的方法接收者為該類(lèi)型,那定義的方法就是該類(lèi)型的方法。

//類(lèi)型定義
type Preson struct {
    name string
}
//類(lèi)型方法,接收者為該類(lèi)型的指針
func (p *Person) SetName(name string) {
    p.name = name
}
//類(lèi)型方法,接收者為該類(lèi)型的值
func (p Person) GetName() string {
    return p.name
}

//調(diào)用示例
p := new(Person)
//SetName()方法接收者為指針,使用指針類(lèi)型或值類(lèi)型去調(diào)用都可以
p.SetName("fun") //可以
*p.SetName("func") //指針取值后再去調(diào)用也可以。

//GetName()方法接收者為值類(lèi)型,所以調(diào)用該方法只能為值
p.GetName() //不可以
*p.GetName() //可以

方法接收者一般有兩種情況:

  • 接收者為指針:允許該類(lèi)型的指針和值調(diào)用該方法;
  • 接收者為值:只允許該類(lèi)型的值調(diào)用該方法。

一般無(wú)特殊需要,建議把接收者直接設(shè)置為指針類(lèi)型

5.沒(méi)有顯式public/private/protected,只有隱式大小寫(xiě)控制

Go的訪問(wèn)控制基于包,包內(nèi)的成員變量、常量、類(lèi)型、函數(shù)基于命名首字母的大小寫(xiě)控制。
其結(jié)構(gòu)體類(lèi)型的屬性方法也類(lèi)似,基于命名首字母的大小寫(xiě)控制。

type Preson struct {
    Name string  //外部可見(jiàn)并可修改
    age int //僅內(nèi)部可見(jiàn)內(nèi)部方法修改
}

//類(lèi)型方法,外部可見(jiàn)
func (p *Person) SetAge(age int) {
    //調(diào)用內(nèi)部方法
    p.setage(name)
}
//類(lèi)型方法,內(nèi)部可見(jiàn)
func (p *Person) setage(age int) {
    p.age = age
}

三、Go面向接口編程

嚴(yán)格意義講,Go因沒(méi)有對(duì)象概念,所以并非面向?qū)ο缶幊陶Z(yǔ)言,但因其對(duì)OOP的深刻理解,使其設(shè)計(jì)理念更切合“面向接口”,接口的多態(tài)特性使其在設(shè)計(jì)高內(nèi)聚低耦合的系統(tǒng)發(fā)揮更重要的作用。面向接口概念是面向?qū)ο蟮难苌?,在多年的開(kāi)發(fā)積累中,人們發(fā)現(xiàn)針對(duì)接口設(shè)計(jì)系統(tǒng)可以讓系統(tǒng)擴(kuò)展性和維護(hù)性更好。因此,Go的OOP針對(duì)接口設(shè)計(jì),可以說(shuō)接口是頭等的類(lèi)型也不為過(guò)。

在Go語(yǔ)言中,接口擁有舉足輕重的地位,而面向接口編程也是Go語(yǔ)言核心的設(shè)計(jì)理念。接口是高度抽象的概念,它是一種類(lèi)型可由type關(guān)鍵字聲明,接口內(nèi)部聲明一個(gè)或多個(gè)方法簽名,因此不能實(shí)例化,一般創(chuàng)建一個(gè)類(lèi)型為接口的變量,它可以被賦值為任何滿足該接口聲明的實(shí)際類(lèi)型的值,作為類(lèi)型傳遞。

1.接口 —— 實(shí)現(xiàn)鴨子類(lèi)型

當(dāng)它走起來(lái)像鴨子,叫起來(lái)也像鴨子,那它就是鴨子。

接口本身是類(lèi)型,但它卻不關(guān)心類(lèi)型,它只關(guān)心行為,如果類(lèi)型T的行為(實(shí)現(xiàn)的方法)和定義的接口I聲明的方法簽名符合,那么類(lèi)型T就實(shí)現(xiàn)了I的接口。在方法參數(shù)傳遞或各種類(lèi)型校驗(yàn)中,T就是I的實(shí)現(xiàn)。

一個(gè)Go接口也是類(lèi)型定義,其內(nèi)部聲明了其規(guī)定的方法簽名:

//任何實(shí)現(xiàn)了IAnimal簽名方法的類(lèi)型都屬于IAnimal類(lèi)型
type IAnimal interface {
    Live()
    Dead()
}

//Humen結(jié)構(gòu)體
type Monkey struct {}
func (m *Monkey) Live() {
    fmt.Println("猴子活著吃香蕉?。。?)
}
func (m *Monkey) Dead() {
    fmt.Println("香蕉有毒,猴子死了?。。?)
}
//Cat結(jié)構(gòu)體
type Cat struct {}
func (c *Cat) Live() {
    fmt.Println("貓活著吃魚(yú)?。?!")
}
func (c *Cat) Dead() {
    fmt.Println("貓吃了河豚死翹翹?。?!")
}

//演示:
//聲明一個(gè)IAnimal的切片
 var zoo []IAnimal 
func addAnimal(animal IAnimal) {
    zoo = append(zoo,animal)
}
func OOPDemo02(){
    m := new(Monkey)
    c := new(Cat)
    addAnimal(m)
    addAnimal(c)
    
    //遍歷
    for _,animal := range zoo {
        animal.Live()
        animal.Dead()
    }
}

//OUTPUT:
//猴子活著吃香蕉?。?!
//香蕉有毒,猴子死了?。。?//貓活著吃魚(yú)?。?!
//貓吃了河豚死翹翹!??!

2.接口類(lèi)型限定賦值

類(lèi)型值賦值給接口類(lèi)型

type IPerson interface{
    GetName()
    SetName()
}

//任何實(shí)現(xiàn)GetName()、SetName()方法的類(lèi)型都可賦值給person
var person IPerson


接口類(lèi)型賦值給另一接口類(lèi)型

type Writer interface{    //父接口
    Write(buf []byte) (n int,err error)
}
type ReadWriter interface{    //子接口
    Read(buf []byte) (n int,err error)
    Write(buf []byte) (n int,err error)
}
var file1 ReadWriter=new(File)   //子接口實(shí)例
var file2 Writer=file1           //子接口實(shí)例賦值給父接口

3.非侵入式接口

上面演示我們看到,接口的運(yùn)用在編碼中是非侵入式的,在經(jīng)典的OOP語(yǔ)言中,實(shí)現(xiàn)接口需要類(lèi)顯式實(shí)現(xiàn),例如:

public class Person implements IPerson {
    //block
}

而Go并不需要顯式實(shí)現(xiàn),類(lèi)型只需實(shí)現(xiàn)特定接口的方法簽名即可。當(dāng)然這種極度寬松的實(shí)現(xiàn)方式有可能讓你定義的類(lèi)型“不小心”就實(shí)現(xiàn)了某些接口能力,一旦你的類(lèi)型方法和某些接口方法簽名一致時(shí)就會(huì)如此。

//該類(lèi)型實(shí)現(xiàn)了標(biāo)準(zhǔn)包的io.Writer接口
type F struct {}

func (f *F) Write(p []byte) (n int, err error) {
    //block
}
4.接口嵌入

和結(jié)構(gòu)體類(lèi)型類(lèi)似,接口也可以嵌入其他接口,接口只能嵌入不能聚合!

以下演示接口嵌入的示例:

//生物接口
type IBeing interface{
    Live()
    Dead()
}
//動(dòng)物接口嵌入生物接口
type IAnimal interface {
    IBeing
    Hunting()
}
//植物接口嵌入生物接口
type IPlant interface {
    IBeing
    Growing()
}

type Tiger struct {}
func (t *Tiger) Live() {
    fmt.Println("老虎活著稱大王?。。?)
}
func (t *Tiger) Dead() {
    fmt.Println("老虎戰(zhàn)斗死了?。?!")
}
func (t *Tiger) Hunting() {
    fmt.Println("老虎捕獵?。。?)
}

type Flower struct{}
func (t *Flower) Live() {
    fmt.Println("花兒享受陽(yáng)光?。?!")
}
func (t *Flower) Dead() {
    fmt.Println("花兒落下死了?。?!")
}
func (t *Flower) Growing() {
    fmt.Println("花兒茁壯成長(zhǎng)!??!")
}


//聲明一個(gè)interface{}的切片
var earth []interface{}

func addBeing(b interface{}) {
    earth = append(earth, b)
}
func OOPDemo03() {
    tiger := new(Tiger)
    flower := new(Flower)
    addBeing(tiger)
    addBeing(flower)

    //遍歷
    for _, being := range earth {
        if animal, ok := being.(IAnimal); ok {
            animal.Live()
            //如果是動(dòng)物則捕獵
            animal.Hunting()
            animal.Dead()
        }
        if plant, ok := being.(IPlant); ok {
            plant.Live()
            //如果是植物則成長(zhǎng)
            plant.Growing()
            plant.Dead()

        }

    }
}

//OUTPUT:
//老虎活著稱大王?。?!
//老虎捕獵?。?!
//老虎戰(zhàn)斗死了?。?!

//花兒享受陽(yáng)光!??!
//花兒茁壯成長(zhǎng)!??!
//花兒落下死了?。?!


...
5.沒(méi)有泛型

在其他面向?qū)ο笳Z(yǔ)言中,我們都會(huì)接觸到泛型的概念,當(dāng)我們遇到要對(duì)不同類(lèi)型做統(tǒng)一的內(nèi)部實(shí)現(xiàn)是,使用泛型是非常常規(guī)的做法,有了泛型后我們不必對(duì)每種數(shù)據(jù)類(lèi)型做同樣的實(shí)現(xiàn)。但是在Go中沒(méi)有泛型,為什么Go團(tuán)隊(duì)設(shè)計(jì)時(shí)不加入泛型?我們需要了解泛型的本質(zhì)是什么。

無(wú)論是JAVA中的泛型支持還是C++中的模板方法,使用泛型的初衷在于程序編碼時(shí)減少同時(shí)支持多種類(lèi)型相同內(nèi)部實(shí)現(xiàn)的方法的編碼量,把編碼的復(fù)雜性延遲到運(yùn)行時(shí)自動(dòng)創(chuàng)建對(duì)相應(yīng)類(lèi)型的支持,Go團(tuán)隊(duì)認(rèn)為泛型的支持在類(lèi)型系統(tǒng)和運(yùn)行時(shí)的復(fù)雜性花費(fèi)太大,還沒(méi)有找到更優(yōu)化的設(shè)計(jì),這是Go團(tuán)隊(duì)比較猶豫的地方。

Go語(yǔ)言對(duì)泛型有其替代方案:interface{},一般叫空接口,空接口內(nèi)部沒(méi)有聲明任何方法,因此指代Go中的任何類(lèi)型,但聲明為空接口類(lèi)型時(shí),指代接收任何數(shù)據(jù)類(lèi)型,在使用通過(guò)空接口傳遞的數(shù)據(jù)時(shí),通過(guò)類(lèi)型斷言的方式接收數(shù)據(jù)并做進(jìn)一步處理。這種做法把運(yùn)行時(shí)出錯(cuò)的可能性提前到編譯時(shí)發(fā)現(xiàn),如有運(yùn)行時(shí)錯(cuò)誤也能等到友好的處理。

type Person struct {
    name string
}

func (p *Person) PrintName() {
    fmt.Println(p.name)
}

func main() {
    var person interface{} = new(Person)
    p, ok := person.(*Person)
    if ok {
        p.name="fun"
        p.PrintName()
    }else{
        fmt.PrintLn("Type Error!")
    }
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

友情鏈接更多精彩內(nèi)容