(轉)使用graphviz繪制流程圖


 

前言

  日常的開發工作中,為代碼添加注釋是代碼可維護性的一個重要方面,但是僅僅提供注釋是不夠的,特別是當系統功能越來越復雜,涉及到的模塊越來越多的時候,僅僅靠代碼就很難從宏觀的層次去理解。因此我們需要圖例的支持,圖例不僅僅包含功能之間的交互,也可以包含復雜的數據結構的示意圖,數據流向等。

但是,常用的UML建模工具,如VISIO等都略顯復雜,且體積龐大。對於開發人員,特別是后台開發人員來說,命令行,腳本才是最友好的,而圖形界面會很大程度的限制開發效率。相對於鼠標,鍵盤才是開發人員最好的朋友。

graphviz簡介

  本文介紹一個高效而簡潔的繪圖工具graphviz。graphviz是貝爾實驗室開發的一個開源的工具包,它使用一個特定的DSL(領域特定語言):dot作為腳本語言,然后使用布局引擎來解析此腳本,並完成自動布局。graphviz提供豐富的導出格式,如常用的圖片格式,SVG,PDF格式等。

graphviz中包含了眾多的布局器:

  1. dot 默認布局方式,主要用於有向圖
  2. neato 基於spring-model(又稱force-based)算法
  3. twopi 徑向布局
  4. circo 圓環布局
  5. fdp 用於無向圖

graphviz的設計初衷是對有向圖/無向圖等進行自動布局,開發人員使用dot腳本定義圖形元素,然后選擇算法進行布局,最終導出結果。

  首先,在dot腳本中定義圖的頂點和邊,頂點和邊都具有各自的屬性,比如形狀,顏色,填充模式,字體,樣式等。然后使用合適的布局算法進行布局。布局算法除了繪制各個頂點和邊之外,需要盡可能的將頂點均勻的分布在畫布上,並且盡可能的減少邊的交叉(如果交叉過多,就很難看清楚頂點之間的關系了)。所以使用graphviz的一般流程為:

  1. 定義一個圖,並向圖中添加需要的頂點和邊
  2. 為頂點和邊添加樣式
  3. 使用布局引擎進行繪制

  一旦熟悉這種開發模式,就可以快速的將你的想法繪制出來。配合一個良好的編輯器(vim/emacs)等,可以極大的提高開發效率,與常見的GUI應用的所見即所得模式對應,此模式稱為所思即所得。比如在我的機器上,使用vim編輯dot腳本,然后將F8映射為調用dot引擎去繪制當前腳本,並打開一個新的窗口來顯示運行結果:

對於開發人員而言,經常會用到的圖形繪制可能包括:函數調用關系,一個復雜的數據結構,系統的模塊組成,抽象語法樹等。

基礎知識

graphviz包含3中元素,圖,頂點和邊。每個元素都可以具有各自的屬性,用來定義字體,樣式,顏色,形狀等。下面是一些簡單的示例,可以幫助我們快速的了解graphviz的基本用法。

第一個graphviz圖

比如,要繪制一個有向圖,包含4個節點a,b,c,d。其中a指向b,b和c指向d。可以定義下列腳本:

   1: digraph abc{

 

   2: a;

 

   3: b;

 

   4: c;

 

   5: d;

 

   6:  

 

   7: a -> b;

 

   8: b -> d;

 

 

   9: c -> d;

 

  10:}

使用dot布局方式,繪制出來的效果如下:

圖 1

默認的頂點中的文字為定義頂點變量的名稱,形狀為橢圓。邊的默認樣式為黑色實線箭頭,我們可以在腳本中做一下修改,將頂點改為方形,邊改為虛線。

定義頂點和邊的樣式

在digraph的花括號內,添加頂點和邊的新定義:

   1: node [shape="record"];

 

   2: edge [style="dashed"];

 

則繪制的效果如下:

進一步修改頂點和邊樣式

進一步,我們將頂點a的顏色改為淡綠色,並將c到d的邊改為紅色,腳本如下:

   1: digraph abc{

 

   2: node [shape="record"];

 

   3: edge [style="dashed"];

 

   4:  

 

   5: a [style="filled", color="black", fillcolor="chartreuse"];

 

   6: b;

 

   7: c;

 

   8: d;

 

   9:  

 

  10: a -> b;

 

  11: b -> d;

 

  12: c -> d [color="red"];

 

  13: }

 

