效率提升 | UML类图
前言
为什么写这篇文章呢?原因是最近在阅读Android源码时,里面涉及的知识点特别多,方法调用链也非常长,而之前我又没有画类图的习惯,导致走了不少弯路。
在经过思考后,我发现合理使用UML
图可以提高我们阅读源码的效率,可以快速构建模块之间(类之间)的关系,更好地理解代码。
本篇文章先从类图来介绍UML图。
正文
文章从俩个方面来介绍UML
类图:
- 首先是
UML
类图的含义,各种图形表示什么意思; - 然后再说一下我们如何使用MD编辑器来绘制UML类图,以及介绍相关插件在IDE中生成
UML
类图。
类的关系
UML
类图中的类关系从强到弱可以分为:泛化关系 = 实现关系 > 组合关系 > 聚合关系 > 关联关系 > 依赖关系,关于这个看似简单的东西,但是想记住和理解清楚还是挺麻烦的。
我找了一些资料,问题大致出在这俩种情况:
- 一种是从概念出发,单独理解这几个概念,脱离实际业务,让人晦涩难懂;
- 一种是觉得类型太多,不好记忆,而且图形的虚线、实线、箭头指向等也不好记忆。
我现在从Java类代码的角度来仔细分析,让你理解其中含义,记住其中的符号。
依赖关系
依赖关系是所有关系中耦合最小的,它可以描述为"Uses a
",即"使用"关系。
这个依赖和我们平时所理解的依赖有一点点不一样,比如下面Car
类:
//汽车
public class Car {
//引擎
private Engine engine;
void installEngine(Engine e){
this.engine = e;
}
//打电话
void callUp(Phone phone){
}
}
按我们正常对依赖的理解:想使Car类完全正常使用,我们需要Engine
和Phone
的实例,所以Car
类就依赖于Engine
和Phone
类。
但是在UML
类图中却不是的,在UML
中定义依赖为偶合最小的关系,这里只有Phone
为依赖关系。因为这里的Phone
是方法的参数,不仅仅是参数,还包括方法中的变量和返回值,Phone
的实例在方法执行完就销毁了,就认为和原类Car
的耦合关系最小。
从另外一个角度来说,假如我们就只想定义一下依赖关系,即Car依赖于Phone,我们使用UML类图表示如图(虚线加箭头):
这时把上述关系转换成代码后,Car
类中并不会有Phone
类型的成员变量,这就是关键。
小节:
- 依赖关系认为是耦合最小的关系,即被依赖的对象只会是方法的局部变量、参数和返回值,不会是成员变量。
- 使用虚线加箭头表示,虚线表示关系很浅,耦合少,箭头指向被依赖的类,表示需要这个类。
关联关系
关联关系比依赖关系更进一步,它可以描述为"Has a
",即"拥有"关系。
上面依赖关系理解清楚后,关联关系就是一句话:被关联类的实例是本类的成员变量。
比如还是上面代码,Engine
的实例engine
是Car
的成员变量,这时Engine
和Car
的关系就是关联关系。
还是一样逆向思维,假如我们用类图说明Car
关联Engine
如图(实线加箭头):
上述UML
图转换成代码后,Car
类中就会生成一个Engine
类型的成员变量,这是关键。
和依赖关系一起小节:
- 关联关系比依赖关系更强,因为被关联的对象是本类成员变量,没有它,更没法使用。
- 从生命周期角度来看,关联关系的类实例和本类的生命周期是一样的,而依赖关系只和其中方法的生命周期一样。
- 表示都是线加箭头,箭头的意思就是"找到"的意思,我需要找到这个类,才可以完成我自己的功能,而线的虚实就是关系的强弱。
聚合关系和组合关系
聚合关系和组合关系是关联关系的细分类,即关联关系可以分为聚合关系和组合关系,其实这俩种关系不用分的如此仔细,在实际使用中就可以用关联关系来表示。
既然提及,就要说一下,组合关系比聚合关系更强,这里的思考角度就要从整体和部分之间的关系来说:
-
聚合关系表示整体和部分之间的关系,比如学校和老师这俩种关系密切的类,我定义学校类时可以在其中定义一个老师列表的成员变量,同时定义老师类时也可以定义在职学校的成员变量。
我们可以说这2个类是聚合关系,这里可以把学校看成整体,老师看成整体的一部分,但是老师可以脱离整体(学校)单独存在,他可以选择不在学校里教书,比如私教。
-
而组合关系比聚合关系更强一步就是上面最后一点不一样,组合从字面意思理解就是是组成的一部分,所以在组合关系中,部分对象不能单独存在,即这个部分不能脱离整体存在。
其实这个聚合和组合从抽象概念的意思上容易理解,比如身体和大脑的关系,大脑就无法单独存在。
说完了概念,就回到开头说的问题之一:太过抽象,那在代码中如何看出区别呢?因为聚合和组合都是关联关系的一种,而关联关系在代码中体现就是成员变量,所以这里关键就是成员变量的表现形式。
首先是GooseGroup
(雁群)和Goose
(大雁)的聚合关系:
//雁群
public class GooseGroup {
public Goose goose;
public GooseGroup(Goose goose){
this.goose = goose;
}
}
用UML图如下:
这里用空心菱形加实线和箭头表示,即在关联关系上更进一步,那这个有什么特点呢?
上面代码中,通过构造函数把大雁传递进来,而大雁是可以单独存在的,这种情况下就是聚合关系。
我们接着看一下Goose
(大雁)和Wing
(翅膀)之间的组合关系:
public class Goose{
//翅膀
private Wing wing;
public Goose(){
this.wing = new Wing();
}
}
用UML图如下:
这里用实心菱形加实线和箭头表示,即在聚合关系上更进一步,那这个有什么特点呢?
从上面代码可以看出,在构造函数中并没有从外面传递翅膀实例进来,这是因为从实际来看翅膀实例是没有意义的,从代码来看,创建Goose
实例时就需要在其内部创建必要的成员变量Wing
,这个和上面雁群有着本质区别,即Wing
对于Goose
来说更不可分。
实现关系
上面说的类关系都是局部变量(包括方法参数和返回值)和成员变量和其他类的关系,而实现关系就是Java中实现接口的意思,就是该类实现了接口中的抽象方法。
实现关系比较容易理解,直接看下面代码:
//飞翔接口
public interface IFly {
void fly();
}
//实现接口
public class Goose implements IFly{
@Override
public void fly() {
}
}
用类图表示如下:
这里使用虚线加三角箭头表示,其中我的理解是这样:前面例子中Car
需要Engine
实例,所以从Car
指向Engine
是我需要这个实例,而本例中Goose
实例创建也需要IFly
类,但是从类角度来说,IFly
为Goose
的父类,所以这里的三角箭头类似找到父类的含义,而虚线则是比后面泛化的实现关系更弱一点。
小节:
- 实现关系就是
Java
中实现接口的意思,表示子类和父类之间的关系。 - 箭头表示需要这个类,而三角箭头表示找到父类的意思。
泛化关系
泛化其实就是我们平时代码中的继承关系,通过类的继承我们可以大量复用父类的功能,所以泛化关系是所有关系中最强的。
这里为什么叫做泛化呢?其实继承是我们从子类的角度来看的,而泛化表示一个通用的模型,他是从父类的角度来看的。
所以泛化的关系更容易理解了:
//大雁继承至动物类
public class Goose extends Animal{
private Wing wing;
public Goose(Wing wing){
this.wing = wing;
}
}
用类图表示如下:
这里用实线加三角箭头来表示,这里我的理解是上面实现关系的更进一步,所以用实线来表示。
到这里,我们已经介绍完了6种类之间的关系,以及他们用什么符号来表示,总结图表如下:
关系 | 简介 | 图形表示 |
---|---|---|
依赖关系 | 最小耦合的关系,表示为类和其局部变量的关系,比如方法参数、返回值,关系的生命周期也是方法的生命周期。 | 虚线 + 箭头 |
关联关系 | 表示类和成员变量之间的关系,关系的生命周期和类一样。 | 实线 + 箭头 |
聚合关系 | 关联关系的一种,注重整体和部分之间的关系,但是部分可以脱离整体存在。就比如学校和老师之间的关系。同时在代码中,虽然也是成员变量,部分的实例一般是通过构造参数传入,即可以单独存在。 | 空心菱形 + 实线 + 箭头 |
组合关系 | 关联关系的一种,强调是组合,也是整体和部分之间的关系,但是部分不能脱离整体存在。就比如大雁类和翅膀类,翅膀类是无法单独存在的。 | 实心菱形 + 实线 + 箭头 |
实现关系 | 即Java中实现接口的关系,是子类和父接口之间的关系。 | 虚线 + 三角箭头 |
泛化关系 | 即Java中继承父类的关系,是子类和父类之间的关系。 | 实线 + 三角箭头 |
Mermaid使用
理解了上面6种关系后,尤其需要从代码角度来理解,还是非常好理解的。然后我们就可以使用工具来画出UML
类图,这里具体的工具,我就不推荐了,我这里推荐一个使用markdown
编辑器来绘图。
为什么我建议使用markdown呢?原因是使用一些画图工具很容易丢失原图文件,而使用一些MD
编辑器绘制的UML
图是和文本保存在一块的,不容易丢失也容易再次修改。
现在很多MD
编辑器都使用了Mermaid
工具,项目地址如下:
简单使用
默认效果如下图:
而上面的UML
类图的代码如下:
所以现在我就来带大家来学习一下这个Mermaid
中类图的使用。
Mermaid
类图使用
定义类
UML
提供了表示一个类信息的机制,包括类的属性和方法,以及一些额外的比如依赖、继承等关系,一个单独的类在UML
中被分为3个部分:
- 顶部部分包含类的名字,加粗和居中显示,第一个字母大写,可以标明该类类型,比如是接口还是枚举等。
- 中间部分包含类的属性,他们都是靠左排列,按类型 属性样式排列。
- 下面部分包含该类可执行的操作,就是指方法,同样靠左排列,按方法 类型样式排列。
比如下面代码:
或者
效果如图:
上面代码非常容易理解,也是使用class
关键字来定义类,然后使用{}
直接定义属性和方法,或者使用:
来定义属性和方法。
同时,这里可以表明类的类型,主要有以下几种:
<<Interface>>
表示类为接口。<<Abstract>>
表示类为抽象类。<<Enumeration>>
表示类为枚举类。
比如下面代码:
定义一个接口:
再比如下面代码:
定义了一个枚举类:
可见性修饰符
我们在定义Java
类时,可以对类的变量设置可见性修饰符,在Mermaid
中,使用下面几个符号来简化可见性修饰符:
+
代表Public
-
代表Private
#
代表Protected
~
代表Package/Internal
这个就不举例了,上面的例子中,都有使用这些符号。
关系符号
类与类之间的关系在前面说类图关系我们已经说过了,相信大家多看几遍,也是很容易记住的,现在我们来看看在Mermaid
中是如何表示的。
所有的关系和使用的符号以及效果如下表:
关系类型 | 描述 | 使用符号 | 效果 |
---|---|---|---|
依赖关系 | 最弱的关系,表示用的关系,使用在类中的局部变量和返回类型,用虚线加箭头表示 | classA ..> classB | ![]() |
关联关系 | 表示拥有的关系,使用在类中表示为成员变量,用实线加箭头表示 | classA --> classB | ![]() |
聚合关系 | 关联关系的一种,表示部分和整体的关系,但是部分可以脱离整体存在,用空的菱形加实线和箭头表示 | classA o--> classB | ![]() |
组合关系 | 比聚合关系更强一点,表示部分不能脱离整体存在,用实心菱形加实线和箭头表示 | classA *--> classB | ![]() |
实现关系 | 表示代码中实现接口的关系,用虚线加三角箭头表示 | classA ..|> classB | ![]() |
泛化关系 | 表示代码中的继承关系,用实线加三角箭头表示 | classA --|> classB | ![]() |
其实把UML
图中各种关系搞明白和图示搞明白,在Mermaid
中用的符号也非常好理解,即: --
代表实线,..
表示虚线,o
表示空菱形,*
表示实菱形,>
表示箭头,|>
表示三角箭头。
添加额外说明
有时为了更好理解,可以在定义关系时,额外添加说明。比如下面代码:
这里添加额外说明是实现关系,效果如图:
再比如下面代码:
这里额外添加说明是继承关系,效果如图:
关系的多重性
这个是啥意思呢?比如前面所说的Car
类和Engine
类,就是一对一的关系,即一辆车就一辆引擎;但是比如GooseGroup
和Goose
就是一对多的关系,所以这里可以使用符号来表示这种关系。
使用的符号和含义如下表:
符号 | 含义 |
---|---|
1 | 仅有一个 |
0..1 | 0个或者1个 |
1..* | 1个或者多个 |
n | n个,n>1 |
0 .. n | 0到n个,n>1 |
1 .. n | 1到n个,n>1 |
上面说起来比较抽象,比如下面代码:
这里对关联关系做了更细一步的说明,即一对一、一对多等情况,效果如下:
这里也很容易理解,就不用赘述。
IDE插件
上面我们介绍了使用MD编辑器来快速绘制类图,可以在我们阅读源码时,快速构建各个类之间的关系,除了这种方法外,我们还可以使用Android Studio
的插件来生成类图,可以快速帮助我们建立思维联系。
这里推荐使用一个插件:PlantUML,大家可以自己在插件市场中进行安装,安装重启后,我们来看看效果。
比如我这里以MagicIndicator
库为例,点击整个Java
代码包,右击->PlantUML Parser
,经过短暂的解析过程后,就得到了如下的类图:
部分截图:
这里可以发现生成的类图特别大,但是内容特别全,对于我们看源码有极大帮助,可以快速建立类之间的关系。
总结
UML
类图的使用,不仅可以帮助我们快速地记录和捋清楚类之间的关系,同时不同关系的区分也让我们更加理解类本身含义的设计。
转载自:https://juejin.cn/post/7130890838658203679