日韩无码专区无码一级三级片|91人人爱网站中日韩无码电影|厨房大战丰满熟妇|AV高清无码在线免费观看|另类AV日韩少妇熟女|中文日本大黄一级黄色片|色情在线视频免费|亚洲成人特黄a片|黄片wwwav色图欧美|欧亚乱色一区二区三区

RELATEED CONSULTING
相關(guān)咨詢
選擇下列產(chǎn)品馬上在線溝通
服務(wù)時(shí)間:8:30-17:00
你可能遇到了下面的問(wèn)題
關(guān)閉右側(cè)工具欄

新聞中心

這里有您想知道的互聯(lián)網(wǎng)營(yíng)銷解決方案
給DSL開(kāi)個(gè)腦洞:無(wú)狀態(tài)的狀態(tài)機(jī)

什么是 DSL ?DSL 是一種工具,其核心價(jià)值在于提供了一種手段,可以更加清晰地就系統(tǒng)某部分的意圖進(jìn)行溝通。本文將通過(guò)實(shí)現(xiàn)一個(gè)狀態(tài)機(jī)引擎來(lái)看清 DSL 的本質(zhì),介紹狀態(tài)機(jī)的核心模型和 Fluent 接口,并解決狀態(tài)機(jī)的性能問(wèn)題。

最近在一個(gè)項(xiàng)目中,因?yàn)樯婕昂芏酄顟B(tài)的流轉(zhuǎn),我們選擇使用狀態(tài)機(jī)引擎來(lái)表達(dá)狀態(tài)流轉(zhuǎn)。因?yàn)闋顟B(tài)機(jī) DSL(Domain Specific Languages)帶來(lái)的表達(dá)能力,相比較于 if-else 的代碼,要更優(yōu)雅更容易理解。另一方面,狀態(tài)機(jī)很簡(jiǎn)單,不像流程引擎那么華而不實(shí)。

一開(kāi)始我們選用了一個(gè)開(kāi)源的狀態(tài)機(jī)引擎,但我覺(jué)得不好用,就自己寫了一個(gè)能滿足我們要求的簡(jiǎn)潔版狀態(tài)機(jī),這樣比較 KISS(Keep It Simple and Stupid)。

作為 COLA 開(kāi)源的一部分,我已經(jīng)將該狀態(tài)機(jī)(cola-statemachine)開(kāi)源,你可以訪問(wèn)獲?。篽ttps://github.com/alibaba/COLA

在實(shí)現(xiàn)狀態(tài)機(jī)的過(guò)程中,有幸看到Martin Fowler寫的《Domain Specific Languages》。書中的內(nèi)容讓我對(duì) DSL 有了不一樣的認(rèn)知。

這也是為什么會(huì)有這篇文章的原因,希望你看完這邊文章以后,可以對(duì)什么是 DSL、如何使用 DSL、如何使用狀態(tài)機(jī)都能有一個(gè)不一樣的體會(huì)。

DSL

在介紹如何實(shí)現(xiàn)狀態(tài)機(jī)之前,不妨讓我們先來(lái)看一下什么是 DSL,在 Martin Fowler 的《Domain Specific Languages》書中。開(kāi)篇就是以 State Machine 來(lái)作為引子介紹 DSL 的。有時(shí)間的話,強(qiáng)烈建議你去讀讀這本書。沒(méi)時(shí)間的話,看看下面的內(nèi)容也能掌握個(gè)大概了。

下面就讓我提煉一下書中的內(nèi)容,帶大家深入了解下 DSL。

什么是 DSL

DSL 是一種工具,它的核心價(jià)值在于,它提供了一種手段,可以更加清晰地就系統(tǒng)某部分的意圖進(jìn)行溝通。

這種清晰并非只是審美追求。一段代碼越容易看懂,就越容易發(fā)現(xiàn)錯(cuò)誤,也就越容易對(duì)系統(tǒng)進(jìn)行修改。因此,我們鼓勵(lì)變量名要有意義,文檔要寫清楚,代碼結(jié)構(gòu)要寫清晰?;谕瑯拥睦碛桑覀儜?yīng)該也鼓勵(lì)采用 DSL。

