PlantUML指北:用UML设计和规划你的项目

5,775 阅读5分钟

随着ChatGPT爆火,网络上贩卖焦虑的声音此起彼伏,我等工程师、程序员似乎马上要被一锅端起,倒入历史涡流。笔者想说,莫慌,软件开发是一门极其复杂的学问,断然不是写两行代码那么简单。产品百万千万行的代码依然需要人来读懂和维护,架构设计、模式、扩展性依然是核心命题。UML是软件设计的利器,而PlantUML是一个很好的UML创作工具。

PlantUML简介

PlantUML是一个开源的UML绘图工具,目前在GitHub上有7.5K Stars,最早于2021年10月发布第一个正式版本,历经二十多个版本迭代,目前已非常成熟。它可用于快速创建UML(统一建模语言,Unified Modeling Language)图表,生成可视模型,帮助开发者将复杂的设计概念表示出来PlantUML 最与众不同的地方在于它提供了一套简单的纯文本语法来定义UML,堪称是UML界的Markdown,起到了桥梁和规范的作用。也就是说,你可以扫描代码来生成PlantUML,也可以解析PlantUML文本来生成代码。

PlantUML开源社区也很繁荣,其结果就是它丰富的插件,可与Visual Studio Code,JetBrains,Eclipse,NetBeans等系列IDE集成,以方便创作和预览。它能生成的视图支持包括SVG,PDF,PNG,EPS和LaTeX等丰富格式,基本上应有尽有。此外,PlantUML甚至还可以支持思维导图、甘特图、JSON、YAML的可视化,把影响力扩大到了UML之外。它已成为越来越多神级工程师、架构师、项目经理的必选工具。我在微软的开发团队出品的开源作品 Hydra Lab 就采用了PlantUML进行架构设计和协议规范可视化,用起来可谓得心应手。

关于PlantUML的细节用法,官方中文文档已经说得很清楚了,本文将不再赘述,而着重将篇幅放在具体使用经验的分享上。

小试牛刀

PlantUML是一个基于Java的工具,需要Java运行环境。本文将基于其当前最新版本1.2023.1进行实战讲解。不过,最快捷的试用PlantUML语言的方法莫过于直接使用官网服务,各位可以尝试把一下内容粘贴进输入框看效果:

以下代码呈现了类之间关系的表达,学习UML时候最容易搞混的就是各种箭头之间的区别和联系:

@startuml class-relations-1

Postman -- Postbox : associate,(关联)
Driver -- Car : drives(关联) >

Car *-- Wheel : have 4 (包含,组合) > 
Car -- Person : < owns(关联)

Folder *-- 字母F : starts with (composition,组合) >
Folder o-- File : contains (aggregation,聚合) >

Department o-- Employee : contains >

@enduml

class-relations-1.png

下面的代码展现了或者扩展、实现、依赖的类间关系:

@startuml class-relations-2

Cat -up-|> Animal : extends >
Dog -up-|> Animal : extends >

class Cat {
    -meow()
}
class Dog {
    +bark()
}

iPhone ..|> SmartPhone : realize (实现) >

DBReader ..> DB : depends on (依赖) >

@enduml

class-relations-2.png

PlantUML会自动处理图表的外观和布局,开发人员可以不用在意细节,同时他也支持一定程度的自定义。他的语法中使用@startuml@enduml作为开头和结尾的声明,此外还有@startjson@startyaml支持JSON\YAML数据结构的可视化,@startgantt用于画甘特图,@startmindmap用于绘制思维导图,@startmath(AsciiMath)或@startlatex(JLaTeXMath)用于绘制数学公式,功能可谓十分强大。这里仅展示一些示例,不再一一赘述,官方PDF写的很清楚了。下面是一段Spring Boot配置文件的可视化:

@startyaml yaml-preview-demo
server:
  port: 9886
  compression:
    enabled: true
    min-response-size: 102400
