很多年前,windows workflow foundation還叫WWF,而直譯過來的名稱讓很多人以為它就是用來開發工作流或者幹脆就是審批流的。
部落客當年還是個懵懂的少年,卻也知道微軟不會大力推一個面向如此具象的業務場景的技術,于是特地找了一本《WF本質論》,當看到“程式即資料”這個論斷時,被深深震撼了。可能這隻是作者的随意一寫,但當時正是泛型方法、lamda表達式、匿名委托啥的開始出現的時候,作者的這一說法在某種程度上暗合了部落客平常的程式設計思想。于是邏輯與資料,算法與結構,它們之間的界限在我眼裡,在我心裡,開始以更詭異的方式模糊了起來。
然而之後并未在工作上使用過WF,是以部落客也就不再關注此項技術。如今重新翻看,突然發現官方的Workflow Team blog最近的更新都是兩年前了,網上的資料都是N年前的,而且大多數是教大家怎麼用。為何要用,何處要用,基本上沒有太多介紹。到了現如今,似乎也被微軟抛棄了,為此我還專門在博問裡提了下,也沒人知道具體詳情。
很少有看到專門使用wf進行開發的場景,好用與否,在小衆的群體裡抽樣得出結論也就顯得不怎麼可靠了。似乎wf在sharepoint環境中用起來比較協調,由于部落客對sharepoint沒有研究過,是以對此不好評論。如若在純代碼環境下開發,用wf自帶的一套activity能畫出看似清晰的流程圖,然而關鍵的和外部互動的環節卻仍然避免不了,需要考慮的因素大部分仍然需要代碼去完成;而各種條件下流程的走向,使用activity的條件判斷,又讓人覺得沒有直接寫代碼來的友善,比如ifelse,用代碼可能隻要在一個方法體中就能完成,而在流程圖中卻要使用兩個activity,如果其中再有互動跳轉的話,流程圖的複雜程度未必在代碼之下,而後期維護也難說容易。
不妨非惡意的揣測,經過幾年時間,微軟慢慢覺得,将代碼間原本隐晦的邏輯關系抽離出來變成看得見的“流程”,對程式員來說,未必有多大的意義,于是就減少了這方面的投入。當然你也可以認為wf已經很成熟了,不過即使如此,部落客還是傾向認為此種所謂成熟是因為找不到改進的方向,雖然“成熟”,但不怎麼好用。
but,wf并非一無是處,wf的bookmark(書簽機制)是最差別于傳統開發的特性,個人認為也是最重要的特性。傳統開發時,我們時不常的會遇到這種情形:判斷流程是否走到特定環節以便決定下一步操作,其實就是判斷A\B\C\D\...等排列組合變數,它們之間可能也是互相關聯的,隻要流程沒有繼續流轉,那麼每次啟動都要進行同樣的判斷;是否可以在各變數滿足條件時生成一個标簽,當我們發現這個标簽存在時,就可以去找标簽對應的環節,就能馬上進行後續操作了。實際開發中,我們常設定一個備援的辨別字段起這個作用。但單個字段未必能完全辨別一個環節,而且也顯得不夠直覺。wf中的bookmark與上述的标簽類似,本質上,它是對委托異步回調爐火純青的應用,更自然的描述是在變數尚未符合條件時的一種“等待”,常用于與外部互動,在流程設計時,也不會像傳統代碼操作(查詢、判斷、更新)标簽那樣顯得突兀(這些操作wf都在更底層幫我們處理了)。
開頭說到微軟推出wf的本意并非用于開發業務上的工作流,我們甚至可以将任意有邏輯順序的一段代碼“wf化”,然而從業務場景來講,wf的很多概念能映射其中,畢竟抽象和具象,都逃不離流程二字。是以項目前期或工期緊張,需要快速開發的時候,wf也能幫助程式員梳理流程——在[對]業務流程尚不清晰的時候,你會發現這非常有用——就算日後擯棄wf,大部分代碼都可以複用,而由于各環節明确的流轉關系,代碼重構也較為容易。部落客就是因為這個原因,在目前的一個項目中使用了wf,下面我會簡單介紹相關要點和個人了解。
版本控制也是它的一個亮點,這個就不細說了。
由于本人對wf鑽研不深,所述難免有誤,請各位同學批評指正。另,本文并不展示任何項目代碼,隻以行業一般流程,展示部落客在開發過程中的思索。
本項目主幹是一手房置購流程,涉及到認籌、認購、成交、租賃四個環節,需求簡化後如下(圖是用visio畫的,畫得很挫,一直沒找到實線弧形箭頭):
1、環節流轉

