使⽤Roslyn的C#语⾔服务实现UML类图的⾃动⽣成
最近在项⽬中实现了⼀套基于Windows Forms的开发框架,个⼈对于本⾝的设计还是⽐较满意的,因此,打算将这部分设计整理成⽂档,通过⼀些UML图形⽐如类图(Class Diagram)来描述整个框架的设计。然⽽,并没有到⼀款合适的UML设计⼯具,商⽤版的功能强⼤,但即便是个⼈许可,⼀个License也不下千元;免费社区版的UML⼯具中,draw.io可以推荐⼀下,画出的图表看上去都⾮常专业,然⽽对于UML图的⽀持不算特别好,画起来也不太⽅便;有⼀款⽐较好的:Astah Community,虽然使⽤⽐较⽅便,但是⾯对着复杂的类之间的关系,要⼀个个⼿⼯去画,显得⾮常⿇烦⽽且容易出错。总⽽⾔之,并没有到⼀个合适的⽅法,能够快速准确地产⽣专业的UML类图,归根结底,还是“穷”+“懒”。
PlantUML
在⽹上搜索调研各个UML制图⼯具的时候,发现了PlantUML,个⼈觉得它的设计理念是⾮常好的:通过简单的⽂本来描述UML图,然后通过专业的渲染引擎将⽂本内容转化成图形。PlantUML的官⽅⽹站是:,它是⼀个开源项⽬,⼯具集本⾝针对不同的许可协议有着不同的编译,因此,你可以根据⾃⼰的需要选择使⽤相对应的版本。PlantUML本⾝不仅可以⽀持UML类图的定义,⽽且可以⽀持包括时序图、⽤例图、活动图等9中UML图形,还可以⽀持包括架构图、⽢特图等6种⾮UML图形。详细内容可以直接参考官⽹,都是中⽂版的,简单容易。不过,今天我们只演⽰UML类图的⾃动化产⽣。
access是基于什么的工具
举个例⼦,下⾯的PlantUML⽂本:
@startuml test
Dummy2 <|-- Dummy1
Dummy1 *... Dummy3
Dummy1 --- Dummy4
IDummy <|.. Dummy4
interface IDummy {
+ DoSomething(parameter: Object): boolean
}
class Dummy1 {
+ myMethods()
}
class Dummy2 {
+ hiddenMethod()
}
class Dummy3 {
String name
}
class Dummy4 {
- field1: string
- field2: int
# field3: DateTime
+ DoSomething(parameter: Object): boolean
}
@enduml
会⽣成下图所⽰的UML类图:
有关PlantUML的语⾔语法定义,这⾥就不多说明了,官⽅⽹站上有详细的⽂档,⽽且还有PDF格式的使⽤⼿册可供免费下载。不过,从上⾯的例⼦,我们⼤概可以得知:
1. PlantUML需要由@startuml和@enduml两条语句来标注起始,@startuml后可以跟上类图的名称
2. 可以通过不同的符号来标注类、接⼝之间的关系,事实上,PlantUML的语法定义还是⾮常随意的,这些定义可以放在⽂件的任何位
置,不过有可能会影响所产⽣的UML图的布局
3. 每个接⼝,每个类中都可以定义字段和⽅法,并通过不同的符号来表⽰这些成员的可访问级别
当然,我们的⽬的不是⼿写这样的PlantUML⽂本来绘制UML类图,我们希望能够有个程序,它可以根
据给定的源程序代码,⾃动产⽣UML 类图。
Roslyn的C#语⾔服务
根据GitHub中的说明,Roslyn提供了开源的C#和Visual Basic编译器,并且提供了丰富的代码分析API,使得开发⼈员能够⾮常⽅便地开发.NET语⾔的代码分析⼯具。总体来说,Roslyn主要提供了以下NuGet包:
Microsoft.Net.Compilers:它包含了C#和Visual Basic的编译器
Microsoft.CodeAnalysis:它包含了代码分析API以及语⾔服务(Language Services)
或许⼤家对于Roslyn并不陌⽣,然⽽对于如何运⽤这套强⼤的语⾔平台却倍感疑惑。嗯,Roslyn是.NET语⾔的编译器基础,基于Apache 2.0开源,很强⼤,可是我们平时没有需要使⽤这些⼯具和库的需求啊,我们知道Visual Studio中的代码分析⼯具会基于Roslyn,可是除了代码分析,还可以在什么场景中使⽤呢?今天,我们就使⽤Roslyn的语⾔服务来为我们画UML类图。
PlantUML⽂本的⾃动⽣成
使⽤Roslyn的C#语⾔服务来⽣成UML类图,⼤致流程如下:
1. 搜索指定⽬录的所有C#代码⽂件,这些⽂件通常都以.cs作为后缀名
2. 使⽤Roslyn的C#语⾔服务,针对每个C#代码⽂件,逐⼀分析出其中的类型(类、接⼝等)以及每个类型下的成员(字段、属性、⽅法
等)
3. 将分析结果转化为PlantUML⽂本
4. 通过某种⼯具,将PlantUML呈现为UML类图
搜索指定⽬录下的C#代码⽂件很简单,使⽤Directory.EnumerateFiles就可以了,接下来就是要把代码⽂件中的类型和成员都解析出来,并保存到⼀个数据模型中,然后,才可以根据这个数据模型来输出PlantUML的⽂本。这个过程其实也就是计算机语⾔相互转换的过程,⽐如你希望将C#语⾔代码转换成Java代码,那么,两者必然要基于同⼀个语⾔数据模型,⽐如,通⽤的表达式树可以描述所有编程语⾔中的表达式。在这⾥的例⼦中,我们可以将PlantUML看成是另⼀种编程语⾔(它其实本⾝也就是⼀种),于是,我们⽬前的⾸要问题就是定义这个数据模型。
根据需要,我定义了如下的数据模型,⽤来保存C#代码解析后的信息:
这个数据模型主体部分的设计如下:
⼀个ClassDiagram类包含了⼀组BasicTypeRelationship,⽤来表达基本类型(类和接⼝)之间的关系;此外,还包含了⼀组类的声明以及⼀组接⼝的声明
类和接⼝都继承于BasicType基类,同时包含了⼀组字段(Field)和⼀组⽅法(Method)的定义
Field和Method都继承于ClassMember
Method包含⼀组参数(Parameter)的定义
⽬前这个模型的定义还是⾮常简单的,并没有包含类似泛型、属性等的设计,不过有了这个基础的模型,今后扩展起来就很简单了。接下来就是通过Roslyn,将C#源代码转换成这个模型。
⾸先,我们需要添加Microsoft.CodeAnalysis.CSharp这个NuGet包,然后,依照访问者设计模式,实现⼀个CSharpSyntaxWalker,它会在遍历C#语法树的时候,根据访问的当前节点的类型来调⽤相应的⽅法,于是,我们的访问器则可以重载这些⽅法,然后构建上述数据模型。代码如下:
public class PlantUmlClassDiagramGenerator : CSharpSyntaxWalker
{
public PlantUmlClassDiagramGenerator(string diagramName)
{
ClassDiagram = new ClassDiagram(diagramName);
}
public override void VisitClassDeclaration(ClassDeclarationSyntax node)
{
if (node.BaseList != null)
{
foreach (var baseType in node.BaseList.Types)
{
ClassDiagram.Relationships.Add(new BasicTypeRelationship
{
Left = baseType.Type.ToString(),
Right = node.Identifier.ToString(),
Type = RelationshipType.Generalization
});
}
}
var clazz = new Class { Name = node.Identifier.ToString() };
if (node.Modifiers.Any(SyntaxKind.AbstractKeyword))
{
clazz.IsAbstract = true;
}
if (node.Modifiers.Any(SyntaxKind.StaticKeyword))
{
clazz.IsStatic = true;
}
var propertyDeclarations = node.Members.Where(m => m is PropertyDeclarationSyntax)
.Select(m => m as PropertyDeclarationSyntax);
foreach (var propertyDeclaration in propertyDeclarations)
{
var field = new Field { Name = propertyDeclaration.Identifier.ToString(), Type = propertyDeclaration.Type.ToString() };
field.AccessModifier = GetAccessModifier(propertyDeclaration.Modifiers);
clazz.Fields.Add(field);
}
var fieldDeclarations = node.Members.Where(m => m is FieldDeclarationSyntax)
.Select(m => m as FieldDeclarationSyntax);
foreach (var fieldDeclaration in fieldDeclarations)
{
clazz.Fields.AddRange(CreateFields(fieldDeclaration));
}
var methodDeclarations = node.Members.Where(m => m is MethodDeclarationSyntax)
.Select(m => m as MethodDeclarationSyntax);
foreach(var methodDeclaration in methodDeclarations)
{
clazz.Methods.Add(CreateMethod(methodDeclaration));
}
ClassDiagram.Classes.Add(clazz);
}
private IEnumerable<Field> CreateFields(FieldDeclarationSyntax fieldDeclaration)
{
var type = fieldDeclaration.Declaration.Type.ToString();
foreach (var variableDeclaration in fieldDeclaration.Declaration.Variables)
{
var field = new Field { Name = variableDeclaration.Identifier.ToString(), Type = type };
field.IsStatic = fieldDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword);
field.AccessModifier = GetAccessModifier(fieldDeclaration.Modifiers);
yield return field;
}
}
private Method CreateMethod(MethodDeclarationSyntax methodDeclaration)
{
var method = new Method { Name = methodDeclaration.Identifier.ToString() };
method.IsAbstract = methodDeclaration.Modifiers.Any(SyntaxKind.AbstractKeyword);
method.IsStatic = methodDeclaration.Modifiers.Any(SyntaxKind.StaticKeyword);
method.AccessModifier = GetAccessModifier(methodDeclaration.Modifiers);
foreach (var parameterDeclaration in methodDeclaration.ParameterList.Parameters)
{
method.Parameters.Add(new Parameter { Name = parameterDeclaration.Identifier.ToString(), Type = parameterDeclaration.Type.ToString() });        }
method.Type = methodDeclaration.ReturnType.ToString();
return method;
}
private AccessModifier GetAccessModifier(SyntaxTokenList modifiers)
{
if (modifiers.Any(SyntaxKind.PublicKeyword))
{
return AccessModifier.Public;
}
else if (modifiers.Any(SyntaxKind.ProtectedKeyword))
{
return AccessModifier.Protected;
}
else if (modifiers.Any(SyntaxKind.InternalKeyword))
{
return AccessModifier.Internal;
}
else
{
return AccessModifier.Private;
}
}
public override void VisitInterfaceDeclaration(InterfaceDeclarationSyntax node)
{
ClassDiagram.Interfaces.Add(new Interface { Name = node.Identifier.ToString() });
}
public ClassDiagram ClassDiagram { get; }
}
以下是调⽤代码:
static void Main(string[] args)
{
const string SourcePath = @"C:\Users\daxne\source\repos\ConsoleApp10\ConsoleApp10\Sample";
var csharpFiles = Directory.EnumerateFiles(SourcePath, "*.cs", SearchOption.AllDirectories);
var walker = new PlantUmlClassDiagramGenerator("sample");
foreach(var csharpFile in csharpFiles)
{
if (csharpFile.EndsWith(".designer.cs", StringComparison.InvariantCultureIgnoreCase))
{
continue;
}
var sourceCode = File.ReadAllText(csharpFile);
var syntaxTree = CSharpSyntaxTree.ParseText(sourceCode);
walker.Visit(syntaxTree.GetRoot());
}
Console.WriteLine($"Classes: {walker.ClassDiagram.Classes.Count}");
Console.WriteLine($"Interfaces: {walker.ClassDiagram.Interfaces.Count}");
File.WriteAllText(@"C:\Users\daxne\Desktop\text.puml", walker.ClassDiagram.ToString());
}
最后输出的PlantUML⽂本如下:
@startuml sample
Person <|-- Student
Person <|-- Employee
Employee <|-- RegularEmployee
Employee <|-- Contractor
abstract class Person {
+ FirstName : string
+ LastName : string
+ MiddleInitial : string
}
class Student {
+ Identification : string
+Register(registerDate: DateTime) : void
}
abstract class Employee {
+ EmployeeId : string
+ DocumentSigned : DateTime
}
class RegularEmployee {
+Resign() : void
}
class Contractor {
+ ContractEndDate : DateTime
+TerminateContract() : void
}
@enduml
PlantUML⽂本的图形化渲染
现在已经⽣成了PlantUML的⽂本,接下来就要将它渲染成UML类图。我推荐使⽤Visual Studio Code的,不仅能够提供代码⾼亮功能,⽽且还可以实时预览渲染结果,⾮常⽅便。
在安装和使⽤PlantUML插件之前,请确保已经安装了以下组件:
Java 8
Graphviz
该插件还⽀持将渲染的UML图导出成各种格式的图⽚,在此就不多说明了。
总结
本⽂对PlantUML进⾏了简单的介绍,并介绍了如何通过.NET的Roslyn语⾔服务和代码分析API,实现类图的动态⽣成。PlantUML将UML图⽂本化,不仅有利于UML图的版本追踪和控制,⽽且在很多第三⽅的⼯具(⽐如Confluence)中都能够很⽅便地集成。⽽⾃动化⽣成UML 图形的意义在于,从代码产⽣设计图变得更加⽅便,⽽且能够始终与代码设计保持⼀致。⽽另⼀⽅⾯,.NET Roslyn编译器服务本⾝也给开发者带来了更多C#、Visual Basic代码处理的机遇,我们可以使⽤这样的服务来帮助我们做更多的事情,简化我们的⽇常⼯作。

版权声明:本站内容均来自互联网,仅供演示用,请勿用于商业和其他非法用途。如果侵犯了您的权益请与我们联系QQ:729038198,我们将在24小时内删除。