上一章我们介绍了类图,我们很清楚,类图是从更加宏观的角度去梳理系统结构的,从类图中我们可以获取到类与类之间:继承,实现等关系信息,是宏观逻辑。下面我们继续换一个思路:作为一名软件工程结构化图的设计者去设计另一种图,要求:
1)这种图要微观的体现调用关系
2)要线性的体现调用的时间关系
3)要能体现不同逻辑的不同结果
而这种图主要用于阅读源码,或者向别人介绍代码思路的等等相关的场景。
1.入手代码
对比设计类图,这个微观一点图显然要更加复杂,为了能成功把这种图设计出来,所以我们从实际的一段代码出发
package se;
/*** @author: Jeffrey* @date: 2024/01/29/11:06* @description:*/
public class TimeSequenceUML {public static void main(String[] args) {Client client = new Client();client.work();}
}
class Device{public void write(String hello) {}
}
class Server{private Device device;public void close() {}public void open() {}public void print(String hello) {device.write(hello);}
}class Client{private Server server;public void work(){server.open();server.print("hello");server.close();}
}
2.代码分析
2.1 图的元素
这段代码并不难理解,调用关系也并不复杂,如果直接去像我们平时阅读源码的思路一样去走方法,可以得到一个线性的调用思路,但是我们经常会遇到这么几个问题:
- 代码的核心对象是谁?
- 代码的调用者是谁?
- 都是对象,这些对象的之间各自承担的逻辑任务其实很容易搞混
说白了:为了设计好这个图,我们要完成的第一件事就是把这些代码的元素抽象出来,用一个图形表示就好了。于是优秀的我们思考了很多相关的对象完成以下的元素设计:
元素: | 含义: | 图像: | plantUml: |
---|---|---|---|
角色 | 一般是逻辑的开始交互者,可以是:人,机器,系统 | actor name | |
对象 | 一般是逻辑的中间参与者,一般代表对象,又可以改变形状为以下具体的类型 | participant client | |
实体 | entity name | ||
控制 | control name | ||
数据库 | database name | ||
集合 | collections name | ||
队列 | queue name | ||
边界 | boundary name |
2.2 图的结构
将对象抽象出来后,下一步我们必须要考虑的就是如何将这些对象组织起来:
学习过线程进程,操作系统,计算机体系结构中任何一门课程的朋友们都知道:我们感觉计算机的某个操作是一瞬间的事情,其实它经历了很多个线性阶段,即使是多核多处理器的计算机,也是由一个个线性的阶段和并行的阶段链接而成,如何让我们简单明了的直接看图就能看懂程序?那么就要在图中体现程序的线性和并行关系。
可是连接起来之后,不得不考虑的一个问题就是元素和元素要怎么摆放?如何才能同时体现一段时间内在调用时间上的线性特征和一个瞬间时方法调用的顺序线性特征?
聪明的我们明白,是时候定下一些规则来了!
- 关系越紧密,交互越密集的元素要尽可能的放到越近的地方
- 我们定义一个坐标轴,纵向代表时间顺序的线性关系
- 横向代表一瞬间方法调用的线性关系,或者说方法调用的层次关系
2.3 消息和生命周期
看看我们现在有什么?有元素,有规则,下一步就是把调用顺序放进去了!记得黑盒模型吗?我们知道封装的思想有一部分就是为了隐藏方法的具体实现细节,我们现在主要关注的是方法的顺序,我们可以把每个调用过程当作是一个黑盒,只关注黑盒的输入输出信息。
我们用:
实线实箭头来表示调用,用虚线实箭头来表示返回信息
在PlantUML中:
-> 实线
--> 虚线
消息已经做好了,回到我们的需求,我们需要反应每个对象在什么阶段是激活的,在什么时候就跟这个对象没有什么关系了。
但是这里我们要注意一个区别,对于Java 来说,大家都知道基于JVM,对象的回收一般是不由我们来负责的【我们负责的主要是一些系统的接口或者说一些通道/流的开关】,而在UML中也类似,对象的激活状态代表的不是如IOC中对象一直的在内存中/在容器中,而是代表:在一个对象工作的同时,另一个对象是否在工作?如果在工作,表示为激活,反之为不活跃。
也就是说:我们要反映同一时刻对象的活跃状态。
考虑到生命周期和时间是线性相关的,我们用在对象上的长矩形来体现生命周期的长短
用横向上的矩形在同一水平来表示同时刻不同对象的活跃状态:
进一步的完善我们的图:
2.4补充语法
至此我们已经能基本完成一个简单代码的线性过程,为了方便我们的绘图和绘制更加直观的图,我们补充一部分PlantUML的语法:
2.4.1 组合片段
ALT
抉择组合片段 ALT:说白了就是要在图中体现IF-ELSE 逻辑
比如这串代码:
package se;/*** @author: Jeffrey* @date: 2024/01/23/21:55* @description:*/
public class Test {public static void main(String[] args) {Student student = new Student();if (student.isGender()){Mapper mysql = new MySqlMapper();mysql.insert(student);}else {Mapper sqlServer =new SqlserverMapper();sqlServer.insert(student);}}
}
class Student{private boolean gender = true;public boolean isGender() {return gender;}
}
interface Mapper{public void insert(Student student);
}
class MySqlMapper implements Mapper{@Overridepublic void insert(Student student) {//持久化到Mysql}
}
class SqlserverMapper implements Mapper{@Overridepublic void insert(Student student) {//持久化到SqlServer}
}
可以绘制为:
如果用plant UML 来表示:
@startuml
actor wo
Participant Test
Participant Student
Participant MySqlMapper
Participant SqlserverMapperautonumber
wo -> Test : main()
Test -> Student: isGender()
activate Student
alt Gender == trueStudent -> MySqlMapper:insert()activate MySqlMapperMySqlMapper --> Studentdeactivate MySqlMapper
elseStudent -> SqlserverMapper:insert()activate SqlserverMapperSqlserverMapper --> Studentdeactivate SqlserverMapper
end
Student --> Test
deactivate Student
@enduml
补充:
- 在plantUML 中if-else 逻辑通过 alt /else 来表示,如果需要else if,只需要再添加一个else,同时这个组合片段必须用end 来结尾
- 一般当向一个对象发出消息时,这个对象被激活,进入生命周期,返回消息时被置为不活跃状态,所以我在发完消息后用
activate name
来激活对象,在回复完消息后用deactivate name
- autonumber 可以用于自动对消息进行编号
- 还可以在第一条消息发送前用
autoactivate on
来自动填充activate,然后每次需要返回消息时用return 代替:
综上可优化为:
autonumber
autoactivate on
wo -> Test : main()
Test -> Student: isGender()
activate Student
alt Gender == trueStudent -> MySqlMapper:insert()return
elseStudent -> SqlserverMapper:insert()return
end
return
Loop
显然这可用于代表循环
loop 1000 timesStudent -> SqlserverMapper:insert()returnend
Group
可直接简单用于将一部分逻辑框起来,再添加对应的注释
group SqlServer逻辑单元 [数据持久化]Student -> SqlserverMapper:insert()returnend
2.4.2 逻辑分割/分隔符
可以将一块代码分割成若干逻辑模块/或者代码分层:
wo -> Test : main()
Test -> Student: isGender()
==初始化==
alt Gender == trueStudent -> MySqlMapper:insert()return
elsegroup SqlServer逻辑单元 [数据持久化]Student -> SqlserverMapper:insert()returnend
end
==持久层==
2.4.3 让响应信息显示在箭头下面
可以使用skinparam responseMessageBelowArrow true
命令,让响应信息显示在箭头下面。
2.4.4 增加彼此空间
可以使用|||
来增加空间。
@startuml
Alice -> Bob: message 1
Bob --> Alice: ok
|||
Alice -> Bob: message 2
Bob --> Alice: ok
||45||
Alice -> Bob: message 3
Bob --> Alice: ok
@enduml
2.4.5 包裹参与者
可以使用box
和end box
画一个盒子将参与者包裹起来。
2.4.5 并行
可以使用par
和end par
来将同一时间段并行逻辑括起来
3.代码实战
我们用之前我写的一个简单Netty 实现的servlet 容器的代码来进行一次简单的项目实战,源码地址是:
Netty实现简易tomcat容器
大概整体项目并不规范的时序思路是这样的:
这个项目实现的基本功能就是根据自定义协议传入网页请求信息,实现对信息的解码,并包装成实现了HttpServlet 接口的容器。
而我们的目标就是用时序图来完成从HelloServlet收到一个登录请求的调用过程的绘图:
业务层
@startuml
autonumber
autoactivate on
Actor browser as br
Participant HelloServlet as hs
Participant HttpServletRequest as req
Participant UserService as us
br -> hs:dePost(req,rep)
hs->req:getParameter(username)
return String:username
hs->req:getParameter(password)
return String:password
hs->us:login(username,password)
return false
return response
@enduml
本篇关键词:时间顺序,方法调用顺序,生命周期=激活状态