繪制的結果如下:

應當注意到,頂點和邊都接受屬性的定義,形式為在頂點和邊的定義之后加上一個由方括號括起來的key-value列表,每個key-value對由逗號隔開。如果圖中頂點和邊采用統一的風格,則可以在圖定義的首部定義node, edge的屬性。比如上圖中,定義所有的頂點為方框,所有的邊為虛線,在具體的頂點和邊之后定義的屬性將覆蓋此全局屬性。如特定與a的綠色,c到d的邊的紅色。

子圖的繪制

graphviz支持子圖,即圖中的部分節點和邊相對對立(軟件的模塊划分經常如此)。比如,我們可以將頂點c和d歸為一個子圖:

   1: digraph abc{

 

   2:  

 

   3: node [shape="record"];

 

   4: edge [style="dashed"];

 

   5:  

 

   6: a [style="filled", color="black", fillcolor="chartreuse"];

 

   7: b;

 

   8:  

 

   9:     subgraph cluster_cd{

 

  10:     label="c and d";

 

  11:     bgcolor="mintcream";

 

  12:     c;

 

  13:     d;

 

  14:     }

 

  15:  

 

  16: a -> b;

 

  17: b -> d;

 

  18: c -> d [color="red"];

 

  19: }

 

將c和d划分到cluster_cd這個子圖中,標簽為”c and d”,並添加背景色,以方便與主圖區分開,繪制結果如下:

應該注意的是,子圖的名稱必須以cluster開頭,否則graphviz無法設別。

數據結構的可視化

實際開發中,經常要用到的是對復雜數據結構的描述,graphviz提供完善的機制來繪制此類圖形。

一個hash表的數據結構

比如一個hash表的內容,可能具有下列結構:

   1: struct st_hash_type {

 

   2:     int (*compare) ();

 

   3:     int (*hash) ();

 

   4: };

 

   5:  

 

   6: struct st_table_entry {

 

   7:     unsigned int hash;

 

   8:     char *key;

 

   9:     char *record;

 

  10:     st_table_entry *next;

 

  11: };

 

  12:  

 

  13: struct st_table {

 

  14:     struct st_hash_type *type;

 

  15:     int num_bins; /* slot count */

 

  16:     int num_entries; /* total number of entries */

 

  17:     struct st_table_entry **bins; /* slot */

 

  18: };

 

繪制hash表的數據結構

從代碼上看,由於結構體存在引用關系,不夠清晰,如果層次較多,則很難以記住各個結構之間的關系,我們可以通過下圖來更清楚的展示:

腳本如下:

   1: digraph st2{

 

   2: fontname = "Verdana";

 

   3: fontsize = 10;

 

   4: rankdir=TB;

 

   5:  

 

   6: node [fontname = "Verdana", fontsize = 10, color="skyblue", shape="record"];

 

   7:  

 

   8: edge [fontname = "Verdana", fontsize = 10, color="crimson", style="solid"];

 

   9:  

 

  10: st_hash_type [label="{<head>st_hash_type|(*compare)|(*hash)}"];

 

  11: st_table_entry [label="{<head>st_table_entry|hash|key|record|<next>next}"];

 

  12: st_table [label="{st_table|<type>type|num_bins|num_entries|<bins>bins}"];

 

  13:  

 

  14: st_table:bins -> st_table_entry:head;

 

  15: st_table:type -> st_hash_type:head;

 

  16: st_table_entry:next -> st_table_entry:head [style="dashed", color="forestgreen"];

 

  17: }

 

應該注意到,在頂點的形狀為”record”的時候,label屬性的語法比較奇怪,但是使用起來非常靈活。比如,用豎線”|”隔開的串會在繪制出來的節點中展現為一條分隔符。用”<>”括起來的串稱為錨點,當一個節點具有多個錨點的時候,這個特性會非常有用,比如節點st_table的type屬性指向st_hash_type,第4個屬性指向st_table_entry等,都是通過錨點來實現的。

我們發現,使用默認的dot布局后,綠色的這條邊覆蓋了數據結構st_table_entry,並不美觀,因此可以使用別的布局方式來重新布局,如使用circo算法:

則可以得到更加合理的布局結果。

hash表的實例

