iT邦幫忙

2022 iThome 鐵人賽

DAY 8
0
Software Development

Kotlin on the way系列 第 8

Day 8 Mutability 是把雙面刃 Mutability is double edged sword

  • 分享至 

  • xImage
  •  

People don't change.
Times do.
John Wick

程式初期,筆者覺得 var 超好用,可讀可寫一直爽,到後期一點變成 val 超棒的,説一就一不會變,直到寫了更多程式後,我終於找到內心的平衡了

此表格整理了 Effective Kotlin 的一些重點

不可變 可變
多線程操作 安全,因為只讀 不安全,因為有機會 race condition
易於推論 可,總是固定的 不可,有潛在變化的可能性
快取 可,因為他不會被改變 麻煩,要在每個變更點以線程安全方式更新快取
預防性拷貝 免,因為無法被改變 要,而且要考慮到深淺拷貝
建構其他物件 可,最適合 可,但可能為該物件增加了更多的更動點
hashcode/ map 符合設計 有潛在違反設計可能

val/ var

你應知道的 Kotlin開始,就提過了 val/ var 的差異在於 getter/ setter,總結一下就是 getter/ setter 允許變化,但 getter 的變化要先定義,且較不易察覺

what does compiler did to const val

如果我們有一段

const val HELLO = "hello"
fun checkUserName(name:String){
    if(name == HELLO){
        //do something
    }
}

編譯後會變成這樣

const val HELLO = "hello"
fun checkUserName(name:String){
    if(name == "hello"){
        //do something
    }
}

算是完全沒有變化點的方式了

被偷渡的 setter

假設我們定義了驗證的介面,並想要為密碼做正規檢查

interface Validator {
    val regexRule:String
    
    fun isValidate():Boolean
}

class PasswordValidator:Validator {
    override val regexRule = "..."
}

但是呢,語法上可以變成var 而不報錯

class PasswordValidator:Validator {
    override var regexRule = "..."
}

如果這種細節沒被檢查到,那這個密碼驗證的正規檢查便不再可信了,下面兩種作法會好得多,不應被變動的東西就不要給他機會

class PasswordValidator:Validator {
    
    companion object {
        const val regexRule = "..."
    }
}
class PasswordValidator:Validator {
    ...
        Constant.PASSWORD_REGEX_RULE
}
object Constant {
    const val PASSWORD_REGEX_RULE = "..."
}

different layer of mutable point

跨層級的變動是最危險的,這個層級是指什麼呢?
module, layer, class, object ...etc, 我們應盡可能隱藏資訊,盡可能減少曝露的細節,拿 viewModel 來說吧

class UserProfileViewModel(){
    var profile = MutableLiveData<Profile>()
}

class UserProfileFragment(){
    private val viewModel:UserProfileViewModel by viewModel()
}

data class Profile(
    val id:Int,
    val name:String,
    var photo:String?= null
)

試問,我在 viewModel 一行代碼裡面曝露了幾個變化點?
.
.
.
.
.
.
.
.
.
3個,厲害吧XD
而這三個都可以在能存取到的其他類別做修改,如果今天 viewModel 是 share 的,根本無法預期資料會如何變化

class UserProfileViewModel(){
    private val _profile = MutableLiveData<Profile>()
    val profile:LiveData<Profile>
        get() = _profile
    
    fun updateProfile(profile:Profile){
        // api request to backend for update
        // if success
            // update _profile
    }
}

data class Profile(
    val id:Int,
    val name:String,
    val photo:String?= null
)

data class copy

一起來拆開看看 data class copy 實際上做了什麼吧~~

data class User(
    val name:String, 
    val age:String
)
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

沒錯,他給了我們一個新的實例,但如果今天裡面是長這樣

data class User(
    val name:String, 
    val age:String,
    val favoriteMovie:MutableList<String> = emptyList()
)

copy 時 favoriteMovie 會是同一個 instance 喔

deep copy?

我們應該為上述情境使用深拷貝嗎?

