準則: 狀態圖
狀態圖是用來建立模型元素之動態行為模型的狀態機,在規格上的正式圖形表示法。這個準則呈現這個表示法及說明如何有效使用它。
關係
主要說明

說明

狀態機用來塑造模型元素的動態行為,更明確地說,就是系統行為的事件驅動觀點(請參閱概念:事件與信號)。狀態機專用來定義相依於狀態的行為,或會隨著模型元素所在狀態而不同的行為。不會隨著元素狀態而改變行為的模型元素,不需要用狀態機來說明它們的行為(這些元素通常是主要責任在於管理資料的被動類別)。尤其是,狀態機必須用來建立會利用呼叫事件和信號事件來實作其作業(在類別狀態機中,就是轉換)之主動類別的行為模型。

狀態機由轉換所鏈結的狀態組成。狀態是物件執行某項作業或等待某個事件的狀況。轉換是兩個狀態之間的關係,通常是由某個事件來觸發,這個事件會執行特定動作或評估,並造成特定的結束狀態。圖 1 描述狀態機的元素。

顯示狀態機表示法的圖。

圖 1. 狀態機表示法。

簡式編輯器可以視為一部有限的狀態機,狀態包括空白等待指令等待文字載入檔案插入文字插入字元以及儲存並退出等事件會在狀態機中造成轉換。下列圖 1 描述編輯器的狀態機。

標題所說明的圖。

圖 2. 簡式編輯器的狀態機。

狀態

狀態是物件執行某項作業或等待某個事件的狀況。物件可能會在有限的時間內保留某個狀態。狀態有幾個內容:

名稱 這是用來區分各個狀態的文字字串;狀態也可以匿名,這表示它沒有名稱。
進入/跳出動作 這是在進入和跳出狀態時所執行的動作。
內部轉換 這是指處理時不會造成狀態變更的轉換。
子狀態 這是指巢狀的狀態結構,其中包括分離(循序作用)或並行(並行作用)狀態。
延遲事件 這是一份未在狀態中進行處理,但已延緩並排入佇列中,以便由物件在另一狀態時處理的事件清單。

如圖 1 所示,物件狀態機可定義兩個特殊狀態。起始狀態表示狀態機或子狀態的預設起始位置。起始狀態用黑色實心圓來表示。最終狀態表示狀態機執行完成,或已完成含括狀態。最終狀態用空心圓圈住黑色實心圓來表示。起始和最終狀態實際上都是假狀態。除了名稱之外,它們都不會有正常狀態通常會有的部分。從起始狀態轉換成最終狀態可能會補充的完整特性,其中包括警戒條件和動作,但不會有觸發事件。

轉換階段

轉換是兩個狀態之間的關係,它表示在第一個狀態的物件執行特定動作,並在發生指定的事件且符合指定的條件時進入第二個狀態。在這類狀態變更上,這項轉換稱為「發動」。在轉換發動之前,物件稱為在「來源」狀態中;發動之後,便稱為在「目標」狀態中。轉換有幾個內容:

來源狀態 這是轉換所影響的狀態;如果物件在來源狀態,當物件收到轉換的觸發事件時,如果滿足警戒條件(有的話),便會發動送出轉換。
事件觸發 這是來源狀態的物件收到之後,會能合格發動轉換的事件(假設滿足它的警戒條件)。
警戒條件 當因為收到事件觸發而觸發轉換時,會評估布林表示式;如果表示式得出 True 值,就可以發動轉換;如果表示式得出 False 值,就不會發動轉換。如果相同事件沒有其他可觸發的轉換,就會失去這個事件。
動作 這是一個可執行的不可分割計算,可以直接處理擁有狀態機的物件,間接處理這個物件所能見到的其他物件。
目標狀態 這是轉換完成之後的主動式狀態。

轉換可能有多個來源,這時它代表多個並行狀態的合併;也可能有多個目標,這時它代表分出多個並行狀態。

事件觸發

