go 學習筆記之萬萬沒想到寵物店竟然催生出面向接口編程?

到底是要貓還是要狗

在上篇文章中,我們編撰了一則簡短的小故事用於講解了什麼是面向對象的繼承特性以及 Go 語言是如何實現這種繼承語義的,這一節我們將繼續探討新的場景,希望能順便講解面向對象的接口概念.

爲了照顧到沒有看過上一節文章的讀取,這裏再簡述一下上節文章關於買寵物的故事,如需詳細瞭解,請自行翻閱歷史文章進行查看.

A: 貓是一種寵物,淘氣可愛會賣萌,看家本領抓老鼠,偶爾還會喵喵喵.
B: 狗是一種寵物,忠實聽話能看家,嗅覺靈敏會破案,一言不合汪汪汪.
C: 我想要買一個寵物,文能賣萌,武可退敵,明個一早給我送來吧!

於是,第二天,A和B各自帶着自己的寵物來拜見C,並附上各自的理由,說的頭頭是道,C總覺得有些哪裏不對,可一時間又無言反駁,只能悻悻收下了貓和狗,白白多花了一份錢!

go-oop-inheritance-one-pet.jpeg

這則故事很簡單,但同時也暴露出一個問題,那就是在這場交易中,賣家實際上虧了,明明只是想買一個寵物,結果卻買了兩個!

當然,在上篇文中最後也給出瞭解決方案,那就是將貓和狗進行抽象封裝,共性的部分提取成寵物,個性的部分纔是貓和狗.

如此一來,顧客買寵物時要麼買的是貓,要麼面對是狗,具體買的是什麼寵物是由顧客自己根據各自寵物的個性決定的,一定程度上解決了交易不公平的問題.

讓我們再簡單回憶一下繼承的實現過程,回憶的過程中不妨思考一下繼承有沒有沒能解決的問題?

  • 寵物默認自帶能文能武技能
type Pet struct {

}

func (p *Pet) Skill() {
    fmt.Println("能文能武的寵物")
}
  • 貓是寵物,還是能抓老鼠的寵物.
type Cat struct {
    p *Pet
}

func (c *Cat) Catch() {
    fmt.Println("老鼠天敵喵喵喵")
}
  • 狗是寵物,還是能認路導航的寵物.

type Dog struct {
    p *Pet
}

func (d *Dog) Navigate() {
    fmt.Println("自帶導航汪汪汪")
}

某一天,C要能文能武的寵物,最好還可以順便抓個老鼠,於是C選擇了喵喵喵!

func TestExtendInstance(t *testing.T) {
    p := new(Pet)
    c := new(Cat)
    c.p = p

    // 老鼠天敵喵喵喵
    c.Catch()
    // 能文能武的寵物
    c.p.Skill()
}

過了一陣子,C覺得貓除了抓老鼠別的什麼都不會,別人遛狗,我遛貓?

於是,想要一種能自帶導航功能的寵物,毫無疑問的是,選擇了狗.

func TestExtendInstance(t *testing.T) {
    p := new(Pet)
    d := new(Dog)
    d.p = p

    // 自帶導航汪汪汪
    d.Navigate()
    // 能文能武的寵物
    d.p.Skill()
}

上述示例,簡而言之就是通過組合的方式實現了面向對象中的繼承特性,解決了貓和狗除了是寵物還是自己的問題.

貓狗隨便是寵物就行

面對貓和狗兩種寵物,顧客犯了選擇困難症,於是第一次全盤照收買下了兩種寵物,吃了一次啞巴虧.

後來在市場監督的介入下,利用面向對象的繼承特性,用 Go 語言實現了貓和狗的個性化與寵物的共性化,從此像C一樣的顧客再也不會面臨選擇困難症,每一次都要根據獨特的需求,最終選擇某一種寵物,要麼是貓,要麼是狗.

不知過了多久,這種相安無事的場景最終被一羣急性子的顧客所打破,這一天寵物市場一大早就來一大批人,一上來就吵吵嚷嚷說快給我們一批寵物,我們要作爲抽獎活動的獎品,一定要快一點!

