【Kotlin】第8課-類別繼承與抽象類別

在程式專案中,應該會有一些物件具有共通的「特徵」。比方說全職員工與兼職員工,都具備「員工」的特徵,像是姓名、薪資與聯絡資料等。我們能夠以這些特徵為基礎,發展出不同種類的員工。

本文會講解如何將類別的屬性與方法繼承下來,衍生出新的類別。並且介紹抽象類別,讓程式碼延後到子類別再完成實作。

一、繼承類別

假設下面我們有個「產品」的類別,建立簡單的產品資料是沒問題的。

class Product constructor(val id: String,
var name: String,
var price: Int,
var unit: String) {
}

fun main(args: Array<String>) {
val book = Product("BK001", "數學參考書", 320, "本")
val clothes = Product("CL002", "襪子", 80, "雙")
val snack = Product("SN003", "提拉米蘇", 149, "盒")
}

若產品具備一些獨有的資料,例如材質、作者等,則原先的產品類別便不敷使用。然而若只是單純地建立這些產品類別,會重複宣告共同的屬性,不利於程式維護,也失去利用「多型」來開發的可能性。

透過繼承的手法,可以在某類別的基礎上,建立新的類別。並同時具備原類別所開放的屬性與方法。

open class Product constructor(val id: String,
var name: String,
var price: Int,
var unit: String) {
}

class Book constructor(id: String,
name: String,
price: Int,
unit: String,
var author: String,
var publisher: String)
: Product(id, name, price, unit) {
}

Kotlin 的類別預設是不能被繼承,因此必須在宣告類別時加上「open」的關鍵字,才能開放給其他類別。

接著在新類別的主要建構式後方使用「:」語法,就能呼叫父類別的建構式,並完成繼承。上面 Book 類別的主要建構式,除了傳遞 Product 建構式需要的參數,還宣告了 Book 的獨有屬性,即作者與出版社。


二、繼承方法

完成繼承後,子類別就能使用父類別的方法。此處在父類別宣告一個 generateSummary 方法,用來產生產品摘要文字。

open class Product constructor(val id: String,
var name: String,
var price: Int,
var unit: String) {
fun generateSummary() = "$id $name $$price/$unit"
}

由於該方法的存取權限是 public,因此能被外部的主程式呼叫。若設為 protected,則只能在類別與子類別內部使用。

// 以父類別宣告,物件本質為子類別
val clothes: Product = Clothes("CL002", "襪子", 80, "雙")
println(clothes.generateSummary())

// 宣告與物件本質均為子類別
val book: Book = Book("BK001", "參考書", 320, "本", "許小明", "東東出版社")
println(book.generateSummary())


三、覆寫方法

雖然父類別的方法能夠被繼承,但若子類別想要有自己的實作方式,還是可以覆寫的。做法與繼承類別相似,在父類別的方法上加上「open」的關鍵字,才能讓子類別覆寫。

open class Product constructor(val id: String,
var name: String,
var price: Int,
var unit: String) {
open fun generateSummary() = "$id $name $$price/$unit"
}

在子類別覆寫的方式與 Java 很像,一樣需要加上「override」。但 Java 是使用標記,而 Kotlin 是關鍵字。

class Book constructor(id: String,
name: String,
price: Int,
unit: String,
var author: String,
var publisher: String)
: Product(id, name, price, unit) {

override fun generateSummary() = "書籍 ${super.generateSummary()}"
}

我們將方法覆寫成回傳產品種類,再加上原先的摘要,此處使用「super」關鍵字來呼叫父類別的方法。

四、抽象類別

父類別具有子類別的共通特性,但可能同一個方法,子類別卻有都有不同的實作方式(多型)。這時可以將父類別定義為抽象,並宣告要由子類別自行實作的方法。而類別的屬性也能這麼做。

接下來將 Product 定義為抽象類別,並由 BookClothes 兩個子類別繼承。

abstract class Product constructor(val id: String,
var name: String,
var price: Int,
var unit: String) {
abstract val TYPE: String
abstract fun getBasicInfo(): String
abstract fun getExtraInfo(): String
fun getDetail() = "${getBasicInfo()}\n${getExtraInfo()}"
}

Product 具備一個抽象屬性 TYPE,代表產品種類。以及兩個抽象方法,分別取得產品的基本資訊和額外資訊。

繼承抽象類別後,必須完成 Product 所定義的抽象屬性與方法。子類別可各自實作,比方說上面的 getBasicInfo 方法,Book 只組合三個屬性,Clothes 則有五個,兩者格式並不同。

class Book constructor(id: String,
name: String,
price: Int,
unit: String,
var author: String,
var publisher: String)
: Product(id, name, price, unit) {

override val TYPE = "書籍"
override fun getBasicInfo() = "$TYPE $name $$price"
override fun getExtraInfo() = "作者:$author$publisher"
}

class Clothes constructor(id :String,
name: String,
price: Int,
unit: String,
var size: String,
var material: String)
: Product(id, name, price, unit) {

override val TYPE = "衣物"
override fun getBasicInfo() = "$TYPE $id $name $$price/$unit"
override fun getExtraInfo() = "材質:$material $size"
}

接下來我們回到主程式分別建立 BookClothes 物件,並呼叫在抽象父類別已經完成的 getDetail 方法。可以發現它們有不同的輸出。

fun main(args: Array<String>) {
val book = Book("BK001", "數學複習講義", 320, "本", "許明", "東東出版社")
val clothes = Clothes("CL002", "襪子", 80, "雙", "M", "棉")

println(book.getDetail())
println()
println(clothes.getDetail())
}

抽象父類別 Product 中,getDetail 方法已設計好輸出詳細資料的流程(先 getBasicInfo,再 getExtraInfo)。呼叫該方法時,實際執行的將會是子類別中的上述兩個方法,因此 BookClothes 有不同格式的輸出。像這樣在抽象類別定義好表面的流程,而細節開放給子類別各自實作,這個設計概念稱為「樣板方法」(Template Method)。

上一篇:【Kotlin】第7課-類別的設計(二)

下一篇:【Kotlin】第9課-介面(Interface)

留言