5、同時至多隻能存在一個稽核中事項。比如認購内容變更稽核中時,就不能發起轉成交申請。
這是一個比較典型的工作流場景。剛接手這個項目時,我考慮用傳統方式開發,不多久我發現自己在各種環節、狀态、辨別、互斥項間暈頭轉向,一團漿糊。即使流程圖已經很清晰明白地擺在面前,将之賦予代碼和資料,并使看似毫無關聯的各塊内容按照預期邏輯運作,似乎顯得相當困難。特别是産品有時會一臉無奈地跟你說,認籌也可以轉租賃,業務說的;隔天又說,不需要了。要維系代碼之間“隐秘”的邏輯關系并快速應對需求變更,随着業務複雜度的提高,難度也更快的升高。
當然,對于熟練工來說,這點難度不算什麼,畢竟我們每天都在做這種工作——将業務流程轉化成層層調用的代碼——我們還可以祭出設計模式、AOP、IOC、ORM啥的讓代碼看起來更清(fu)晰(za)。我們一直在接受面向對象、面向架構、面向服務的訓練,而缺少真正面向流程程式設計的經驗,我想這才是為什麼微軟當年會推廣wf的原因(此為病句,意為強調)。
wf有三種内置流程:順序流、Flowchart、StateMachine。本項目可考慮後兩種,部落客選擇的是StateMachine,整出來的主流程如下:
看上去很淩亂,隻能怪vs自帶的wf設計器沒有visio好用,其實上圖就是第1幅流程圖的wf版本。
再來看下每條連線的邏輯,以認購環節出去的連線為例,觸發器如下:
任何類型的稽核不通過or申請人撤銷就回到目前環節,和之前我們用visio畫的流程圖一樣直覺。不過,這裡有個問題。部落客剛接觸statemachine時,選擇entry還是trigger放置業務邏輯比較困惑,當時認為兩者皆可,畢竟最終隻是通過condition來指定狀态的轉變方向,用不着care condition在哪裡生成;部落客當時認為微軟設計statemachine時,是将trigger當做bookmark activity的容器,以便于與外部決策進行互動,當然我們也可以将bookmark放在entry中,說到底這就是一個規範問題,不必深究。然而部落客發現,若流程從一個state流向同一個state——即state指向自己——其實前後兩個環節已不是同一個“執行個體”了 ,是以原state的變量等狀态不會儲存,entry會再次執行。為了避免這種狀況,有停留在目前狀态的情形,應該将業務邏輯寫在entry中,不進入到觸發後的流轉,因為一旦流轉,就算流轉回目前狀态,也是先出再進。當然,如果目前狀态沒有需要保留的資訊,寫在trigger亦可。
再來看看稽核到底發生了什麼。
這個更不用我多說了,不過我還是要多說兩句。雖然上圖很清晰地還原了對應的需求,但未必是最合适的做法,我最後還是将這些邏輯統一到一個activity裡。前面說過,我們能将絕大多數多行代碼wf化,but,複雜的流程需要借助wf,一個簡單方法就能搞定的邏輯如果還硬要畫出幾道條條理理來,那就是偏執了。(話說,寫程式的大多數人都是偏執的,非黑即白,我沒說錯吧)
對于前述需求的第5條,單條wf流程天生就能滿足,不需要我們做額外的coding。思考一下,若幾個稽核可以并行,或流程流轉到下一環節後,上一環節需要變更了,怎麼辦?用多流程或子流程試試吧。
代碼活動中普通的成員變量,持久化在bookmark恢複後,值丢失不可用;若需要在持久化前後保持變量值,應該使用Variable,或應用Serializable特性,兩者都是部落客推測沒試過。
有些事務不能失敗了就全部回退重來,比如單據狀态從a->b,經過多位上司審批同意後,結果因為最後一步送出不成功,讓單據回退到a重審?這種情況,隻能将最後一步重新送出,實際開發中,可将此類“髒送出”定時再送出。
一個activity裡可create多個bookmark,當所有bookmark都恢複執行後,流程才繼續往下。
wf在流程流轉過程中,并不能傳回資料給調用者,比如發起認籌轉認購的申請,調用ResumeBookmark方法,并不能知曉是直接通過還是等待稽核,需要另外查資料庫得到狀态結果。當然可以使用WorkflowApplication.Extensions與外部程式互動,但不能滿足某些場景,比如用戶端調用webapi,服務端action發起流程,action自然需要知道流程跑完後(暫停or完結)的結果是什麼并傳回給用戶端;這時候WorkflowApplication.Extensions就然并卵了,除非改寫底層,比如讓wf能取消action的後續執行并将結果資料寫入http連接配接。
流程改版or外部依賴項變化,wf下,要麼新舊版本并行,要麼研究如何将舊版本遷移到新版本,很多情況下沒有純代碼控制來得友善。
部落客觀點:同步SaaS并非wf較适用的場景,wf适用于異步消息推送場景,比如外賣點餐狀态、快遞狀态、銀行資金流轉等,用戶端并不等待馬上的結果,而是事項狀态改變時接收服務端消息即可。當然硬要在同步環境下使用也可以,此時需要更松散的設計和底層架構的配合,以适應wf“封閉式流程”的特點。
需繼續研究的點:
1、有些流程大同小異,能否封裝成一個可配置的流程,在設計時進行簡單的參數配置就能顯示不同的流程步驟;比如a狀态可轉為b、c狀态,而b隻能轉c狀态,那麼a,b在轉換到下一個環節的判斷邏輯就有少許差異了。這貌似隻能通過中繼資料實作。
2、InstanceOwner,網上實在找不到更多關于它的介紹,目前可知是用于多宿主環境下,宿主對wf執行個體的lock。沒找到給執行個體指派InstanceOwner的直接途徑,網上找到的基本上是下面兩行代碼:
InstanceView view = instore.Execute(instore.CreateInstanceHandle(), new CreateWorkflowOwnerCommand(), TimeSpan.FromSeconds(30));
instore.DefaultInstanceOwner = view.InstanceOwner;
不知是基于什麼因素考慮,總是覺得不太了解是什麼意思,為什麼要以及以這種方式設定DefaultInstanceOwner,如果不設定的話,那麼下面這行代碼運作時就會報錯:
WorkflowApplicationInstance instance = WorkflowApplication.GetInstance(Guid.Parse(flowinstance.InstanceID), instore);
架構應該完全可以在底層就自動給我們處理InstanceOwner相關的東東,比如下面這句在沒設定DefaultInstanceOwner的時候就不會報錯:
wfApp.Load(Guid.Parse(flowinstance.InstanceID));
如何能設定自己的InstanceOwner呢,maybe可以通過InstanceOwnerMetadata。
CreateWorkflowOwnerCommand createCommand = new CreateWorkflowOwnerCommand()
{
InstanceOwnerMetadata =
{
{ WorkflowHostTypeName, new InstanceValue(WFInstanceScopeName) },
}
};
不過設定完了,怎麼擷取又是一個問題,有興趣的朋友可以研究下這個檔案,也可以在官方WF_WCF_示例裡找到。總之持久化這趟子水夠深。
其它參考資料:
Loading persisted workflow instances with WorkflowApplication