fun deepCopy():MyCustomClass {
        val JSON = Gson().toJson(this)
        return Gson().fromJson(JSON, MyCustomClass::class.java)
    }

其實,不建議,除非該做法完美符合業務邏輯

data class 本身設計更適合不可變資料,如果使用者需要資料變動,再利用copy 的方式改變即可

欲知更多資訊??? 快來報名 10/20 號的 Effective Kotlin 讀書會

English

I collect some point from Effective Kotlin

Immutable Mutable
Parallel operator safe, read only Notsafe, race condition
figure out error easy, always same not easy, might change at run time
cache yes, always same annoying, need update in every change point, also race condition
defensive copy no need yes, think about shadow copy and deep copy
use for build object yes, appropriate yes, but increase potential multable point
hashcode/ map respeact contract might break the contract

val/ var

From everything you should know about Kotlin, I mentioned about different between val/ var, in conclusion both getter/ setter allow change, but getter require defined first, and it is harder to find out

what does compiler did to const val

For sample code like this

const val HELLO = "hello"
fun checkUserName(name:String){
    if(name == HELLO){
        //do something
    }
}

It will compile to this

const val HELLO = "hello"
fun checkUserName(name:String){
    if(name == "hello"){
        //do something
    }
}

It is a safe way to maintain mutability

smuggling setter

For example we define an interface for validate, and a class for check password regex rule

interface Validator {
    val regexRule:String
    
    fun isValidate():Boolean
}

class PasswordValidator:Validator {
    override val regexRule = "..."
}

But in Kotlin syntax it can change to var without syntax error

class PasswordValidator:Validator {
    override var regexRule = "..."
}

detail like this is dangerous, the regex for password check is no longer be trusted, the following solution is better. we should never give chance t immutable things

class PasswordValidator:Validator {
    
    companion object {
        const val regexRule = "..."
    }
}
class PasswordValidator:Validator {
    ...
        Constant.PASSWORD_REGEX_RULE
}
object Constant {
    const val PASSWORD_REGEX_RULE = "..."
}

different layer of mutable point

Mutable across layer is danger, what does th layer means?

module, layer, class, object ...etc, we should hide our information as much as possible, take viewmodel as example

class UserProfileViewModel(){
    var profile = MutableLiveData<Profile>()
}

class UserProfileFragment(){
    private val viewModel:UserProfileViewModel by viewModel()
}

data class Profile(
    val id:Int,
    val name:String,
    var photo:String?= null
)

How many mutable points I exposed in one line of code of viewmode?
.
.
.
.
.
.
.
.
.
Three, amazing LOL

var, mutableLiveData, Profile.photo those three mutable point could be change from everywhere can access them, we can't expect when and how the data change

class UserProfileViewModel(){
    private val _profile = MutableLiveData<Profile>()
    val profile:LiveData<Profile>
        get() = _profile
    
    fun updateProfile(profile:Profile){
        // api request to backend for update
        // if success
            // update _profile
    }
}

data class Profile(
    val id:Int,
    val name:String,
    val photo:String?= null
)

data class copy

Let's check it out how does copy works in data class

data class User(
    val name:String, 
    val age:String
)
fun copy(name: String = this.name, age: Int = this.age) = User(name, age)

That's right, we received a new instance, but if we contain a mutablelist

data class User(
    val name:String, 
    val age:String,
    val favoriteMovie:MutableList<String> = emptyList()
)

the MutableList will be same instance for original data class and copied data class

deep copy?

Should we use deep copy for the above sample?

fun deepCopy():MyCustomClass {
        val JSON = Gson().toJson(this)
        return Gson().fromJson(JSON, MyCustomClass::class.java)
    }

Unless the situation fit your business logic, otherwise won't recommend

the data class itself suit for immutable data, if user need data change, the design force user use copy to protect original data

For more information??? Sign up for Effective Kotlin study jam

Reference:
Effective Kotlin


上一篇
Day 7 註解鬼故事 horrible story about comment
下一篇
Day 9 程式碼保衛戰 Defensive programming
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言