另外,這個hash表的一個實例如下:

腳本如下:

   1: digraph st{

 

   2:  

 

   3: fontname = "Verdana";

 

   4: fontsize = 10;

 

   5: rankdir = LR;

 

   6: rotate = 90;

 

   7:  

 

   8: node [ shape="record", width=.1, height=.1];

 

   9: node [fontname = "Verdana", fontsize = 10, color="skyblue", shape="record"];

 

  10:  

 

  11: edge [fontname = "Verdana", fontsize = 10, color="crimson", style="solid"];

 

  12: node [shape="plaintext"];

 

  13:  

 

  14: st_table [label=<

 

  15:     <table border="0" cellborder="1" cellspacing="0" align="left">

 

  16:     <tr>

 

  17:     <td>st_table</td>

 

  18:     </tr>

 

  19:     <tr>

 

  20:     <td>num_bins=5</td>

 

  21:     </tr>

 

  22:     <tr>

 

  23:     <td>num_entries=3</td>

 

  24:     </tr>

 

  25:     <tr>

 

  26:     <td port="bins">bins</td>

 

  27:     </tr>

 

  28:     </table>

 

  29: >];

 

  30:  

 

  31: node [shape="record"];

 

  32: num_bins [label=" <b1> | <b2> | <b3> | <b4> | <b5> ", height=2];

 

  33: node[ width=2 ];

 

  34:  

 

  35: entry_1 [label="{<e>st_table_entry|<next>next}"];

 

  36: entry_2 [label="{<e>st_table_entry|<next>null}"];

 

  37: entry_3 [label="{<e>st_table_entry|<next>null}"];

 

  38:  

 

  39: st_table:bins -> num_bins:b1;

 

  40: num_bins:b1 -> entry_1:e;

 

  41: entry_1:next -> entry_2:e;

 

  42: num_bins:b3 -> entry_3:e;

 

  43:  

 

  44: }

 

上例中可以看到,節點的label屬性支持類似於HTML語言中的TABLE形式的定義,通過行列的數目來定義節點的形狀,從而使得節點的組成更加靈活。

軟件模塊組成圖

Apache httpd模塊關系

IDPV2后台的模塊組成關系

在實際的開發中,隨着系統功能的完善,軟件整體的結構會越來越復雜,通常開發人員會將軟件划分為可理解的多個子模塊,各個子模塊通過協作,完成各種各樣的需求。

下面有個例子,是在IDPV2設計時的一個草稿:

IDP支持層為一個相對獨立的子系統,其中包括如數據庫管理器,配置信息管理器等模塊,另外為了提供更大的靈活性,將很多其他的模塊抽取出來作為外部模塊,而支持層提供一個模塊管理器,來負責加載/卸載這些外部的模塊集合。

這些模塊間的關系較為復雜,並且有部分模塊關系密切,應歸類為一個子系統中,上圖對應的dot腳本為:

   1: digraph idp_modules{

 

   2:  

 

   3: rankdir = TB;

 

   4: fontname = "Microsoft YaHei";

 

   5: fontsize = 12;

 

   6:  

 

   7: node [ fontname = "Microsoft YaHei", fontsize = 12, shape = "record" ]; 

 

   8: edge [ fontname = "Microsoft YaHei", fontsize = 12 ];

 

   9:  

 

  10:     subgraph cluster_sl{

 

  11:         label="IDP支持層";

 

  12:         bgcolor="mintcream";

 

  13:         node [shape="Mrecord", color="skyblue", style="filled"];

 

  14:         network_mgr [label="網絡管理器"];

 

  15:         log_mgr [label="日志管理器"];

 

  16:         module_mgr [label="模塊管理器"];

 

  17:         conf_mgr [label="配置管理器"];

 

  18:         db_mgr [label="數據庫管理器"];

 

  19:     };

 

  20:  

 

  21:     subgraph cluster_md{

 

  22:         label="可插拔模塊集";

 

  23:         bgcolor="lightcyan";

 

  24:         node [color="chartreuse2", style="filled"];

 

  25:         mod_dev [label="開發支持模塊"];

 

  26:         mod_dm [label="數據建模模塊"];

 

  27:         mod_dp [label="部署發布模塊"];

 

  28:     };

 

  29:  

 

  30: mod_dp -> mod_dev [label="依賴..."];

 

  31: mod_dp -> mod_dm [label="依賴..."];

 

  32: mod_dp -> module_mgr [label="安裝...", color="yellowgreen", arrowhead="none"];

 

  33: mod_dev -> mod_dm [label="依賴..."];

 

  34: mod_dev -> module_mgr [label="安裝...", color="yellowgreen", arrowhead="none"];

 

  35: mod_dm -> module_mgr [label="安裝...", color="yellowgreen", arrowhead="none"];

 

  36:  

 

  37: }

 

  38:  

 

