淺談Go語言中的面向對象

前言

若是說最純粹的面嚮對象語言,我以爲是Java無疑。並且Java語言的面向對象也是很直觀,很容易理解的。class是基礎,其餘都是要寫在class裏的。html

最近學習了Go語言,有了一些對比和思考。雖然我尚未徹底領悟Go語言「Less is more」的編程哲學,思考的方式仍是習慣從Java的角度出發,可是我仍是深深的喜歡上了這門語言。java

這篇文章僅是我學習過程當中的一些想法,歡迎留言探討,批評指正。編程

 

封裝

Java中的封裝

Java語言中,封裝是天然而來的,也是強制的。你所寫的代碼,都要屬於某個類,某個class文件。類的屬性封裝了數據,方法則是對這些數據的操做。經過private和public來控制數據的可訪問性。dom

每一個類(java文件),天然的就是一個對象的模板。學習

Go中的封裝

Go語言並非徹底面向對象的。其實Go語言中並無類和對象的概念。spa

首先,Go語言是徹底能夠寫成面向過程風格的。Go語言中有不少的function是不屬於任何對象的。(之前我寫過一些ABAP語言,ABAP是從面向過程轉爲支持面向對象的語言,因此也是有相似的function的)。設計

而後,Go語言中,封裝有包範圍的封裝和結構體範圍的封裝。htm

在Java語言中,咱們組織程序的方式通常是經過project-package-class。每一個class,對應一個文件,文件名和class名相同。其實我以爲這樣組織是很清晰也很直觀的。對象

在Go語言中,只有一個package的概念。package就是一個文件夾。在這個文件夾下的全部文件,都是屬於這個package的。這些文件能夠任意起名字,只要在文件頭加上package名字blog

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中的繼承

Java語言中,繼承經過extends關鍵字實現。有很是清晰的父類和子類的概念以及繼承關係。Java不支持多繼承。

Go中的繼承

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語言這是搞什麼,沒有繼承還怎麼愉快的玩耍。又有的人可能以爲:沒錯,就是要幹掉繼承,組合優於繼承。

其實關於繼承或是組合的問題,我查了不少說法,目前我我的認同以下觀點:

繼承VS組合

  繼承 組合

優勢

建立子類的對象時,無須建立父類的對象 不破壞封裝,總體類與局部類之間鬆耦合,彼此相對獨立
子類能自動繼承父類的接口 具備較好的可擴展性
  支持動態組合。在運行時,總體對象能夠選擇不一樣類型的局部對象
  總體類能夠對局部類進行包裝,封裝局部類的接口,提供新的接口

缺點

子類不能改變父類的接口 總體類不能自動得到和局部類一樣的接口
破壞封裝,子類與父類之間緊密耦合,子類依賴於父類的實現,子類缺少獨立性 建立總體類的對象時,須要建立全部局部類的對象
不支持動態繼承。在運行時,子類沒法選擇不一樣的父類  
支持擴展,可是每每以增長系統結構的複雜度爲代價  

 

 

 

 

 

 

 

 

 

 

 

 

 

那麼何時用繼承,何時用組合呢?

  1. 除非考慮使用多態,不然優先使用組合。
  2. 要實現相似」多重繼承「的設計的時候,使用組合。
  3. 要考慮多態又要考慮實現「多重繼承」的時候,使用組合+接口。

多態

我認爲多態是面向對象編程中最重要的部分。

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中的類型轉換和類型斷言,留在之後探討吧。

若是本文對你有幫助,請點贊鼓勵一下吧^_^

相關文章
相關標籤/搜索