• Stars
    star
    145
  • Rank 254,144 (Top 6 %)
  • Language
    TeX
  • Created about 5 years ago
  • Updated almost 3 years ago

Reviews

There are no reviews yet. Be the first to send feedback to the community and the maintainers!

Repository Details

使用 Graphviz 绘画 UML 图

% 使用 Graphviz 绘画 UML 图 % Milo Yip % 2019/10/28

简介

本文为开放文档,可在 GitHub 提交 issue/PR。本文的 PDF 版本可在 在此下载

Graphviz 是 AT&T 实验室开发的一个开源软件,它以一种文本语言去描述图(graph),然后自动排布节点和边去生成图片。它已有近 30 年历史。

UML(unified modeling language,统一建模语言)是一种常用的面向对象设计的方法。其中最常用的是类图(class diagram),用于表示类的构成以及类之间的关系。

利用 Graphviz 去生成 UML 类图有几个好处:

  1. 用文本表示图,容易更新,容易做版本管理。
  2. 能自动排布节点位置,在大型复杂的图特别方便。
  3. 统一文档风格。

实际上,文档生成工具Doxygen 也是采用 Graphviz 生成类图的。不过,我们在软件设计中,经常以类图表示系统中某个部分,并且按需展示某些重点,而不是简单地全部列出,所以还是需要手工去描述我们想要画什么,表示我们的软件设计。

使用 Graphviz dot

首先,下载 Graphviz 安装包。macOS 用户可以 brew install graphviz

建立一个测试源文件 hello.dotDOT语言为 Graphviz 的图形描述语言):

digraph {
    Hello -> World
}

在命令行执行:

dot -Tpng hello.dot -o hello.png

就能生成:

作为程序员,我们可以用常用的 GNU make 去做这个生成,以下的 makefile 也展示生成 PDF 矢量格式:

DOTFILES = $(basename $(wildcard *.dot))

all: \
    $(addsuffix .png, $(DOTFILES)) \
    $(addsuffix .pdf, $(DOTFILES))

%.png: %.dot
    dot $< -Tpng -o $@

%.pdf: %.dot
    dot $< -Tpdf -o $@

类图

UML 类图(class diagram)是最常见的图,用于表示系统的静态结构。UML 中类是以矩形表示。我们可以在 dot 文件中预设节点的形状,并且设置一些如字体等属性:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Foo
    Bar
}

稍后我们再谈如何加入类的成员。

继承

继承(inheritance)是类之间很重要的关系,在 UML 中又称其为泛化(generalization)关系,以空心箭头表示派生类指向基类。在 DOT 语言中,可以设置边的箭头形状,不过要注意,通常我们会把基类放在上面,因此我通常会这样设置:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Animal, Mammal, Reptile, Dog, Cat, Snake

    /* inheritance */
    {
        edge [arrowtail=onormal, dir=back]

        Animal -> { Mammal, Reptile }
        Mammal -> { Dog, Cat}
        Reptile -> Snake
    }
}

关联

UML 中的关联(association)描述两个类的关系,以类之间的实线表示。例如人和杂志的关系是订阅:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Person, Magazine

    /* Association */
    {
        edge [dir=none]
        Person -> Magazine [label=" subscribe"]
    }
}

我们经常会表示关联之间的多重性(multiplicity),例如 Person 类的实例最多可订阅 5 本杂志,而每本杂志可被任意数目的人订阅:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Person, Magazine

    /* Association with multiplicity */
    {
        edge [dir=none]
        Person -> Magazine [label=" subscribe", headlabel="0..10", taillabel="* "]
    }
}

注意,上面的例子在 labelheadlabeltaillabel 加入空格避免它们太贴近连线(这不完美)。

关联可以是单向或双向的,以线形箭头表示,无箭头也表示双向关联。以下展示单向关联,面试官知道他对应的候选人,但候选人不知道面试官:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Interviewer, Candidate

    /* Unidirection association */
    {
        Interviewer -> Candidate [arrowhead=vee]
    }
}

聚合

聚合(aggregation)是一种特殊的关系,是一种弱的包含关系,包含方以空心菱形表示。例如,一个部门含有一些员工:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Department, Employeee

    /* Aggregation */
    {
        edge [dir=back, arrowtail=odiamond, headlabel="* "]
        Department -> Employeee
    }
}

组成

组成(composition)是更强的包含关系,说明一个类的实例是另一个类的组成部分,它们有一致的生命周期,组成方以实心菱形表示。例如,一家公司由多个部门组成,若果公司结业,部门也不存在了:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Company, Department, Employeee

    /* Composition */
    {
        edge [dir=back, arrowtail=diamond, headlabel="* "]
        Company -> Department        
    }

    /* Aggregation */
    {
        edge [dir=back, arrowtail=odiamond, headlabel="* "]
        Department -> Employeee
    }
}

依赖

依赖(depedency)关系说明一个类会使用到另一个类,例如表示以一个类作为成员方法的参数或返回值。UML 中采用线形箭头和虚线表示。以下的例子表示工厂创建产品,常见于各种工厂模式,工厂不拥有产品。

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Factory, Product

    /* Dependency */
    {
        edge [arrowhead=vee, style=dashed]
        Factory -> Product [label=" <<create>>"]
    }
}