go-oop-interface-hurry.jpeg

誰知道銷售人員不緊不慢地說: "彆着急,我們這裏的寵物有很多種,有貓,有狗,有兔子,有金魚,有烏龜,有蝸牛..."

"別整那些虛頭巴腦的,我只要寵物,趕緊給我寵物就行,別盡扯沒用的",顧客吵吵說.

果然是一羣急性子的顧客,還沒等銷售人員介紹完各個寵物的差異性亮點直接被打斷了.

寵物市場吵吵鬧鬧引來了市場監督人員的注意,顧客和商家均向官方訴苦,期望能給出一個解決辦法!

市場監督人員心想: 商家和顧客原本和諧相處的,今天怎麼會吵鬧起來?

仔細聽了事情來龍去脈,雙方都沒有過錯,看來還真的是市場趕不上實際需求的變化,真得儘快研究新的解決辦法才行啊!

go-oop-interface-issue-new.jpg

"冷靜一下,你們的意見我們這邊已經知道了,這樣吧,給我們三天的時間,我們一定會想出一個萬全之策,到時候再公佈新的交易規則,現在我宣佈暫時關閉交易,省的再惹出不必要的爭端!"

原本吵吵嚷嚷的市場頓時冷卻了不少,畢竟誰也不敢違抗市場老大的命令,衆人只得悻悻而去,期待三天後的重新開市.

視角切換到市場監督大會上,主席首先開始發言:"各位,現在市場面臨的問題想必大家都有所耳聞吧,我們已經鄭重承諾,三天後必須給市場一個答覆,時間緊,任務重,大家要集思廣益,一起解決這個難題!"

go-oop-interface-issue-meeting.jpg

"現在的顧客到底是怎麼了,連自己到底想要什麼都搞不清楚,還急衝衝地跑來買寵物,自己都不知道要買啥,鬼才知道呢!",資歷老練的繼承經理抱怨道.

"經理說得對,他們自己都不知道到底想要啥寵物,怎麼能埋怨商家太羅裏吧嗦呢?人家那麼賣力介紹寵物的特點,不也是幫助顧客更好的選擇嘛!",發言的是繼承經理的小弟.

"..."

"咳咳,我理解大家的心情,繼承項目組確實在解決寵物問題上立下了很大功勞,大家爲他們抱不平也是情理之中的事情,過去的就讓他過去吧!當務之急,還是要解決現實問題!",主席首先安撫前幾位激動情緒,又挑出重點提醒在場的各位迴歸到主題的討論上,不要再揪住過去的功勞簿.

"我覺得,心急顧客的真正需求只是想要一種寵物,而不再關注寵物的種類,管他是貓是狗,只要是寵物就行.所以我們應該提供一種新的機制,對外宣傳時只說這是寵物,至於這種寵物到底是貓還是狗,都可以!"

"貓和狗明明已經是寵物了啊,難道不可以直接賣給顧客嗎?爲啥還要提供新機制?"

"貓和狗雖然是寵物,但對於用戶來說,這種寵物有點浪費了,用戶實際使用到可能只是寵物的功能,並不會用到也不能用到具體寵物的功能,所以對用戶來說,這就是一種浪費."

"哦哦,明白了,這就像是顧客需要的寵物是能賣萌的,是要送給女朋友作爲禮物的,並不關心這個寵物能不能抓老鼠.所以對於抓老鼠的技能就是沒用的,而買家卻要爲抓老鼠的技能額外買單,這對於買家來說並不公平!"

經過一番激烈的討論,大家基本上達成一致,先前存在的繼承模型確實有些不足,不能適應快速變化的市場,過於強調差異性而非共性.

這樣就導致無法滿足急性子顧客批量購買的需求,所以需要提供類似於繼承那種抽象的概念來表達某種約定,只要滿足這種約定的動物就是寵,不管是貓還是狗,哪怕是玩具也行!

go-oop-interface-pet-toy.jpeg

讓繼承變得更加抽象

透過現象看本質,從紛繁冗雜的事務中抽象出精簡的模型是各個編程語言都必不可少的一個環節,Go 語言當然也不例外.

