奇思妙想-java實現另類的pipeline模式
阿新 • • 發佈:2020-03-22
---
# 磕叨
在公司做專案是見到前輩們寫的一段任務鏈的程式碼,大概如下
```
Runnable task = new TaskA(new TaskB(new TaskC(new taskD())));
task.run();
```
taskA執行run呼叫並完成TaskA宣告的任務邏輯之後,內部會自動呼叫構造引數傳入的TaskB的run方法,過程類似TaskA,TaskB完成之後一樣會呼叫引數傳入的task,直到最後一個沒有帶下一個task類傳入的任務完成,即完成一個管道式呼叫。
愛思考的我在想,可用,不好用重用,於是動手改改。
---
# 準備
經過一段時間開發後,有了一個常用的工具類,方便快速開發,但是這裡用到的東西很少,還是要說明一下,這裡用到一個我稱作ecommon的包,當然我只用了兩個很基礎的額部分。這兩個部分完全可以用你自己的實現,是非常簡單的。
- 函式介面
jdk8之後很方便讓我們寫出lambda,但是我覺得理解起來不直觀,於是自己重寫了 12 個介面,按引數個數和返回型別可以直接根據函式名直接選出你要的。具體在 https://github.com/kimffy24/EJoker/tree/dev/ejoker-common/src/main/java/pro/jiefzz/ejoker/common/system/functional , IVoid打頭的就是無返回的,後面的數字就是要帶多少個引數,引數和返回型別全部都是泛型。1-6個引數已經能包括大部分情況了,需要更多引數的情況完全可以自定義一個上下文傳遞過去。
- 字串填充類
類似`String.format`,但是我不用正則,而是類似slf4j那種,`log.info("This is a template, keyA={}, keyB={}", "valueA", "valueB")` , 類似這種佔位填充。我的實現在 https://github.com/kimffy24/EJoker/blob/dev/ejoker-common/src/main/java/pro/jiefzz/ejoker/common/system/helper/StringHelper.java 的fill方法中。你也可以用 `String.format` 代替。
# 思路簡述
我們先明確,jdk8以下的情況不作考慮。
pipeline我更多的印象是來自終端上的應用
![命令終端中使用管道](https://img2020.cnblogs.com/blog/1704011/202003/1704011-20200322030753951-253545695.png)
pipeline是單向的,上個task的輸出作為下個task的輸入,直到沒有下一個task,最後一個task的作用就應該是你期望的。且`後續任務只關心前者的輸出結果,對於的他是誰,怎麼做的,是不關心的`。記為 `Point1`
這個特點是我視為管道與切面或職責鏈模式的區別所在。
首先,我們得有第一推動,讓管道流能有個開始,再就是有中間task,他必定是能接收到上一個任務的輸出的,並且,可能有自帶引數,並且有自己的輸出,最後,有latest的task 與中間task區別在於他不用返回了,latest一般是以副作用的形式實現我們的企圖的,如上圖的 `wc -l` 作為最後一個任務是直接把結果列印到螢幕上,而不是返回一個變數給我們讀取。根據java的強型別屬性,以及剛剛一段的分析,可以得知,有3種類型的任務,開始任務,中間任務,最後任務,並且中間任務的個數是不限的,所有任務至於相鄰的任務有一個關聯點,那就是 ` 前者的輸出型別與後者的輸入型別一致 ` (網文中大部分說自己實現的pipeline的模式都是傳遞Object型別,到各個子任務中自己強轉到需要的型別的,不說好與不好,但我肯定不喜歡)。這個特性記為 `Point2`。
而且,每個子任務,本身是可以帶引數的,這是一個需要支援的點。像上圖命令中的管道,每個子命令(除第一個)都是同時接受前一個命令的輸出作為輸入,且自帶引數的。但是java在這裡其實並不靈活,因此我們約定 `後續任務的第一個引數就是前一個任務的輸入` , 這個約定是直接影響到我們的程式碼實現的。這個特性記為 `Point3`。
另外,管道的入口唯一的,一定是從開始任務往後流的。如果入口不一樣,那麼就是像個不同的管道,他們的意圖以及輸入輸出的期望都是不同的。這個特性記為 `Point4`。
最後,在java中使用,我肯定不能像終端那種,錯了重敲命令就是了,所以需要異常控制以及做一些相鄰任務承上啟下的時刻做點什麼,例如日子列印,斷言等。這個算附加題。
# 提起鍵盤擼
(因為我已經寫完並測試完了,所以我就反過來解析我是怎麼想的了)
這裡以Runnable介面作為基礎介面。給出其中一個測試的例子
![](https://img2020.cnblogs.com/blog/1704011/202003/1704011-20200322030735113-1079892834.png)
這裡初始任務是給出一個日期,中間任務是拼接成人類友好的1句話,最終任務是直接列印到螢幕。(現實中要實現這樣一句話,當然是直接擼啦。這裡只是為了演示),看看Pipeline初始任務的定義
![](https://img2020.cnblogs.com/blog/1704011/202003/1704011-20200322030725911-1345377620.png)
先不看其他屬性,看構造方法,傳入一個 `IFunction` ,按照準備一節的定義,他是一個返回型別為宣告泛型R,且無引數輸入的閉包函式(或稱作lambda表示式)。對照上面PipelineTest中就是那個 `() -> { return new Date(); }` , (得益於jdk8的型別推斷,在 `new Pipeline<>` 構造時,不用再宣告其泛型,編譯器能根據閉包函式的return型別推斷出這裡是個Date型別)。`next`, `end` 是指明管道的下接任務,這可以看出管道是極其類似於任務鏈/職責鏈的(需要注意`next`和`end`同時`只能有個一個存在`)。hook是異常管理以及任務間承接時做一個切面方法的,argCxt是記下傳遞引數,方便hook中的方法使用(這個是因為java需要的,跟管道模式並沒有關係)。
再看add方法的一個過載,新增並返回中間task
![](https://img2020.cnblogs.com/blog/1704011/202003/1704011-20200322030715837-1066405785.png)
add方法傳入一個`IFunction1