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 嗎?
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 是比較好的,為什麼?
第二次修改的設計
fun main(){
repeat(2) { greeting() }
}
fun greeting(name:String = "") = println(hello + name)
const val hello = "hello"
什麼是純函式?
相同的 input 會有 相同的 output ,且沒有副作用
副作用是程式運行時,系統狀態的改變,或是和外部世界可觀察的交互作用
常見的副作用包含但不限於
可以說,幾乎沒有專案可以避免作用,真實專案總是在和外界交互,經常需要 io 操作,所謂作用,本質上也是我們的意圖,有問題的地方在於 副
,就像是吃感冒藥,退燒是作用,嗜睡是副作用
那要如何正確的處理副作用呢?
用架構、抽象、封裝、命名、隱藏資訊,這些每個都是一個主題,簡言之,就是為高層次模組和低層次模組分離
來看看前面的範例,我們要如何為 greeting 做測試呢?
fun main(){
repeat(2) { greeting() }
}
fun greeting(name:String = "") = println(hello + name)
const val hello = "hello"
寫不出來QQ
對,一個混用了邏輯、副作用、框架的功能,編寫測試不是很麻煩就是做不了
就有人說了,「 閉上你的嘴 Dora ,用眼睛看不是靠張嘴 」請回他說,「 糙扣仔,別寫糙扣 」
那我們要如何解決呢?
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 團隊在設計集合方法時,已經充分考慮到副作用,對回傳集合的操作,都會回傳一個新的集合實體,下面我拿一段源碼做範例,可以直接看到他使用 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)
}
}
}
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
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?
result:
fun main(){
repeat(2) { greeting() }
}
fun greeting(name:String = "") = println(hello + name)
const val hello = "hello"
What is pure function?
same input will return same output, and there is no side effect
how a function changes the outside world.
common side effects include but not limited to following list
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
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?
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
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 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);
//=> []
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
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)
}
}
}
javascript function programming
Jetpack compose offical sample