自定义DSL生成序列图
一、DSL样例
bff : 用户APP服务层
us: 用户系统
acct: 账户系统
pay: 收银台
bff -> us : 验证用户登陆
us ...> bff : 返回用户基本信息
if xxx>0
bff --> acct : 查询账户余额
bff -> pay : 充值
pay...> bff: 充值成功
end
二、生成效果
自定义DSL生成序列图代码下载:https://github.com/workwind/ssd
三、背景说明
架构图有的是需要向公司领导汇报的,有的是用于项目开发的,有的是用于日常架构治理的,架构图也是公司重要的软件资产。十几年的工作,画各种架构图已成了家常便饭。就个人经验而言,大部分的架构图都是用PPT最实用。以后如果有时间,再整理一篇关于各种架构图的。这里重点说的序列图,其实序列图一般很少用于汇报的,它是比较技术细节的。但有时为了能清晰的说明,一张序列图是必不可少的。
我是很不喜欢一张非常大的图,然后在电脑屏幕上右拖下拉的。我认为一张好的架构图应该是能在有限的屏幕空间内重点说明问题的。架构图本身是抽象的,并不追求非常具体的细节,所谓抽象就是去除与问题无关的细枝末节,重点突出问题的主体脉络。这也是我为什么喜欢用PPT画架构图,而不喜欢用某些UML工具。绝大部分的架构问题,我都习惯了直接用PPT画,不需要先用其它工具画好,再剪贴到PPT。但序列图是个例外,它在PPT中很难画。而在各种工具中visio画的UML是比较好的。但visio只能在公司用,我在家使用的MAC电脑没有visio licence。
序列图的形式是非常固定的,我为什么要花那么多的时间去在各种UML工具中对齐元素、设置样式呢?我已经习惯了熟悉的序列图的样式了,不想每一次画序列图都设置一遍。序列图重要的是事件发生的时序,我能不能像写代码那样,快速的表达出序列图的内容呢?就像Markdown语法通过简单的几个特殊的符号可能方便在网页上写作。花时间拖拽元件,然后对齐元件,设置元件,在键盘与鼠标之间来回切换也没多少意思。
Go语言在Webassembly方面能做到什么程度,能不能为web前端开发带来一些实用的价值呢?近期估计是没有时间实现这个webassembly的版本了,以后有时间实现了再作对比。
基于以上原因,设计了一个DSL(Domain Special Language)领域特定语言,用于简单直接的描述序列图。以后如果有需要就可以借助于生成器基于DSL直接生成序列图了。
四、DSL指令解释
这个DSL非常简单,只有两个部分。
第一部分:声明
"objectName":"className"
第一部分是对Actor(或者Object/Entity)的声明,冒号左边是对象名,实际就是一个标识符,冒号右边是类名,但我在画架构图的时候一般不会画细节,画的基本都是应用系统与应用系统之间的交互,所以这里冒号右边我就直接用来表示系统名字了。
第二部:消息顺序
->
这个是用于表示同步调用,例如A系统对B系统发起一个Restful同步调用。
-->
这个是用于表示异步调用,例如C系统对D系统发起一个MQ或者FutureTask之类的异步调用。
...>
这个是用于表示同步调用或异步调用返回的消息,我一般也比较喜欢用于表示数据流,例如大数据推送到某个应用系统,至于是如何推送的,有时候不是问题的主干就不表达细节了。
if else end
这个是用于表示序列图中的Alt / Opt / Else 等一些符合条件时会发生的事件(或调用),用一个框将它们框起来。
ref
这个是用于表示需要引用到的其它序列图。
五、HTML代码
<html>
<head>
<meta charset="utf-8"/>
<title> 简单序列图生成器</title>
<script type="text/javascript" src="ssd.js"></script>
</head>
<body>
<form id="dsl_form" style="float:left">
<textarea id="ssdd" name="simple sequence diagram descriptor" autofocus="true" cols="60" rows="12" style="margin-right:20px;"></textarea>
<div>
<input type="button" value="生成序列图" onclick=" generate();"
style="display:block; float:right;margin-top:20px; margin-right:20px;">
</div>
</form>
<canvas id="ssd" width="1280" height="800" style="border:1px solid #c3c3c3;" style="float:left" >
Your browser does not support the canvas element.
</canvas>
</body>
</html>
六、Javascript代码
const style = {
canvas : {
width: 1280,
height: 800
},
actor:{
width : 120,
height:60
},
margin : {
top: 50,
bottom: 50
},
arrow:{
triangle:{
dx:9,
dy:5
}
}
}
//去左空格bai;
function ltrim(s){
return s.replace( /^\s*/, "");
}
//去右空格;
function rtrim(s){
return s.replace( /\s*$/, "");
}
//左右空格;
function trim(s){
return rtrim(ltrim(s));
}
function Arrow(from ,type, to , message){
this.from = from;
this.to = to ;
this.type = type; //1 is -> ; 2 is -->; 3 is ...>
this.message = message;
this.arrowY = 0;
this.fromX = function (){
return this.from.x + this.from.width/2 ;
}
this.toX = function (){
return this.to.x + this.to.width/2;
}
this.draw = function (ctx,style){
ctx.strokeStyle = "black";
ctx.fillStyle = "black";
ctx.beginPath();
if( this.type == 3 ){
//画虚线
ctx.setLineDash([10,3]);
}else{
ctx.setLineDash([10,0]);
}
//画实线
ctx.moveTo(this.fromX() , this.arrowY );
ctx.lineTo(this.toX() ,this.arrowY);
ctx.stroke();
//画箭头
ctx.beginPath();
const dx = this.fromX() < this.toX() ? (-1) * style.arrow.triangle.dx : style.arrow.triangle.dx;
ctx.moveTo(this.toX() + dx , this.arrowY - style.arrow.triangle.dy);
ctx.lineTo(this.toX() ,this.arrowY);
ctx.lineTo(this.toX() +dx ,this.arrowY + style.arrow.triangle.dy);
if(this.type == 1){
ctx.closePath();
ctx.fill();
}
ctx.stroke();
//画文字消息
ctx.font="10pt Arial";
ctx.textBaseline = "bottom";
ctx.fillText(this.message, (this.fromX() + this.toX()) / 2 , this.arrowY , this.toX() - this.fromX() );
}
}
function Actor(objectName,className){
this.objectName = trim(objectName);
this.className = trim(className);
this.x = 0;
this.y = 0;
this.width = style.actor.width;//default value
this.height =style.actor.height;//default value
this.draw = function (ctx){
//画参与者框框
ctx.beginPath();
ctx.fillStyle="#176AA6";
ctx.fillRect(this.x, this.y,this.width , this.height);
//填写文字
ctx.fillStyle = "#FFFFFF";
ctx.textAlign="center";
ctx.font="15pt Arial";
ctx.fillText(this.className, this.x + this.width / 2 , this.y + this.height *3/4, this.width);
//画生命线
ctx.strokeStyle = "#176AA6";
ctx.setLineDash([10,3]);
const lifeLineX = this.x + this.width / 2;
const y1 = this.y + this.height;
const y2 = y1 +500; //TODO 抽出变量
ctx.moveTo( lifeLineX,y1 );
ctx.lineTo( lifeLineX ,y2);
ctx.stroke();
}
}
function findActor(actors,objectName){
for(let i = 0 ; i< actors.length; i++){
if(actors[i].objectName=== trim(objectName)){
return actors[i];
}
}
}
/*
将一个字符串解释为参与者实体与箭头实体,并放入相应的数组
*/
function analyse(ssdd,actors,arrows){
let rows = ssdd.split('\n');
for (let i = 0; i < rows.length; i++) {
const row = rows[i];
console.log(row);
const index = row.indexOf(":");
if( index>0 && index< row.length){
const tag = row.substring(0,index)
const message = row.substring(index+1, row.length);
//判断处理返回消息
let ti = tag.indexOf("...>");
if (ti >0 && ti < tag.length){
from = findActor(actors,tag.substring(0,ti) );
to = findActor(actors,tag.substring(ti+4, tag.length) );
let returnArrow =new Arrow(from,3,to, message);
console.log(returnArrow);
arrows.push(returnArrow);
continue;
}
//判断处理异步消息
ti = tag.indexOf("-->");
if (ti >0 && ti < tag.length){
from = findActor(actors,tag.substring(0,ti) );
to = findActor(actors,tag.substring(ti+3, tag.length) );
let asyncArrow =new Arrow(from ,2, to, message);
console.log(asyncArrow);
arrows.push(asyncArrow);
continue;
}
//判断同步消息
ti = tag.indexOf("->");
if (ti >0 && ti < tag.length){
from = findActor(actors,tag.substring(0,ti) );
to = findActor(actors,tag.substring(ti+2, tag.length) );
let syncArrow =new Arrow(from ,1, to, message);
console.log(syncArrow);
arrows.push(syncArrow);
continue;
}
//剩余的是Actor声明
actors.push(new Actor(tag,message));
}
}
console.log("actors.length : "+actors.length);
console.log("arrows.length : "+arrows.length);
}
function initLocation(actors,style){
let margin = ( style.canvas.width -( style.actor.width * actors.length) ) / (actors.length + 1 ) ;
let x = margin;
for(let i =0; i<actors.length ; i++){
const a = actors[i];
a.x = x;
a.y = style.margin.top;
x = x + style.actor.width + margin;
}
}
function generate(){
const ssdd = document.getElementById("ssdd").value;
console.log(ssdd);
let actors = new Array();
let arrows = new Array();
analyse(ssdd, actors, arrows);
initLocation(actors, style);
const c = document.getElementById("ssd");
const ctx = c.getContext("2d");
for (let i = 0; i < actors.length; i++) {
actors[i].draw(ctx);
}
let arrowY = style.actor.height + 100;
for (let i = 0; i < arrows.length; i++) {
const arrow = arrows[i];
arrowY = arrowY + 50;
arrow.arrowY = arrowY;
arrow.draw(ctx, style);
}
event.preventDefault();
}
七、不足与改进
时间有限,这个小实验不能做得完美,是有很多的缺点的。例如对样式的自定义,对实心箭头的支持,对DSL的if else end的支持。但这个实验还是很有意义的,至少证明了DSL在某些时候某些场合特别的有用,可以提升不少的效率。另外发现有个意外的惊喜,GoLand这个IDE竟然对Javascript的支持挺到位的。
如果以后有机会有时间,这个小工具还可以有哪些改进呢?
- 通过开源的canvas操作函数库,可以让画出来的图形元件更美观些。
- 可以将一些图形元件封装得更内聚强大。
- 可以自定义样式。
- 可以通过Go 改写 实现webassembly,看看性能能否有更好的提升。(目前Javascript的速度也看不出性能慢)
应该还可以思考的是,还有哪些适合DSL的其它领域呢(不是序列图,甚至不一定是软件行业)?