狀態圖

有限自動機示意圖

上圖是一個簡易有限自動機,接受a及a結尾的任意長度的串。其腳本定義如下:

   1: digraph automata_0 {

 

   2:  

 

   3: size = "8.5, 11";

 

   4: fontname = "Microsoft YaHei";

 

   5: fontsize = 10;

 

   6:  

 

   7: node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];

 

   8: edge [fontname = "Microsoft YaHei", fontsize = 10];

 

   9:  

 

  10: 0 [ style = filled, color=lightgrey ];

 

  11: 2 [ shape = doublecircle ];

 

  12:  

 

  13: 0 -> 2 [ label = "a " ];

 

  14: 0 -> 1 [ label = "other " ];

 

  15: 1 -> 2 [ label = "a " ];

 

  16: 1 -> 1 [ label = "other " ];

 

  17: 2 -> 2 [ label = "a " ];

 

  18: 2 -> 1 [ label = "other " ];

 

  19:  

 

  20: "Machine: a" [ shape = plaintext ];

 

  21: }

 

形狀值為plaintext的表示不用繪制邊框,僅展示純文本內容,這個在繪圖中,繪制指示性的文本時很有用,如上圖中的”Machine: a”。

OSGi中模塊的生命周期圖

OSGi中,模塊具有生命周期,從安裝到卸載,可能的狀態具有已安裝,已就緒,正在啟動,已啟動,正在停止,已卸載等。如下圖所示:

對應的腳本如下:

   1: digraph module_lc{

 

   2:  

 

   3: rankdir=TB;

 

   4: fontname = "Microsoft YaHei";

 

   5: fontsize = 12;

 

   6:  

 

   7: node [fontname = "Microsoft YaHei", fontsize = 12, shape = "Mrecord", color="skyblue", style="filled"]; 

 

   8: edge [fontname = "Microsoft YaHei", fontsize = 12, color="darkgreen" ];

 

   9:  

 

  10: installed [label="已安裝狀態"];

 

  11: resolved [label="已就緒狀態"];

 

  12: uninstalled [label="已卸載狀態"];

 

  13: starting [label="正在啟動"];

 

  14: active [label="已激活(運行)狀態"];

 

  15: stopping [label="正在停止"];

 

  16: start [label="", shape="circle", width=0.5, fixedsize=true, style="filled", color="black"];

 

  17:  

 

  18: start -> installed [label="安裝"];

 

  19: installed -> uninstalled [label="卸載"];

 

  20: installed -> resolved [label="准備"];

 

  21: installed -> installed [label="更新"];

 

  22: resolved -> installed [label="更新"];

 

  23: resolved -> uninstalled [label="卸載"];

 

  24: resolved -> starting [label="啟動"];

 

  25: starting -> active [label=""];

 

  26: active -> stopping [label="停止"];

 

  27: stopping -> resolved [label=""];

 

  28:  

 

  29: }

 

其他實例

一棵簡單的抽象語法樹(AST)

表達式 (3+4)*5 在編譯時期,會形成一棵語法樹,一邊在計算時,先計算3+4的值,最后與5相乘。

對應的腳本如下:

   1: digraph ast{

 

   2: fontname = "Microsoft YaHei";

 

   3: fontsize = 10;

 

   4:  

 

   5: node [shape = circle, fontname = "Microsoft YaHei", fontsize = 10];

 

   6: edge [fontname = "Microsoft YaHei", fontsize = 10];

 

   7: node [shape="plaintext"];

 

   8:  

 

   9: mul [label="mul(*)"];

 

  10: add [label="add(+)"];

 

  11:  

 

  12: add -> 3

 

  13: add -> 4;

 

  14: mul -> add;

 

  15: mul -> 5;

 

  16: }

 

  17:  

 

簡單的UML類圖

