文章目录
【摘要】本文介绍UML基本概念及相关类型作用,分析UML在研发场景下存在痛点, 分析改进思路,并阐述实现结构化json绘制uml类图的技术方案。最后总结下根据新技术方案,可进行的衍生拓展设计。
【关键词】: UML;JSON;建模
1.UML简介
1.1 什么是UML建模
UML(Unified Modeling Language,统一建模语言)是一种标准化的建模语言,用于在软件工程中可视化、描述、构建和记录软件系统的各种构件。UML图是用UML语言绘制的图表,用于表示系统的静态结构和动态行为。
下面主要列举一些常用的uml图如下
类图 (Class Diagram)
作用:表示系统中的类及其相互关系,描述系统的静态结构。
元素:类、接口、属性、方法、关联关系、继承关系、实现关系等。
用例图 (Use Case Diagram)
作用:展示系统的功能及其用户(称为参与者),描述系统的功能需求。
元素:用例、参与者、系统边界、关联关系等。
序列图 (Sequence Diagram)
作用:显示对象之间的交互及其调用顺序,描述系统的动态行为。
元素:对象、生命线、消息、激活等。
活动图 (Activity Diagram)
作用:表示系统中活动的流程,类似于流程图,用于描述业务流程或方法的逻辑。
元素:活动、决策节点、并行节点、开始和结束节点等。
状态图 (State Diagram)
作用:表示对象在生命周期中的状态变化及其触发事件。
元素:状态、转换、事件、起始状态、终止状态等。
组件图 (Component Diagram)
作用:展示系统的物理结构,表示系统中的软件组件及其关系。
元素:组件、接口、依赖关系等。
部署图 (Deployment Diagram)
作用:表示系统的硬件配置及其部署的组件。
元素:节点、组件、关联等。
对象图 (Object Diagram)
作用:显示系统在某个时刻的对象实例及其关系,是类图的实例化。
元素:对象、链接等。
1.2 使用UML建模的好处
系统设计和分析 :
- 帮助系统设计人员和开发人员理解和设计系统的结构和行为。
- 在系统开发的早期阶段,用于捕捉和分析需求。
文档记录:
- 提供系统的图形化文档,有助于维护和升级系统。
- 作为开发过程中的参考资料。
沟通和交流:
- 帮助团队成员之间更好地沟通和交流系统的设计和需求。
- 为项目利益相关者提供直观的系统视图。
代码生成和逆向工程:
- 有助于从模型生成代码或从代码生成模型,支持自动化开发工具。
质量保证和测试:
- 通过图形化的模型,可以更容易地发现系统设计中的问题和缺陷。
- 有助于测试用例的设计和验证。
2.当前UML在研发场景下痛点
uml工具虽然好,但是在我们实际工作工作中,会发现大部分软件开发都没有使用起来。
原因笔者分析如下:
- 研发觉得开发前规范设计是额外的工作量,没有直接写代码来的快, 特别在项目周期短,甚至产品生命都不长的研发工作中,显得特别没有性价比。
- UML设计侧重点更多是设计沟通工具,UML建模和实际的代码之间关系,是比较弱的逻辑关系,而且需要人为手动维护。可能设计之初,UML建模和代码是一致的。但是随着产品迭代,UML忘记及时维护,就会UML版本落后于真实的代码。一个不准确的设计,对于理解系统可能会误导。这样反过来就导致更不愿意使用UML。
那么该如何解决呢?思路如下
- 互联网下班场更多的存量系统之间的竞争,系统运行时间长,业务复杂,拥有良好设计文档工具的系统更够降低维护成本,快速应对新需求。
- 从上面UML建模优点可以分析,UML实际上是可以作为代码生成的模板,也就是不仅仅是设计工具,也是代码的生产工具。笔者人为只有把uml和生产结合起来,才能保证保留UML建模优点,同时准确反映最新代码的设计。
有了解决思路下面围绕“代码即设计”和“设计及代码”的思路,来改变UML使用方式。
3.UML工具的优化实现
研发过程中绘制UML,主要有两种方式使用,在线UML建模网站的图形界面快速绘制和基于文本的图表绘制工具(mermaid/plantUML)
方式一: 以UML中的类图为例,在语雀的在线uml绘制类图
这种方式优点是方便快捷,可以最快方式绘图。确定依赖第三方,纯设计工具无法解析内容,实现拓展功能。
方式二:mermaid示例
classDiagram
Animal <|-- Duck
Animal <|-- Fish
Animal <|-- Zebra
Animal : +int age
Animal : +String gender
Animal: +isMammal()
Animal: +mate()
class Duck{
+String beakColor
+swim()
+quack()
}
class Fish{
-int sizeInFeet
-canEat()
}
class Zebra{
+bool is_wild
+run()
}
并通过解析器将这些文本渲染成图形效果:
这种方式优点是可控性强,使用纯文本记录,和git管理工具结合可以版本化。缺点是需要手写,非技术人员上手难度大,而且符号很容易写错,导致图片渲染失败。
因此需要以下改进
- 非结构化的写法使用结构化,如使用json替代。
- json数据通过解析器自动翻译成mermaid的代码
- 支持在线实时预览
3.1 json结构设计
uml 分解为节点与边,分别代表类与类之间的关系。节点内部包含属性和方法,对应java的字段及成员方法
- 节点(数组)
- 属性
- 方法
- 关系(数组)
json 示例
{
"classDiagram": {
"nodes": [
{
"id": "Order",
"type": "AggregateRoot",
"properties": [
{ "name": "orderId", "type": "String", "description": "订单 ID", "pk": true },
{ "name": "orderItems", "type": "List[OrderItem]", "description": "订单项列表" },
{ "name": "accountId", "type": "String", "description": "账户 ID" },
{ "name": "address", "type": "Address", "description": "地址对象" },
{ "name": "status", "type": "Integer", "description": "订单状态" },
{ "name": "createTime", "type": "Date", "description": "创建时间" },
{ "name": "updateTime", "type": "Date", "description": "更新时间" },
{ "name": "paymentTime", "type": "Date", "description": "支付时间" },
{ "name": "amount", "type": "BigDecimal", "description": "订单总金额" },
{ "name": "currency", "type": "String", "description": "货币类型", "defaultValue": "CNY" }
],
"methods": [
{ "name": "addItem", "parameters": [{ "name": "product", "type": "Product" }, { "name": "quantity", "type": "int" }], "description": "添加商品到订单" },
{ "name": "removeItem", "parameters": [{ "name": "productId", "type": "String" }], "description": "移除商品" },
{ "name": "clearItems", "description": "清空订单项" },
{ "name": "calculateTotalPrice", "returnType": "BigDecimal", "description": "计算订单总金额" },
{ "name": "calculateGifts", "description": "计算赠品" },
{ "name": "verifyCloseStatus", "returnType": "Order", "description": "验证关闭状态" },
{ "name": "verifyOwner", "parameters": [{ "name": "accountId", "type": "String" }], "returnType": "Order", "description": "验证所有者" }
]
},
{
"id": "OrderItem",
"type": "Entity",
"properties": [
{ "name": "itemId", "type": "String", "description": "订单项ID" , "pk": true},
{ "name": "product", "type": "ProductSnapShot", "description": "商品对象" },
{ "name": "quantity", "type": "int", "description": "数量" },
{ "name": "price", "type": "Price", "description": "价格对象" },
{ "name": "remarks", "type": "String", "description": "备注" }
]
},
],
"relationships": [
{ "source": "Order", "target": "OrderItem", "type": "contains", "description": "包含" }
]
}
}
3.2 json类图解析器实现
public class MermaidClassDiagramGenerator {
private static final Logger LOGGER = LoggerFactory.getLogger(MermaidClassDiagramGenerator.class);
public static void main(String[] args) throws IOException {
// 或者直接提交 JSON 内容进行解析
String jsonContent = "{\n" +
" \"classDiagram\": {\n" +
" \"nodes\": [\n" +
" {\n" +
" \"id\": \"Order\",\n" +
" \"type\": \"AggregateRoot\",\n" +
" \"properties\": [\n" +
" {\n" +
" \"name\": \"orderId\",\n" +
" \"type\": \"String\",\n" +
" \"description\": \"订单 ID\",\n" +
" \"pk\": true\n" +
" }\n" +
" ],\n" +
" \"methods\": [],\n" +
" \"relationships\": []\n" +
" }\n" +
" ]\n" +
" }\n" +
"}";
String markdownFromContent = generateMermaidMarkdownFromContent(jsonContent);
System.out.println(markdownFromContent);
}
public static String generateMermaidMarkdownFromFile(String jsonFilePath) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(new File(jsonFilePath));
return generateMermaidMarkdown(rootNode);
}
public static String generateMermaidMarkdownFromContent(String jsonContent) throws IOException {
ObjectMapper objectMapper = new ObjectMapper();
JsonNode rootNode = objectMapper.readTree(jsonContent);
return generateMermaidMarkdown(rootNode);
}
private static String generateMermaidMarkdown(JsonNode rootNode) {
StringBuilder markdownBuilder = new StringBuilder();
// markdownBuilder.append("<script class='mermaid'>\n");
markdownBuilder.append("classDiagram\n\n");
// Process nodes
Iterator<JsonNode> nodesIterator = rootNode.path("classDiagram").path("nodes").elements();
while (nodesIterator.hasNext()) {
JsonNode node = nodesIterator.next();
String id = node.path("id").asText();
String type = node.path("type").asText();
LOGGER.info("解析ID[{}] 类型[{}]", id, type);
String classText = "class " + id + " {\n";
if (!type.isEmpty()) {
classText += "\t<< " + type + " >>\n";
}
markdownBuilder.append(classText);
// Process properties
Iterator<JsonNode> propertiesIterator = node.path("properties").elements();
while (propertiesIterator.hasNext()) {
JsonNode property = propertiesIterator.next();
String propertyName = property.path("name").asText();
String propertyType = property.path("type").asText();
String propertyDescription = property.path("description").asText();
String propertyExtra = property.path("pk").asBoolean() ? "[pk]" : "";
String propertyText = "\t- " + propertyName + ": " + propertyType + " [" + propertyDescription + "]" + propertyExtra + "\n";
markdownBuilder.append(propertyText);
// 引用关系推断
buildEntityRelationShip(propertyType, node, rootNode);
}
// Process methods
Iterator<JsonNode> methodsIterator = node.path("methods").elements();
while (methodsIterator.hasNext()) {
JsonNode method = methodsIterator.next();
String methodName = method.path("name").asText();
String methodReturnType = method.path("returnType").asText();
String methodDescription = method.path("description").asText();
StringBuilder parametersBuilder = new StringBuilder();
Iterator<JsonNode> parametersIterator = method.path("parameters").elements();
while (parametersIterator.hasNext()) {
JsonNode parameter = parametersIterator.next();
String parameterName = parameter.path("name").asText();
String parameterType = parameter.path("type").asText();
parametersBuilder.append(parameterName).append(": ").append(parameterType).append(", ");
}
if (parametersBuilder.length() > 0) {
parametersBuilder.setLength(parametersBuilder.length() - 2); // Remove the trailing comma and space
}
String methodText = "\t+ ";
if (!methodReturnType.isEmpty()) {
methodText += methodReturnType + " ";
} else {
methodText += "void ";
}
methodText += methodName + "(" + parametersBuilder.toString() + ") : " + methodDescription + "\n";
markdownBuilder.append(methodText);
}
markdownBuilder.append("}\n\n");
}
// Process relationships
Iterator<JsonNode> relationshipsIterator = rootNode.path("classDiagram").path("relationships").elements();
while (relationshipsIterator.hasNext()) {
JsonNode relationship = relationshipsIterator.next();
String source = relationship.path("source").asText();
String target = relationship.path("target").asText();
String type = relationship.path("type").asText();
String description = relationship.path("description").asText();
String relationshipText = "";
if ("contains".equals(type) || "has".equals(type)) {
relationshipText = source + " *-- " + target + " : " + type + "\n";
} else if ("use".equals(type)) {
relationshipText = source + " ..> " + target + " : " + type + "\n";
}
markdownBuilder.append(relationshipText);
}
// markdownBuilder.append("</script>\n");
System.out.println(markdownBuilder);
return markdownBuilder.toString();
}
private static void buildEntityRelationShip(String propertyType, JsonNode currentNode, JsonNode rootNode) {
ObjectMapper objectMapper = new ObjectMapper();
ObjectNode classDiagramNode = (ObjectNode) rootNode.path("classDiagram");
ArrayNode relationshipsNode;
if (classDiagramNode.has("relationships")) {
relationshipsNode = (ArrayNode) classDiagramNode.path("relationships");
} else {
relationshipsNode = objectMapper.createArrayNode();
classDiagramNode.set("relationships", relationshipsNode);
}
Iterator<JsonNode> nodesIterator = rootNode.path("classDiagram").path("nodes").elements();
String realType = propertyType;
if (propertyType.endsWith("[]")) { // 解析数组类型如 Date[]
realType = propertyType.substring(0, propertyType.length() - 2);
} else if (propertyType.startsWith("List[")) {
realType = propertyType.substring(5, propertyType.length() - 1);
}
while (nodesIterator.hasNext()) {
JsonNode node = nodesIterator.next();
String id = node.path("id").asText();
if (realType.equals(id)) {
String source = currentNode.path("id").asText();
String target = node.path("id").asText();
// Check if the relationship already exists
boolean relationshipExists = false;
Iterator<JsonNode> existingRelationships = relationshipsNode.elements();
while (existingRelationships.hasNext()) {
JsonNode existingRelationship = existingRelationships.next();
if (existingRelationship.path("source").asText().equals(source) &&
existingRelationship.path("target").asText().equals(target)) {
relationshipExists = true;
break;
}
}
// If relationship does not exist, add it
if (!relationshipExists) {
ObjectNode relationshipNode = objectMapper.createObjectNode();
relationshipNode.put("source", source);
relationshipNode.put("target", target);
relationshipNode.put("type", "has");
relationshipsNode.add(relationshipNode);
}
}
}
}
}
3.3 在线uml类图渲染实现
3.3.1 前端渲染页面
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
<script src="https://cdn.bootcdn.net/ajax/libs/jquery/3.6.4/jquery.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/mermaid@10.7.0/dist/mermaid.min.js"></script>
</head>
<body>
<div>
<label for="diagram-input">Diagram Type:</label>
<input type="text" id="diagram-input" value="class_diagram">
<button id="button_1" type="button" onclick="fetchAndRender()">实时渲染</button>
</div>
<div id="mermaid-container">
</div>
</body>
<script type="text/javascript">
function fetchAndRender() {
var rootValue = $('#diagram-input').val();
$.ajax({
url: 'mermaid/classDiagram', // Replace 'your_backend_url' with your actual backend URL
data: {root: rootValue},
method: 'GET',
success: function(response) {
renderMermaid(response.data);
},
error: function(xhr, status, error) {
console.error('Error fetching data:', error);
}
});
}
function renderMermaid(graphDefinition) {
console.log(graphDefinition)
$('#mermaid-container').html(graphDefinition).removeAttr('data-processed');
mermaid.init(undefined, $("#mermaid-container"));
}
// Call fetchAndRender() once when the page loads to initially render the graph
$(document).ready(function() {
// fetchAndRender();
});
</script>
</html>
3.3.2 后端数据接口
package com.sample.controller;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.util.FileCopyUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.InputStream;
import java.text.MessageFormat;
import java.util.Map;
@RestController
public class MermaidController {
@GetMapping("/mermaid/classDiagram")
public Map<String, Object> getClassDiagram(String root) throws IOException {
Resource data = new ClassPathResource(MessageFormat.format("data/{0}.json", root));
if (!data.exists()) {
data = new ClassPathResource("data/class_diagram_v1.json");
}
try (InputStream inputStream = data.getInputStream()) {
String json = new String(FileCopyUtils.copyToByteArray(inputStream));
// 根据json生成mermaid的语法构建uml类图
String classDiagram = MermaidClassDiagramGenerator.generateMermaidMarkdownFromContent(json);
return Map.of("data", classDiagram);
}
}
}
3.4 在线渲染效果
完整json示例
{
"classDiagram": {
"nodes": [
{
"id": "Order",
"type": "AggregateRoot",
"properties": [
{ "name": "orderId", "type": "String", "description": "订单 ID", "pk": true },
{ "name": "orderItems", "type": "List[OrderItem]", "description": "订单项列表" },
{ "name": "accountId", "type": "String", "description": "账户 ID" },
{ "name": "address", "type": "Address", "description": "地址对象" },
{ "name": "status", "type": "Integer", "description": "订单状态" },
{ "name": "createTime", "type": "Date", "description": "创建时间" },
{ "name": "updateTime", "type": "Date", "description": "更新时间" },
{ "name": "paymentTime", "type": "Date", "description": "支付时间" },
{ "name": "amount", "type": "BigDecimal", "description": "订单总金额" },
{ "name": "currency", "type": "String", "description": "货币类型", "defaultValue": "CNY" }
],
"methods": [
{ "name": "addItem", "parameters": [{ "name": "product", "type": "Product" }, { "name": "quantity", "type": "int" }], "description": "添加商品到订单" },
{ "name": "removeItem", "parameters": [{ "name": "productId", "type": "String" }], "description": "移除商品" },
{ "name": "clearItems", "description": "清空订单项" },
{ "name": "calculateTotalPrice", "returnType": "BigDecimal", "description": "计算订单总金额" },
{ "name": "calculateGifts", "description": "计算赠品" },
{ "name": "verifyCloseStatus", "returnType": "Order", "description": "验证关闭状态" },
{ "name": "verifyOwner", "parameters": [{ "name": "accountId", "type": "String" }], "returnType": "Order", "description": "验证所有者" }
]
},
{
"id": "OrderItem",
"type": "Entity",
"properties": [
{ "name": "itemId", "type": "String", "description": "订单项ID" , "pk": true},
{ "name": "product", "type": "ProductSnapShot", "description": "商品对象" },
{ "name": "quantity", "type": "int", "description": "数量" },
{ "name": "price", "type": "Price", "description": "价格对象" },
{ "name": "remarks", "type": "String", "description": "备注" }
]
},
{
"id": "Address",
"type": "ValueObject",
"properties": [
{ "name": "street", "type": "String", "description": "街道" },
{ "name": "city", "type": "String", "description": "城市" },
{ "name": "state", "type": "String", "description": "省份" },
{ "name": "zipCode", "type": "String", "description": "邮政编码" }
]
},
{
"id": "Price",
"type": "ValueObject",
"properties": [
{ "name": "amount", "type": "BigDecimal", "description": "金额" },
{ "name": "currency", "type": "String", "description": "货币类型" }
]
},
{
"id": "ProductSnapShot",
"type": "ValueObject",
"properties": [
{ "name": "id", "type": "Long", "description": "商品 ID" },
{ "name": "code", "type": "String", "description": "商品编码" },
{ "name": "name", "type": "String", "description": "商品名称" },
{ "name": "price", "type": "Price", "description": "商品价格" }
]
},
{
"id": "Product",
"type": "Entity",
"properties": [
{ "name": "id", "type": "Long", "description": "商品 ID" },
{ "name": "code", "type": "String", "description": "商品编码" },
{ "name": "productName", "type": "String", "description": "商品名称" },
{ "name": "price", "type": "Price", "description": "商品价格" }
]
},
{
"id": "OrderDomainService",
"type": "DomainService",
"methods": [
{ "name": "addItem", "parameters": [{ "name": "product", "type": "Product" }, { "name": "quantity", "type": "int" }], "description": "添加商品到订单" },
{ "name": "removeItem", "parameters": [{ "name": "productId", "type": "String" }], "description": "移除商品" },
{ "name": "clearItems", "description": "清空订单项" },
{ "name": "verifyCloseStatus", "returnType": "Order", "description": "验证关闭状态" },
{ "name": "verifyOwner", "parameters": [{ "name": "accountId", "type": "String" }], "returnType": "Order", "description": "验证所有者" }
]
},
{
"id": "OrderAppService",
"type": "AppService",
"methods": [
{ "name": "bpmnInvoke", "description": "流程编排" }
]
},
{
"id": "OrderInterfaceService",
"type": "Service"
},
{
"id": "OrderPageQuery",
"type": "Query",
"properties": [
{ "name": "productName", "type": "String", "description": "商品名称"},
{ "name": "createTime", "type": "Date", "description": "订单创建时间" }
],
"methods": [
{ "name": "invoke", "description": "执行查询", "returnType": "List[OrderPageQueryDTO]"}
]
},
{
"id": "OrderDetailQuery",
"type": "Query",
"properties": [
{ "name": "orderId", "type": "String", "description": "订单 ID"}
]
},
{
"id": "OrderPageQueryDTO",
"type": "DTO",
"properties": [
{ "name": "orderId", "type": "String", "description": "订单 ID" },
{ "name": "accountId", "type": "String", "description": "账户 ID" },
{ "name": "address", "type": "Address", "description": "地址对象" },
{ "name": "status", "type": "Integer", "description": "订单状态" },
{ "name": "createTime", "type": "Date", "description": "创建时间" },
{ "name": "updateTime", "type": "Date", "description": "更新时间" },
{ "name": "paymentTime", "type": "Date", "description": "支付时间" },
{ "name": "amount", "type": "BigDecimal", "description": "订单总金额" },
{ "name": "currency", "type": "String", "description": "货币类型", "defaultValue": "CNY" }
]
}
]
}
}
4. 总结
本文分析UML作用及使用现状,并且通过json结构方式实现的UML模型中类图构建,降低了文本解析的绘图成本。同时后续可以根据json结构化信息,已经包含了类描述信息,可以从两个方向进行进一步伸延设计。
- 根据类图定义注册接口
- 根据类定义自动生成数据库定义
通过这样思路可以将UML建模和后续工作结合起来,设计工具变成代码生产的工具,或者设计出低代码工具。