[Day 11] Class (類別) vol. 2

制定合理的屬性可見性

延續昨天的話題, 其實如果是一個 balance 的屬性, 我們不應該直接可以讓人修改,應該要特別制定函數提供別人呼叫修改

所以在這把原本的 set 都加上了 private 的可見性

var balance = _balance
    get() {
        println("取得 balance: $field")
        return field
    }
    private set(value) {
        println("更新 balance: $value")
        field = value
    }

var balaceUpdateTime: LocalDateTime? = null
    private set(value) {
        println("更新 balance 時間: $value")
        field = value
    }

如此在 main 就無法直接設定這兩個變數了

https://ithelp.ithome.com.tw/upload/images/20200920/20129902KIdha8UfPX.png

接著我開放了兩個函數, 提供儲值和扣錢的功能, 並且會紀錄更新的時間

fun increaseBalance(amount: BigDecimal) {
    println("儲值...$amount")
    if (amount.compareTo(BigDecimal.ZERO) < 0) throw RuntimeException("輸入金額必須大於或等於 0")

    balance = balance.add(amount)
    balaceUpdateTime = LocalDateTime.now()
}

fun decreaseBalance(amount: BigDecimal) {
    println("消費金額...$amount")
    if (amount.compareTo(BigDecimal.ZERO) < 0) throw RuntimeException("金額必須大於或等於 0")
    if (amount.compareTo(balance) > 0) throw RuntimeException("輸入金額不能大於結餘(balance)")

    balance = balance.subtract(amount)
    balaceUpdateTime = LocalDateTime.now()
}

在 main 呼叫後

wallet.increaseBalance(BigDecimal(100))
println("結餘(balance): ${wallet.balance}, 修改時間: ${wallet.balaceUpdateTime}")

wallet.decreaseBalance(BigDecimal(50))
println("結餘(balance): ${wallet.balance}, 修改時間: ${wallet.balaceUpdateTime}")

可以看到以下 output 的結果

取得 balance: 200

儲值...100
取得 balance: 200
更新 balance: 300
更新 balance 時間: 2020-09-30T09:27:27.852
取得 balance: 300
結餘(balance): 300, 修改時間: 2020-09-30T09:27:27.852

消費金額...50
取得 balance: 300
取得 balance: 300
更新 balance: 250
更新 balance 時間: 2020-09-30T09:27:27.853
取得 balance: 250
結餘(balance): 250, 修改時間: 2020-09-30T09:27:27.853

會發現只要顯示 balance, 的確就會呼叫 balance 的 get() 印出 取得 balance: xxx

設定 balance 就會呼叫 balance 的 set() 印出 更新 balance: xxx

計算屬性

昨天講到 backing field,但其實有個狀況是不需要產生 backing field。

class Dice() {
    val rolledValue
        get() = (1..6).shuffled().first()
}

因為每次讀取時,都會重新計算,沒有初始值或預設值,所以就不需要 backing field 來存放。

val dice = Dice()
println("骰子擲出點數: ${dice.rolledValue}") 
println("骰子擲出點數: ${dice.rolledValue}")

結果:
骰子擲出點數: 3
骰子擲出點數: 2

Race condition

還有一個很特殊的狀況,如果今天多新增了一個 PaymentWay 的 class 在 Wallet 裡面使用, showPaymentWay(),如果僅僅是做 null 判斷,compiler 會認為這樣在 multi thread 時會有危險

class PaymentWay(val name: String)

class Wallet(val id: Long, _balance: BigDecimal, _createTime: LocalDateTime) {

    var paymentWay: PaymentWay? = PaymentWay("Apple Pay")

    fun showPaymentWay() {
        if (paymentWay != null) {
            println(paymentWay.name)
        }
    }
}

如下, 提示提到了可能會 PaymentWay 是 可變的屬性 可能會在某時間點被改變

https://ithelp.ithome.com.tw/upload/images/20200920/20129902uiYiTE5JXn.png

在 Kotlin 我們可以輕鬆簡單的使用 also, let 匿名函數(這兩個方法之後會再提到)的特性來創造一個 local 的範圍,來確保不會有 race condition

fun showPaymentWay() {
    paymentWay?.also { println(it.name) }
}

或

fun showPaymentWay() {
        paymentWay?.let { println(it.name) }
}

最後再來複習一下,可見性

Kotlin 中的 class 可見性修飾符號(Visibility Modifiers)有以下幾種

  • public: 其實可以不用寫,constructor, function, property 預設就是 public,代表是隨處都可以看見
  • private: 在同一 class 下可以看見
  • protected: 在同一 class 或 class 的子 class 下可以看見
  • internal: 在相同 module 下可以看見

其實跟 function 那時提到的一樣

private constructor

constructor 預設都會是 public

但有時候會需要 private,像是當你不想要提供別人透過 contructor 來建立一個新物件的時候

就可以像這樣做,constuctor 是 private 的,無法訪問,然後透過 companion object (伴生物件)提供建立的方法,companion object 在後面會再提到。

class Foo private constructor(val someData: String) {
    companion object {
        fun constructorA(): Foo {
            // do stuff
            return Foo("someData")
        }
    }
}

今天就講到這裡啦!謝謝大家,我們明天見!

今日練習的程式在這: 請點我