iT邦幫忙

2022 iThome 鐵人賽

DAY 6
0
Software Development

Kotlin on the way系列 第 6

Day 6 函式職人,一生懸命 Make your function simple

  • 分享至 

  • xImage
  •  

Why do I have this feeling you're about to mess up my entire life?
If I stay.

English version is down below

function 應該只做一件事,且只為此事而存在,好的 function 設計,能讓任何開發者可以從名稱猜到實作內容,而其中也不會有預期以外的行為

哲學上的糙 code

你覺得這裡面有糙 code 嗎?

fun main(){
    println("hello")
    greeting()
}


fun greeting(){
    println("hello")
}

肯定有吧?這不是重複了嗎?

那下面兩種改法你覺得哪個好呢?

fun main(){
    repeat(2) { greeting() }
}

fun greeting() = println("hello")
fun main(){
    repeat(2) { println(hello) }
}

const val hello = "hello"

我想或許會各有支持的人,但以我的角度,包成 function 是比較好的,為什麼?

  1. function 展示了意圖
  2. 提供了潛在的擴充性
  3. 覆用的情況會有統一的修改點

第二次修改的設計

fun main(){
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(hello + name)

const val hello = "hello"

pure function

什麼是純函式?

相同的 input 會有 相同的 output ,且沒有副作用

副作用是有限度的必要行為

副作用是程式運行時,系統狀態的改變,或是和外部世界可觀察的交互作用

常見的副作用包含但不限於

  1. io 操作
  2. log/ print
  3. mutable data

可以說,幾乎沒有專案可以避免作用,真實專案總是在和外界交互,經常需要 io 操作,所謂作用,本質上也是我們的意圖,有問題的地方在於 ,就像是吃感冒藥,退燒是作用,嗜睡是副作用

那要如何正確的處理副作用呢?
用架構、抽象、封裝、命名、隱藏資訊,這些每個都是一個主題,簡言之,就是為高層次模組和低層次模組分離

為什麼純函式更好測試

來看看前面的範例,我們要如何為 greeting 做測試呢?

fun main(){
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(hello + name)

const val hello = "hello"

寫不出來QQ
對,一個混用了邏輯、副作用、框架的功能,編寫測試不是很麻煩就是做不了

就有人說了,「 閉上你的嘴 Dora ,用眼睛看不是靠張嘴 」
請回他說,「 糙扣仔,別寫糙扣 」

那我們要如何解決呢?

  1. 分離業務邏輯
  2. 分離副作用
fun main() {
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(generateGreetingContent(name))

fun generateGreetingContent(name:String) = hello + name

const val hello = "hello"

一個可測試的業務邏輯就出來啦

這是過度設計嗎?

我們原本的邏輯從 3 行變成了 9 行,是否過度設計了?

fun main(){
    repeat(2) { println("hello") }
}

說實話,很看情境,我們幾乎不可能在真實專案裡寫結構和邏輯如此簡單的代碼
要考量的是,在真實開發中,邏輯的重複使用性、更動頻率、架構層的影響層級,儘管我建議越乾淨越好,很少有一次到位的設計,每個東西,都需要不斷地微重構

kotlin collection operator is so pure

Kotlin 團隊在設計集合方法時,已經充分考慮到副作用,對回傳集合的操作,都會回傳一個新的集合實體,下面我拿一段源碼做範例,可以直接看到他使用 copyOfRange 方法

/**
 * Returns a list containing elements at indices in the specified [indices] range.
 */
public fun <T> Array<out T>.slice(indices: IntRange): List<T> {
    if (indices.isEmpty()) return listOf()
    return copyOfRange(indices.start, indices.endInclusive + 1).asList()
}

而一段有副作用的集合操作是

javascript 範例

var arr = [1, 2, 3, 4, 5];

// pure
arr.slice(0, 3);
//=> [1, 2, 3]

arr.slice(0, 3);
//=> [1, 2, 3]

arr.slice(0, 3);
//=> [1, 2, 3]


// impure
arr.splice(0, 3);
//=> [1, 2, 3]

arr.splice(0, 3);
//=> [4, 5]

arr.splice(0, 3);
//=> []

限制參數

function 的參數越少越容易測試,零參數是最好的,而超過三個的參數就該停下來看看這個 function 是不是做了超過一件事,在 code complete 裡面提到,一個函式的參數量級應該在 7 +- 2
為什麼參數的數量重要,因為我們撰寫測試時,需要為每個案例寫出對應的測試,而參數數量越多,其排列組合也就越多

不要混用框架語法和邏輯語法

要想奇怪的寫法好難

像是這個範例,混用了不同層次的概念,混用了不同設計語法,function 不再只是處理 ui,也不再尊重 contract of framework

@Composable
fun chipGroups(chips:List<String>){

    var articles = mutableStateOf(emptyList())
    Row {
        chips.forEach { chip ->
            Chip(
                title = chip,
                onClick = {
                    //call network request
                    val themeArticles = HttpConnection()
                    .get()
                    ...
                    .body() as Articles
                    
                    articles = themeArticles
                }
            )
        }
        
        articles.forEach{ article ->
            Article(article = article)
        }
    }
}

english

function should build for one task, and it is the reason that function exists, a good design of function, capabali for any developer to guess its intention from its name, and no other unexpected action

think of shit code

Do you think this part of code is not good design?

fun main(){
    println("hello")
    greeting()
}


fun greeting(){
    println("hello")
}

Of course, there is redundancy

then which way to refactor is better?

fun main(){
    repeat(2) { greeting() }
}

fun greeting() = println("hello")
fun main(){
    repeat(2) { println(hello) }
}

const val hello = "hello"

I guess there are two different voices, but in my opinion, it will be better to wrap it as a function, why?

  1. function name shows its intention
  2. provide potential extensionable
  3. for reuse situation, there is a certain point to change

result:

fun main(){
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(hello + name)

const val hello = "hello"

pure function

What is pure function?

same input will return same output, and there is no side effect

side effect is a limited necessary task

how a function changes the outside world.

common side effects include but not limited to following list

  1. io operator
  2. log/ print
  3. mutate data

In conclusion, there is no project can avoid side effect, project in real world always interactor with outside world, those effect, is actually our intention, what we should aware is side effect, just like take a pill, bring down the fever is effect, lethargy is side effect

How do we properly deal with side effects?

We can use architecture, abstract, encapsulate, naming, hide information those technique, well each of those is a topic, in short to speak, seperate high layer module and low layer module

Why pure function is easier to test

Check the demo, how do we write tests for greeting?

fun main(){
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(hello + name)

const val hello = "hello"

we can't QQ
a function mixed in logic, side effect, framework is hard to test

someone might say,「 close your mouth Dora, check but your eyes not your mouth 」
please reply,「 shut up, shit code maker 」

So how do we deal with it?

  1. separate business logic
  2. separate side effect
fun main() {
    repeat(2) { greeting() }
}

fun greeting(name:String = "") = println(generateGreetingContent(name))

fun generateGreetingContent(name:String) = hello + name

const val hello = "hello"

A testable business logic is show up

does this over design?

To be honestly, we barely don't have chance to write code with its architecture and logic is so simple

what we care about is reusability, how often does logic change, how it impact in architecture, I will recommend cleaner is better, but the design is not one move, usually it require constantly micro refactor

kotlin collection operator is so pure

Kotlin team is collection operator well, they had consider about side effect, for operator return a collection, they will build a new instance, just like the following example

/**
 * Returns a list containing elements at indices in the specified [indices] range.
 */
public fun <T> Array<out T>.slice(indices: IntRange): List<T> {
    if (indices.isEmpty()) return listOf()
    return copyOfRange(indices.start, indices.endInclusive + 1).asList()
}

and an operator has side effect is

javascript sample

var arr = [1, 2, 3, 4, 5];

// pure
arr.slice(0, 3);
//=> [1, 2, 3]

arr.slice(0, 3);
//=> [1, 2, 3]

arr.slice(0, 3);
//=> [1, 2, 3]


// impure
arr.splice(0, 3);
//=> [1, 2, 3]

arr.splice(0, 3);
//=> [4, 5]

arr.splice(0, 3);
//=> []

limit number of parameter

if your function required parameter more than three, you might want to check its intention, in code complete, it says the number of function should be less than 7 +- 2

why does it matter, when we write test, we have to think about test case for all possibility

Do not mixed framework syntax and logic syntax

It is so hard to write weird sample

check this sampe, mixed concept from different layer, different design syntax, function is not dela with ui anymore, and it does not respect contract of framework

@Composable
fun chipGroups(chips:List<String>){

    var articles = mutableStateOf(emptyList())
    Row {
        chips.forEach { chip ->
            Chip(
                title = chip,
                onClick = {
                    //call network request
                    val themeArticles = HttpConnection()
                    .get()
                    ...
                    .body() as Articles
                   
                    articles = themeArticles
                }
            )
        }
       
        articles.forEach{ article ->
            Article(article = article)
        }
    }
}

reference

javascript function programming
Jetpack compose offical sample


上一篇
Day 5 會失控的變數範圍 Limited the scope of variable
下一篇
Day 7 註解鬼故事 horrible story about comment
系列文
Kotlin on the way31
圖片
  直播研討會
圖片
{{ item.channelVendor }} {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言