在狀態機的環境定義中,事件是一次可觸發狀態轉移的刺激。事件可包括信號事件、呼叫事件、經歷時間,或狀態變更。信號或呼叫可能會有轉換能夠使用其值的參數,其中包括警戒條件和動作的表示式。另外,也可能會有無觸發的轉換,以不含事件觸發的轉換來表示。這些轉換也稱為完成轉換,當它的來源狀態作業完成時,便會隱含地觸發。

警戒條件

在轉換的觸發事件發生之後,會評估警戒條件。只要警戒條件不重疊,相同的來源狀態和相同的事件觸發有可能產生多項轉換。針對事件發生時所進行的轉換,警戒條件只會評估一次。布林表示式可以參照物件的狀態。

動作

動作是一項可執行的不可分割計算,這表示事件無法岔斷它,因此,它會執行至完成。與作業相比,作業有可能被其他事件岔斷。 動作可包括作業呼叫(呼叫狀態機的擁有者及其他可見的物件)、另一個物件的建立或消滅,或將信號傳給另一個物件。在信號的傳送中,信號名稱字首會附加 'send' 關鍵字。

進入和跳出動作

進入和跳出動作可在每次進入或離開狀態時,讓相同動作分別得到分派。進入和跳出動作可讓這項作業潔淨地完成,動作完全不需要明確放在送入或送出的轉換上。進入和跳出動作不能有引數或警戒條件。模型元素的狀態機最上層進入動作可能會有一些參數,代表建立元素時機器所收到的引數。

內部轉換

內部轉換可讓事件不需要離開狀態,就能在狀態內獲得處理,因而可以避免觸發進入或跳出動作。內部轉換可能會有事件含有參數和警戒條件,基本上,它代表岔斷處理常式。

延遲事件

延遲事件是延後到未延遲的事件成為主動式的狀態,才處理的事件。當這個狀態成為主動式,就會觸發事件出現,且可能會造成轉換,如同它剛剛才發生一樣。實作延遲事件需要內部事件佇列。如果發生事件,但事件列為已延遲,它就是已放在佇列中。只要物件進入不延遲這些事件的狀態,事件就會離開佇列。

子狀態

簡單狀態是沒有子結構的狀態。有子狀態(巢狀狀態)的狀態稱為組合狀態。子狀態可以巢狀放在任何層次中。巢狀狀態機最多可有一個起始狀態、一個最終狀態。子狀態用來簡化複雜平面狀態機,它會顯示某些狀態只可能在特定環境定義(含括狀態)內。

顯示子狀態的圖解。

圖 3. 子狀態。

轉換可以從在含括組合狀態之外的來源,將目標鎖定組合狀態,也可以鎖定子狀態。如果目標是組合狀態,巢狀狀態機必須包括起始狀態,在進入組合狀態以及分派進入動作(如果有的話)之後,控制權會傳給這個狀態。如果目標是巢狀狀態,在分派組合狀態的進入動作(如果有的話),進而分派巢狀狀態的進入動作(如果有的話)之後,控制權會傳給這個巢狀狀態。

離開組合狀態的轉換,來源可能是組合狀態或子狀態。不論任何一種情況,控制權都會先離開巢狀狀態(如果有它的跳出動作,會分派這個動作),再離開組合狀態(如果有它的跳出動作,會分派這個動作)。基本上,來源是組合狀態的轉換會岔斷巢狀狀態機的作業。

歷程狀態

除非另有指定,當轉換進入組合狀態時,巢狀狀態機的動作會重新開始起始狀態(除非轉換目標直接是子狀態)。歷程狀態可讓狀態機重新進入離開組合狀態之前的最後一個主動式子狀態。圖 3 是歷程狀態用法的範例。

顯示歷程狀態的圖。

圖 4. 歷程狀態。

共用建模技術

