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 語法來說也還算不難理解,但說糙肯定還是糙的
const val EMAIL_FORMAT_REGEX_RULE = "..."
android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches()
那應該改寫成如何呢?
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) {
}
}
在和後端拿資料時,有時會拿到奇妙數字 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
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 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
const val EMAIL_FORMAT_REGEX_RULE = "..."
android.util.Patterns.EMAIL_ADDRESS.matcher(target).matches()
So we can change it to
if(
isEmailValid() &&
isPasswordValid() &&
isNickNameValid()
)
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
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)
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) {
}
}
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 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)...
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
}