傳統的過程編碼方式帶來的弊端是顯而易見,我們經常有這樣的經驗,一段時間不維護的代碼或者別人的代碼,突然拉回來看需要花費較長的時間,理解原來的思路,如果此時有個文檔或者注釋寫的很好的話,可能花的時間會短一點,但是即便如此,很多調用關系也要反復確認才敢動手改動。下面是一斷偽代碼,描述過程編碼方式:
func A(){ B() C() } func B(){ do something D() } func C(){ do something } func D(){ do something } func main(){ A() }
對照流式風格的寫法:
NewStream().
Next(A).
Next(B).
Next(D).
Next(C).
Go()
當過程風格的代碼調用關系復雜時,程序員需要謹慎仔細行事,相比較流式風格的代碼比較清爽,主干清晰,尤其是應對需求變更的時候優勢明顯。
java8里借用lamda表達式實現了一套比較完美的流式編程風格,golang作為一個簡潔的語言還沒有官方的流式風格的包(可能早就有了,可能是我孤陋寡聞了)有點可惜了。
我參考了gorequest的代碼,實現了一套相對比較通用的流式風格的包,實現原理是組成一個任務鏈表,每一個節點都保存了首節點和下一個節點以及該節點應該執行的回調函數指針,流式任務啟動后從第一個節點開始,逐個執行,遇到異常則終止流式任務,直到執行到最后一個,結束任務鏈。先來看看代碼吧:
package Stream import ( "errors" "fmt" ) /** 流式工作原理: 各個任務都過指針鏈表的方式組成一個任務鏈,這個任務鏈從第一個開始執行,直到最后一個 每一個任務節點執行完畢會將結果帶入到下一級任務節點中。 每一個任務是一個Stream節點,每個任務節點都包含首節點和下一個任務節點的指針, 除了首節點,每個節都會設置一個回調函數的指針,用本節點的任務執行, 最后一個節點的nextStream為空,表示任務鏈結束。 **/ //定回調函數指針的類型 type CB func(interface{}) (interface{}, error) //任務節點結構定義 type Stream struct { //任務鏈表首節點,其他非首節點此指針永遠指向首節點 firstStream *Stream //任務鏈表下一個節點,為空表示任務結束 nextStream *Stream //當前任務對應的執行處理函數,首節點沒有可執行任務,處理函數指針為空 cb CB } /** 創建新的流 **/ func NewStream() *Stream { //生成新的節點 stream := &Stream{} //設置第一個首節點,為自己 //其他節點會調用run方法將從firs指針開始執行,直到next為空 stream.firstStream = stream //fmt.Println("new first", stream) return stream } /** 流結束 arg為流初始參數,初始參數放在End方法中是考慮到初始參數不需在任務鏈中傳遞 **/ func (this *Stream) Go(arg interface{}) (interface{}, error) { //設置為任務鏈結束 this.nextStream = nil //fmt.Println("first=", this.firstStream, "second=", this.firstStream.nextStream) //檢查是否有任務節點存在,存在則調用run方法 //run方法是首先執行本任務回調函數指針,然后查找下一個任務節點,並調用run方法 if this.firstStream.nextStream != nil { return this.firstStream.nextStream.run(arg) } else { //流式任務終止 return nil, errors.New("Not found execute node.") } } func (this *Stream) run(arg interface{}) (interface{}, error) { //fmt.Println("run,args=", args) //執行本節點函數指針 result, err := this.cb(arg) //然后調用下一個節點的Run方法 if this.nextStream != nil && err == nil { return this.nextStream.run(result) } else { //任務鏈終端,流式任務執行完畢 return result, err } } func (this *Stream) Next(cb CB) *Stream { //創建新的Stream,將新的任務節點Stream連接在后面 this.nextStream = &Stream{} //設置流式任務鏈的首節點 this.nextStream.firstStream = this.firstStream //設置本任務的回調函數指針 this.nextStream.cb = cb //fmt.Println("next=", this.nextStream) return this.nextStream }
下面是一個流式的例子,這里以早上起床到出門上班的流程為例:
//起床 func GetUP(arg interface{}) (interface{}, error) { t, _ := arg.(string) fmt.Println("鈴鈴.......", t, "###到時間啦,再不起又要遲到了!") return "醒着的狀態", nil } //蹲坑 func GetPit(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###每早必做的功課,蹲坑!") return "舒服啦", nil } //洗臉 func GetFace(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###洗臉很重要!") return "臉已經洗干凈了,可以去見人了", nil } //刷牙 func GetTooth(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###刷牙也很重要!") return "牙也刷干凈了,可以放心的大笑", nil } //吃早飯 func GetEat(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###吃飯是必須的(需求變更了,原來的流程里沒有,這次加上)") return "吃飽飽了", nil } //換衣服 func GetCloth(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###還要增加一個換衣服的流程!") return "找到心儀的衣服了", nil } //出門 func GetOut(arg interface{}) (interface{}, error) { s, _ := arg.(string) fmt.Println(s, "###一切就緒,可以出門啦!") return "", nil } func main() { NewStream(). Next(GetUP). Next(GetPit). Next(GetTooth). Next(GetFace). Next(GetEat).//需求變更了后加上的 Next(GetCloth). Next(GetOut). Go("2018年1月28日8點10分") }
從上面的代碼看,流式編碼風格對於一個大的任務被分解成多個小任務后,在代碼層面是非常直觀的,不在費勁心思去查找到底那個調用了那個,另外對於需求的變更更容易了,上例中的吃早飯是第一個版本沒有實現的,客戶說了早上要吃飯,不然容易的膽結石,第二版要加上,我們需要完成吃飯的函數,然后加到響應的位置。相對過程編碼簡單了不少。