狀態機最常用來建立物件整個生命期限的行為模型。當物件有相依於狀態的行為時,尤其需要它們。可能會有狀態機的物件包括類別、子系統、使用案例和介面(用來確認實現介面的物件必須滿足的狀態)。在即時系統中,狀態機用在封裝體和通訊協定上(用來確認實現通訊協定的物件必須滿足的狀態)。

並非所有物件都需要狀態機。如果物件的行為很簡單,例如它只是儲存或擷取資料,這個物件行為便是無狀態變更,它的狀態機便沒什麼用。

建立物件生命期限的模型包括三件事:指定物件可以回應的事件、對於這些事件的回應,以及過去的情況對於目前行為的影響。建立物件生命期限的模型也包括決定物件有意義地回應事件的順序,從建立物件開始,直到它消滅。

如果要建立物件生命期限模型,請執行下列動作:

  • 設定狀態機的環境定義,不論它是一個類別、使用案例或系統整體。
    • 如果環境定義是類別或使用案例,請收集鄰接的類別,其中包括母類別或關聯或相依性所能及的類別。這些鄰接項是動作的候選目標,也是併入警戒條件的候選目標。
    • 如果環境定義是系統整體,請將焦點縮小到系統的單一行為,之後,再考慮這個方面所涉及之物件的生命期限。整個系統的生命期限太長,無法成為有意義的焦點。
  • 建立物件的起始和最終狀態。如果起始和最終狀態有前置條件或後置條件,您也要定義它們。
  • 判斷物件所對應的事件。您可以在物件的介面中找到這些。在即時系統的案例中,您也可以在物件通訊協定中找到這些。
  • 從起始狀態開始到最終狀態,佈置物件所可能呈現的最上層狀態。請將這些狀態和適當的事件所觸發的轉換連接起來。之後,再繼續加入這些轉換。
  • 識別任何進入或跳出動作。
  • 利用子狀態來展開或簡化狀態機。
  • 確認狀態機中觸發轉換的所有事件都符合物件實現的介面所預期的事件。另外,也請確認狀態機會處理物件的介面所預期的所有事件。在即時系統的情況中,請針對封裝體的通訊協定來進行同等檢查。最後,請查看您要明確忽略事件之處(如延遲的事件)。
  • 確認含括物件的關係、方法和作業支援狀態機中的所有動作。
  • 追蹤狀態機,比較它和預期的事件與回應序列。請搜尋無法觸及的狀態及機器陷入其中的狀態。
  • 如果您重新排列或重組狀態機,請確定語意沒有改變。