按照定義來(lái)說(shuō),DSL 是針對(duì)某一特定領(lǐng)域,具有受限表達(dá)性的一種計(jì)算機(jī)程序設(shè)計(jì)語(yǔ)言。這一定義包含 3 個(gè)關(guān)鍵元素:

  • 語(yǔ)言性(language nature):DSL 是一種程序設(shè)計(jì)語(yǔ)言,因此它必須具備連貫的表達(dá)能力——不管是一個(gè)表達(dá)式還是多個(gè)表達(dá)式組合在一起。
  • 受限的表達(dá)性(limited expressiveness):通用程序設(shè)計(jì)語(yǔ)言提供廣泛的能力:支持各種數(shù)據(jù)、控制,以及抽象結(jié)構(gòu)。這些能力很有用,但也會(huì)讓語(yǔ)言難于學(xué)習(xí)和使用。DSL 只支持特定領(lǐng)域所需要特性的最小集。使用 DSL,無(wú)法構(gòu)建一個(gè)完整的系統(tǒng),相反,卻可以解決系統(tǒng)某一方面的問(wèn)題。
  • 針對(duì)領(lǐng)域(domain focus):只有在一個(gè)明確的小領(lǐng)域下,這種能力有限的語(yǔ)言才會(huì)有用。這個(gè)領(lǐng)域才使得這種語(yǔ)言值得使用。

比如正則表達(dá)式:

/\d{3}-\d{3}-\d{4}/

就是一個(gè)典型的 DSL,解決的是字符串匹配這個(gè)特定領(lǐng)域的問(wèn)題。

DSL 的分類

按照類型,DSL 可以分為三類:內(nèi)部 DSL(Internal DSL)、外部 DSL(External DSL)、以及語(yǔ)言工作臺(tái)(Language Workbench)。

  • Internal DSL 是一種通用語(yǔ)言的特定用法。用內(nèi)部 DSL 寫成的腳本是一段合法的程序,但是它具有特定的風(fēng)格,而且只用到了語(yǔ)言的一部分特性,用于處理整個(gè)系統(tǒng)一個(gè)小方面的問(wèn)題。用這種 DSL 寫出的程序有一種自定義語(yǔ)言的風(fēng)格,與其所使用的宿主語(yǔ)言有所區(qū)別。例如我們的狀態(tài)機(jī)就是 Internal DSL,它不支持腳本配置,使用的時(shí)候還是 Java 語(yǔ)言,但并不妨礙它也是 DSL。
builder.externalTransition() 
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());
  • External DSL 是一種“不同于應(yīng)用系統(tǒng)主要使用語(yǔ)言”的語(yǔ)言。外部 DSL 通常采用自定義語(yǔ)法,不過(guò)選擇其他語(yǔ)言的語(yǔ)法也很常見(jiàn)(XML 就是一個(gè)常見(jiàn)選 擇)。比如像 Struts 和 Hibernate 這樣的系統(tǒng)所使用的 XML 配置文件。
  • Workbench 是一個(gè)專用的 IDE,簡(jiǎn)單點(diǎn)說(shuō),工作臺(tái)是 DSL 的產(chǎn)品化和可視化形態(tài)。

三個(gè)類別 DSL 從前往后是有一種遞進(jìn)關(guān)系,Internal DSL 最簡(jiǎn)單,實(shí)現(xiàn)成本也低,但是不支持“外部配置”。Workbench 不僅實(shí)現(xiàn)了配置化,還實(shí)現(xiàn)了可視化,但是實(shí)現(xiàn)成本也最高。他們的關(guān)系如下圖所示:

??

??

不同 DSL 該如何選擇

