摘要:狀態(tài)機(jī)狀態(tài)機(jī)是模型層面的概念,與編程語(yǔ)言無(wú)關(guān)。狀態(tài)機(jī)具有良好的可實(shí)現(xiàn)性和可測(cè)試性。在代碼里,這是一個(gè),但是我們?cè)跔顟B(tài)機(jī)模型中要把他理解為事件。
這一篇是這個(gè)系列的開(kāi)篇,沒(méi)有任何高級(jí)內(nèi)容,就講講狀態(tài)機(jī)。
狀態(tài)機(jī)狀態(tài)機(jī)是模型層面的概念,與編程語(yǔ)言無(wú)關(guān)。它的目的是為對(duì)象行為建模,屬于設(shè)計(jì)范疇。它的基礎(chǔ)概念是狀態(tài)(state)和事件(event)。
對(duì)象的內(nèi)部結(jié)構(gòu)描述為一組狀態(tài)S1, S2, ... Sn,它的行為的trigger,包括內(nèi)部的和外部的,描述成為一組事件E1, E2, ... En,在任何狀態(tài)下,任何事件到來(lái),對(duì)象狀態(tài)的改變用Sx -> Sy的狀態(tài)遷移(State Transition)來(lái)描述,這個(gè)狀態(tài)遷移就是對(duì)象的行為(behavior)。
對(duì)對(duì)象行為的完備定義就是{ S } x { E }的矩陣,如果存在(Sx, Ey)的組合未定義行為,這個(gè)對(duì)象行為模型在設(shè)計(jì)層面上就不完備,當(dāng)然實(shí)際的代碼不可能沒(méi)有行為,這往往就是錯(cuò)誤發(fā)生的地方。
狀態(tài)機(jī)具有良好的可實(shí)現(xiàn)性和可測(cè)試性。完備定義的狀態(tài)機(jī)很容易寫(xiě)出對(duì)應(yīng)的代碼,也很容易遍歷全部的狀態(tài)遷移過(guò)程完成測(cè)試,當(dāng)然這只意味著代碼實(shí)現(xiàn)和設(shè)計(jì)(模型)相符,并不意味著設(shè)計(jì)是正確的。
設(shè)計(jì)的正確性是一個(gè)復(fù)雜的多的話題,嚴(yán)格的定義是設(shè)計(jì)符合Specification。什么是符合Specification?要去看Robin Milner, Tony Hoare, Leslie Lamport等人的書(shū)了,老實(shí)說(shuō)我也不懂,所以就此打住。
這篇文章不會(huì)詳細(xì)介紹狀態(tài)機(jī),網(wǎng)上有非常多的資料,四人幫的書(shū)上有State Pattern - OO語(yǔ)言下的狀態(tài)機(jī)實(shí)現(xiàn),UML有State Diagram,是非常好的圖示工具;這里只給出一個(gè)代碼例子,對(duì)照這個(gè)實(shí)例幫助理解狀態(tài)機(jī)模型的代碼實(shí)現(xiàn)。
一個(gè)例子假定我們要解決這樣一個(gè)任務(wù):我們有一個(gè)模塊是為了存儲(chǔ)(save)一個(gè)文件,寫(xiě)狀態(tài)機(jī)的目的是為了解決并發(fā)操作時(shí)排隊(duì)存儲(chǔ)的請(qǐng)求,因?yàn)檎?qǐng)求是并發(fā)的,如果寫(xiě)入文件的io操作也是并發(fā)的話,這個(gè)文件可能被損壞。這是一個(gè)非常常見(jiàn)的應(yīng)用場(chǎng)景。
這個(gè)模塊定義有三種狀態(tài):
Idle, 這是不工作的狀態(tài);
Pending,這是等待工作的狀態(tài),等待的目的是為了,如果在很短的時(shí)間內(nèi)出現(xiàn)連續(xù)多次的寫(xiě)入請(qǐng)求,我們只寫(xiě)入最后一個(gè),減少io操作的次數(shù);
Working,該狀態(tài)下在執(zhí)行寫(xiě)入操作,如果在執(zhí)行io操作的時(shí)候收到寫(xiě)入請(qǐng)求,我們把請(qǐng)求內(nèi)容保存在一個(gè)臨時(shí)的位置;
Idle狀態(tài)沒(méi)有任何特殊資源,只有一個(gè)save請(qǐng)求事件;當(dāng)有save請(qǐng)求時(shí),它遷移到Pending狀態(tài)。
Pending狀態(tài)具有的特殊資源是一個(gè)timer,它可能有兩個(gè)事件:來(lái)自外部的save請(qǐng)求,和來(lái)自內(nèi)部的timeout。在JavaScript代碼里,這是一個(gè)callback,但是我們?cè)跔顟B(tài)機(jī)模型中要把他理解為事件。在Pending狀態(tài)中如果有save請(qǐng)求,不發(fā)生狀態(tài)遷移,新的請(qǐng)求數(shù)據(jù)會(huì)覆蓋舊的版本,原來(lái)的timer會(huì)被清除,重新開(kāi)始新的timer。在timeout發(fā)生時(shí),遷移到Working狀態(tài)。
Working狀態(tài)在進(jìn)入時(shí)會(huì)啟動(dòng)存儲(chǔ)文件的操作,它可能有兩個(gè)事件:來(lái)自外部的save請(qǐng)求,和來(lái)自內(nèi)部的保存文件操作的異步返回,同樣的,它也被理解為一個(gè)(內(nèi)部)事件。當(dāng)外部的save請(qǐng)求到來(lái)時(shí),該請(qǐng)求被保存在內(nèi)部的next變量里;當(dāng)文件操作返回時(shí):
如果操作成功
如果存在next,向Pending狀態(tài)遷移
如果不存在next,向Idle狀態(tài)遷移
如果操作失敗
如果存在next,向Pending狀態(tài)遷移,用next作為數(shù)據(jù)
如果不存在next,也向Pending狀態(tài)遷移,仍然使用當(dāng)前數(shù)據(jù),相當(dāng)于等待后retry
我偷個(gè)懶,沒(méi)給出圖示,實(shí)際上這樣的語(yǔ)言描述不如State Diagram來(lái)得直觀。下面的表格是上述語(yǔ)言描述的歸納,史稱狀態(tài)遷移表(State Transition Table),有了State Diagram或者State Transition Table,任何人寫(xiě)出來(lái)的代碼都一樣。為了表述清晰,表中把Working狀態(tài)的文件操作返回分成了兩個(gè)事件:success和error。
StateEvent | Save | Timeout | Success | Error |
---|---|---|---|---|
Idle | -> Pending | n/a | n/a | n/a |
Pending | overwrite data, restart timer | -> Working | n/a | n/a |
Working | set next | n/a | if next, -> Pending; else -> Idle | -> Pending(next ? next : data) |
下面是代碼,首先有個(gè)base class,三個(gè)狀態(tài)都繼承自這個(gè)base class:
class State { constructor(ctx) { this.ctx = ctx } setState(nextState, ...args) { this.exit() this.ctx.state = new nextState(this.ctx, ...args) } exit() {} }
在狀態(tài)機(jī)的代碼實(shí)現(xiàn)上,標(biāo)志性的方法名稱是setState,它負(fù)責(zé)狀態(tài)遷移;其次是enter和exit,分別對(duì)應(yīng)進(jìn)入該狀態(tài)和離開(kāi)該狀態(tài)。
狀態(tài)機(jī)模式(State Pattern)的一個(gè)顯著的編程收益是:每個(gè)狀態(tài)都有自己的資源,在遷入該狀態(tài)的時(shí)候建立資源,在離開(kāi)該狀態(tài)的時(shí)候釋放資源,這很容易保證資源的正確使用。
在上述代碼中,constructor充當(dāng)了enter邏輯的角色,所以沒(méi)有提供獨(dú)立的enter方法;JavaScript Class是一個(gè)語(yǔ)法糖,沒(méi)有和constructor相對(duì)應(yīng)的destructor,所以我們這里寫(xiě)一個(gè)exit函數(shù),如果繼承類里沒(méi)有exit邏輯,這個(gè)基類上的方法就是一個(gè)fallback。
ctx是一個(gè)外部容器,相當(dāng)于所有狀態(tài)對(duì)象的上下文(context),它同時(shí)具有一個(gè)叫做state的成員,該成員是一個(gè)Idle,Pending,或者Working類的實(shí)例;無(wú)論ctx.state是哪個(gè)狀態(tài),ctx都把save方法forward到state上,這樣寫(xiě)是一個(gè)很標(biāo)準(zhǔn)的State Pattern。
setState的實(shí)現(xiàn)有點(diǎn)tricky,是JavaScript的特色。它首先調(diào)用當(dāng)前類的exit函數(shù)遷出狀態(tài),然后使用new來(lái)構(gòu)造下一個(gè)類,這意味著第一個(gè)參數(shù)nextState是一個(gè)構(gòu)造函數(shù);后面的參數(shù)使用了spread operator,把這些參數(shù)傳入了構(gòu)造函數(shù),同時(shí)把新對(duì)象安裝到了ctx,即把自己替換了;這不是唯一的做法,是比較簡(jiǎn)潔的一種寫(xiě)法。
Idle類的實(shí)現(xiàn)非常簡(jiǎn)單,在save的時(shí)候用data作為參數(shù)構(gòu)造了Pending對(duì)象。
class Idle extends State{ save(data) { this.setState(Pending, data) } }
Pending類的save方法里保存了data和啟動(dòng)timer。它的構(gòu)造函數(shù)重用了save方法。因?yàn)镴avaScript的clearTimeout方法是安全的,這樣寫(xiě)沒(méi)什么問(wèn)題。
exit函數(shù)實(shí)際上沒(méi)有必要,但這樣書(shū)寫(xiě)是推薦的,它確保資源清理,如果未來(lái)設(shè)計(jì)變更出現(xiàn)其他的狀態(tài)遷出邏輯,這個(gè)代碼就有用了。
timeout時(shí)Pending向Working狀態(tài)遷移。
class Pending extends State { constructor(ctx, data) { super(ctx) this.save(data) } save(data) { clearTimeout(this.timer) this.data = data this.timer = setTimeout(() => { this.setState(Working, this.data) }, this.ctx.delay) } exit() { clearTimeout(this.timer) } }
Working代碼稍微多點(diǎn),但是對(duì)照狀態(tài)遷移表很容易讀懂。不贅述每個(gè)方法了。保存文件的操作采用了先寫(xiě)入臨時(shí)文件然后重命名的做法,這是保證文件完整性的常規(guī)做法,系統(tǒng)即使斷電也不會(huì)損壞磁盤文件。
class Working extends State { constructor(ctx, data) { super(ctx) this.data = data // console.log("start saving data", data) let tmpfile = path.join(this.ctx.tmpdir, UUID.v4()) fs.writeFile(tmpfile, JSON.stringify(this.data), err => { if (err) return this.error(err) fs.rename(tmpfile, this.ctx.target, err => { // console.log("finished saving data", data, err) if (err) return this.error(err) this.success() }) }) } error(e) { // console.log("error writing persistent file", e) if (this.next) this.setState(Pending, this.next) else this.setState(Pending, this.data) } success() { if (this.next) this.setState(Pending, this.next) else this.setState(Idle) } save(data) { // console.log("Working save", data) this.next = data } }
最后是ctx,我們?cè)趯?shí)際項(xiàng)目中稱之為Persistence。它初始化時(shí)設(shè)置state為Idle狀態(tài);把所有的save操作都forward到內(nèi)部對(duì)象state上。
class Persistence { constructor(target, tmpdir, delay) { this.target = target this.tmpdir = tmpdir this.delay = delay || 500 this.state = new Idle(this) } save(data) { this.state.save(data) } }要點(diǎn)
這一篇粗略的講了兩個(gè)問(wèn)題:狀態(tài)機(jī)模型和狀態(tài)機(jī)模式(State Pattern)。他們兩個(gè)不是一回事。
狀態(tài)機(jī)模式是一種寫(xiě)法,上述寫(xiě)法不唯一;不使用Class,僅僅在Persistence類中使用(枚舉)變量表示狀態(tài)也是可以的,使用Class則相當(dāng)于用變量類型來(lái)代表狀態(tài)。
狀態(tài)機(jī)模式的顯著優(yōu)點(diǎn)在于:
不同狀態(tài)的資源和行為邏輯分開(kāi)
在setState, enter, exit等標(biāo)志性方法中不需要使用if / then或switch語(yǔ)句
在對(duì)象行為定義發(fā)生變化時(shí),修改容易,不易犯錯(cuò)誤;感謝enter和exit的封裝,它強(qiáng)制了資源回收邏輯
狀態(tài)機(jī)模型的意義對(duì)后面的內(nèi)容更為重要。上面的例子具有這樣幾個(gè)特征:
狀態(tài)具有顯式定義
事件內(nèi)外有別
外部事件對(duì)所有狀態(tài)成立,因此Persistence類的使用非常簡(jiǎn)單,從外部其實(shí)看不到內(nèi)部有什么狀態(tài)定義,黑盒意義上說(shuō),Persistence是無(wú)態(tài)的,這對(duì)使用便利性來(lái)說(shuō)極為重要;
內(nèi)部事件僅僅對(duì)某些狀態(tài)成立,所有異步函數(shù)的返回都理解為事件,而且是唯一的內(nèi)部事件;
從并發(fā)角度說(shuō),Persistence類是一個(gè)同步器(Synchronizer),即并發(fā)的save操作在這里被排序執(zhí)行了;當(dāng)然我們沒(méi)有設(shè)計(jì)更復(fù)雜的邏輯,例如任務(wù)隊(duì)列,但顯然那不是很難;
問(wèn)題純粹的狀態(tài)機(jī)(automata)對(duì)于并發(fā)編程是無(wú)力的,這是一種共識(shí),因?yàn)椴l(fā)帶來(lái)的狀態(tài)組合會(huì)迅速爆炸狀態(tài)空間,我們要找到辦法對(duì)付這個(gè)問(wèn)題,此其一。
其二,實(shí)際的程序模塊組合時(shí)常見(jiàn)包含關(guān)系,用經(jīng)典的狀態(tài)機(jī)模型會(huì)產(chǎn)生組合狀態(tài)機(jī)(Hierarchical State Machine),它的代碼書(shū)寫(xiě)遠(yuǎn)比上述例子的Flat State Machine難寫(xiě),除非在語(yǔ)言一級(jí)或者有類庫(kù)支持,否則可讀性和可維護(hù)性都很差,設(shè)計(jì)變更時(shí)代碼改動(dòng)幅度非常大,不是解決常見(jiàn)問(wèn)題的好辦法,雖然在一些特殊應(yīng)用領(lǐng)域卓有建樹(shù),例如嵌入式設(shè)備的通訊協(xié)議棧。
事件(Event)和線程(Thread)是形式上對(duì)立,但是數(shù)學(xué)上對(duì)等,的兩個(gè)編程方式。兩者各有利弊,戰(zhàn)爭(zhēng)也是古老的,你在網(wǎng)絡(luò)上很容易搜索到Why Event (Model) is Evil或者Why Thread (Model) is Evil的學(xué)術(shù)文章,都有大量的支持者。
Node.js的與眾不同之處在于它的強(qiáng)制non-blocking i/o設(shè)計(jì)。這給習(xí)慣Thread編程的開(kāi)發(fā)者制造了麻煩,所以在過(guò)去的幾年里新的過(guò)程原語(yǔ)被發(fā)明出來(lái)解決這個(gè)問(wèn)題,包括promise,generator,async/await。bluebird的使用者越來(lái)越多,而caolan的曾經(jīng)很流行的async庫(kù)用戶越來(lái)越少。
但是眾所周知JavaScript語(yǔ)言是事件模型的。在基礎(chǔ)特性上尋求類thread編程形式去解決一切問(wèn)題本身就是表里不一的,而且promise和async/await的實(shí)現(xiàn)本身也有很多不盡人意的地方。
這讓我們倒回來(lái)思考兩個(gè)問(wèn)題:
尋求各種CPS(Continuation Passing Style)是解決non-blocking i/o的必經(jīng)之路嗎?
事件和狀態(tài)機(jī)模型真的沒(méi)有辦法寫(xiě)規(guī)?;牟l(fā)程序嗎?
Node原作者Ryan Dahl最近在一次訪談里說(shuō)了他對(duì)Node的看法。他說(shuō)在最初的兩三年中他是狂熱的支持Node的強(qiáng)制non-blocking i/o設(shè)計(jì)的,達(dá)到那種認(rèn)為“原來(lái)我們都做錯(cuò)了”的程度,但是慢慢的他的態(tài)度發(fā)生了轉(zhuǎn)變,尤其是在接觸了Go語(yǔ)言之后;現(xiàn)在他的看法是,最初他以為Node可以做到是end-all或者for-all的,但是現(xiàn)在他沒(méi)那么有信心了,在并發(fā)編程上他認(rèn)為Go可能是更好的設(shè)計(jì)。
我的個(gè)人觀點(diǎn),談Node必談callback hell的開(kāi)發(fā)者,并不熟悉在Event Model下的并發(fā)編程技術(shù),promise和async/await本質(zhì)上,絕大多數(shù)情況下是在serialize過(guò)程,如果只是serialize,那么結(jié)果和blocking i/o的編程并不會(huì)有區(qū)別。Promise對(duì)parallel的支持很有限,它只是在serial的過(guò)程序列上偶爾撒一點(diǎn)parallel的flavor。而且如果你喜歡的就是Thread Model,那么就應(yīng)該選擇對(duì)它有良好支持的編程語(yǔ)言或環(huán)境,例如Go或者fibjs。
如果你像我一樣,喜歡JavaScript語(yǔ)言的簡(jiǎn)單,喜歡Event Model的簡(jiǎn)單,而不只是因?yàn)镹ode有良好的生態(tài)圈和海量的npm包可用而選擇了Node——如果你只是因?yàn)檫@兩點(diǎn)選擇了Node,你肯定會(huì)后悔的——那么擺在我們面前的問(wèn)題就是:事件模型,顯式狀態(tài),non-blocking i/o,我們能不能找到一種辦法,一種end-all和for-all的辦法,最好能夠直接體現(xiàn)在代碼形式上,或者至少體現(xiàn)在一個(gè)簡(jiǎn)單、直覺(jué)、不易錯(cuò)、同時(shí)保持經(jīng)典狀態(tài)機(jī)模型的完備性的Mental Model上,能夠?yàn)閺?fù)雜的并發(fā)編程問(wèn)題建模和書(shū)寫(xiě)代碼?
在這里經(jīng)典狀態(tài)機(jī)模式可以給我們一個(gè)簡(jiǎn)單啟迪:我們不僅可以用值來(lái)表示狀態(tài),我們也可以用對(duì)象類型表示狀態(tài),而且有明顯的收益。同樣的,在事件模型下解決并發(fā)問(wèn)題的關(guān)鍵,就是把這個(gè)設(shè)計(jì)繼續(xù)向前推進(jìn)一步,我們還可以用結(jié)構(gòu)來(lái)表示狀態(tài)。具體怎么寫(xiě)和怎么思考建模,則是這個(gè)系列文章的主旨。
這在數(shù)學(xué)層面上非常容易理解:所謂并發(fā)編程,它就是在structure過(guò)程(Rob Pike)。函數(shù)或者類函數(shù),包括promise,async function,generator,coroutine,他們是Thread Model下的(黑盒)原語(yǔ)和原語(yǔ)組合,對(duì)應(yīng)的,我們要找到事件模型下的顯式狀態(tài)方法來(lái)應(yīng)對(duì)這個(gè)問(wèn)題,如果能做到這一點(diǎn),我們就可以回到純粹的事件模型下編寫(xiě)程序。
這個(gè)結(jié)果并不難,但是,它也確實(shí)有一段路要走,我們需要仔細(xì)梳理過(guò)程原語(yǔ)的優(yōu)點(diǎn)缺點(diǎn),梳理并發(fā)編程的本質(zhì),梳理常見(jiàn)問(wèn)題的各種編程方式,最后回到我們的事件模型和狀態(tài)機(jī)上來(lái)。等這個(gè)系列寫(xiě)完,你也讀完,我向你保證,你再次看到callback函數(shù)時(shí)會(huì)覺(jué)得原來(lái)它那么簡(jiǎn)單且美。
下一篇我們開(kāi)始談并發(fā)編程,敬請(qǐng)期待。
文章版權(quán)歸作者所有,未經(jīng)允許請(qǐng)勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請(qǐng)注明本文地址:http://m.hztianpu.com/yun/88452.html
摘要:在這些動(dòng)作結(jié)束后,所有的隊(duì)列變化,就是整個(gè)組合任務(wù)狀態(tài)機(jī)的下一個(gè)狀態(tài)。如果組合狀態(tài)機(jī)停止了,向其中的任何一個(gè)對(duì)象執(zhí)行或者操作都可以讓整個(gè)狀態(tài)機(jī)繼續(xù)動(dòng)起來(lái)。 預(yù)覽。 先給出一個(gè)基礎(chǔ)類代碼。 const EventEmitter = require(events) const debug = require(debug)(transform) class Transform extend...
摘要:前端日?qǐng)?bào)精選譯用搭建探索生命周期中的匿名遞歸瀏覽器端機(jī)器智能框架深入理解筆記和屬性中文上海線下活動(dòng)前端工程化架構(gòu)實(shí)踐滬江技術(shù)沙龍掘金周二放送追加視頻知乎專欄第期聊一聊前端自動(dòng)化測(cè)試上雙關(guān)語(yǔ)來(lái)自前端的小段子,你看得懂嗎眾成翻 2017-08-10 前端日?qǐng)?bào) 精選 [譯] 用 Node.js 搭建 API Gateway探索 Service Worker 「生命周期」JavaScript ...
摘要:匿名函數(shù)是我們喜歡的一個(gè)重要原因,也是,它們分別消除了很多代碼細(xì)節(jié)上需要命名變量名或函數(shù)名的需要。這個(gè)匿名函數(shù)內(nèi),有更多的操作,根據(jù)的結(jié)果針對(duì)目錄和文件做了不同處理,而且有遞歸。 能和微博上的 @響馬 (fibjs作者)掰扯這個(gè)問(wèn)題是我的榮幸。 事情緣起于知乎上的一個(gè)熱貼,諸神都發(fā)表了意見(jiàn): https://www.zhihu.com/questio... 這一篇不是要說(shuō)明白什么是as...
摘要:前端日?qǐng)?bào)精選瀏覽器渲染過(guò)程與性能優(yōu)化新版采用新引擎,速度是舊版的倍,名字和也變了中文與的使用個(gè)人文章在對(duì)比中理解掘金白潔血戰(zhàn)并發(fā)編程異步英文 2017-10-05 前端日?qǐng)?bào) 精選 瀏覽器渲染過(guò)程與性能優(yōu)化Firefox 新版采用新引擎,速度是舊版的 2 倍,名字和 Logo 也變了8 Key React Component DecisionsThe Intl.PluralRules A...
摘要:的科學(xué)定義是或者,它的標(biāo)志性原語(yǔ)是。能解決一類對(duì)語(yǔ)言的實(shí)現(xiàn)來(lái)說(shuō)特別無(wú)力的狀態(tài)機(jī)模型流程即狀態(tài)。容易實(shí)現(xiàn)是需要和的一個(gè)重要原因。 前面寫(xiě)了一篇,寫(xiě)的很粗,這篇講講一些細(xì)節(jié)。實(shí)際上Fiber/Coroutine vs Async/Await之爭(zhēng)不是一個(gè)簡(jiǎn)單的continuation如何實(shí)現(xiàn)的問(wèn)題,而是兩個(gè)完全不同的problem和solution domain。 Event Model 我...
閱讀 723·2021-10-09 09:41
閱讀 716·2019-08-30 15:53
閱讀 1143·2019-08-30 15:53
閱讀 1273·2019-08-30 11:01
閱讀 1639·2019-08-29 17:31
閱讀 1063·2019-08-29 14:05
閱讀 1788·2019-08-29 12:49
閱讀 471·2019-08-28 18:17