提示和要訣

  • 得到選項之後,請使用狀態機的視覺語意,不要撰寫詳細的轉換碼。例如,不要在多個信號上觸發單一轉換,再依照信號,利用詳細程式碼,以不同的方式來管理控制流程。請使用個別信號所觸發的個別轉換。請避免在轉換程式碼中,使用隱藏了其他行為的邏輯。
  • 根據您在等待什麼,或狀態期間發生什麼,來命名狀態。請記住,狀態並不是一個「時間點」,它是一個時段,在這期間,狀態機會等待某些情況的發生。例如,'waitingForEnd' 是比 'end' 好的名稱;'timingSomeTask' 是比 'timeout' 好的名稱。請勿將狀態命名成彷彿它們是動作。
  • 唯一命名狀態機內的所有狀態和轉換;這會使來源層次的除錯變成比較容易。
  • 小心使用狀態變數(用來控制行為的屬性);請勿利用它們來替化新狀態的建立。當狀態不多且很少或沒有相依於狀態的行為,以及當很少或沒有與包含狀態機的物件可能並存的行為,或很少或沒有與這個物件可能無關的行為,這時便可以使用狀態變數。如果有複雜而相依於狀態的行為可能並存,或有可能在包含狀態機的物件之外產生必須處理的事件,請考慮讓兩個或更多主動物件協同作業(可能定義為一項組合)。在即時系統中,應該利用包含子封裝體的封裝體來建立複雜而相依於狀態之並行行為的模型。
  • 如果單一圖解有 5 ± 2 個以上的狀態,請考慮使用子狀態。請使用常識:在絕對正規的型樣中,10 個狀態可能沒問題,但彼此之間有 40 項轉換的兩個狀態,便需要重新考慮。請確定狀態機是可理解的。
  • 針對什麼觸發事件和/或轉換期間發生什麼來命名轉換。請選擇比較容易理解的名稱。
  • 當您看到一個選項頂點時,您應該問這個選項的責任是否能委派給另一個元件,使它能夠以一組明確的待處理信號來呈現給物件(也就是說,取代訊息 -> 資料 > x 的選項),讓傳送者或某個其他中間參與者來做決定,以及傳送一個名稱中含有明確決策的信號(例如,使用名稱為 isFull 和 isEmpty 的信號,而不用名稱為 value 的信號並檢查訊息資料)。
  • 以描述性的方式來命名選項頂點所回答的問題,如 'isThereStillLife' 或 'isItTimeToComplain'。
  • 在任何給定的物件內,嘗試維持選項頂端名稱的唯一性(原因如同維持轉換名稱的唯一性)。
  • 有太長的轉換程式碼片段嗎?應該改用函數嗎?共用程式碼片段是擷取成函數嗎? 轉換應該讀起來像高階虛擬程式碼,且應該遵循與 C++ 函數相同或更嚴格的長度規則。例如,超出 25 行程式碼的轉換便算太長。
  • 函數應該依照它們的動作來命名。
  • 特別注意進入和跳出動作:往往會出現做了變更,但忘了改變進入和跳出動作。
  • 跳出動作可用來提供安全特性,如跳出 'heaterOn' 狀態的動作會將加熱器關掉,這些動作用來強制確認。
  • 一般而言,除非狀態機是抽象的,且含括元素的子類別將修正它,否則,子狀態應該包含兩個或更多狀態。
  • 選擇點應該用來替代動作或轉換中的條件性邏輯。選擇點很容易見到,但程式碼中的條件性邏輯見不到,很容易被忽略。
  • 避免警戒條件
    • 如果事件觸發多項轉換,便無法控制會先評估哪個警戒條件。因此,結果可能無法預期。
    • 可能有多個警戒條件是 'true',但只能遵循一項轉換。選擇的路徑可能無法預期。
    • 警戒條件不可見;很不容易「見到」它們的存在。
  • 避免類似流程圖的狀態機。
    • 這可能表示試圖為並不真正存在的抽象建立模型,例如:
      • 利用主動類別來建立最適合被動類別或資料類別之行為的模型
      • 利用緊密耦合的資料類別和主動式別來建立資料類別的模型(也就是說,利用資料類別來傳遞類型資訊,但主動類別包含大部分應該關聯於資料類別的資料)。
    • 下列症狀可以反映狀態機的這項誤用:
      • 傳給「自己」的訊息,主要只是用來重複使用程式碼
      • 只有少數狀態,但有許多選擇點
      • 在某些情況下,狀態機不循環。在流程控制應用程式中,或在試圖控制一個事件序列時,這類狀態機有效;它們在分析期間存在,通常代表狀態機退化成流程圖。
    • 當識別了這個問題時:
      • 考慮將主動類別分割成責任較明確的較小單元,
      • 將更多行為移到有問題的主動類別所關聯的資料類別。
      • 將更多行為移到主動類別函數中。
      • 產生更有意義的信號,而不依賴資料。

利用抽象狀態機進行設計

抽象狀態機是需要加入更多細節才能用於實際用途的狀態機。抽象狀態機可用來定義一般、可重複使用的行為,後續的模型元素會進一步修正這個行為。

標題所說明的圖。

圖 5. 抽象狀態機。

請設想圖 5 的抽象狀態機。所描述的簡式狀態機是事件驅動系統中多種不同元素類型最抽象之行為層次(「控制」自動化)的代表。雖然它們都共用這個高層次的形式,但不同元素類型在執行狀態中,會有非常不同的詳細行為,這隨著它們的目的而不同。因此,這個狀態機最可能定義在作為不同特殊化主動類別之根類別的抽象類別中。