spring:
  cache:
    type: ehcache
  application:
    name: 'my-app'
  datasource:
    driver-class-name: org.sqlite.JDBC
@endyaml

yaml-preview-demo.png

以下是一个活动图(activity diagram)示例:

@startuml activity-diagram-preview-demo
start
if (Graphviz installed?) then (yes)
:process all\ndiagrams;
else (no)
:process only
__sequence__ and __activity__ diagrams;
endif
stop
@enduml

activity-diagram-preview-demo.png

更多示例可以看这里:REAL WORLD PlantUML

实际应用

在实际的开发场景中,我们可以运用PlantUML语法规范在代码库中进行撰写设计,以文本的形式输出内容到.puml文件中,然后调用PlantUML来生成可视化的图表,从而方便用来在Markdown技术文档当中引用,支持技术评审。那么,我们是否可以自动化这一流程,形成闭环?答案当然是可以的,PlantUML提供了Java依赖库,可以在代码中直接调用。这里演示一个小小的实例,利用Gradle创建构建任务,实现UML图片生成。

首先我们在项目中添加PlantUML依赖:

    implementation 'net.sourceforge.plantuml:plantuml:1.2023.1'

接着,我们在Gradle中创建一个任务,扫描特定目录下的所有puml文件,将他们交给生成逻辑(UMLImageGenerator#generateUMLImageFromFile)进行处理:

task generateUMLImage(group: 'documentation') {
    doFirst {
        def scanningDirList = ['agent/doc/UML']
        def outputDir = new File(projectDir, 'docs/images/UML')

        def generator = new UMLImageGenerator()
        scanningDirList.each {
            fileTree(new File(projectDir, it)).filter { it.name.endsWith(".puml") }.files.each {
                generator.generateUMLImageFromFile(it.absoluteFile, outputDir)
            }
        }
    }
}

具体的从文件生成图片的逻辑如下,这里调用的核心类就是PlantUML提供的SourceFileReader API:

import net.sourceforge.plantuml.FileFormat;
import net.sourceforge.plantuml.FileFormatOption;
import net.sourceforge.plantuml.GeneratedImage;
import net.sourceforge.plantuml.SourceFileReader;

import java.io.File;
import java.io.IOException;
import java.util.List;

public class UMLImageGenerator {
    public void generateUMLImageFromFile(File source, File outputDir) throws IOException {
        generateUMLImageFromFile(source, outputDir, false);
    }

    public void generateUMLImageFromFile(File source, File outputDir, boolean svg) throws IOException {
        if (!source.exists()) throw new RuntimeException(source.getAbsolutePath() + " file doesn't exist");

        SourceFileReader reader = svg ?
                new SourceFileReader(source, outputDir, new FileFormatOption(FileFormat.SVG)) :
                new SourceFileReader(source, outputDir);

        List<GeneratedImage> list = reader.getGeneratedImages();
        System.out.printf("Successfully generated %d UML images.\n", list.size());
    }
}

与上逻辑写好之后,我们在Gradle的任务列表documentation组中就可以看到这个新的任务了:

task-in-position.png

执行这个任务,就可以看到图片图片说出到了预期的目录下。本实战案例详细代码如下:

进一步探究

我们是否有可能根据UML设计来生成代码?那么首先我们需要读取出PlantUML的解析信息,顺着SourceFileReader的API我们可以进一步找到如下调用链:

        Diagram diagram = reader.getBlocks().get(0).getDiagram();
        if(diagram instanceof net.sourceforge.plantuml.classdiagram.ClassDiagram){
            ClassDiagram classDiagram= (ClassDiagram) diagram;
            classDiagram.getLeafsvalues().forEach(leaf -> System.out.println(leaf.getCodeGetName()));
        }

这里的net.sourceforge.plantuml.baraye.ILeaf类型的对象就是解析后的叶子实体了,包含了代码生成所需要的信息,可以利用他来生成一些接口代码,从而进一步实现从代码设计到初始实现的自动化。