什麼是封裝?
封裝 (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會被別的模組怎麼樣的使用。舉個例子,假設我們有一個電腦軟體,有固定的卸載流程,一定要照順序呼叫:
- 暫停程式
stop()
- 儲存現有狀態
save()
- 反註冊系統 callback
unregister()
- 清除記憶體
clear()
- 通知系統卸載
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
,然後後續真的需要再開放,會是最穩的做法!