面向對象編程中的繼承概念表達是一種上下級的抽象關係,也就是說某一個封裝對象是從屬於特定上級的封裝對象,默認擁有該上級的行爲方法,這裏的上級概念就是父類就是對所有子類共性的抽象實現.

當研究的問題就是具體的子類實現時,此時使用繼承的概念,語義上比較清晰,子類只需要關注自己的特性,共性部分由父類去完成,這種思路也是非常自然的,貓是貓的同時也是一種寵物.

go-oop-inheritance-one-cat.jpeg

但是當我們研究的問題不再關注具體的子類實現而是着眼於父類的共性時,此時如果再提供具體的子類實現當然也能用,但是殺雞焉用牛刀?

明明我僅僅需要一滴水,你卻給了我整個海洋!

go-oop-interface-water.jpeg

本來,真正需要的可能只是父類的某一個方法,你卻提供給我一個具體的子類實現,這個子類不但擁有目標方法還有很多的其他方法.

既然有這麼多的附加價值,你說浪費不浪費,銷售時可不得漲價嗎,這樣不相當於捆綁銷售了嘛!

所以,我們需要對繼承的概念進一步抽象,使這種抽象達到一種極致狀態以至於只存在非常少量的行爲方法,凡是繼承自這種極致抽象的子類都是它的子民.

爲了之後討論方便,業界將這種抽象到極致的繼承關係稱之爲接口,雖然看似只是稱呼的改變,但實際上思維方式上已經發生了翻天覆地的變化.

繼承的概念是描述子類和父類的關係,子類繼承自父類,關注點在子類,共性部分完全由父類實現,子類自然擁有這些行爲能力.

而接口的概念衍生於繼承,只不過是這種抽象程度已經達到了一種不能再抽象的地步,所有子類都要有一個最終的父類,這個父類擁有最公共性的行爲方法,所以這種極致的抽象也就無法體現出子類的共性行爲的具體表現.此時這種極致的抽象沒有太大的意義,是一種非常非常寬泛的概念,等於什麼都沒說,所以也適合絕大部分封裝對象.

所以,乾脆取消了極致抽象中對於行爲共性的實現,轉而僅僅定義共性的行爲,具體這種行爲到底如何表現,完全由具體子類自行決定.

這樣做有兩個顯而易見的好處,一是解決了太寬泛概念等於沒說的尷尬,同時保留了對共性行爲的描述.二是將控制權轉移到具體的子類實現,實現了體制內的個性化!

所以這種專業名詞的轉變背後是思維方式的轉變,而接口更是很好地描述了這種轉變的語義.

回憶一下生活總隨處可見的 USB 數據線,對於計算機來說,對外暴露的是 USB 插口,行爲描述是隻要插入就能連接到電腦,能夠同電腦進行溝通交流,這種交流可能是傳遞數據,也可能是連接電源等等不同的行爲表現.

go-oop-interface-usb.jpg

基於接口設計,USB 數據線提供了訪問電腦的能力,一端連着電腦,另一端連着手機,雙方進行數據交換.
有線鼠標的數據線也提供了訪問電腦的能力,實現鼠標的左擊還是又擊都能反饋到電腦.

諸如此類的案例不勝枚舉,生活中不缺少計算機哲學,缺少的只是我們的思考.

所以,如果讓我來給這種機制進行命名的話,我可能會將其稱呼爲插口,意思是只要能適配指定的插口,那麼就說滿足插口要求,對外暴露的抽象概念是插口,真正的實現可能是數據線或者工具等.

當然,這只是我的一廂情願,因爲面向對象中這種機制叫做接口,滿足接口的規範叫做實現了接口.

接口這種概念顯得比較專業,提出這個概念的人估計也是厲害人物,基本上所有的面嚮對象語言中都採用了接口的概念,即使不是面嚮對象語言但支持面向對象編程風格的 Go 語言也採用了接口概念.

由此可見,接口的概念應該是通俗易懂,可移值性比較強的,獲得了相當高的認可度.