下面是一簡單的UML類圖,Dog和Cat都是Animal的子類,Dog和Cat同屬一個包,且有可能有聯系(0..n)。

腳本:

   1: digraph G{

 

   2:  

 

   3: fontname = "Courier New"

 

   4: fontsize = 10

 

   5:  

 

   6: node [ fontname = "Courier New", fontsize = 10, shape = "record" ];

 

   7: edge [ fontname = "Courier New", fontsize = 10 ];

 

   8:  

 

   9: Animal [ label = "{Animal |+ name : Stringl+ age : intl|+ die() : voidl}" ];

 

  10:  

 

  11:     subgraph clusterAnimalImpl{

 

  12:         bgcolor="yellow"

 

  13:         Dog [ label = "{Dog||+ bark() : voidl}" ];

 

  14:         Cat [ label = "{Cat||+ meow() : voidl}" ];

 

  15:     };

 

  16:  

 

  17: edge [ arrowhead = "empty" ];

 

  18:  

 

  19: Dog->Animal;

 

  20: Cat->Animal;

 

  21: Dog->Cat [arrowhead="none", label="0..*"];

 

  22: }

 

狀態圖

腳本:

   1: digraph finite_state_machine {

 

   2:  

 

   3: rankdir = LR;

 

   4: size = "8,5"

 

   5:  

 

   6: node [shape = doublecircle]; 

 

   7:  

 

   8: LR_0 LR_3 LR_4 LR_8;

 

   9:  

 

  10: node [shape = circle];

 

  11:  

 

  12: LR_0 -> LR_2 [ label = "SS(B)" ];

 

  13: LR_0 -> LR_1 [ label = "SS(S)" ];

 

  14: LR_1 -> LR_3 [ label = "S($end)" ];

 

  15: LR_2 -> LR_6 [ label = "SS(b)" ];

 

  16: LR_2 -> LR_5 [ label = "SS(a)" ];

 

  17: LR_2 -> LR_4 [ label = "S(A)" ];

 

  18: LR_5 -> LR_7 [ label = "S(b)" ];

 

  19: LR_5 -> LR_5 [ label = "S(a)" ];

 

  20: LR_6 -> LR_6 [ label = "S(b)" ];

 

  21: LR_6 -> LR_5 [ label = "S(a)" ];

 

  22: LR_7 -> LR_8 [ label = "S(b)" ];

 

  23: LR_7 -> LR_5 [ label = "S(a)" ];

 

  24: LR_8 -> LR_6 [ label = "S(b)" ];

 

  25: LR_8 -> LR_5 [ label = "S(a)" ];

 

  26:  

 

  27: }

 

附錄

事實上,從dot的語法及上述的示例中,很容易看出,dot腳本很容易被其他語言生成。比如,使用一些簡單的數據庫查詢就可以生成數據庫中的ER圖的dot腳本。

如果你追求高效的開發速度,並希望快速的將自己的想法出來,那么graphviz是一個很不錯的選擇。

當然,graphviz也有一定的局限,比如繪制時序圖(序列圖)就很難實現。graphviz的節點出現在畫布上的位置事實上是不確定的,依賴於所使用的布局算法,而不是在腳本中出現的位置,這可能使剛開始接觸graphviz的開發人員有點不適應。graphviz的強項在於自動布局,當圖中的頂點和邊的數目變得很多的時候,才能很好的體會這一特性的好處:

比如上圖,或者較上圖更復雜的圖,如果采用手工繪制顯然是不可能的,只能通過graphviz提供的自動布局引擎來完成。如果僅用於展示模塊間的關系,子模塊與子模塊間通信的方式,模塊的邏輯位置等,graphviz完全可以勝任,但是如果圖中對象的物理位置必須是准確的,如節點A必須位於左上角,節點B必須與A相鄰等特性,使用graphviz則很難做到。畢竟,它的強項是自動布局,事實上,所有的節點對與布局引擎而言,權重在初始時都是相同的,只是在渲染之后,節點的大小,形狀等特性才會影響權重。

本文只是初步介紹了graphviz的簡單應用,如圖的定義,頂點/邊的屬性定義,如果運行等,事實上還有很多的屬性,如畫布的大小,字體的選擇,顏色列表等,大家可以通過graphviz的官網來找到更詳細的資料。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM