國慶前,參與了一個c# .net 專案,真正重新體驗了一把搬磚感覺:在一個多月時間好像不加任何思考,不斷敲鍵盤加代碼,我想,這也許是行業內大部分中小型公司程式猿的真實寫照:都是坐在電腦前的搬磚工人,不過也不是沒有任何識訓,在搬磚的程序中我似乎發現了一些現象和造成這些現象背后的原因及OOP思維、習慣模式,和大部分IT公司一樣,這間公司在行業里存在了一定時間(不是初創)所以在產品和技術方面有一定的積累,通俗點就是一堆現成的c# .net 代碼,然后就是專案截止日期壓力,為了按時完成任務的我只能在原有代碼基礎上不斷加功能,根本沒有機會去考慮用什么樣的代碼模式、結構去達到更好的效果,在這個程序中有個有趣的現象引起了我的注意:基本上我只需按照某種流程(多數是業務需求)一個個增加環節就可以實作一項完整功能,當然我是不會計較這些環節對軟體其它部分是否產生影響,又或者以后代碼維護會不會很麻煩,只要能及時交貨就行,想想這種做法恰恰是面向物件編程或所謂行令式編程的特點,即:通過逐行執行命令引導程式的狀態改變,最終狀態就是運行程式的結果了,或者就是功能的實作了,通過一行行增加代碼最終總會到達預期的狀態,不是嗎,這正是OO編程的思維模式:因為程式狀態體現在每行代碼上,隨時可以檢查,驗證思路,所以OOP比較容易上手(相對函式式編程而言),
回顧一下函式式編程:好像很難按照自然邏輯思維順序來實作一個功能,這是因為函式式編程是一種嵌套式間接性的編程模式,即程式是在某種嵌套里運行的,函式式編程又被稱為monatic-programming,即在monad里編程,monad就是我所說的嵌套,是一種型別結構,最常用的是Future型別,在現代編程里多執行緒編程非常普遍,實際上往往我們離不開各種各樣的Future,舉個形象的例子:如果實作把臟水從A點引到B點輸出純凈水作為某種函式式程式,編程如同搭建管道網,必須首先準備好各式各樣功能的喉管,實作每種喉管的特殊功能如過濾、消毒等,然后再連接組合形成送水管道,
我在進行函式式編程時總是要把所以問題前前后后都考慮清楚了才能開始動手,首先會把一項功能的所有環節先總結出來,這些都是一些函式,然后嘗試把這些函式的型別統一了,就像上面提到的喉管一樣,因為不同規格的喉管是無法連接的,同樣,不同型別的嵌套monad是無法實作函陣列合的,然后先根據需求實作這些函式的輸入輸出,最后把這些函陣列合起來形成完整功能,你看,在函式式編程里是無法做到隨意想到那就寫到那的,必須先進行整體的思量,所以,函式式編程在代碼重用和維護上有先天的優勢,這個例子也體現了函式式編程的思維模式,
下面我想用一個實際的例子來示范函式式編程模式:前面幾篇討論的例子里有一個是把前端httpclient上傳httpserver的圖片存放入服務器端mongodb資料庫的,現在發現客戶端上傳圖片資料流有困難,希望上傳一個圖片下載網址,由httpserver自行下載圖片并寫入mongodb,單從這個功能來講,應該由幾個環節組成:
1、從上傳的資料中抽出圖片下載網址
2、下載圖片,通過http的request請求,從response里獲取圖片資料流
3、通過mongodb的count功能獲取圖片系列序號
4、將圖片寫入mongodb
首先,我需要把這幾個環節形成函式,然后統一函式型別,無可爭議,最好選擇Future[A]這樣的函式回傳型別:
假設資料是用json格式傳上來的,那得有個型別作為資料結構:
case class UpData (pid: String, url: String)
可以如下獲取上傳的資料:
entity(as[String]) { json => val upData: UpData = fromJson[UpData](json) ...}獲取圖片系列序號:回傳Future[Long]
repository.count(upData.pid).toFuture[Long]
下載圖片:這個回傳Future[ByteString]
import akka.actor.ActorSystem import akka.http.scaladsl.model._ import akka.http.scaladsl.Http def downloadPicture(url: String)(implicit sys: ActorSystem): Future[ByteString] = { val dlRequest = HttpRequest(HttpMethods.GET, uri = url) Http(sys).singleRequest(dlRequest).flatMap { case HttpResponse(StatusCodes.OK, _, entity, _) => entity.dataBytes.runFold(ByteString()) { case (hd, bs) => hd ++ bs } case _ => Future.failed(new RuntimeException("failed getting picture!")) } }寫入mongodb:這個函式也回傳Future[?]
def addPicuture(pid: String,seqno: Int, optDesc: Option[String] ,optWid:Option[Int],optHgh:Option[Int], bytes: Array[Byte]):Future[Completed] ={ var doc = Document( "pid" -> pid, "seqno" -> seqno, "pic" -> bytes ) if (optDesc != None) doc = doc + ("desc" -> optDesc.get) if (optWid != None) doc = doc + ("width" -> optWid.get) if (optHgh != None) doc = doc + ("height" -> optHgh.get) repository.insert(doc).toFuture[Completed] }好了,現在這幾個函式都是Future型別的,可以進行組合了:
val futSeqno: Future[Long] = for { cnt <- repository.count(upData.pid).toFuture[Long] barr <- downloadPicture(upData.url) _ <- addPicuture(upData.pid, cnt.toInt, None, None, None, barr.toArray) } yield cntfutSeqNo是個組合的運算流程,注意它的型別還是future:意味這我們無法預測這個運算什么時候會完成,特別如果下載一張超大圖片又或者網速緩慢的話,很可能在下載完成之前就執行了complete(),所以我們必須保證圖片下載完成后才向終端httpclient回傳response,就用onComplete來實作:
onComplete(futSeqno) { case Success(lv) => complete(lv.toString()) case _ => complete("error saving picture!") }所以整段宏觀代碼如下:
post { entity(as[String]) { json => val upData: UpData = fromJson[UpData](json) val futSeqno: Future[Long] = for { cnt <- repository.count(upData.pid).toFuture[Long] barr <- downloadPicture(upData.url) _ <- addPicuture(upData.pid, cnt.toInt, None, None, None, barr.toArray) } yield cnt onComplete(futSeqno) { case Success(lv) => complete(lv.toString()) case _ => complete("error saving picture!") } } }~是不是很容易讀懂理解?實際上我們把復雜的細節函式藏在背后,而這些函式是高度可重復利用的,這也是我們在動手之前通盤考慮的成果,
轉載請註明出處,本文鏈接:https://www.uj5u.com/houduan/1985.html
標籤:Scala
上一篇:Scala Future