除了面向對象編程風格外,與接口相關的編程風格中還有一種叫做面向接口編程,這個會在以後的文章中繼續分享這封面的內容.

個人理解封裝和繼承的概念,講的就是面向對象編程,關注點在於具體的對象以及對象之間的層次關係.

而接口的出現則是另外一種維度的思考,當關注點不再是具體的子類而是抽象的父類,這種情況下則根據實際情況抽象出了接口的概念,由此看出,面向對象編程中高內聚部分說的是封裝和繼承,低耦合則是接口和多態.

所以面向接口編程在應用而生,由此可見,不同的應用場景關注點不同,面向對象和麪向接口也並不是互斥關係,是互補關係.

在未來的某種需求繼續發生改變時,可能還會產生新的概念,進而提出新的一套理論,到時候是面向需求編程還是面向思維編程亦或是面向搜索編程,那就就不得而知了.

聰明的讀者,你們有什麼看法呢?

如何設計又怎麼實現

市場監督大會散會後,繼承小組接受了設計接口的任務挑戰.大會之所以推舉繼承小組領頭,是因爲與會人員一致認爲繼承小組在處理抽象概念上十分擅長,相信設計出接口這種機制也是可以的.

繼承小組深感此次任務責任重大,任重而道遠,一定要設計出接口概念才能不辜負參會人員的認可和領導的厚愛.

go-oop-interface-dashing.jpg

於是,繼承小組內部在一起開了個會,會上大家暢所欲言談談自己的看法.

小王: "我覺得這種接口的概念是抽象的終極狀態,我們可能沒辦法一下子到達終點,但是按照現有的理論應該可以逐步逼近終點."
小李: "我也是有類似的感覺,抽象到什麼程度纔是終點呢?拿什麼判定這個抽象程度呢?貓和狗到寵物的過程是一種抽象過程,我們先前也是基於此過程提出了繼承的概念,解決了重複定義的問題.現在應該沿着這種思路繼續抽象,直到小王說的那種接口概念."
小張: "從貓和狗抽象到寵物,是封裝對象的演進過程,顧客需要的不是具體的貓和狗,而是寵物.但是這個寵物直覺上感覺和原來繼承中實現的寵物還是有點不一樣啊?"
小王: "我也有同感,這次的寵物必須具備某種能力,只要是滿足這種能力的,管他是貓還是狗或者是別的什麼蜥蜴蟑螂的都是顧客眼中的寵物.所以這種寵物更加單一化,並不在乎有沒有其他能力."

...

大家你一言我一語的討論了好長時間,最終在項目經理的引導整理下有了有了初步的思路.

  • 接口是一種抽象,這種抽象可能並不關注父類本身的全部能力,只在於關注的能力.
  • 普通的抽象父類既有行爲的約束還順便實現了該行爲,但抽象到接口這種程度時是否實現並不在乎,但必須要有行爲的約束.
  • 接口本身的語義是一種行爲約束,滿足這種約束行爲的具體對象可能會有很多,同時這些具體對象可能也滿足其他接口約束.
  • 接口約束變化時,滿足接口約束的具體子類到底要不要隨之變化?如果需要的話,有道理,如果不需要的話,也有道理.

"等一下,我有疑問?你怎麼一會說需要,一會又說不需要,這不自相矛盾了嗎?",大家幾乎不約而同舉手示意經理.似乎早就料到這幫小子搞不懂其中緣由,經理故弄玄虛地迴應說:"嗯嗯,我就知道你們會有疑惑,下面容我談一下我的看法,你們聽聽看.”

go-oop-interface-expert.jpg

如果站在接口的定義者角度上看問題,一旦發佈了接口規範,子類肯定會屁顛屁顛滿足接口約束,於是對外暴露時都是接口那一套理論,忽略了自己的特色.

統一了接口規範這種情況對於接口設計者最爲方便,所有的控制權全部掌握在自己手中,一道命令即可號令羣雄,莫敢不從,如若不從,輕則千夫所指,重則驅逐出境!

對於接口設計者來說,這些實現了接口的對象並沒有什麼不同,地球離了誰照樣自轉,隨時隨地想換就換.

