初探物件導向程式設計 OOP (二)

Kash Yang
6 min readJul 13, 2023

--

講完了什麼是類別class,以及什麼是物件之後,我們今天要來聊一下幾個物件導向的特性,他們分別是

繼承、封裝、多型

這三個特性是物件導向學習過程中必定要了解的,繼承多型通常會一起討論,要理解的觀念也比較多,封裝則是相對單純的概念,就讓我們由簡入深,探討一下這三個特性吧

什麼是封裝?

封裝 (Encapsulation),指的就是

定義類別 (class),用以描述物件的特性,對物件資訊進行包裝的行為

一樣我們用例子來說明,上一篇我們定義了類別Person

class Person constructor (
var firstName: String,
val lastName: String //姓
) {
// Person的 class member 有五個,姓、名、身高、體重以及地址
var height: Int = 0 //身高
var weight: Int = 0 //體重
var address: String? = null //地址

// Person的 class method有二個
fun eat() { /*...*/ } //吃
fun sleep() { /*...*/ } //睡覺

}

包含姓名身高體重,還有這個類別的物件可以具備的 吃飯睡覺 的行為,我們將這些資訊封裝進去Person 裏面

只要取得Person的實體,就能透過這個實體,取用被封裝在裡面的物件資訊

當我們提到取用資訊的時候,必須介紹一個很重要的觀念,叫做:可見度 (Scope / Visibility),物件導向程式語言會設計幾個修飾字 (modifiers),來定義資訊的可見度,這個可見度決定了資料可以在哪邊被取用。

一般來說可見度分為三個主要階層

  • private不公開,只有定義的類別可見
  • protected部分公開,只有定義的類別以及其衍生類別可見 (繼承詳見之後的章節)
  • public完全公開,隨處可見

Kotlin的可見度則是還有一個模組內的可見度

  • internal可以在同一個模組的任何地方被取用。

四種 Kotlin 的可見度的限制由嚴格到寬鬆依序是

private protected internal public

不過,有些範例程式沒寫可見度的修飾字耶,也可以嗎?

可以的,在 Kotlin 中沒寫可見度的話,系統預設是public

我們先從一個簡單 Kotlin 程式開始看:

// 沒寫可見度修飾字,表示為public,程式的任何地方都能new出Scope這個class的物件
open class Scope {
private val privateInt = 0 // 只有Scope這個class內部可以取用
protected val protectedInt = 1 // 只有Scope以及Scope的衍生class內部可以取用
public val publicInt = 2 // 程式的任何地方都能取用

private fun printScope() {
println(privateInt) // 印出 0
println(protectedInt) // 印出 1
println(publicInt) // 印出 2
}
}

// 定義一個MyScope的類別來繼承Scope
class MyScope : Scope() {
// 定義一個printMyScope method
fun printMyScope() {
printScope() // 編譯失敗, MyScope不能取用Scope的private method
privatePrintEverything() // 可以取用 MyScope 中的 private method
}

// 定義一個private 的 printMyScope method
private fun privatePrintMyScope() {
println(privateInt) // 編譯失敗, MyScope不能取用Scope的private變數
println(protectedInt) // 印出 1
println(publicInt) // 印出 2
}
}

fun main() {
val scope = MyScope()
println(scope.privateInt) // 編譯失敗
println(scope.protectedInt) // 編譯失敗
println(scope.publicInt) // 印出 2
scope.privatePrintMyScope() // 編譯失敗
scope.printMyScope() // 印出 1, 2
}

因為 Kotlin 更自由的 (相對於 Java ) 可以把成員宣告放在 top-level 的位置,上述四種可見度會在不同的位置有不同的效果,細節可以看這邊:https://kotlinlang.org/docs/visibility-modifiers.html#class-members

可見度是一個很重要的觀念,在定義class的時候要切記:

不要開放不應該開放的資訊

因為你永遠不會知道這個class會被別的模組怎麼樣的使用。舉個例子,假設我們有一個電腦軟體,有固定的卸載流程,一定要照順序呼叫:

  1. 暫停程式 stop()
  2. 儲存現有狀態 save()
  3. 反註冊系統 callback unregister()
  4. 清除記憶體 clear()
  5. 通知系統卸載 notify()

試想如果這些 methods 都是public, 就無法確保呼叫的模組都有正確的照順序呼叫,所以你需要將這些 methods 隱藏在自己的程式內部,才能確保順序:

public fun offload() {
stop()
save()
unregister()
clear()
notify()
}

private fun stop() {}
private fun save() {}
private fun unregister() {}
private fun clear() {}
private fun notify() {}

如此一來,你自己的程式就能完全掌控流程囉。

透過對可見度的控制,讓取得物件實體的程式只會取得被授權開放的資訊,讓封裝的特性更顯意義,而不是單純把資訊集中到某一個物件裡面而已。

在了解封裝以及可見度之後,我們下一篇就正式進入繼承與多型的探討囉

祝各位 happy coding~

實務面上設計可見度真的是很重要的喔,特別是當你要設計 library 給別人使用的時候,真的不確定該怎麼做的話就:

都先宣告成private,然後後續真的需要再開放,會是最穩的做法!

--

--