若是說最純粹的面嚮對象語言,我以爲是Java無疑。並且Java語言的面向對象也是很直觀,很容易理解的。class是基礎,其餘都是要寫在class裏的。java
最近學習了Go語言,有了一些對比和思考。雖然我尚未徹底領悟Go語言「Less is more」的編程哲學,思考的方式仍是習慣從Java的角度出發,可是我仍是深深的喜歡上了這門語言。編程
這篇文章僅是我學習過程當中的一些想法,歡迎留言探討,批評指正。dom
Java語言中,封裝是天然而來的,也是強制的。你所寫的代碼,都要屬於某個類,某個class文件。類的屬性封裝了數據,方法則是對這些數據的操做。經過private和public來控制數據的可訪問性。學習
每一個類(java文件),天然的就是一個對象的模板。.net
Go語言並非徹底面向對象的。其實Go語言中並無類和對象的概念。設計
首先,Go語言是徹底能夠寫成面向過程風格的。Go語言中有不少的function是不屬於任何對象的。(之前我寫過一些ABAP語言,ABAP是從面向過程轉爲支持面向對象的語言,因此也是有相似的function的)。code
而後,Go語言中,封裝有包範圍的封裝和結構體範圍的封裝。對象
在Java語言中,咱們組織程序的方式通常是經過project-package-class。每一個class,對應一個文件,文件名和class名相同。其實我以爲這樣組織是很清晰也很直觀的。blog
在Go語言中,只有一個package的概念。package就是一個文件夾。在這個文件夾下的全部文件,都是屬於這個package的。這些文件能夠任意起名字,只要在文件頭加上package名字繼承
package handler
那麼這個文件就是屬於這個package的。在package內部全部的變量是互相可見的,是不能夠重複的。
你能夠這樣理解:文件夾(package)就是你封裝的一個單元(好比你想封裝一個Handler處理一些問題)。裏邊其實只有一個文件,可是爲了管理方便,你把它拆成了好幾個文件(FileHandler、ImageHandler、HTTPHandler、CommonUtils),但其實這些文件寫成一個和寫成幾個,他們之間的變量都是互相可見的。
若是變量是大寫字母開頭命名,那麼對包外可見。若是是小寫則包外不可見。
其實一開始我是很不習慣這種封裝方式的,由於寫Java的時候是不可思議一個文件裏的變量在另外一個文件裏也可見的。
Go中的另一種封裝,就是結構體struct。沒錯,相似C語言中的struct,咱們把一些變量用一個struct封裝在一塊兒
type Dog struct { Name string Age int64 Sex int }
咱們還能夠給struct添加方法,作法就是把一個function指定給某個struct。
func (dog *Dog) bark() { fmt.Println("wangwang") }
這時候看起來是否是頗有面向對象的感受了?起碼咱們有對象(struct)和方法(綁定到struct的function)了,是否是?具體的Go語法不在這裏過多探討。
封裝只是基礎,爲繼承和多態提供可能。繼承和多態纔是面向對象最有意思也最有用的地方。
Java語言中,繼承經過extends關鍵字實現。有很是清晰的父類和子類的概念以及繼承關係。Java不支持多繼承。
Go語言中其實並無繼承。看到這裏你可能會說:什麼鬼?面嚮對象語言裏沒有繼承?好吧其實一開始我也是懵逼的。可是Go中確實只是提供了一種僞繼承,經過embedding實現的「僞」繼承。
type father struct { Name string Age int } type son struct { father hobby string } type son2 struct { someFather father hobby string }
如上代碼所示,在son中聲明一個匿名的father類型結構體,那麼son僞繼承了father,而son2則僅僅是把father做爲一個屬性使用。
son中能夠直接使用father中的Name、Age等屬性,不須要寫成son.father.Name,直接寫成son.Name便可。若是father有方法,也遵循同理。
但爲何說是僞繼承呢?
在Java的繼承原則上,子類繼承了父類,不光是子類能夠複用父類的代碼,並且子類是能夠當作父類來使用的。參見面向對象六大原則之一的里氏替換原則。即在須要用到父類的地方,我用了一個子類,應該是能夠正常工做的。
然而Go中的這種embedding,son和father徹底是兩個類型,若是在須要用father的地方直接放上一個son,編譯是不經過的。
關於Go語言中的這種僞繼承,我還踩過一個深坑,分享在這裏。
看起來Go語言中的繼承是否是更像一種提供了語法糖的has-a的關係,並非is-a的關係。說到這裏,可能有的人會說Go語言這是搞什麼,沒有繼承還怎麼愉快的玩耍。又有的人可能以爲:沒錯,就是要幹掉繼承,組合優於繼承。
其實關於繼承或是組合的問題,我查了不少說法,目前我我的認同以下觀點:
繼承 | 組合 | |
---|---|---|
優勢 | 建立子類的對象時,無須建立父類的對象 | 不破壞封裝,總體類與局部類之間鬆耦合,彼此相對獨立 |
子類能自動繼承父類的接口 | 具備較好的可擴展性 | |
支持動態組合。在運行時,總體對象能夠選擇不一樣類型的局部對象 | ||
總體類能夠對局部類進行包裝,封裝局部類的接口,提供新的接口 | ||
缺點 | 子類不能改變父類的接口 | 總體類不能自動得到和局部類一樣的接口 |
破壞封裝,子類與父類之間緊密耦合,子類依賴於父類的實現,子類缺少獨立性 | 建立總體類的對象時,須要建立全部局部類的對象 | |
不支持動態繼承。在運行時,子類沒法選擇不一樣的父類 | ||
支持擴展,可是每每以增長系統結構的複雜度爲代價 |
那麼何時用繼承,何時用組合呢?
我認爲多態是面向對象編程中最重要的部分。
By the way,方法重載也是多態的一種。可是Go語言中是不支持方法重載的。
兩種語言都支持方法重寫(Go中的僞繼承,son若是重寫了father中的方法,默認是會使用son的方法的)。
不過要注意的是,在Java中重寫父類的非抽象方法,已經違背了里氏替換原則。而Go語言中是沒有抽象方法一說的。
Go中的多態採用和JavaScript同樣的鴨式辯型:若是一隻鳥走路像鴨子,叫起來像鴨子,那麼它就是一隻鴨子。
在Java中,咱們要顯式的使用implements關鍵字,聲明一個類實現了某個接口,才能將這個類當作這個接口的一個實現來使用。在Go中,沒有implements關鍵字。只要一個struct實現了某個接口規定的全部方法,就認爲它實現了這個接口。
type Animal interface { bark() } type Dog struct { Name string Age int64 Sex int } func (dog *Dog) bark() { fmt.Println("wangwang") }
如上代碼,Dog實現了Animal接口,無需任何顯式聲明。
讓咱們先從一個簡單的多態開始。貓和狗都是動物,貓叫起來是miaomiao的,狗叫起來是wagnwang的。
Java代碼:
import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { Animal animal; animal= new Cat(); animal.shout(); animal = new Dog(); animal.shout(); } } abstract class Animal{ abstract void shout(); } class Cat extends Animal{ public void shout(){ System.out.println("miaomiao"); } } class Dog extends Animal{ public void shout(){ System.out.println("wangwang"); } }
輸出以下:
miaomiao wangwang
可是咱們在繼承的部分已經說過了,Go的繼承是僞繼承,「子類」和「父類」並非同一種類型。若是咱們嘗試經過繼承來實現多態,是行不通的。
Go代碼:
package main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal = &Dog{} animal.shout() } type Animal struct { } type Cat struct { //僞繼承 Animal } type Dog struct { //僞繼承 Animal } func (a *Animal) shout() { //Go has no abstract method } func (c *Cat) shout() { fmt.Println("miaomiao") } func (d *Dog) shout() { fmt.Println("wangwang") }
上邊的代碼是編譯報錯的。輸出以下:
# command-line-arguments dome/demo.Go:9:9: cannot use Cat literal (type *Cat) as type Animal in assignment dome/demo.Go:11:9: cannot use Dog literal (type *Dog) as type Animal in assignment
其實就算是在Java裏,若是不考慮代碼複用,咱們也是首先推薦接口而不是抽象類的。那麼咱們把上邊的實現改進一下。
Java代碼:
import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { Animal animal; animal= new Cat(); animal.shout(); animal = new Dog(); animal.shout(); } } interface Animal{ void shout(); } class Cat implements Animal{ public void shout(){ System.out.println("miaomiao"); } } class Dog implements Animal{ public void shout(){ System.out.println("wangwang"); } }
輸出以下:
miaomiao wangwang
Go裏邊的接口是鴨式辯型,代碼以下:
package main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal = &Dog{} animal.shout() } type Animal interface { shout() } type Cat struct { } type Dog struct { } func (c *Cat) shout() { fmt.Println("miaomiao") } func (d *Dog) shout() { fmt.Println("wangwang") }
輸出以下:
miaomiao wangwang
看起來很棒對不對。那咱們爲何不直接都用接口呢?還要繼承和抽象類幹什麼?這裏咱們來捋一捋一個老生常談的問題:接口和抽象類的區別。
這裏引用了知乎用戶chao wang的觀點。感興趣的請前往他的回答。
abstract class的核心在於,我知道一類物體的部分行爲(和屬性),可是不清楚另外一部分的行爲(和屬性),因此我不能本身實例化(不知道的這部分)。如咱們的例子,abstract class是Animal,那麼咱們能夠定義他們胎生,恆定體溫,run()等共同的行爲,可是具體到「叫」這個行爲時,得留着讓非abstract的狗和貓等等子類具體實現。
interface的核心在於,我只知道這個物體能幹什麼,具體是什麼不須要聽從類的繼承關係。若是咱們定一個Shouter interface,狗有狗的叫法,貓有貓的叫法,只要能叫的對象均可以有shout()方法,只要這個對象實現了Shouter接口,咱們就能把它當shouter使用,讓它叫。
因此abstract class和interface是不能互相替代的,interface不能定義(它只作了聲明)共同的行爲,事實上它也不能定義「很是量」的變量。而abstract class只是一種分類的抽象,它不能橫跨類別來描述一類行爲,它使得針對「別的分類方式」的抽象變得沒法實現(因此須要接口來幫忙)。
考慮這樣一個需求:貓和狗都會跑,而且它們跑起來沒什麼區別。咱們並不想在Cat類和Dog類裏邊都實現一遍一樣的run方法。因此咱們引入一個父類:四足動物Quadruped
Java代碼:
import java.io.*; class test { public static void main (String[] args) throws java.lang.Exception { Animal animal; animal= new Cat(); animal.shout(); animal.run(); animal = new Dog(); animal.shout(); animal.run(); } } interface Animal{ void shout(); void run(); } abstract class Quadruped implements Animal{ abstract public void shout(); public void run(){ System.out.println("running with four legs"); } } class Cat extends Quadruped{ public void shout(){ System.out.println("miaomiao"); } } class Dog extends Quadruped{ public void shout(){ System.out.println("wangwang"); } }
輸出以下:
miaomiao running with four legs wangwang running with four legs
Go語言中是沒有抽象類的,那咱們嘗試用Embedding來實現代碼複用:
package main import ( "fmt" ) func main() { var animal Animal animal = &Cat{} animal.shout() animal.run() animal = &Dog{} animal.shout() animal.run() } type Animal interface { shout() run() } type Quadruped struct { } type Cat struct { Quadruped } type Dog struct { Quadruped } func (q *Quadruped) run() { fmt.Println("running with four legs") } func (c *Cat) shout() { fmt.Println("miaomiao") } func (d *Dog) shout() { fmt.Println("wangwang") }
輸出以下:
miaomiao running with four legs wangwang running with four legs
可是因爲Go語言並無抽象類,因此Quadruped是能夠被實例化的。可是它並無shout方法,因此它並不能被當作Animal使用,尷尬。固然咱們能夠給Quadruped加上shout方法,那麼咱們如何保證Quadruped類不會被錯誤的實例化並使用呢?
換句話說,我指望經過對抽象類的非抽象方法的繼承來實現代碼的複用,經過接口和抽象方法來實現(符合里氏替換原則的)多態,那麼若是有一個非抽象的父類出現(其實Java裏也很容易出現),極可能會破壞這一規則。
其實Go語言是有它本身的編程邏輯的,我這裏也只是經過Java的角度來解讀Go語言中如何實現初步的面向對象。關於Go中的類型轉換和類型斷言,留在之後探討吧。
若是本文對你有幫助,請點贊鼓勵一下吧^_^