类成员

类除了名字,也可以展示其成员。成员包括属性(attribute)和方法(method)。

每个成员的可见性(visibility)以一个前置符号表示:

  1. + 公有(public)
  2. - 私有(private)
  3. # 保护(protected)
  4. ~ 包(package)

如果成员为静态(static)的,则加下划线。

属性的格式为:

<visibility> <attribute name> : <type>

方法的格式为:

<visibility> <method name> (<param1 name> : <param1 type>, ...) : <return type>

Graphviz 可使用 record shape 或 HTML table 来分隔类名字、属性和方法,例如以下的 C++ 类:

class Account {
public:
    void Deposite(int amount);
    void Withdraw(int amount);
    int GetAmount();
protected:
    int balance;
private:
    string owner;
};

用 record shape 的话可写作:

digraph {
    node [shape=record, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Account [label="{
Account
|
# balance : int\l
- owner : string\l
|
+ Deposite(amount : int)\l
+ Withdraw(amount : int)\l
+ GetBalance() : int\l
}"]

}

当中,\l 是代表该行向左对齐并换行。

如需更多控制,则可使用 HTML table,但就会更冗长:

digraph {
    node [shape=plaintext, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Account [label=<
<table border="0" cellborder="1" cellspacing="0">
<tr><td align="left" balign="left">
# balance : int<br/>
- owner : string<br/>
</td></tr>
<tr><td>Account</td></tr>
<tr><td align="left" balign="left">
+ Deposite(amount : int)<br/>
+ Withdraw(amount : int)<br/>
+ GetBalance() : int<br/>
</td></tr>
</table>
>]

}

使用 HTML table 可加入 <U></U>(下划线)、<I></I> (斜体)等字体控制,但只在一些渲染器中有效。如需表示静态或抽像,可利用 stereotype <<abstract>><<static>> 等说明。

再重申一次,类图不必要展示所有细节,可按想表达的意思仅加入部分成员,每个方法也可忽略一些参数细节。

在比较大的系统里,类通常会用包(package)的方式来组织。Graphviz 不能简单还完 UML 包的图形,但可以使用 subgraph cluster 功能去近似地表示类属于那个包。

例如:

digraph {
    graph [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5,
        labeljust=left] 
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    subgraph clusterView {
        label="View"
        AccountView, CustomerView
    }
    
    subgraph clusterModel {
        label="Model"
        Account, Customer
    }

    /* Unidirecitonal association */
    {
        edge [arrowhead=vee]
        AccountView -> Account
        CustomerView -> Customer
    }
}

注意,subgraph 的名字必须以 cluster 为前缀。

排布技巧

自动排布故然很方便,但有时候我们想做出一些修改。例如,dot 描述的是有向图,从来源节点指向目标节点时,目标节点就会成为下一级,预设设置下,节点会垂直排列,如以下例子:

digraph {
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    Canvas, Shape, Rectangle, Circle

    /* Inheritance */
    {
        edge [arrowtail=onormal, dir=back]
        Shape -> { Rectangle, Circle }
    }

    /* Composition */
    {
        edge [dir=back, arrowtail=diamond, headlabel="* "]        
        Canvas -> Shape
    }
}

但有时候我们想作一些改动,例如继承沿用这种方式,但关联时则以水平。我们可以使用 rank=same 去设置一组节点为同一级,节点之间的距离可整体设置 nodesep 属性:

digraph {
    graph [nodesep=1]
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    {
        rank=same
        Canvas, Shape
    }

    Rectangle, Circle

    /* inheritance */
    {
        edge [arrowtail=onormal, dir=back]
        Shape -> { Rectangle, Circle }
    }

    /* composition */
    {
        edge [dir=back, arrowtail=diamond, headlabel="* "]
        Canvas -> Shape
    }
}

颜色

UML 图也不一定是黑白的。做软件设计时可以加入颜色去加入一些意思,例如不同包的类可设置为不同颜色。挑选颜色是一个头痛的问题,可以采用 Graphviz 的配色方案(color scheme)功能。例如用 colorscheme=spectral7 设置 7 个光谱色配色方案,然后我们可以用 fillcolor=17 去填充节点形状:

digraph {
    graph [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5, labeljust=left] 
    node [shape=box, fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5,
        style=filled, colorscheme=spectral7]
    edge [fontname="Inconsolata, Consolas", fontsize=10, penwidth=0.5]

    subgraph clusterView {
        label="View"
        node [fillcolor=4]
        AccountView, CustomerView
    }

    subgraph clusterModel {
        label="Model"
        node [fillcolor=6]
        Account, Customer
    }

    /* Unidirecitonal association */
    {
        edge [arrowhead=vee]
        AccountView -> Account
        CustomerView -> Customer
    }
}

状态图

T.B.W.

参考

  1. Koutsofios, Eleftherios, and Stephen C. North. "Drawing graphs with dot." (1996).
  2. Node, Edge and Graph Attributes
  3. Node Shapes
  4. Arrow Shapes