iT邦幫忙

2022 iThome 鐵人賽

DAY 13
0

structure

  • 抽象
  • 資料
    • 隱藏
    • 公開
    • enum
    • sealed class
  • 小心狄米特原則
  • summary

封裝抽象概念

封裝,是要裝什麼? 在了解封裝之前,我們先來看看沒封裝的程式吧

val isElectric = false
val wheelsAmount = 4
val brand = "Toyota"
fun createCar() = Triple(isElectric,wheelsAmount,brand)

嗯嗯,不知所云,完全不知道我在幹嘛

那一樣的東西如果給他一層抽象的封裝呢?

val isElectric = false
val wheelsAmount = 4

class Car(
    isElectric:Boolean,
    wheelsAmount:Int,
    brand:CarBrand
){
    fun drive(){
        //...
    }
}
enum CarBrand {
    Toyato, Tesla
}
fun createCar(brand:CarBrand):Car = Car(isElectric,wheelsAmount,brand)
fun Car.checkState(){
    
}

這不就清楚多了嗎?
另一方面,封裝不僅可以讓類別持有屬性,還能為該類別定義屬於他的方法,像是這邊的 drive

現在來比較一下兩者的差異,封裝後的程式

  1. 有類別名稱可以更好的判讀
  2. 可以對資料下限制 CarBrand
  3. 可以用類別的可見操作符
  4. 可以定義類別方法
  5. 可以對類別定義擴展方法

可以說是好處多多,超讚的推推

封裝和資訊

上面我們已經了解了將抽象概念封裝後的好處,那封裝之後,我們還該注意許多細節,尤其是封裝的資訊可見度,從結論來說,我們實作封裝時,應該盡可能公開越少細節,且會針對不同的設計而選擇適合的 visibility modifier

資訊公開

公開資訊,比隱藏資訊簡單的多,但通常也會是造成麻煩的來源,急得前面的文章說過了可變點限制的問題嗎?

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

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

同一個例子,那你看我們現在公開了 profile 出去,所有能存取他的其他代碼,都有可能利用 MutableLiveData 去更改資料

class UserProfileViewModel(){
    private val _profile = MutableLiveData<Profile>()
    val profile: LiveData<Profile>
        get() = _profile
}

而我們通常的寫法是這樣,對外公開讀取,讓寫入限制在一個類別裡,那到底如何定義要公開哪些資訊呢?
除非必要,否則一律不公開,而對寫入操作的公開則應更加嚴謹

資訊隱藏

資訊隱藏可以說是類別設計中非常重要的一點,資訊隱藏的帶給我們的好處,遠遠超過資訊公開,可以將每個公開點都視為一種依賴,公開的資訊越多,外界對這個類別的耦合程度就越高,在修改公開區塊時,就需要考量到越多事情,我們應盡可能透過資訊隱藏限制範圍

透過 visibility modifier 做資訊隱藏的方式,都在這篇 會失控的變數範圍

而在前一個範例中,我們看到了 enum,他是另一種封裝與資訊隱藏的好範例

enum CarBrand {
    Toyato, Tesla
}

這是官方的使用方法,封裝一系列有限的類別,並用命名達到可讀性,那如果我們在這以上,再加點東西如何,比如後端給了我們用戶身份識別數字

 enum class UserPermission(val value: Int) {
    Normal(0), Admin(1) , SuperAdmin(2)
    companion object {
        fun fromInt(value: Int) = CoffeeType.values().firstOrNull  { it.value == value }
    }
}

我們可以利用建構子的方式定義,而在取值時,也是透過 .value 去拿到該數值

sealed class

儘管enum 已經為我們提供了方便的封裝,但我們依據不同的情境有時需要更為複雜的類別來輔佐,這時 sealed class 就可以很大程度地幫助我們

比如為畫面行為設置 uiEvent


sealed class UiEvent {
    data class Navigate(val route: String) : UiEvent()
    object NavigateUp : UiEvent()
    data class ShowSnackBar(val message: UiText) : UiEvent()
}

或是為 io 行為封裝狀態

sealed class Resource<out T>(
    data: T? = null,
    message: String? = null,
) {
    class Success<out T>(data: T) : Resource<T>(data)
    class Error<T>(
        message: String,
        data: T? = null,
    ) : Resource<T>(data, message)
    //class Loading<T>(data: T? = null) : Resource<T>(data)
    object Loading : Resource<Any>()
}

用 sealed class 可以做到更多複雜的狀態管理,彈性也會更大,能夠讓 class, object, data class 繼承

狄米特原則

最後來聊聊,資訊公開與狄米特原則,被簡單描述為「對任何函式傳回的物件,不該再呼叫上頭之方法」

