1. 程式人生 > >Go語言中的面向對象

Go語言中的面向對象

ble 類型結構 前言 java .sh 行為 想法 script 如果

前言

如果說最純粹的面向對象語言,我覺得是Java無疑。而且Java語言的面向對象也是很直觀,很容易理解的。class是基礎,其他都是要寫在class裏的。

最近學習了Go語言,有了一些對比和思考。雖然我還沒有完全領悟Go語言“Less is more”的編程哲學,思考的方式還是習慣從Java的角度出發,但是我還是深深的喜歡上了這門語言。

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

封裝

Java中的封裝

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

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

Go中的封裝

Go語言並不是完全面向對象的。其實Go語言中並沒有類和對象的概念。

首先,Go語言是完全可以寫成面向過程風格的。Go語言中有很多的function是不屬於任何對象的。(以前我寫過一些ABAP語言,ABAP是從面向過程轉為支持面向對象的語言,所以也是有類似的function的)。

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

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

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

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

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

  

Go語言中的面向對象