讓我們從一段代碼開始肤京,引入函數(shù)式編程
function buyCoffee(creditCard){
charging(creditCard,1.00)
let cup=new Coffee()
return cup
}
這是平時我們常用的描述買一杯咖啡的過程抗斤,buyCoffee方法接收一個信用卡賬號作為參數(shù)蹈垢,我們在方法里直接調(diào)用另一個計費的方法,計費成功之后驶臊,再返回一個咖啡的實例挪挤,這樣我們就完成了一個買咖啡的操作叼丑。這個方法看起來沒有什么問題关翎,但是,我們想多買幾杯咖啡的是時候鸠信,應(yīng)該怎么做呢纵寝?不考慮性能的話,我們會寫循環(huán)調(diào)用這個買咖啡的方法星立,但是考慮到性能爽茴,或者萬一中途調(diào)用失敗的話,處理起來就比較尷尬了绰垂,所以我們一般會寫一個批量處理的版本室奏,代碼大同小異。我這就不再用代碼表示了劲装。比較遺憾的是胧沫,批量版本沒辦法復(fù)用單一版本的處理結(jié)果昌简。
下面我再寫出另一個處理這個過程的方法,請大家做下對比绒怨。
function buyCoffee(creditCard){
const coffee=new Coffee()
const fee=1.00
const charge={creditCard,fee}
return {coffee,charge}
}
function buyCoffees(creditCard,count){
const turples=new Array(count).fill(buyCoffee(creditCard))
const coffees=turples.map({coffee}=>coffee)
const charges=turples.map({charge}=>charge)
const charge=charges.reduce((a1,a2)=>{creditCard:a1.creditCard,fee:a1.fee+a2.fee})
return {coffees,charge}
}
const {coffees,charge}=buyCoffees('1233445',12)
const {creditCard,fee}=charge
charging(creditCard,fee)
由于ES6的語法不是我所想說的重點纯赎,大概給大家介紹下寫法二的意思,buyCoffee方法實際上是返回了一個對象南蹂,對象里有兩個屬性犬金,一個屬性是coffee,另一個屬性是賬單。buyCoffees方法就比較有意思了六剥,我復(fù)用了buyCoffee方法晚顷,在這個方法里,我主要的操作是合并賬單疗疟,并返回一個咖啡列表和一個總賬單音同。然后我在外面調(diào)用了付費方法。
那么問題來了秃嗜,第二種寫法有什么好處权均?
先不說第二種寫法有什么好處,先說第一種寫法有什么壞處锅锨。
作為一個長期與業(yè)務(wù)邏輯打交道的一線碼農(nóng)叽赊,不知道大家有沒有這種感覺,那就是特別不愿意遇到一個沒有返回值的方法必搞,沒有返回值必指,基本上就意味著這個方法有輸入輸出,或者會改變你的參數(shù)恕洲,尤其你傳入的是一個比較大的對象的時候塔橡,你并不知道這個方法如何處理你的對象,所以得顛顛的去讀一下這個方法如何實現(xiàn)霜第,看看你的對象有沒有遭到意外的破壞葛家。
一般的,我們認為一個方法沒有返回值泌类,肯定會產(chǎn)生副作用癞谒。相對的,不會產(chǎn)生副作用的方法刃榨,我們稱之為純函數(shù)弹砚。這里解釋下,是不是有返回值的方法枢希,就一定是純函數(shù)呢桌吃?大多數(shù)情況下,并不是這樣的苞轿,檢驗一個函數(shù)是不是純函數(shù)茅诱,有一個很簡單的準則就是为流,如果用這個函數(shù)的返回值替代這個函數(shù),產(chǎn)生的結(jié)果不變让簿,這樣一個函數(shù)才是純函數(shù)敬察,說起來繞嘴,實際上用下面的公式來表示的話
y=f(x)尔当,g(y)=g(f(x))
此時莲祸,我們就認為f(x)是一個純函數(shù)。
如果還不明白的話椭迎,參考第一段代碼锐帜,我們的運行buyCoffee,實際上返回的是一個new Coffee(),我們在另一個地方引用到了buyCoffee畜号,我們用new Coffee替換掉buyCoffee,顯然這兩個不能互相替代缴阎,因為buyCoffee發(fā)生了計費動作。
純函數(shù)的優(yōu)點就是不會產(chǎn)生副作用简软。
我們在業(yè)務(wù)處理中蛮拔,尤其是需要對一個參數(shù)對象做一連串的處理過程中,可能會調(diào)用很多的方法痹升,這些方法有可能是不同時期不同的人寫的,如果有的方法產(chǎn)生了副作用疼蛾,而其他人沒有覺察到,這會導(dǎo)致很多問題衍慎。這里我再引入一個概念,叫不可變對象皮钠,一個對象是不可變對象稳捆,意味著鳞芙,其是一個安全的對象期虾,不論在哪里用到了原朝,都不會被修改,我們可以安全的使用這個對象镶苞,即使在多線程環(huán)境下喳坠。當你在讀代碼的過程中,如果寫這段代碼的那個人把某個對象定義成了不可變對象壕鹉,意味著你可以直接跳到你關(guān)心的那塊代碼上,而不用小心翼翼的去通過上下文推斷這個對象到底經(jīng)歷了什么晾浴。
所以,一個良好的編程習(xí)慣是抖棘,在處理過程中不要改變對象狸涌,如果你真的需要改變一個對象的某個值的話,把這個副作用推到最外層帕胆。
之所以花這么大篇幅介紹什么叫做無副作用,因為無副作用是函數(shù)式編程的基石芙盘。
const originArray=[1,2,3,4,5,6,7,8,9,0]
const targetArray=originArray.filter(num=>num%2==0).map(num=>num*num)
console.log(originArray) //[1,2,3,4,5,6,7,8,9,0]
console.log(targetArray) //[2,16,36,64,0]
// 無副作用意味著我們原始的數(shù)據(jù)不會遭到修改
那么函數(shù)式編程除了讓我們不用擔心我們的參數(shù)被改變脸秽,還有什么好處呢?我還要以一段代碼來演示:
有一個場景贷盲,我需要知道一個容器中有沒有包含我想要的對象剥扣,如果有的話,我就做一種處理钠怯,沒有的話就做另一種處理,平時我們一般是這樣寫的
List<Charge> charges=service.getCharges();
boolean contains=false;
for(Charge charge: charges){
if(charge.getCreditCard().equals("123456789")){
contains=true;
break;
}
}
if(contains){
doSomething();
}else{
doAnothering();
}
作為這段代碼的作者鞠鲜,很難察覺到for循環(huán)有沒有什么問題断国。但是作為一個讀者,你要非常關(guān)心這個for循環(huán)里面做了什么事稳衬,然后才能繼續(xù)閱讀下面的代碼,如果你沒有覺察到這個問題的話碧信,請看下面這段代碼:
List<Charge> charges=service.getCharges();
boolean contains= charges.stream().anyMatch(charge->{charge.getCreditCard().equals("123456789")})
if(contains){
doSomething();
}else{
doAnothering();
}
這段代碼的意義不僅是幫我們省了一些代碼,更是明確的指出了躏筏,我們的contains如何得出呈枉,最重要的是,它把contains定義的地方和使用的地方放到了一起碴卧,讓我們保證了思維的連貫性。順便吐槽一句婶博,用Java就是麻煩荧飞,即使用函數(shù)式編程,語法也很啰嗦叹阔,scala里面這樣表示constains
val contains=charges contains { _.creditCard == "123456789" }
scala用val表示一個不可變對象耳幢,能進一步保證了代碼的安全性。java中我們可以用final關(guān)鍵字來約束這個contains睛藻,但是一般情況下拾因,final這個關(guān)鍵字好像被我們忘了一樣,很少被使用按摘。
直到這里,我還沒有介紹函數(shù)式編程中的另一個重要的概念炫贤,那就是函數(shù)是一等公民照激,它可以像Int,String或者其他Object一樣被傳來傳去俩垃,在前面的例子中
boolean contains= charges.stream().anyMatch(charge->{charge.getCreditCard().equals("123456789")})
anyMatch 方法接收的參數(shù) charge->{charge.getCreditCard().equals("123456789")} 是一個lambda
表達式,也就是我們通常說的匿名函數(shù)苹粟。如果你讀Java的api跃闹,你會發(fā)現(xiàn)anyMatch接收的參數(shù)是一個Predicat接口的實例,那Predicat接口又是啥望艺,跟進去發(fā)現(xiàn)Predicat是只有一個方法需要實現(xiàn)的接口找默,我們實際上是現(xiàn)實的是boolean test(T t)方法,也就是說實際上這段代碼是這樣的:
Predicat<Charge> predicat=new Predicat<Charge>({
boolean test(Charge charge){
return charge.getCreditCard().equals("123456789")
}
})
boolean contains= charges.stream().anyMatch(predicat)
so,Java8的函數(shù)式編程只不過是有點甜的語法糖而已。很多三方的庫比如guava,rxjava都能夠幫助我們在Java7甚至Java6下寫出這樣的代碼惩激,所以风钻,你還認為我們在老的Java項目上無法實現(xiàn)函數(shù)式編程嗎?
函數(shù)式編程思想不僅能夠幫助我們編寫更可讀的代碼骡技,還能幫助我們優(yōu)化架構(gòu)
想象下,如果你面對著一個超級復(fù)雜的業(yè)務(wù)系統(tǒng)毛萌,當一個數(shù)據(jù)發(fā)生改變的時候喝滞,可能涉及到多條業(yè)務(wù)線上的操作,編寫傳統(tǒng)的流水代碼意味著我們需要對這個系統(tǒng)所承擔的所有的業(yè)務(wù)線都要熟悉右遭,否則的話窘哈,任何一點點修改可能引起很多麻煩。這樣的系統(tǒng)滚婉,我們?nèi)绾卫煤瘮?shù)式編程思想優(yōu)化我們的架構(gòu)呢。
答案就是远剩,利用不可變對象。
在前端上瓜晤,現(xiàn)在最火的框架無非就是 vue/react,這兩個框架思路非常的一致,自己維護一個virtual dom,當virtual dom上的值發(fā)生改變的時候驱犹,它們幫著我們?nèi)ネㄖ鄳?yīng)的組件足画,這些組件響應(yīng)改變,但是這些組件不能反過來改變我們的值医舆。如果某個組件在響應(yīng)狀態(tài)變化的過程中產(chǎn)生了新的值桑涎,那么它把這個值再放回virtual dom,框架再幫我們把新的值廣播出去。
這個聽起來有點像觀察者模式攻冷。沒錯等曼,在復(fù)雜的系統(tǒng)中,需要一個消息總線來解耦各方的關(guān)系禁谦。引入消息總線之后,我們的代碼雖然遍布在各個地方丧蘸,但是代碼在執(zhí)行的時候遥皂,感覺起來就有點像這樣
listeners foreach {listern->listern(message)}
這種形式有一個好處,我們的listener只需要在消息總線上注冊一下就行弟孟,不需要耦合在一起样悟,這樣對系統(tǒng)的伸縮性非常有幫助庭猩。
如果你的某個listener非常在意某個消息陈症,可以將這個listener實現(xiàn)成同步的,一旦執(zhí)行不成功爬凑,立馬拋出異常试伙,讓整個消息處理都回滾。反之潘靖,如果你的listener只需要接受到這個消息蚤蔓,但是不需要一定等它執(zhí)行成功,則可以把它實現(xiàn)成異步的单寂,以讓出寶貴的時間片執(zhí)行下面的方法吐辙。
“高內(nèi)聚,低耦合”一直是我們作為一個碼農(nóng)的追求昏苏。在文章的最后,我只是簡單的引入了一個代碼解耦的方案洼专,其實這種方案已經(jīng)被很多大牛應(yīng)用過了孵构,只不過我們還沒有意識到和聽說到而已 。最近比較關(guān)注領(lǐng)域驅(qū)動設(shè)計(DDD)蜡镶,這種架構(gòu)設(shè)計真正實現(xiàn)了“高內(nèi)聚精盅,低耦合”這六個字,消息總線是DDD在實現(xiàn)過程中引入的一種角色妻枕,然而它存在的意義是非凡的,等我真正領(lǐng)會了DDD屡谐,我希望我能再寫出一篇文章介紹它。