但是對於接口的實現類來說,只要一收到天子詔令,立馬無條件停下手上的活,熬夜加班也要滿足新的接口規範,敢怒不敢言,除非是不想混了,哪怕怠慢了一步也會引發巨大的動盪!

所以說接口更改時,具體的實現類必須要隨之改變以實現新的接口規範約束.

go-oop-interface-designer.jpeg

如果站在接口的使用者角度上看問題,是否實現接口應該是我的地盤我做主,是自主決定的事情,管你接口是否更改,老子愛實現就實現,不樂意實現就不實現!你奈我何?我的王國我當家,尊你敬你你纔是國王,把我們惹惱了,所謂的聯合王國到時候只剩你這麼一個孤家寡人去吧!

所以說接口更改時,具體的實現類不需要隨之更改,想不想滿足新的接口規範完全在於自己,並不是強迫的,不必立即實現新的接口規範.

go-oop-interface-impler.jpeg

真的是公說公有理婆說婆有理,既然如此,那麼問題來了,Go 語言選擇是哪一種?其他主流的編程語言又是選擇哪一種的呢?

先說其他主流的編程語言,這類編程語言大多是站在接口設計者角度出發,控制慾特別強,一言不合就報錯,接口更新了實現類必須同步更新,違令者殺無赦!

這樣有優點也有缺點,優點是皇帝一聲令下,天下臣民莫敢不從,屢教不敢者,千夫所指,王國崩潰也不是沒有可能!
正是這種優點,換另外一種角度看就是缺點了,俗話說天高皇帝遠,聖旨雖下但還沒傳達到邊境要塞,那邊監察御史就上奏你一本,說你怠政目無尊上,引發帝國動盪,罪大惡極,理應凌遲處死!

你說冤不冤,不管是朝令夕改還是煥然一新的改革,凡是曾經實現過接口的類都要實時更新,否則後果不堪設想.

真的是成也蕭何敗蕭何,控制慾太強有利有弊.

go-oop-interface-xiaohe.jpg

所以,Go 與衆不同,選擇了另一種思路來解決問題,放棄中央集權轉向分封制,將權力下放給地方.

名義上還是由國王制定統一標準,由地方負責自主實施,具體如何實現標準完全是諸侯國自己的事情,萬一哪天國王需要使用統一標準時,實現了該標準的諸侯王國都可以無障礙使用.

即使以後接口規範有變,舊的接口不再適合新時代要求,國王只需要制定了一套新的標準,昭告天下後,當詔令傳到地方時,地方可以根據新的規範更新自己的實現類,萬一消息閉塞或者不願意立即更新,也沒關係,王國不會崩潰,只不過需要使用新規範時,沒有實現接口的地方自然是不能使用的.

go-oop-interface-kingdom.jpg

因此,不論是集中制還是民主制,接口的規範都是自頂向下實施的,不同之處在於底下的人因各種原因沒有實現新的接口規範時,集中制會直接崩潰而民主制依舊正常運行,僅此而已.

下面就演示一下兩種思路的實現方式.

  • java 等傳統的面嚮對象語言

賣家首先定義到底什麼是寵物這種接口.

public interface Pet {
    void actingCute();
}

喵喵喵,人家能賣萌,就是寵物嘛,爲啥還非得證明一下啊!

public static class Cat implements Pet {
    @Override
    public void actingCute() {
        System.out.println("喵星人喵喵喵來賣萌");
    }
}
這裏說的證明一下貓是寵物,指的是必須使用關鍵字 implements 聲明實現了寵物的接口,頒發了合格證纔算是寵物,否則就不是.

汪星人說,這年頭自帶賣萌天賦的貓咪都要通過專業認證纔算是寵物,我也乖乖去認證寵物吧!

public static class Dog implements Pet {
    @Override
    public void actingCute() {
        System.out.println("汪星人汪汪汪來賣萌");
    }
}

第二天,市場上又來了一羣急性子的買家,一上來就要買寵物,管他是貓還是狗,並不在乎,只要是寵物就行.