fun main() {
    //wrong
    val myCustomer = Customer()
    
    val wallet = myCustomer.getWallet()
    
    wallet.getCridetCard()//破壞封裝
    wallet.subtractMoney()//錢包被別人動了
    
}

fun main() {
    //better
    val myCustomer = Customer()
    
    myCustomer.payMoney()
}

summary

封裝、抽象都是資訊隱藏的好方式,可以讓同一時間處理的本質複雜度減到最少,對資訊公開應盡可能嚴謹,並注意不要讓外部呼叫回傳物件的方法

English

structure

  • abstract
  • data
    • hide
    • open
    • enum
    • sealed class
  • law of demeter
  • summary

encapuslate of abstartc

encapsulate? What is this for? before we discuss about it, let's check out code without it

val isElectric = false
val wheelsAmount = 4
val brand = "Toyota"
fun createCar() = Triple(isElectric,wheelsAmount,brand)

hmm, I have no idea what am I doing

What if we add encapsulate to the abstract idea?

val isElectric = false
val wheelsAmount = 4

class Car(
    isElectric:Boolean,
    wheelsAmount:Int,
    brand:CarBrand
){
    fun drive(){
        //...
    }
}
enum CarBrand {
    Toyato, Tesla
}
fun createCar(brand:CarBrand):Car = Car(isElectric,wheelsAmount,brand)
fun Car.checkState(){
    
}

Isn't it clearer than before?
On The other side, encapsulate to class can holding state, and define function to class itself, like the drive function

Now if we compare these two, the program after encapsulate

  1. better readability
  2. limited CarBrand
  3. visibility modifier
  4. class fucntion
  5. entension fucntion

data

We discussed the advantage of encapsulate abstract idea above, and there are more detail we should care about, in conclusion, when we using encapsulate, we should hide information as much as we can, and choose between different visibility modifier

open data

open visibility is earlier than hide information, but it usually is the source of problem, in previous article we talk about mutability

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

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

In the same example, we make the profile visible, and all the class can access it, can use MutableLiveData to change data

class UserProfileViewModel(){
    private val _profile = MutableLiveData<Profile>()
    val profile: LiveData<Profile>
        get() = _profile
}

The common use case is this, we allow reading to open, and restrict writing inside the class. Usually we limited the visibility inside a class unless necessary

hide information

Hiding information is one of the most important in class design, the advantage if hiding information is more than open it, each visible point is kind of couple, we should limited it by hiding it

using visibility modifier to hide data in this article

In the previous sample, we saw enum, it is the other way to encapsulate and hide data

enum CarBrand {
    Toyato, Tesla
}

this is official way, encapsulate a series of class, using naming for readability, but if we add a little more above this, like number to identify permission

 enum class UserPermission(val value: Int) {
    Normal(0), Admin(1) , SuperAdmin(2)
    companion object {
        fun fromInt(value: Int) = CoffeeType.values().firstOrNull  { it.value == value }
    }
}

We can define it by using constructor, and when we need the value, we can get it by .value

sealed class

Although enum provides convention encapsu.state, sometimes we need. complexity class to use, and sealed class can help us

like setting uiEvent for user behavior


sealed class UiEvent {
    data class Navigate(val route: String) : UiEvent()
    object NavigateUp : UiEvent()
    data class ShowSnackBar(val message: UiText) : UiEvent()
}

or state for io operator

sealed class Resource<out T>(
    data: T? = null,
    message: String? = null,
) {
    class Success<out T>(data: T) : Resource<T>(data)
    class Error<T>(
        message: String,
        data: T? = null,
    ) : Resource<T>(data, message)
    //class Loading<T>(data: T? = null) : Resource<T>(data)
    object Loading : Resource<Any>()
}

with sealed class can do more complexity state management, and more flexibility to allow class, object, data class to inheritance

law of demeter

in the finally, the law of demeter is describe about shouldn;t call method on anything return by function

fun main() {
    //wrong
    val myCustomer = Customer()
    
    val wallet = myCustomer.getWallet()
    
    wallet.getCridetCard()//破壞封裝
    wallet.subtractMoney()//錢包被別人動了
    
}

fun main() {
    //better
    val myCustomer = Customer()
    
    myCustomer.payMoney()
}

summary

encapsulate and abstract is good solution to hide information, can decrease the complexity in same time, we should always restrict data visibility, and mind on law of demeter


上一篇
Day 12 OO 能吃嗎? 介面、抽象與釀酒 abstract design
下一篇
Day 14 OO 能吃嗎? 多型 polymorphism
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言