iT邦幫忙

2022 iThome 鐵人賽

DAY 4
0
Software Development

Kotlin on the way系列 第 4

Day 4 別當連 if 都寫爛的工程師 Make your if statement better

  • 分享至 

  • xImage
  •  

if it is so simple why haven't you done it already.
Bat man.

A/B test, English version is down below ?

前一篇簡單聊了可讀性,當我寫到常見結構時,這篇就注定出來了,可讀性的維度不只是命名,但命名一定是高可讀性的基礎,可讀關乎了整個程式結構,從一個變數,到整體專案架構,讓我們從 if 了解那些不好的控制流程

我是控制流程圖

多重否定

圖源 火星軍情局

參加過公投的應該都記得那精彩的多重否定吧!!

在程式碼中,偶而能看到這樣的判斷

if(!isNotValid())

其實就是 isValid() ,但這已經是簡單的爛東西了,來看看更精彩的

if(!isNotValid() && shouldn't() && !use() && !capable() && can't())

當然這不是上面梗圖的直譯,直譯應該是

if(!isNotValid()){
   if(shouldn't()){
       if(!use()){
            if(!capable()){
                if(can't(answer)){
       
               }
            }         
       }
   }
}

如果你看不懂,那就對了,永遠不要這樣設計,這個範例只是一個結構上的示範,來看看實務的例子

if(!isNotBlank() && isValid() && !isNotFill() || isRegexFormatValid())

if(isBlank() && isValid() && isFill() || isRegexFormatValid())

哪個比較清楚呢?

隱藏資訊

隱藏資訊重要嗎?重要,好的程式應該只讓你關注眼前的代碼,而不必在意其他地方

if(
    email.isBlank() && 
    "/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$".toRegex().matches(email) &&
    password.isBlank() &&
    password != email &&
    password.length >= 8 &&
    nickName.check.reposnse.body
)

檢查三個欄位,其實以 Kotlin 語法來說也還算不難理解,但說糙肯定還是糙的

  1. 欄位檢查是通用商業邏輯,註冊、登入、修改資訊等等,會重複用到,且每個調用點都應保持一致,就應該抽出
    • 如果檢查邏輯不一致,那可能註冊的格式不嚴謹但通過了,到了登入時反而登不進去
  2. 到處都是的正規表達很難維護,看也不確定格式是否正確
    • 非用不可時用變數包裝 const val EMAIL_FORMAT_REGEX_RULE = "..."
    • 用封裝過的原生方法 android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches()
  3. 檢查暱稱是否重複,要呼叫 api ,但只傳入 body 意圖不明

那應該改寫成如何呢?

if(
    isEmailValid() &&
    isPasswordValid() &&
    isNickNameValid()
)

短路求值

我們會在判斷式內用 And Or 連接不同的判斷

if( a && b)// 兩個都要 true
if(a || b)//其中一個 true

短路求值(Short-circuit evaluation; minimal evaluation; McCarthy evaluation; 又稱最小化求值)[1],是一種邏輯運算符的求值策略。只有當第一個運算數的值無法確定邏輯運算的結果時,才對第二個運算數進行求值。
wiki

or 的判斷就是採用短路求值的策略,a 一定會被檢查,但如果 a 是true,就不會去檢查 b

數線順序

code complete 2

對同一變數的數值區間判斷,以值順序表達對我們而言更好理解

if(2 < x && x < 8)
if(y > 2 || 8 < y)

相比

if(x < 8 && 2 < x)
if(8 < y || y > 2)

好懂的多
但在 Kotlin 我們還能用 range 處理

if((1..10).contains(x))
if(i in 1..10)

巢狀拆解

儘管 Kotlin 有 Coroutine 可以避免 callback hell,但仍無法避免巢狀的邏輯判斷,想為其重構,你應先為其編寫測試,而後試著把它拆解

if(consition 1){
    if(condition 2) {
        if(condiotion 3) {
            if(condition 4) {
                
            }
        }
    }
}
if(consition 1){
    if(condition 2) {

    }
}
if(condition 1 && condition2 && condition 3){
    
    if(condition 4) {
        
    }
}

善用既有工具

enum and sealed class

在和後端拿資料時,有時會拿到奇妙數字 admin:1,其實這是後端在使用數字來定義每個用戶的權限,但如果你寫了

when(admin) {
    0 -> {}
    1 -> {}
    2 -> {}
    ...
}

根本不知道你想幹嘛,好點是會先定義成

const val SUPER_ADMIN = 0
const val ADMIN = 1
const val NORMAL_USER = 2

when(admin) {
    SUPER_ADMIN -> {}
    ADMIN -> {}
    NORMAL_USER -> {}
    ...
    else -> throw Exception{}
}

這樣也行,起碼看得懂了

enum class UserState(code:Int){
    VALID(0), INVALID(1), EXPIRED(2);
}

但在 Kotlin 裡面,有更好的寫法,在官方的範例裡面,enum 並沒有被寫出可以包含值,但我們可以透過將值傳入,使其自動對應到該設置,而且這樣的封裝可以免去寫 else 的問題

when(state){
    UserState.VALID -> {}
    UserState.INVALID ->{}
    UserState.EXPIRED ->{}
}

多型

多型也是 OOP 的一大特色,我們會利用介面定義抽象行為,並在實作方定義詳細操作

interface Animal {
    
}

class Cat:Animal {
    
}
class Fish:Animal {
    
}

這樣的優點在於,依賴於抽象介面的元件,可以呼叫介面定義好的方法,只需在意輸入輸出即可,對實作不用關心,更不用在

if(animal is Cat)...

常見的隱藏問題 - 浮點數精度

你知道 0.1 + 0.2 是多少嗎?

0.1 + 0.2

= 0.30000000000000004

咦不是 0.3 嗎?

