使用Roslyn的C#语言服务实现UML类图的自动生成

摘要:
然而,今天我们将只演示UML类图的自动生成。Roslyn的C#语言服务根据GitHub中Roslyn项目的描述,Roslyn提供了开源的C#和Visual Basic编译器,并提供了丰富的代码分析API,使开发人员能够为开发代码分析工具。NET语言。今天,我们将使用Roslyn的语言服务为我们绘制UML类图。下一步是通过Roslyn将C#源代码转换成这个模型。

最近在项目中实现了一套基于Windows Forms的开发框架,个人对于本身的设计还是比较满意的,因此,打算将这部分设计整理成文档,通过一些UML图形比如类图(Class Diagram)来描述整个框架的设计。然而,并没有找到一款合适的UML设计工具,商用版的功能强大,但即便是个人许可,一个License也不下千元;免费社区版的UML工具中,draw.io可以推荐一下,画出的图表看上去都非常专业,然而对于UML图的支持不算特别好,画起来也不太方便;有一款比较好的:Astah Community,虽然使用比较方便,但是面对着复杂的类之间的关系,要一个个手工去画,显得非常麻烦而且容易出错。总而言之,并没有找到一个合适的方法,能够快速准确地产生专业的UML类图,归根结底,还是“穷”+“懒”。

PlantUML

在网上搜索调研各个UML制图工具的时候,发现了PlantUML,个人觉得它的设计理念是非常好的:通过简单的文本来描述UML图,然后通过专业的渲染引擎将文本内容转化成图形。PlantUML的官方网站是:http://plantuml.com/,它是一个开源项目,工具集本身针对不同的许可协议有着不同的编译,因此,你可以根据自己的需要选择使用相对应的版本。PlantUML本身不仅可以支持UML类图的定义,而且可以支持包括时序图、用例图、活动图等9中UML图形,还可以支持包括架构图、甘特图等6种非UML图形。详细内容可以直接参考官网,都是中文版的,简单容易。不过,今天我们只演示UML类图的自动化产生。

举个例子,下面的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类图:

image

有关PlantUML的语言语法定义,这里就不多说明了,官方网站上有详细的文档,而且还有PDF格式的使用手册可供免费下载。不过,从上面的例子,我们大概可以得知:

  1. PlantUML需要由@startuml和@enduml两条语句来标注起始,@startuml后可以跟上类图的名称
  2. 可以通过不同的符号来标注类、接口之间的关系,事实上,PlantUML的语法定义还是非常随意的,这些定义可以放在文件的任何位置,不过有可能会影响所产生的UML图的布局
  3. 每个接口,每个类中都可以定义字段和方法,并通过不同的符号来表示这些成员的可访问级别

当然,我们的目的不是手写这样的PlantUML文本来绘制UML类图,我们希望能够有个程序,它可以根据给定的源程序代码,自动产生UML类图。

Roslyn的C#语言服务

根据GitHub中Roslyn项目的说明,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看成是另一种编程语言(它其实本身也就是一种领域特定语言(DSL)),于是,我们目前的首要问题就是定义这个数据模型。

根据需要,我定义了如下的数据模型,用来保存C#代码解析后的信息:

test3


这个数据模型主体部分的设计如下:

  • 一个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:Usersdaxnesource
eposConsoleApp10ConsoleApp10Sample";
    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:UsersdaxneDesktop	ext.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插件,不仅能够提供代码高亮功能,而且还可以实时预览渲染结果,非常方便。

image

在安装和使用PlantUML插件之前,请确保已经安装了以下组件:

  • Java 8
  • Graphviz

该插件还支持将渲染的UML图导出成各种格式的图片,在此就不多说明了。

总结

本文对PlantUML进行了简单的介绍,并介绍了如何通过.NET的Roslyn语言服务和代码分析API,实现类图的动态生成。PlantUML将UML图文本化,不仅有利于UML图的版本追踪和控制,而且在很多第三方的工具(比如Confluence)中都能够很方便地集成。而自动化生成UML图形的意义在于,从代码产生设计图变得更加方便,而且能够始终与代码设计保持一致。而另一方面,.NET Roslyn编译器服务本身也给开发者带来了更多C#、Visual Basic代码处理的机遇,我们可以使用这样的服务来帮助我们做更多的事情,简化我们的日常工作。

免责声明:文章转载自《使用Roslyn的C#语言服务实现UML类图的自动生成》仅用于学习参考。如对内容有疑问,请及时联系本站处理。

上篇SVN版本管理与大型代码上线java socket 实现多个一对一聊天下篇

宿迁高防,2C2G15M,22元/月;香港BGP,2C5G5M,25元/月 雨云优惠码:MjYwNzM=

相关文章

UML的九种模型图

本文转自UML 的九种模型图,仅供学习交流! 一、作为一种建模语言,UML的定义包括UML语义和UML表示法两个部分。 UML语义:描述基于UML的精确元模型定义。 UML表示法:定义UML符号的表示法,为开发者或开发工具使用这些图形符号和文本语法为系统建模提供了标准。这些图形符号和文字所表达的是应用级的模型,在语义上它是UML元模型的实例。 二、标准建模...

[转载] 常用CASE工具介绍

(因为学习ERWin无意发现的此文章,非常不错,转载一篇!) 一,概述 今天, 代码变得日益简单, 在Model的指导下, 思想, 设计, 分析都变得异常重要。企业业务建模工具, 产品非常多, 特别是在MDA日益流行的今天. WorkFlow是典型的业务及流程建模。 二,软件开发CASE工具简介 (一)图稿绘制: 1,visio:这是目前国内用得最...

MATROSKA 文件格式

MATROSKA 文件格式 1.EBML (Extensible Binary Meta Language): EBML语言使用不定长整数,这种方式相对于固定长度的32位/64位字长的整数值更节约空间.放置的位置也不受字节对齐约束..这种长度编码方式来自于UTF-8编码规范. 不定长度的无符号整数值(“vint”): 长度的计算方法: 长度 = 1 + 整...

R语言代写之文本分析:主题建模LDA

原文:http://tecdat.cn/?p=3897 文本分析:主题建模 library(tidyverse) theme_set( theme_bw()) 目标 定义主题建模 解释Latent Dirichlet分配以及此过程的工作原理 演示如何使用LDA从一组已知主题中恢复主题结构 演示如何使用LDA从一组未知主题中恢复主题结构 确定为k 选择适...

MATLAB对于文本文件(txt)数据读取的技巧总结(经典中的经典)

特别说明:由于大家在 I/O 存取上以 txt 文件为主,且读取比存储更麻烦(存储的话 fwrite, fprintf 基本够用),因此下面的讨论主要集中在“txt 文件的读取”上。除了标注了“转”之外,其余心得均出于本人经验之结果,欢迎大家指正、补充。一. 基本知识:--------------------------------------------...

Java打印程序设计

1 前言在我们的实际工作中,经常需要实现打印功能。但由于历史原因,Java提供的打印功能一直都比较弱。实际上最初的jdk根本不支持打印,直到jdk1.1才引入了很轻量的打印支持。所以,在以前用Java/Applet/JSP/Servlet设计的程序中,较复杂的打印都是通过调用ActiveX/OCX控件或者VB/VC程序来实现的,非常麻烦。实际上,SUN公司...