public static void main(String[] args) {
    Pet p;
    
    p = new Cat();

    // 喵星人喵喵喵來賣萌
    p.actingCute();

    p = new Dog();

    // 汪星人汪汪汪來賣萌
    p.actingCute();
}

終於送走了這批顧客,賣家也舒了一口氣,默默唸叨着,市場監督那幫人真牛逼,竟然設計出接口的方案,只要是寵物,別管是貓還是狗,隨便給一個都行,給這幫人點個贊!

  • go 等非傳統非面嚮對象語言

首先定義接口規範,寵物一定要能賣萌,不然怎麼討得女神歡心?

type Pet interface {
    ActingCute()
}

喵喵喵說我會賣萌啊,那我就是寵物啦!

type Cat struct {

}

func (c *Cat) ActingCute() {
    fmt.Println("喵星人喵喵喵來賣萌")
}

汪汪汪說我也會賣萌,我也要給女神當寵物!

type Dog struct {

}

func (d *Dog) ActingCute() {
    fmt.Println("汪星人汪汪汪來賣萌")
}

既然你們都會賣萌,對於直男來說這就夠了,隨便拿一個就行了,快點準備送禮物啦!

func SendGift(p Pet) {
    p.ActingCute()
}

於是乎,既然買家並不在乎到底是貓還是狗,那就賣給他一個貓好了,於是小夥子打包了寵物準備送給女神.

func TestActingCute(t *testing.T) {
    var p Pet
    p = new(Cat)

    // 喵星人喵喵喵來賣萌
    SendGift(p)
}

第二天,女神打來電話說,你不知道對貓毛過敏的嗎,送的啥破禮物!哼!

可憐的小夥子跑去寵物店找賣家算賬,氣沖沖地質問賣家,賣家一臉毫不在意的樣子,笑嘻嘻的說,小夥子想不想將功補過啊,這一次保準你能獲得女神青睞.

只見,賣家這一次找來了一條寵物狗,打打包還放到原來的包裝盒遞給你小夥子.

func TestActingCute(t *testing.T) {
    var p Pet
    p = new(Dog)

    // 汪星人汪汪汪來賣萌
    SendGift(p)
}

我擦,還是原來的配方,有點擔心,一樣的包裝這一次真的能討得女神歡心,原諒自己嗎?

go-oop-inheritance-one-dog.jpeg

親愛的讀者,你們說呢,同樣的配方不一樣的味道,女神會原諒自己嗎?

同一個問題思路不同

不論是站在設計者角度上解決抽象問題還是站在使用者角度思考,兩者的解決方案沒有高低優劣之分,選用好恰當的應用場景就是最好的解決方案.

只不過這種選擇往往不是開發者能左右的事情,因爲這種底層的語言級別框架設計屬於締造者的工作,他們一旦覺得了一種模式,語言使用者很難改變,我們唯一能做的就是理解並使用罷了!

當站在接口設計者角度上時,接口的定義和具體實現類的關係就好比是集中制,皇帝一聲令下,不管身處何處,天下臣民皆惟命是從,如有懶政懈怠者,千夫所指,立馬崩潰.

當站在接口實現者角度上時,此時接口的設計者和具體實現者的關係是鬆耦合的,猶如分封制,國王一聲令下,諸侯國可以聽從差遣也可以抗旨不遵,對於整個王國而言並不會造成顛覆性混亂,諸侯國和國王更像是一種契約精神而不是隸屬服從關係.

Go 語言中的接口採用的就是後一種鬆耦合的關係,接口設計者和接口實現者是鬆耦合的,實現的關係也是隱式的,這也是另一種理論"鴨子模型"的體現.

go-oop-interface-dock.jpg

好了,本文主要介紹了爲什麼要有接口設計的需求以及接口設計是怎麼思考的,並簡單介紹了 Go 是如何實現這種模型的.

下一節我們將真正開始介紹 Go 語言關於接口的設計,順便講解面向對象最後一個知識點---多態.

如果本文對你理解面向對象有所幫助,歡迎你的的轉發分享,如果文章有描述不當之處,希望你能留言告訴我,謝謝你的閱讀.

雪之夢技術驛站.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章