以函數(shù)式編程思想優(yōu)化我們的代碼


讓我們從一段代碼開始肤京,引入函數(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屡谐,我希望我能再寫出一篇文章介紹它。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末度秘,一起剝皮案震驚了整個濱河市饵撑,隨后出現(xiàn)的幾起案子滑潘,更是在濱河造成了極大的恐慌,老刑警劉巖语卤,帶你破解...
    沈念sama閱讀 211,884評論 6 492
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件粹舵,死亡現(xiàn)場離奇詭異,居然都是意外死亡眼滤,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,347評論 3 385
  • 文/潘曉璐 我一進店門情妖,熙熙樓的掌柜王于貴愁眉苦臉地迎上來毡证,“玉大人蔫仙,你說我怎么就攤上這事∫“睿” “怎么了?”我有些...
    開封第一講書人閱讀 157,435評論 0 348
  • 文/不壞的土叔 我叫張陵居扒,是天一觀的道長喜喂。 經(jīng)常有香客問我,道長玉吁,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 56,509評論 1 284
  • 正文 為了忘掉前任这揣,我火速辦了婚禮影斑,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘塞俱。我一直安慰自己吏垮,他們只是感情好罐旗,可當我...
    茶點故事閱讀 65,611評論 6 386
  • 文/花漫 我一把揭開白布九秀。 她就那樣靜靜地躺著,像睡著了一般鼓蜒。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上娇豫,一...
    開封第一講書人閱讀 49,837評論 1 290
  • 那天畅厢,我揣著相機與錄音,去河邊找鬼浦楣。 笑死咪辱,一個胖子當著我的面吹牛,可吹牛的內(nèi)容都是我干的油狂。 我是一名探鬼主播庐杨,決...
    沈念sama閱讀 38,987評論 3 408
  • 文/蒼蘭香墨 我猛地睜開眼灵份,長吁一口氣:“原來是場噩夢啊……” “哼哮洽!你這毒婦竟也來了?” 一聲冷哼從身側(cè)響起氛什,我...
    開封第一講書人閱讀 37,730評論 0 267
  • 序言:老撾萬榮一對情侶失蹤匪凉,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后贸铜,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體聂受,經(jīng)...
    沈念sama閱讀 44,194評論 1 303
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 36,525評論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了碗旅。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 38,664評論 1 340
  • 序言:一個原本活蹦亂跳的男人離奇死亡医瘫,死狀恐怖川尖,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情被芳,我是刑警寧澤馍悟,帶...
    沈念sama閱讀 34,334評論 4 330
  • 正文 年R本政府宣布,位于F島的核電站侵状,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏趣兄。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 39,944評論 3 313
  • 文/蒙蒙 一拼窥、第九天 我趴在偏房一處隱蔽的房頂上張望蹋凝。 院中可真熱鬧鲁纠,春花似錦鳍寂、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 30,764評論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春笼裳,著一層夾襖步出監(jiān)牢的瞬間躬柬,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 31,997評論 1 266
  • 我被黑心中介騙來泰國打工允青, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人法牲。 一個月前我還...
    沈念sama閱讀 46,389評論 2 360
  • 正文 我出身青樓琼掠,卻偏偏與公主長得像,于是被迫代替她去往敵國和親悼瓮。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 43,554評論 2 349

推薦閱讀更多精彩內(nèi)容

  • 原文鏈接:https://github.com/EasyKotlin 值就是函數(shù)埋市,函數(shù)就是值命贴。所有函數(shù)都消費函數(shù),...
    JackChen1024閱讀 5,957評論 1 17
  • 第5章 引用類型(返回首頁) 本章內(nèi)容 使用對象 創(chuàng)建并操作數(shù)組 理解基本的JavaScript類型 使用基本類型...
    大學(xué)一百閱讀 3,216評論 0 4
  • 文/程大師 大千世界,蕓蕓眾生胚泌。我們每一個人都生活在這個變化萬千,無邊無際的世界當中零蓉。人可以避免的了其他穷缤,唯獨無法...
    c7d9204d01d5閱讀 583評論 0 0
  • 淡入淡出效果 使用.transitionCrossDissolve 立方體旋轉(zhuǎn)效果
    ted005閱讀 500評論 0 49
  • 我每天 都在思考自己究竟想要怎樣的生活身坐,卻又日復(fù)一日做著相同的事:吃飯,睡覺摊唇,上課涯鲁,刷朋友圈,看以往的同學(xué)在大學(xué)...
    蘇蓁閱讀 2,321評論 21 22