其實所有程式語言使用 IEEE 754 的儲存格式都會有這個問題

像是 js

console.log(0.1 + 0.2)
0.30000000000000004
//js solution
(0.1+0.2).toFixed(1) // 0.3

而在 Kotlin 裡面,我們可以使用

println(0.1.toBigDecimal() + 0.2.toBigDecimal()) // 0.3

來解決

那這跟 if 有什麼關係?如果你寫了這種判斷就會在計算優惠的時候,使邏輯和原先的不同,金額越大損失越多

val localTax = 0.2f
val nationalTax = 0.1f
if(amuont*(1f+localTax + nationalTax) > 103){
    //get discount
}  

In the previous article, we discuss about readability, and mentioned common structure, the dimension of readability is not just about naming, but naming is the basic of readability, readability contains the entire code structure, from a variable to full project, let's start with those bad example of is condition

I'm control flow diagram

Multiple deny

You should write for positive expression, but sometimes we can see code like this

if(!isNotValid())

It actually represent isValid(), but this sample is easy one

//this is a structure sample translated from a meme in Mandarin
if(!isNotValid()){
   if(shouldn't()){
       if(!use()){
            if(!capable()){
                if(can't(answer)){
       
               }
            }         
       }
   }
}

Hide information

Hide information is important, a good code should let us focus in a small scope, and don't need to worry other part

if(
    email.isBlank() && 
    "/^\w+([\.-]?\w+)*@\w+([\.-]?\w+)*(\.\w{2,3})+$".toRegex().matches(email) &&
    password.isBlank() &&
    password != email &&
    password.length >= 8 &&
    nickName.check.reposnse.body
)

check the three field, although it is readable, but it still a shit code

  1. validate the field is common business logic, register, login, change person information require the same logic
    • If validate is not consistent, it is possible that register is success, but user can't login
  2. Raw string Regex is difficult to maintain, and hard to check the logic
    • wrap with valuable if necessary const val EMAIL_FORMAT_REGEX_RULE = "..."
    • use the existing tool android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches()
  3. check nickname is used by others require api call, but no one know what is the intention to pass in body

So we can change it to

if(
    isEmailValid() &&
    isPasswordValid() &&
    isNickNameValid()
)

Short-circuit evaluation

We use and or to connect different condition

if( a && b)// both true
if(a || b)// one is true

Short-circuit evaluation, minimal evaluation, or McCarthy evaluation (after John McCarthy) is the semantics of some Boolean operators in some programming languages in which the second argument is executed or evaluated only if the first argument does not suffice to determine the value of the expression
wiki

number order

code complete 2

condition for int in a range, expression with order is easier to understand

if(2 < x && x < 8)
if(y > 2 || 8 < y)

compare above to this one

if(x < 8 && 2 < x)
if(8 < y || y > 2)

but inside Kotlin we can deal with range

if((1..10).contains(x))
if(i in 1..10)

destruct nest condition

Although Kotlin has Coroutine to avoid callback hell, but we still saw nest condition judgement, to refactor it, you should write test first, then destruct it

if(condition 1){
    if(condition 2) {
        if(condition 3) {
            if(condition 4) {
                
            }
        }
    }
}
if(condition 1){
    if(condition 2) {

    }
}
if(condition 1 && condition2 && condition 3){
    
    if(condition 4) {
        
    }
}

Use exists tool

enum and sealed class

When we take data from backend, sometimes we get a number admin:1, it actually represent different permission for users, if you wrote somethings like

when(admin) {
    0 -> {}
    1 -> {}
    2 -> {}
    ...
}

nobody know what does it means, it will be better define user first

const val SUPER_ADMIN = 0
const val ADMIN = 1
const val NORMAL_USER = 2

when(admin) {
    SUPER_ADMIN -> {}
    ADMIN -> {}
    NORMAL_USER -> {}
    ...
    else -> throw Exception{}
}

Now is better, but we can make it simpler

enum class UserState(code:Int){
    VALID(0), INVALID(1), EXPIRED(2);
}

In the official demonstrate, enum doesn't pass a value inside, but actually we could, by set a parameter to it, it will auto cast to correspond type, and now our when statement don't need else anymore

when(state){
    UserState.VALID -> {}
    UserState.INVALID ->{}
    UserState.EXPIRED ->{}
}

polymorphism

polymorphism is big feature of OOP, we define abstract behavior, and define it in implementation

interface Animal {
    
}

class Cat:Animal {
    
}
class Fish:Animal {
    
}

The advantage is component relay on abstract, can call use those function define in abstract, only care about input, output, and no concern to implementation, of course they don;t need to check type like this

if(animal is Cat)...

common hidden issue - Float point math

do you know how much is 0.1 + 02?

0.1 + 0.2

= 0.30000000000000004

why is the result is not 0.3?

Actually all language IEEE 754 store format have the same issue

Like js

console.log(0.1 + 0.2)
0.30000000000000004
//js solution
(0.1+0.2).toFixed(1) // 0.3

In Kotlin we can use

println(0.1.toBigDecimal() + 0.2.toBigDecimal()) // 0.3

to fix it

Why does this matter? If you use this logic in accumulate discount, you might cost unexpected money

val localTax = 0.2f
val nationalTax = 0.1f
if(amuont*(localTax + nationalTax) > 103){
    //get discount
}  

reference

https://taiwan-kotlin-user-group.github.io/kotlin-0.1+0.2/


上一篇
Day3 Readability 為可讀性做設計
下一篇
Day 5 會失控的變數範圍 Limited the scope of variable
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

1 則留言

0
CathyShen
iT邦新手 4 級 ‧ 2022-10-12 22:19:29

用數線順序來講解錯誤流程判斷好棒!
學到新知識了!

我要留言

立即登入留言