目录
介绍
为什么要使用它?
分布式系统的端到端跟踪
调试器
背景
使用代码
示例
如何使用控件
怎么运行的
Tick()方法
元素渲染
API引用
顺序
参加者
消息
激活
方框
SequenceDiagramControl + AOP库=调试器
兴趣点
顺序图特征未实现
PlantUML
NConcern和CNeptune
本文介绍了一个Windows .NET控件,用于以UML顺序图的形式动态可视化对象及其交互。该控件具有类似于调试器的中断功能。另外,它提供了一个实际使用的示例,其中实时拦截和可视化了应用程序的执行。
- 下载源1.2 MB
顺序图是UML图的基本类型之一。他们的重点是对系统进行动态建模。它们允许描述系统与环境参与者之间或系统参与者之间随着时间的推移的交互作用。它们清晰的图形布局有助于快速直观地了解系统的行为。
本文介绍了一个Windows .NET控件,用于动态对象可视化及其相互作用(作为顺序图)。该控件具有类似于调试器的中断功能。另外,它提供了一个实际使用的示例,其中实时拦截和可视化了应用程序的执行。
这是使用控件的两种情况:
分布式系统的端到端跟踪您有一个分布在不同计算机上的,由独立组件(例如微服务)组成的分布式应用程序,很难跟踪整个执行过程。您将每个微服务的日志事件转发到收集器,以便可以在单个顺序图中可视化整个流程,以便于分析和调试:
该控件最初是为这种情况创建的,并构成其一部分。将其重新打包以用于通用用途。
调试器您已经开发了一种软件工具,可以使用AOP(面向切面的编程)技术来拦截任何.NET应用程序的方法调用,现在您希望将捕获的调用跟踪可视化为流程图:
在本文结尾处,我提供了这种用法的概念证明。
背景顺序图显示了对象的行为。因此,时间的概念以及对象之间的依赖关系出现在顺序图中。反过来,这使顺序图可以显示系统中的“情况”。
顺序图的大多数现有用法仅限于静态模型:顺序图基于模型或静态代码的定义。它们充当流程的蓝图,并用于在系统内进行交互。
但是,顺序图也可以用于可视化系统的实时活动。此活动可能是计算机程序中的实时方法调用,分布式应用程序组件内的通信等等。动态分析与静态分析的不同之处在于,它需要一个活动的系统进行建模。即,在运行的系统上执行动态分析,而在系统工件(例如源代码)上执行静态分析。
使用代码 示例SequenceDiagram解决方案包含两个项目:SequenceDiagramLib具有实际控件,而SequenceDiagramTestApp项目具有演示控件功能的各种示例。
除了一个示例外,所有示例都完全在主线程中运行。底部包含“基本示例(线程)”示例,用于在不同线程中修改序列的情况。
下面是一个简单的顺序图定义,涉及两个参与者以及他们之间的一些消息传递:
Sequence sequence = this.sequenceControl.Sequence;
Participant alice = sequence.Participants.Create("Alice");
Participant bob = sequence.Participants.Create("Bob");
sequence.Messages.Add("AuthenticationRequest", alice, bob);
sequence.Tick();
sequence.Messages.Add("AuthenticationResponse", bob, alice, dashStyle: DashStyle.Dash);
sequence.Tick();
sequence.Messages.Add("Another authentication request", alice, bob);
sequence.Tick();
sequence.Messages.Add("Another authentication Response", bob, alice);
sequence.Tick();
该序列由SequenceDiagramControl呈现:
控件用水平线表示时间的流逝。每条水平线表示一个时间刻度,而当前时间标记则表示为红线。
第二个更复杂的顺序图定义了我们的SequenceDiagramControl:
Sequence sequence = this.sequenceControl.Sequence;
Participant user = sequence.Participants.Create("User");
Participant a = sequence.Participants.Create("A");
Participant b = sequence.Participants.Create("B");
Participant c = sequence.Participants.Create("C");
sequence.Messages.Add("DoWork", user, a);
a.Activate();
sequence.Tick();
sequence.Messages.Add(">", a, b);
b.Activate();
sequence.Tick();
sequence.Messages.Add("DoWork", b, c);
c.Activate();
sequence.Tick();
sequence.Messages.Add("WorkDone", c, b, dashStyle: DashStyle.Dot);
c.Deactivate();
c.Destroy();
sequence.Tick();
sequence.Messages.Add("RequestCreated", b, a, dashStyle: DashStyle.Dot);
b.Deactivate();
sequence.Tick();
sequence.Messages.Add("Done", a, user);
a.Deactivate();
sequence.Tick();
...以及SequenceDiagramControl的演绎:
1、创建一个名为Form1的Windows窗体。
2、在表单中添加一个SequenceDiagramControl名称为sequenceDiagram。
3、将两个按钮添加到您的窗体。他们的名字为runButton和continueButton。
4、设置两个按钮的处理程序。
runButton的事件处理程序会创建一个包含两个参与者和两个时间步长的序列:
private void runButton_Click(object sender, EventArgs e)
{
Sequence sequence = this.sequenceDiagram.Sequence;
Participant a = sequence.Participants.CreateOrGet("A");
Participant b = sequence.Participants.CreateOrGet("B");
sequence.Messages.Add("Create request", a, b);
sequence.Tick();
sequence.Messages.Add("Return", b, a);
sequence.Tick();
}
当序列处于等待状态时,continueButton的事件处理程序可用。它的功能是恢复应用程序的执行。
private void continueButton_Click(object sender, EventArgs e)
{
Sequence sequence = this.sequenceDiagram.Sequence;
sequence.Continue();
}
5、定义序列的OnEnter()和OnExit()事件处理程序。
该序列在中断开始时调用OnEnterBreak()事件处理程序,并在执行恢复时调用OnExitBreak()事件处理程序。
private void Sequence_OnEnterBreak()
{
this.runButton.Enabled = false;
this.continueButton.Enabled = true;
}
private void Sequence_OnExitBreak()
{
this.runButton.Enabled = true;
this.continueButton.Enabled = false;
}
6、窗体的构造函数初始化两个按钮的启用/禁用状态,并设置序列事件处理程序。
public Form1()
{
InitializeComponent();
this.runButton.Enabled = true;
this.continueButton.Enabled = false;
Sequence sequence = this.sequenceDiagram.Sequence;
sequence.OnEnterBreak += Sequence_OnEnterBreak;
sequence.OnExitBreak += Sequence_OnExitBreak;
}
怎么运行的
该控件包含对Sequence类的引用,该类封装了序列的全部信息。诸如参与者,激活,消息,时间步之类的元素存储在此类的实例中。
Tick()方法在同一时间范围内发生的事件将以任意顺序添加到序列中。sequence.Tick()调用特别重要:正是在这些调用中:
- 发生中断事件并且SequenceDiagramControl抓住程序执行,直到用户采取行动以恢复。
- 序列的逻辑时钟增加1。
断点功能的实现细节如下:
- 每当调用sequence.Tick()方法时,基础Sequence对象都会调用其注册的OnEnterBreak()处理程序。这将启用其父窗体以执行诸如启用其continueButton的任务。
- 使用以下方法,序列保持等待状态:
private void ResponsiveWait()
{
this.wait = true;
for (;;)
{
if (!this.wait)
break;
if (this.exit)
break;
System.Windows.Forms.Application.DoEvents();
System.Threading.Thread.Sleep(200);
}
}
Application.DoEvent()允许在不阻塞自身或其所在应用程序的情况下获取执行流。不鼓励使用Application.DoEvents(),而应使用其他技术,如线程。但是对于该项目的需求(例如对调试后的应用程序的干扰最小),我发现它是最合适的方法。
- 当用户单击时continueButton,将调用sequence.Continue()方法,等待状态结束。
- 序列类调用已注册的OnExitBreak()事件处理程序。这将启用其父窗体执行任务,例如禁用其continueButton,直到下一次中断。
元素的视觉呈现在SequenceDiagramControl类中进行。每个UML元素都有一个方法和一个名为p0的参考点变量。
API引用Sequence类是SequenceDiagramControl的数据模型的基础对象。有关序列的所有信息都存储在此类中。它具有可用参与者、激活、消息和框的集合。
Box是对相关参与者进行分组的可选元素。
Participant拥有一个激活集合,每个激活都有一个称为标签的名称/值对集合。参与者通过Message进行交流。
顺序Sequence类封装序列内的全部数据。它着重于参与者之间消息的时间顺序以及消息的发送顺序。顺序中的重点是第一件发生的事情,然后发生第二件事,依此类推。
API用法
sequence.Clear();
清除当前状态和序列的所有成员。序列返回到其初始状态。
sequence.Tick();
顺序图的重点是可视化系统的变化以及随时间推移的元素通信。x轴显示序列的成员(称为参与者),而y轴表示时间。
在SequenceDiagramControl模型中,时间标记表示为离散值。每当出现时间刻度时,序列都会前进到下一个时间步长值。时间刻度是用户可以做出响应的中断点。
参与者是参与序列的代表或对象。它们通常放在图的顶部。参与者通常以矩形表示,其名称放在框中。根据UML规范,此名称可以带下划线,表示参与者在顺序图中表示类的特定实例。诸如UML构造型之类的其他信息也可以包含在参与者矩形内。
参与者的生命线从参与者的底部开始显示为垂直虚线。它们代表了参与者随着时间的生命周期和互动。
Participant foo1 = sequence.Participants.Create("Foo1", type: EParticipantType.Actor);
Participant foo2 = sequence.Participants.Create("Foo2", type: EParticipantType.Boundary);
Participant foo3 = sequence.Participants.Create("Foo3", type: EParticipantType.Control);
Participant foo4 = sequence.Participants.Create("Foo4", type: EParticipantType.Entity);
Participant foo5 = sequence.Participants.Create("Foo5", type: EParticipantType.Database);
Participant foo6 = sequence.Participants.Create("Foo6", type: EParticipantType.Collections);
sequence.Messages.Add("To boundary", foo1, foo2);
sequence.Tick();
sequence.Messages.Add("To control", foo1, foo3);
sequence.Tick();
sequence.Messages.Add("To entity", foo1, foo4);
sequence.Tick();
sequence.Messages.Add("To database", foo1, foo5);
sequence.Tick();
sequence.Messages.Add("To collections", foo1, foo6);
sequence.Tick();
API用法
Participant participant = sequence.Participants.Create(string name, bool underlined = false,
Color? color = null, Color? textColor = null, EParticipantType? type = null, Box box = null,
bool createNow = false);
它创建一个新的参与者并将其放置在序列中。如果序列中已有指定名称的参与者,则调用失败。
- name:参与者名称。
- underlined:参与者的姓名是否将显示为带下划线的文本。UML中带下划线的文本表示类的特定实例。
- color:参与者的背景色。
- textColor:参与者的文字颜色。
- type:参与者的类型。它影响参与者的显示方式(方框,边界,控件,实体,数据库,集合)。
- box:参与者所属的方框。此值可能是null。
- createNow:通常会省略此值,从而导致参与者位于图的顶部。设置此值将强调正在创建参与者,并且其生存期将从当前时间步开始。
Participant participant = sequence.Participants.CreateOrGet
(string name, bool underlined = false, Color? color = null, Color? textColor = null,
EParticipantType? type = null, Box box = null, bool createNow = false);
如果存在具有给定名称的参与者,则将其返回。
如果不存在具有给定名称的参与者,它将创建一个新参与者,将其放置在序列中,然后返回。
- name:参与者名称。
- underlined:参与者的姓名是否将显示为带下划线的文本。UML图中带下划线的文本表示类的特定实例。
- color:参与者的背景色。
- textColor:参与者的文字颜色。
- type:参与者的类型。它影响参与者的显示方式(方框,边界,控件,实体,数据库,集合)。
- box:参与者所属的方框。此值可能是null。
- createNow:忽略此值,导致参与者被放置在图的顶部。设置此值将强调正在创建参与者,并且其生存期将从当前时间步开始。
Participant participant = sequence.Participants[name];
它返回给定名称的参与者。如果不存在具有给定名称的参与者,则调用失败。
- name:参与者名称。
participant.Destroy();
结束参与者的当前激活状态。
消息代表两个参与者之间传输的信息。参与者可能会向自己发送消息。消息可以是同步的也可以是异步的,它们可以反映操作的开始和执行或信号的发送和接收。
Participant bob = sequence.Participants.Create("Bob");
Participant alice = sequence.Participants.Create("Alice");
sequence.Messages.Add("hello", bob, alice, color: Color.Red);
sequence.Tick();
sequence.Messages.Add("ok", alice, bob, color: Color.Blue);
sequence.Tick();
Participant alice = sequence.Participants.Create("Alice");
sequence.Messages.Add("signal to self", alice);
sequence.Tick();
API用法
Message message = sequence.Messages.Add(string name, Participant from,
Participant to, Color? color = null, DashStyle? dashStyle = null);
在源参与者和目标参与者之间创建新消息。
- name:消息名称。
- from:消息的源参与者。
- to:消息的目标参与者。
- color:呈现消息的颜色。
- arrowHead:消息的箭头。不同的箭头代表不同类型的消息。
- dashStyle:消息行的短划线样式。不同的破折号样式表示不同类型的消息。
Message message = sequence.Messages.Add(string name, Participant self,
Color? color = null, DashStyle? dashStyle = null);
创建一个新的自消息。参与者向自己发送一条消息。
- name:消息名称。
- self:消息的所有者。
- color:呈现消息的颜色。
- arrowHead:消息的箭头。不同的箭头代表不同类型的消息。
- dashStyle:消息行的短划线样式。不同的破折号样式表示不同类型的消息。
激活(也称为控制元素/执行焦点)是参与者执行操作的时间段。对象忙于执行过程或等待回复的时间表示为垂直放置在其生命线上的矩形。矩形的顶部和底部分别与开始时间和完成时间对齐。激活可以是递归的。
Participant user = this.sequence.Participants.Create("User");
Participant a = this.sequence.Participants.Create("A");
Participant b = this.sequence.Participants.Create("B");
sequence.Messages.Add("DoWork", user, a);
a.Activate(color: Color.FromArgb(0xff, 0xbb, 0xbb));
sequence.Tick();
sequence.Messages.Add("Internal call", a);
a.Activate(color: Color.DarkSalmon);
sequence.Tick();
sequence.Messages.Add(">", a, b);
b.Activate();
sequence.Tick();
sequence.Messages.Add("RequestCreated", b, a, dashStyle: DashStyle.Dash);
b.Deactivate();
a.Deactivate();
sequence.Tick();
sequence.Messages.Add("Done", a, user);
a.Deactivate();
sequence.Tick();
API用法
participant.Activate(string name = null, Color? color = null);
为参与者初始化激活。激活可以是递归的。
- name:激活名称。
- color:激活的背景色。
participant.Deactive();
停用参与者的当前激活。
方框有助于组织顺序图。相互关联的参与者可以分组在一个框中。
Box box = sequence.Boxes.Create("Internal Service");
box.Color = Color.LightBlue;
Participant bob = sequence.Participants.Create("bob", box: box);
Participant alice = sequence.Participants.Create("alice", box: box);
Participant other = sequence.Participants.Create("other");
sequence.Messages.Add("hello", bob, alice);
sequence.Tick();
sequence.Messages.Add("hello", alice, other);
sequence.Tick();
API用法
Box box = sequence.Boxes.Create(string name, Color? color = null);
它创建一个新框并将其放置在序列中。如果序列中已经有一个具有指定名称的框,则调用将失败。
- name:框名称。
- color:框的背景色。
Box box = sequence.Boxes.CreateOrGet(string name, Color? color = null);
如果存在具有给定名称的参与者,则将其返回。
如果不存在具有给定名称的参与者,它将创建一个新参与者,将其放置在序列中,然后返回。
- name:框名称。
- color:框的背景色。
Box box = sequence.Boxes[name];
它返回具有给定名称的框。如果不存在具有给定名称的框,则调用失败。
- name:参与者名称。
SequenceDiagramDebugger不是SequenceDiagramControl的一部分。而是,它是对SequenceDiagramControl的具体实际使用的演示。除此之外,对于现有的AOP(面向方面编程)示例一直存在一种批评,即几乎所有示例都涉及方法调用的日志记录或访问控制。在这种情况下,AOP用于替代需求:执行可视化。
调试器示例用作概念证明,并具有诸如仅支持使用单个线程的应用程序之类的限制。
AOP启用应用程序的拦截方法。然后,这些被拦截的方法调用将通过SequenceDiagramControl可视化。使用控件的附加内置中断功能,可以调试应用程序的执行流程。
在应用程序和顺序图元素之间的映射如下实现:
- 应用->顺序
- 申请类别->参加者
- 类方法持续时间->激活
- 方法调用->消息
一个重要的目标是启用调试程序,而对主机应用程序的更改最少。这是通过向调试后的应用程序添加一行代码来实现的,以便我们的工具与之集成:
public MainForm()
{
InitializeComponent();
SequenceDiagramDebugger.Init(this, "TestApp.");
}
SequenceDiagramDebugger.Init(this, "TestApp"); 有两个目的:
- 它将已调试应用程序的所有方法编织在“TestApp”命名空间中。
- 它打开我们的DebuggerForm,其中包含一个SequenceDiagramControl。
在执行过程中,AOP库通过advisors通知我们方法入口和出口的每一次相关事件。这些被拦截的方法入口和出口然后在我们的SequenceDiagramControlbased中可视化SequenceDiagramDebugger。
在该示例中,单击一次TestApp的“计算”按钮,然后单击DebuggerForm的“继续”,直到执行结束。
UML顺序图规范的某些部分(例如表示循环,分支和替代执行流程的元素)仅在该图用于记录流程的文档时才有意义。在SequenceDiagramControl中省略了这些功能,因为控件的焦点是动态可视化已发生的事情,而不是可能发生的事情。
PlantUMLPlantUML是一种开放源代码工具,可帮助从符合其图定义语法的文本文件中创建各种类型的UML和非UML图。您可以使用工具的文本格式指定UML图表定义,而与SequenceDiagramControl不同,它会创建图表的静态图像表示形式。它支持序列、用例、类、活动、组件、状态、对象、部署和时间UML图,以及许多非UML图。
长期以来,我一直在使用PlantUML工具来获取技术文档,其易于使用的术语给我留下了深刻的印象。因此,我的设计目标之一就是尽可能地遵循PlantUML的元素命名,并比较相同序列的PlantUML和SequenceDiagramControl的表达。我提供的所有功能示例在PlantUml工具顺序图创建页面中都有一个对应的示例。
在本节中,我提供了PlantUML文本定义等效项以及我在本文开头描述的两个功能示例的输出,因此您可以比较它们的异同:
与第一个功能示例等效的PlantUML文本定义:
@startuml
Alice -> Bob: Authentication Request
Bob --> Alice: Authentication Response
Alice -> Bob: Another authentication Request
Alice A: DoWork
activate A
A -> B: >
activate B
B -> C: DoWork
activate C
C --> B: WorkDone
destroy C
B --> A: RequestCreated
deactivate B
A -> User: Done
deactivate A
@enduml
其PlantUML输出为:
NConcern和CNeptune是用于截取SequenceDiagramDebugger示例中外部应用程序的方法调用的AOP库。这些捕获的方法调用在SequenceDiagramControl 中可视化。这些库功能强大且设计合理:我能够以最小的努力将它们集成在一起。
NConcern提供了AOP的功能和API(例如advisors)。
CNeptune是基于mono.cecil的实用程序,用于重写.NET程序集以使其可注入。