幾種 DSL 類型各有各的使用場(chǎng)景,選擇的時(shí)候,可以這樣去做一個(gè)判斷。

  • Internal DSL:假如你只是為了增加代碼的可理解性,不需要做外部配置,我建議使用 Internal DSL,簡(jiǎn)單、方便、直觀。
  • External DSL:如果你需要在 Runtime 的時(shí)候進(jìn)行配置,或者配置完,不想重新部署代碼,可以考慮這種方式。比如,你有一個(gè)規(guī)則引擎,希望增加一條規(guī)則的時(shí)候,不需要重復(fù)發(fā)布代碼,那么可以考慮 External。
  • Workbench:配置也好,DSL Script 也好,這東西對(duì)用戶不夠友好。比如在淘寶,各種針對(duì)商品的活動(dòng)和管控規(guī)則非常復(fù)雜,變化也快。我們需要一個(gè)給運(yùn)營(yíng)提供一個(gè) workbench,讓他們自己設(shè)置各種規(guī)則,并及時(shí)生效。這時(shí)的 workbench 將會(huì)非常有用。

??

??

總而言之,在合適的地方用合適的解決方案,不能一招鮮吃遍天。就像最臭名昭著的 DSL —— 流程引擎,就屬于那種嚴(yán)重的被濫用和過(guò)渡設(shè)計(jì)的典型,是把簡(jiǎn)單的問(wèn)題復(fù)雜化的典型。

最好不要無(wú)端增加復(fù)雜性。然而,想做簡(jiǎn)單也不是一件容易的事,特別是在大公司,我們不僅要寫代碼,還要能沉淀“NB 的技術(shù)”,最好是那種可以把老板說(shuō)的一愣一愣的技術(shù),就像尼古拉斯在《反脆弱》里面說(shuō)的:

