我們來(lái)一起看一下兩個(gè)程序員之間的故事。
以下示例代碼是用Scala寫(xiě)的教藻,不過(guò)本文所講的話題并不僅限于Scala,任何有Future/Promise支持的語(yǔ)言都是適用的告组。
下面這個(gè)wiki頁(yè)面羅列了各個(gè)有Future/Promise支持的語(yǔ)言,已經(jīng)涵蓋了大多數(shù)的常用語(yǔ)言籍滴。
我是異步函數(shù)的編寫(xiě)者
我寫(xiě)了兩個(gè)異步函數(shù),來(lái)提供給其他程序員同事使用待逞。
type CallBack = Try[String] => Unit
def pretendCallAPI(callBack: CallBack, okMsg: String, failedMsg: String) = {
val task = new TimerTask {
override def run() = {
val percentage = Random.between(1, 100)
if (percentage >= 50)
callBack(Success(okMsg))
else if (percentage <= 30)
callBack(Failure(new Exception(failedMsg)))
else
callBack(Failure(new Exception("network problem")))
}
}
new Timer().schedule(task, Random.between(200, 500))
}
val searchTB = pretendCallAPI(_, "product price found", "product not listed")
val buyFromTB = pretendCallAPI(_, "product bought", "can not buy, no money left")
這兩個(gè)異步函數(shù): searchTB用來(lái)從淘寶搜索物品,另一個(gè)buyFromTB用來(lái)購(gòu)買(mǎi)搜到的物品轩缤。
由于僅僅是為了演示而寫(xiě)的,他們兩個(gè)都是基于一個(gè)叫做pretendCallAPI的函數(shù)實(shí)現(xiàn)的转唉。
顧名思義,pretendCallAPI并不會(huì)真的去調(diào)用淘寶的API,而只是模擬API的行為皮钠。
這個(gè)pretendCallAPI函數(shù)有幾個(gè)行為特征:
- 每次耗時(shí)200到500毫秒之間
- 每次執(zhí)行有50%的幾率成功
- 20%的幾率遇到網(wǎng)絡(luò)故障
- 另外30%的幾率雖然網(wǎng)絡(luò)沒(méi)問(wèn)題但是服務(wù)器會(huì)給你一個(gè)非正常的結(jié)果
當(dāng)然,由于我寫(xiě)的是異步算法,需要避免block caller thread。
所以當(dāng)你調(diào)用pretendCallAPI的時(shí)候,這個(gè)函數(shù)是瞬間立即返回的赠法。
那么當(dāng)然我就無(wú)法在函數(shù)返回的時(shí)候return什么有用的東西給你了鳞芙。
如果你想知道執(zhí)行的結(jié)果到底是啥,你需要傳給我一個(gè)CallBack,在我執(zhí)行完后,通過(guò)CallBack來(lái)告知你執(zhí)行的結(jié)果。
這個(gè)CallBack的完整簽名表達(dá)式展開(kāi)是Try[String] => Unit
大家看searchTB和buyFromTB可能覺(jué)得他們長(zhǎng)的有點(diǎn)奇怪,這是Scala里柯里化的寫(xiě)法期虾。
也就是通過(guò)把pretendCallAPI包一層來(lái)構(gòu)造新的函數(shù),鎖死兩個(gè)參數(shù),剩下的一個(gè)參數(shù)(也就是CallBack)就變成了新構(gòu)造出來(lái)的函數(shù)的唯一參數(shù)了。
也就是說(shuō)searchTB和buyFromTB的簽名是(Try[String] => Unit) => Unit驯嘱。
關(guān)于柯里化這個(gè)語(yǔ)言特性的更多信息:
https://cuipengfei.me/blog/2013/12/25/desugar-scala-6/
好了,現(xiàn)在這兩個(gè)函數(shù)可以提供給大家使用了镶苞。
我是異步函數(shù)的調(diào)用者
聽(tīng)說(shuō)異步函數(shù)已經(jīng)寫(xiě)好了,我終于可以用他們來(lái)實(shí)現(xiàn)剁手業(yè)務(wù)了。
聽(tīng)函數(shù)作者講了一下,用起來(lái)應(yīng)該不會(huì)很難,那我來(lái)實(shí)現(xiàn)一下吧鞠评。
def searchPriceThenBuy() = {
searchTB {
case Success(searchMsg) =>
println(searchMsg)
buyFromTB {
case Success(buyMsg) => println(buyMsg)
case Failure(err) => println(err.getMessage)
}
case Failure(err) => println(err.getMessage)
}
}
使用searchTB和buyFromTB并不難. 他們兩個(gè)都是接受CallBack作為參數(shù)的函數(shù)茂蚓。
CallBack本身是個(gè)函數(shù),它的簽名是Try[String] => Unit。
而Try有兩種形式,分別是Success和Failure剃幌。
所以在調(diào)用searchTB和buyFromTB的時(shí)候,必須把兩個(gè)分支都給到(以免pattern match不到)聋涨。
這樣在異步函數(shù)有結(jié)果的時(shí)候(無(wú)論成敗)才能call back過(guò)來(lái)到我的代碼,以便我能夠在合適的時(shí)機(jī)做后續(xù)的處理(無(wú)論是基于成功做后續(xù)業(yè)務(wù),還是做error handling)。
關(guān)于pattern match,可以參考這里:
https://cuipengfei.me/blog/2013/12/29/desugar-scala-8/
https://cuipengfei.me/blog/2015/06/16/visitor-pattern-pattern-match/
這段代碼跑一下的話,會(huì)有這么幾種結(jié)果:
- 搜到了,也買(mǎi)到了
- 搜到了,購(gòu)買(mǎi)時(shí)遇到了網(wǎng)絡(luò)故障
- 搜到了,由于支付寶錢(qián)不夠而沒(méi)買(mǎi)到
- 沒(méi)搜到,購(gòu)買(mǎi)行為未觸發(fā)
- 搜索遇到網(wǎng)絡(luò)故障,購(gòu)買(mǎi)行為未觸發(fā)
一共就這么幾種可能,因?yàn)閜retendCallAPI是跑概率的,多跑幾次這些情況都能遇到负乡。
雖然實(shí)現(xiàn)出來(lái)不難,執(zhí)行結(jié)果也沒(méi)問(wèn)題,但是總有點(diǎn)隱憂牍白。
這里只有searchTB和buyFromTB兩個(gè)函數(shù),如果其他場(chǎng)景下我需要把更多的異步函數(shù)組合起來(lái)使用呢?豈不是要縮進(jìn)很多層?
當(dāng)然,縮進(jìn)只是個(gè)視覺(jué)審美問(wèn)題,是個(gè)表象,不是特別要緊.關(guān)鍵是我的業(yè)務(wù)邏輯很容易被這樣的代碼給割裂的雞零狗碎,那就不好了。
我要給上游編寫(xiě)異步函數(shù)的同事反饋一下抖棘,看是否有辦法解決這個(gè)問(wèn)題茂腥。
鏡頭切回到異步函數(shù)編寫(xiě)者
之前寫(xiě)的兩個(gè)函數(shù)反饋不太好,主要是因?yàn)橥聜冋J(rèn)為使用CallBack不是最優(yōu)的方式切省。
這個(gè)反饋確實(shí)很中肯最岗,如果只有一個(gè)異步函數(shù)單獨(dú)使用,用CallBack也沒(méi)什么太大的問(wèn)題朝捆,如果是很多個(gè)異步函數(shù)組合使用確實(shí)會(huì)形成多層嵌套的問(wèn)題般渡。
我作為上游程序員,確實(shí)需要更多地為下游調(diào)用者考慮。
既然如此驯用,那我改版一下脸秽,免除掉讓下游使用CallBack的必要性。
type CallBackBasedFunction = (CallBack) => Unit
def futurize(f: CallBackBasedFunction) = () => {
val promise = Promise[String]()
f {
case Success(msg) => promise.success(msg)
case Failure(err) => promise.failure(err)
}
promise.future
}
val searchTBFutureVersion = futurize(searchTB)
val buyFromTBFutureVersion = futurize(buyFromTB)
先定義一個(gè)CallBackBasedFunction晨汹,它代表一個(gè)接受CallBack為參數(shù)的函數(shù)的簽名豹储。
表達(dá)式展開(kāi)后就是: (Try[String] => Unit) => Unit
這就符合了searchTB和buyFromTB兩個(gè)函數(shù)的簽名。
futurize算是個(gè)higher order function,它接受一個(gè)CallBackBasedFunction作為參數(shù)淘这,返回一個(gè)() => Future[String]剥扣。
(Future是Scala標(biāo)準(zhǔn)庫(kù)的內(nèi)容,可以認(rèn)為和JS Promises/A+是類(lèi)似的概念)
也就是說(shuō)futurize可以把searchTB和buyFromTB改造成返回Future的函數(shù)铝穷。上面代碼最后兩行就是改造的結(jié)果钠怯。
這樣,原本接受CallBack做為參數(shù)且沒(méi)有返回值的函數(shù)曙聂,就變成了不接受參數(shù)且返回Future的函數(shù)晦炊。
再看futurize的具體實(shí)現(xiàn),它使用了Scala的Promise宁脊,讓返回的Future在原版函數(shù)成功時(shí)成功断国,在原版函數(shù)失敗時(shí)失敗。
這樣榆苞,我就得到了searchTBFutureVersion和buyFromTBFutureVersion這兩個(gè)仍然是立即瞬間返回稳衬,不會(huì)block caller thread的函數(shù)。
關(guān)于Scala中Promise和Future的更多信息:
https://docs.scala-lang.org/overviews/core/futures.html
鏡頭再切到異步函數(shù)調(diào)用者
現(xiàn)在有了searchTBFutureVersion和buyFromTBFutureVersion坐漏,我來(lái)試著重新實(shí)現(xiàn)一次:
def searchPriceThenBuyFutureVersion() = {
val eventualResult = for {
searchResult <- searchTBFutureVersion().map(msg => println(msg))
buyResult <- buyFromTBFutureVersion().map(msg => println(msg))
} yield (searchResult, buyResult)
eventualResult.onComplete {
case Failure(err) => println(err.getMessage)
case _ =>
}
}
這里用到了Scala的for comprehension薄疚,編譯后會(huì)變成map,flatMap等等monadic operator赊琳。
而map,flatMap等操作符正是Scala中Future拿來(lái)做組合用的街夭。
這樣,用for把兩個(gè)返回Future的異步函數(shù)組織起來(lái)躏筏,形成一個(gè)新的Future板丽,然后在新的Future complete時(shí)統(tǒng)一處理異常。
關(guān)于for的更多信息:
https://cuipengfei.me/blog/2014/08/30/options-for/
這次實(shí)現(xiàn)的代碼與上次的行為是一致的,沒(méi)什么兩樣趁尼。
不過(guò)我的業(yè)務(wù)代碼從雞零狗碎變成了平鋪直敘平易近人檐什。
(這種效果在這里表現(xiàn)的并不是特別突出,不過(guò)很容易想象如果需要組合使用的異步函數(shù)更多一些的話弱卡,這種效果的好處就顯露出來(lái)了)
當(dāng)然了乃正,讓業(yè)務(wù)代碼易讀易懂主要還是要靠個(gè)人奮斗,而有了Promise和Future這種歷史進(jìn)程的推力婶博,則更有增益作用瓮具。
小結(jié)
最近在看Scala Reactive的一些內(nèi)容
想起了很久之前寫(xiě)過(guò)一篇叫做自己動(dòng)手實(shí)現(xiàn)Promises/A+規(guī)范的博客,用JS實(shí)現(xiàn)了一個(gè)簡(jiǎn)版的Promise:
https://cuipengfei.me/blog/2016/05/15/promise/
我在當(dāng)時(shí)的一段演示代碼里面寫(xiě)了兩句注釋?zhuān)?/p>
Promise的作用在于
- 給異步算法的編寫(xiě)者和使用者之間提供一種統(tǒng)一的交流手段
- 給異步算法的使用者提供一種組織代碼的手段,以便于將一層又一層嵌套的業(yè)務(wù)主流程變成一次一次的對(duì)then的調(diào)用
不過(guò)當(dāng)時(shí)的博客里只講了實(shí)現(xiàn)Promise規(guī)范的事情,并沒(méi)有詳細(xì)解釋過(guò)這兩句話。
既然又遇到了這個(gè)話題名党,于是寫(xiě)點(diǎn)Scala來(lái)把當(dāng)時(shí)沒(méi)展開(kāi)寫(xiě)到的內(nèi)容補(bǔ)充了一下叹阔。
上文的四個(gè)鏡頭展現(xiàn)了兩個(gè)角色的思考過(guò)程,通過(guò)這個(gè)過(guò)程其實(shí)也就解釋了上面兩句注釋的含義传睹。
1.給異步算法的編寫(xiě)者和使用者之間提供一種統(tǒng)一的交流手段
所謂統(tǒng)一的交流手段耳幢,其實(shí)就是異步函數(shù)的簽名問(wèn)題。
由于需要處理的業(yè)務(wù)五花八門(mén)欧啤,異步函數(shù)接受的參數(shù)列表沒(méi)法統(tǒng)一睛藻,但是返回值是可以統(tǒng)一的。
一個(gè)異步函數(shù)邢隧,接受了外界給的參數(shù)店印,立即瞬間返回一個(gè)Js的Promise或者Scala的Future(或者是任何語(yǔ)言中類(lèi)似概念的叫法)。
然后在異步任務(wù)執(zhí)行完的時(shí)候把Promise resolve/reject掉(讓Future success或者failure),借此來(lái)讓調(diào)用方的代碼知道該到了它跑后續(xù)處理的時(shí)候了倒慧。
這樣我們就獲得了一個(gè)sensible default按摘,無(wú)需在每次設(shè)計(jì)異步函數(shù)的時(shí)候都去商議該返回什么東西,該怎么獲得異步執(zhí)行的結(jié)果纫谅。
2.給異步算法的使用者提供一種組織代碼的手段,以便于將一層又一層嵌套的業(yè)務(wù)主流程變成一次一次的對(duì)then的調(diào)用
所謂組織代碼的手段炫贤,就是關(guān)于異步函數(shù)調(diào)用者的那兩個(gè)鏡頭的內(nèi)容了。
一開(kāi)始CallBack套著CallBack付秕,異步的味道很重兰珍,這體現(xiàn)出了代碼的組織方式在向代碼的技術(shù)實(shí)現(xiàn)低頭№锬粒或者說(shuō)是代碼的技術(shù)實(shí)現(xiàn)干擾了我行文的風(fēng)格。
后來(lái)變成了看起來(lái)很像是消費(fèi)同步函數(shù)結(jié)果的寫(xiě)法励幼。從而讓我慣常的文風(fēng)得以保持汰寓。
文/ThoughtWorks 崔鵬飛 更多洞見(jiàn)