因此,我們利用繼承關係來定義這個抽象狀態機的兩個這類不同修正。圖 6 顯示 R1 和 R2 這兩項修正。為了更清楚,我們用灰筆畫了繼承自母類別的元素。

標題所說明的圖。

圖 6. 圖 5 狀態機的兩項修正。

兩項修正拆解執行中狀態以及延伸原始「啟動」轉換的方式,明顯不同。這些選擇當然只能在修正已知之後進行,因此,無法利用抽象類別中的單一端對端轉換來進行。

鏈接狀態

「繼續」送入轉換和送出轉換的能力是上述修正類型的基礎。表面上,進入點和最終狀態,加上延續轉換,便足以提供這些語意。不幸,當需要延伸多個不同的轉換時,這不夠。

抽象行為型樣所需要的,是將全部在單一執行至完成步驟之範圍內執行的兩個或更多轉換區段鏈接起來的方式。這表示進入階層式狀態的轉換會分割成送入部分和延伸部分,送入部分有效終止於狀態界限,延伸部分則在狀態內繼續下去。同樣地,起於階層式巢狀狀態的送出轉換也會分段,它會成為終止於含括狀態界限的部分,以及從狀態界限繼續到目標狀態的部分。在 UML 中,引進鏈接狀態概念可以實現這個效果。這個模型是利用 UML 狀態概念的模板 (<<chainState>>) 來建立的。這個狀態的唯一目的是將進一步的自動(無觸發)轉換「鏈接」到輸入轉換。鏈接狀態沒有內部結構,也就是說沒有進入動作,沒有內部作業,也沒有跳出動作。它也沒有事件所觸發的轉換。它可以有任意數目的輸入轉換。它可能會有不含觸發事件的送出轉換;當輸入轉換啟動這個狀態時,會自動發動這項轉換。這個狀態的目的是將輸入轉換鏈接到個別的輸出轉換。在輸入轉換和鏈接的輸出轉換之間,一個連接到含括狀態內的另一個狀態,另一個連接到含括狀態外的另一個狀態。引進鏈接狀態之目的,在於分開含括狀態的內部規格與外部環境;這與封裝相關。

事實上,鏈接狀態代表一種將某項轉換鏈接到特定連續轉換的「透通」狀態。如果未定義任何延續轉換,轉換就會在鏈接狀態中終止,最後必須發動含括狀態的某些轉換來向前移動事物。

圖 7 的範例狀態機區段說明鏈接狀態及其表示法。在狀態機圖解中,鏈接狀態是用在適當階層狀態的小白圈來表示(這個表示法類似於與它們相似的起始和最終狀態)。這些圈是鏈接狀態模板的模板圖示,為了方便,通常畫在界限旁。(事實上,表示法的變式是將這些畫在含括狀態的邊框上。)

圖解說明詳見隨附的文字。

圖 7. 鏈接狀態和鏈接的轉換。

這個範例中的鏈接轉換由三個鏈接的轉換區段 e1/a11-/a12-/a13 組成。 當收到信號 e1 時,會採用轉換標籤 e1/a11,它的所有動作都會執行,之後,會到達鏈接狀態 c1。之後,會採取 c1 和 c2 之間的延續轉換,最後,由於 c2 也是一個鏈接狀態,因此,會從 c2 轉換到 S21。如果這些路徑上的狀態都有跳出和進入動作,實際的動作執行序列會進行如下:

  • S11 的跳出動作
  • 動作 a11
  • S1 的跳出動作
  • 動作 a12
  • S2 的進入動作
  • 動作 a13
  • S21 的進入動作

這些全部是在單一執行至完成步驟的範圍內執行。

這應該對照直接轉換 e2/a2 的動作執行語意,它們是:

  • S11 的跳出動作
  • S1 的跳出動作
  • 動作 a2
  • S2 狀態的進入動作
  • S1 狀態的進入動作