在現(xiàn)代生活中,簡(jiǎn)單的做法一直難以實(shí)現(xiàn),因?yàn)樗羞`某些努力尋求復(fù)雜化以證明其工作合理性的人所秉持的精神。

Fluent Interfaces

在編寫軟件庫(kù)的時(shí)候,我們有兩種選擇。一種是提供 Command-Query API,另一種是 Fluent Interfaces。比如 Mockito 的 API :

when(mockedList.get(anyInt())).thenReturn("element")

就是一種典型連貫接口的用法。

連貫接口(fluent interfaces)是實(shí)現(xiàn) Internal DSL 的重要方式,為什么這么說(shuō)呢?

因?yàn)?Fluent 的這種連貫性帶來(lái)的可讀性和可理解的提升,其本質(zhì)不僅僅是在提供 API,更是一種領(lǐng)域語(yǔ)言,是一種 Internal DSL。

比如 Mockito 的 API:

when(mockedList.get(anyInt())).thenReturn("element")

就非常適合用 Fluent 的形式,實(shí)際上,它也是單元測(cè)試這個(gè)特定領(lǐng)域的 DSL。

如果把這個(gè) Fluent 換成是 Command-Query API,將很難表達(dá)出測(cè)試框架的領(lǐng)域。

String element = mockedList.get(anyInt()); 
boolean isExpected = "element".equals(element);

這里需要注意的是,連貫接口不僅僅可以提供類似于 method chaining 和 builder 模式的方法級(jí)聯(lián)調(diào)用,比如 OkHttpClient 中的 Builder:

OkHttpClient.Builder builder=new OkHttpClient.Builder(); 
OkHttpClient okHttpClient=builder
.readTimeout(5*1000, TimeUnit.SECONDS)
.writeTimeout(5*1000, TimeUnit.SECONDS)
.connectTimeout(5*1000, TimeUnit.SECONDS)

他更重要的作用是,限定方法調(diào)用的順序。比如,在構(gòu)建狀態(tài)機(jī)的時(shí)候,我們只有在調(diào)用了 from 方法后,才能調(diào)用 to 方法,Builder 模式?jīng)]有這個(gè)功能。

怎么做呢?我們可以使用 Builder 和 Fluent 接口結(jié)合起來(lái)的方式來(lái)實(shí)現(xiàn),下面的狀態(tài)機(jī)實(shí)現(xiàn)部分,我會(huì)進(jìn)一步介紹。

狀態(tài)機(jī)

好的,關(guān)于 DSL 的知識(shí)我就介紹這么多。接下來(lái),讓我們看看應(yīng)該如何實(shí)現(xiàn)一個(gè) Internal DSL 的狀態(tài)機(jī)引擎。

狀態(tài)機(jī)選型

我反對(duì)濫用流程引擎,但并不排斥狀態(tài)機(jī),主要有以下兩個(gè)原因:

  • 首先,狀態(tài)機(jī)的實(shí)現(xiàn)可以非常的輕量,最簡(jiǎn)單的狀態(tài)機(jī)用一個(gè) Enum 就能實(shí)現(xiàn),基本是零成本。
  • 其次,使用狀態(tài)機(jī)的 DSL 來(lái)表達(dá)狀態(tài)的流轉(zhuǎn),語(yǔ)義會(huì)更加清晰,會(huì)增強(qiáng)代碼的可讀性和可維護(hù)性。

然而,我們的業(yè)務(wù)場(chǎng)景雖然也不是特別復(fù)雜,但還是超出了 Enum 僅支持線性狀態(tài)流轉(zhuǎn)的范疇。因此不得不先向外看看。

開(kāi)源狀態(tài)機(jī)太復(fù)雜

和流程引擎一樣,開(kāi)源的狀態(tài)機(jī)引擎不可謂不多,我著重看了兩個(gè)狀態(tài)機(jī)引擎的實(shí)現(xiàn),一個(gè)是 Spring Statemachine,一個(gè)是 Squirrel statemachine。這是目前在 github 上的 Top 2 的狀態(tài)機(jī)實(shí)現(xiàn),他們的優(yōu)點(diǎn)是功能很完備,缺點(diǎn)也是功能很完備。

當(dāng)然,這也不能怪開(kāi)源軟件的作者,你好不容易開(kāi)源一個(gè)項(xiàng)目,至少要把 UML State Machine 上羅列的功能點(diǎn)都支持掉吧。

就我們的項(xiàng)目而言(其實(shí)大部分項(xiàng)目都是如此),我實(shí)在不需要那么多狀態(tài)機(jī)的高級(jí)玩法:比如狀態(tài)的嵌套(substate),狀態(tài)的并行(parallel,fork,join)、子狀態(tài)機(jī)等等。

開(kāi)源狀態(tài)機(jī)性能差

除此之外,還有一個(gè)我不能容忍的問(wèn)題是,這些開(kāi)源的狀態(tài)機(jī)都是有狀態(tài)的(Stateful)的,表面上來(lái)看,狀態(tài)機(jī)理所當(dāng)然是應(yīng)該維持狀態(tài)的。但是深入想一下,這種狀態(tài)性并不是必須的,因?yàn)橛袪顟B(tài),狀態(tài)機(jī)的實(shí)例就不是線程安全的,而我們的應(yīng)用服務(wù)器是分布式多線程的,所以在每一次狀態(tài)機(jī)在接受請(qǐng)求的時(shí)候,都不得不重新 build 一個(gè)新的狀態(tài)機(jī)實(shí)例。

以電商交易為例,用戶下單后,我們調(diào)用狀態(tài)機(jī)實(shí)例將狀態(tài)改為“Order Placed”。當(dāng)用戶支付訂單的時(shí)候,可能是另一個(gè)線程,也可能是另一臺(tái)服務(wù)器,所以我們必須重新創(chuàng)建一個(gè)狀態(tài)機(jī)實(shí)例。因?yàn)樵瓉?lái)的 instance 不是線程安全的。

??

??

這種 new instance per request 的做法,耗電不說(shuō)。倘若狀態(tài)機(jī)的構(gòu)建很復(fù)雜,QPS 又很高的話,肯定會(huì)遇到性能問(wèn)題。

鑒于復(fù)雜性和性能(公司電費(fèi))的考慮,我們決定自己實(shí)現(xiàn)一個(gè)狀態(tài)機(jī)引擎,設(shè)計(jì)的目標(biāo)很明確,有兩個(gè)要求:

簡(jiǎn)潔的僅支持狀態(tài)流轉(zhuǎn)的狀態(tài)機(jī),不需要支持嵌套、并行等高級(jí)玩法。

狀態(tài)機(jī)本身需要是 Stateless(無(wú)狀態(tài))的,這樣一個(gè) Singleton Instance 就能服務(wù)所有的狀態(tài)流轉(zhuǎn)請(qǐng)求了。

狀態(tài)機(jī)實(shí)現(xiàn)

  • 狀態(tài)機(jī)領(lǐng)域模型

鑒于我們的訴求是實(shí)現(xiàn)一個(gè)僅支持簡(jiǎn)單狀態(tài)流轉(zhuǎn)的狀態(tài)機(jī),該狀態(tài)機(jī)的核心概念如下圖所示,主要包括:

  1. State:狀態(tài)
  2. Event:事件,狀態(tài)由事件觸發(fā),引起變化
  3. Transition:流轉(zhuǎn),表示從一個(gè)狀態(tài)到另一個(gè)狀態(tài)
  4. External Transition:外部流轉(zhuǎn),兩個(gè)不同狀態(tài)之間的流轉(zhuǎn)
  5. Internal Transition:內(nèi)部流轉(zhuǎn),同一個(gè)狀態(tài)之間的流轉(zhuǎn)
  6. Condition:條件,表示是否允許到達(dá)某個(gè)狀態(tài)
  7. Action:動(dòng)作,到達(dá)某個(gè)狀態(tài)之后,可以做什么
  8. StateMachine:狀態(tài)機(jī)

??

??

整個(gè)狀態(tài)機(jī)的核心語(yǔ)義模型(Semantic Model)也很簡(jiǎn)單,就是如下圖所示:

??

??

Note:這里之所以叫 Semantic Model,用的是《DSL》書里的術(shù)語(yǔ),你也可以理解為是狀態(tài)機(jī)的領(lǐng)域模型。Martin 用 Semantic 這個(gè)詞,是想說(shuō),外部的 DSL script 代表語(yǔ)法(Syntax),里面的 model 代表語(yǔ)義(Semantic),我覺(jué)得這個(gè)隱喻還是很恰當(dāng)?shù)摹?/p>

OK,狀態(tài)機(jī)語(yǔ)義模型的核心代碼如下所示:

//StateMachine 
public class StateMachineImpl implements StateMachine {

private String machineId;
private final Map> stateMap;

...
}

//State
public class StateImpl implements State {
protected final S stateId;
private Map> transitions = new HashMap<>();

...
}

//Transition
public class TransitionImpl implements Transition {

private State source;
private State target;
private E event;
private Condition condition;
private Action action;

...
}
  • 狀態(tài)機(jī)的 Fluent API

實(shí)際上,我用來(lái)寫 Builder 和 Fluent Interface 的代碼甚至比核心代碼還要多,比如我們的 TransitionBuilder 是這樣寫的

class  TransitionBuilderImpl implements ExternalTransitionBuilder, InternalTransitionBuilder, From, On, To {     
...
@Override
public From from(S stateId) {
source = StateHelper.getState(stateMap,stateId);
return this;
}

@Override
public To to(S stateId) {
target = StateHelper.getState(stateMap, stateId);
return this;
}
...
}

通過(guò)這種 Fluent Interface 的方式,我們確保了 Fluent 調(diào)用的順序,如下圖所示,在 externalTransition 的后面你只能調(diào)用 from,在 from 的后面你只能調(diào)用 to,從而保證了狀態(tài)機(jī)構(gòu)建的語(yǔ)義正確性和連貫性。

??

??

  • 狀態(tài)機(jī)的無(wú)狀態(tài)設(shè)計(jì)

至此,狀態(tài)機(jī)的核心模型和 Fluent 接口我已經(jīng)介紹完了。我們還需要解決一個(gè)性能問(wèn)題,也就是我前面說(shuō)的,要把狀態(tài)機(jī)變成無(wú)狀態(tài)的。

分析一下市面上的開(kāi)源狀態(tài)機(jī)引擎,不難發(fā)現(xiàn),它們之所以有狀態(tài),主要是在狀態(tài)機(jī)里面維護(hù)了兩個(gè)狀態(tài):初始狀態(tài)(initial state)和當(dāng)前狀態(tài)(current state),如果我們能把這兩個(gè)實(shí)例變量去掉的話,就可以實(shí)現(xiàn)無(wú)狀態(tài),從而實(shí)現(xiàn)一個(gè)狀態(tài)機(jī)只需要有一個(gè) instance 就夠了。

關(guān)鍵是這兩個(gè)狀態(tài)可以不要嗎?當(dāng)然可以,唯一的副作用是,我們沒(méi)辦法獲取到狀態(tài)機(jī) instance 的 current state。然而,我也不需要知道,因?yàn)槲覀兪褂脿顟B(tài)機(jī),僅僅是接受一下 source state,check 一下 condition,execute 一下 action,然后返回 target state 而已。它只是實(shí)現(xiàn)了一個(gè)狀態(tài)流轉(zhuǎn)的 DSL 表達(dá),僅此而已,全程操作完全可以是無(wú)狀態(tài)的。

采用了無(wú)狀態(tài)設(shè)計(jì)之后,我們就可以使用一個(gè)狀態(tài)機(jī) Instance 來(lái)響應(yīng)所有的請(qǐng)求了,性能會(huì)大大的提升。

??

??

使用狀態(tài)機(jī)

狀態(tài)機(jī)的實(shí)現(xiàn)很簡(jiǎn)單,同樣,他的使用也不難。如下面的代碼所示,它展現(xiàn)了 cola 狀態(tài)機(jī)支持的全部三種 transition 方式。

StateMachineBuilder builder = StateMachineBuilderFactory.create(); 
//external transition
builder.externalTransition()
.from(States.STATE1)
.to(States.STATE2)
.on(Events.EVENT1)
.when(checkCondition())
.perform(doAction());

//internal transition
builder.internalTransition()
.within(States.STATE2)
.on(Events.INTERNAL_EVENT)
.when(checkCondition())
.perform(doAction());

//external transitions
builder.externalTransitions()
.fromAmong(States.STATE1, States.STATE2, States.STATE3)
.to(States.STATE4)
.on(Events.EVENT4)
.when(checkCondition())
.perform(doAction());

builder.build(machineId);

可以看到,這種 Internal DSL 的狀態(tài)機(jī)顯著的提升了代碼的可讀性和可理解性。特別是在相對(duì)復(fù)雜的業(yè)務(wù)狀態(tài)流轉(zhuǎn)中,比如下圖就是我們用 cola-statemachine 生成的我們實(shí)際項(xiàng)目中的 plantUML 圖。如果沒(méi)有狀態(tài)機(jī)的支持,像這樣的業(yè)務(wù)代碼將會(huì)很難看懂和維護(hù)。

??

??

這就是 DSL 的核心價(jià)值——更加清晰地表達(dá)系統(tǒng)中,某一部分的設(shè)計(jì)意圖和業(yè)務(wù)語(yǔ)義。當(dāng)然 External DSL 所帶來(lái)的可配置性和靈活性也很有價(jià)值,只是 cola-statemachine 還沒(méi)有支持,原因很簡(jiǎn)單,暫時(shí)用不上。


網(wǎng)站標(biāo)題:給DSL開(kāi)個(gè)腦洞:無(wú)狀態(tài)的狀態(tài)機(jī)
網(wǎng)址分享:http://www.5511xx.com/article/djodpdh.html