Go 程式設計教學:類別 (Class)

PUBLISHED ON OCT 6, 2017 — PROGRAMMING

    傳統的程序式程式設計 (procedural programming) 或是指令式程式設計 (imperative programming) 學到函式大概就算學完基本概念。不過,近年來,物件導向程式設計 (object-oriented programming) 是程式設計主流的模式 (paradigm),即使 C 這種非物件導向的語言,我們也會用結構和函式模擬物件的特性。本文將介紹如何在 Go 撰寫物件導向程式。

    十分鐘的物件導向概論

    由於物件導向是程式設計主流的模式 (paradigm),很多語言都直接在語法機制中支援物件導向,然而,每個語言支援的物件導向特性略有不同,像 C++ 的物件系統相當完整,而 Perl 的原生物件系統則相對原始。物件導向在理論上是和語言無關的,但在實務上卻受到不同語言特性 (features) 的影響。學習物件導向時,除了學習在某個特定語言下的實作方式外,更應該學習其抽象層次的思維,有時候,暫時放下實作細節,從更高的視角看物件及物件間訊息的流動,對於學習物件導向有相當的幫助。

    物件導向是一種將程式碼以更高的層次組織起來的方法。大部分的物件導向以類別 (class) 為基礎,透過類別可產生實際的物件 (object) 或實體 (instance) ,類別和物件就像是餅乾模子和餅乾的關係,透過同一個模子可以產生很多片餅乾。物件擁有屬性 (field) 和方法 (method),屬性是其內在狀態,而方法是其外在行為。透過物件,狀態和方法是連動的,比起傳統的程序式程式設計,更容易組織程式碼。

    許多物件導向語言支援封裝 (encapsulation),透過封裝,程式設計者可以決定物件的那些部分要對外公開,那些部分僅由內部使用,封裝不僅限於靜態的資料,決定物件應該對外公開的行為也是封裝。當多個物件間互動時,封裝可使得程式碼容易維護,反之,過度暴露物件的內在屬性和細部行為會使得程式碼相互糾結,難以除錯。

    物件間可以透過組合 (composition) 再利用程式碼。物件的屬性不一定要是基本型別,也可以是其他物件。組合是透過有… (has-a) 關係建立物件間的關連。例如,汽車物件有引擎物件,而引擎物件本身又有許多的狀態和行為。繼承 (inheritance) 是另一個再利用程式碼的方式,透過繼承,子類別 (child class) 可以再利用父類別 (parent class) 的狀態和行為。繼承是透過是… (is-a) 關係建立物件間的關連。例如,研究生物件是學生物件的特例。然而,過度濫用繼承,容易使程式碼間高度相依,造成程式難以維護。可參考組合勝過繼承 (composition over inheritance) 這個指導原則來設計自己的專案。

    透過多型 (polymorphism) 使用物件,不需要在意物件的實作,只需依照其公開介面使用即可。例如,我們想要開車,不論駕駛 Honda 汽車或是 Ford 汽車,由於汽車的儀表板都大同小異,都可以執行開車這項行為,而不需在意不同廠牌的汽車的內部差異。多型有許多種形式,如:

    • 特定多態 (ad hoc polymorphism):
      • 函數重載 (functional overloading):同名而不同參數型別的方法 (method)
      • 運算子重載 (operator overloading) : 對不同型別的物件使用相同運算子 (operator)
    • 泛型 (generics):對不同型別使用相同實作
    • 子類型 (Subtyping):不同子類別共享相同的公開介面,不同語言有不同的繼承機制

    以物件導向實作程式,需要從宏觀的角度來思考,不僅要設計單一物件的公開行為,還有物件間如何互動,以達到良好且易於維護的程式碼結構。除了閱讀本教程或其他程式設計的書籍以學習如何實作物件外,可閱讀關於 物件導向分析及設計 (object-oriented analysis and design) 或是設計模式 (design pattern) 的書籍,以增進對物件導向的了解。

    [Update on 2018/05/20] 嚴格來說,Go 只能撰寫基於物件的程式 (object-based programming),無法撰寫物件導向程式 (object-oriented programming),因為 Go 僅支援一部分的物件導向特性,像是 Go 不支援繼承。

    由於 Go 的設計思維,以 Go 實作基於物件的程式時,會和 Java 或 Python 等相對傳統的物件系統略有不同,本文會在相關處提及相同及相異處,供讀者參考。

    建立物件

    在一些程式語言中,會有為了建立物件使用特定的建構子 (constructor),而 Go 沒有引入額外的新語法,直接以函式建立物件即可:

    在我們的 Point 物件 p 中,我們直接存取 p 的屬性 XY,這在物件導向上不是好的習慣,因為我們無法控管屬性,物件可能會產生預期外的行為,比較好的方法,是將屬性隱藏在物件內部,由公開方法去存取。我們在後文中會討論。

    雖然大部分的 Go 物件都使用結構,但其實 Go 物件內部可用其他的型別,如下例:

    由此例可知,Go 不限定建立函式的語法,我們可以視需求使用多個建立函式的方式。

    方法

    在物件導向程式中,我們很少直接操作屬性,通常會將屬性私有化,再加入相關的公開方法。我們將先前的 Point 物件改寫如下:

    在 Go 語言中,沒有 thisself 這種代表物件的關鍵字,而是由程式設計者自訂代表物件的變數,在本例中,我們用 p 表示物件本身。透過這種帶有物件的函式宣告後,函式會和物件連動;在物件導向中,將這種和物件連動的函式稱為方法 (method)。

    雖然在這個例子中,暫時無法直接看出使用方法的好處,比起直接操作屬性,透過私有屬性搭配公開方法帶來許多的益處。例如,如果我們希望 Point 在建立之後是唯讀的,我們只要將 SetXSetY 改為私有方法即可;或者,我們希望限定 Point 所在的範圍為 0.0 至 1000.0,我們可以在 SetXSetY 中檢查參數是否符合我們的要求。

    靜態方法

    有些讀者學過 Java 或 C#,可能有聽過過靜態方法 (static method)。這是因為 Java 和 C# 直接將物件導向的概念融入其語法中,然而,為了要讓某些方法在不建立物件時即可使用,所使用的一種補償性的語法機制。在 Go 語言中,不需要用這種語法,直接用頂層函式即可。例如:我們撰寫一個計算兩點間長度的函式:

    或許有讀者會擔心,使用過多的頂層函式會造成全域空間的汙染和衝突;實際上不需擔心,雖然我們目前將物件和主程式寫在一起,實務上,物件會寫在獨立的套件 (package) 中,藉由套件即可大幅減低命名空間衝突的議題。

    嵌入

    繼承是一種重用程式碼的方式,透過從父類別 (parent class) 繼承程式碼,子類別 (child class) 可以少寫一些程式碼。此外,對於靜態型別語言來說,繼承也是實現多型的方式。然而,Go 語言卻刻意地拿掉繼承,這是出自於其他語言的經驗。

    繼承雖然好用,但也引起許多的問題。像是 C++ 相對自由,可以直接使用多重繼承,但這項特性會引來菱型繼承 (diamond inheritance) 的議題,Java 和 C# 刻意把這個機制去掉,改以介面 (interface) 進行有限制的多重繼承。從過往經驗可知過度地使用繼承,會增加程式碼的複雜度,使得專案難以維護。出自於工程上的考量,Go 捨去繼承這個語法特性。

    不過,Go 加入了嵌入 (embedding) 這個新的語法特性,透過嵌入,也可以達到程式碼共享的功能。例如,我們擴展 Point 類別至三維空間:

    在本例中,我們重用了 Point 的方法,再加入 Point3D 特有的方法。

    然而,Point 和 Point3D 兩者在類別關係上卻是不相干的獨立物件。在以下例子中,我們想將 Point3D 加入 Point 物件組成的切片,而引發程式的錯誤:

    在 Go 語言中,需要使用介面 (interface) 來解決這個議題,這就是我們下一篇文章所要探討的主題。

    嵌入指標

    除了嵌入其他結構外,結構也可以嵌入指標。我們將上例改寫如下:

    同樣地,仍然不能透過嵌入指楆讓型別直接互通,而需要透過介面。

    comments powered by Disqus