likes
comments
collection
share

效率提升 | UML类图

作者站长头像
站长
· 阅读数 17

前言

为什么写这篇文章呢?原因是最近在阅读Android源码时,里面涉及的知识点特别多,方法调用链也非常长,而之前我又没有画类图的习惯,导致走了不少弯路。

在经过思考后,我发现合理使用UML图可以提高我们阅读源码的效率可以快速构建模块之间(类之间)的关系,更好地理解代码

本篇文章先从类图来介绍UML图。

正文

文章从俩个方面来介绍UML类图:

  1. 首先是UML类图的含义,各种图形表示什么意思;
  2. 然后再说一下我们如何使用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类完全正常使用,我们需要EnginePhone的实例,所以Car类就依赖于EnginePhone类。

但是在UML类图中却不是的,UML中定义依赖为偶合最小的关系,这里只有Phone为依赖关系。因为这里的Phone是方法的参数,不仅仅是参数,还包括方法中的变量和返回值Phone的实例在方法执行完就销毁了,就认为和原类Car的耦合关系最小。

从另外一个角度来说,假如我们就只想定义一下依赖关系,即Car依赖于Phone,我们使用UML类图表示如图(虚线加箭头):

Car
Phone

这时把上述关系转换成代码后,Car类中并不会有Phone类型的成员变量,这就是关键。

小节

  • 依赖关系认为是耦合最小的关系,即被依赖的对象只会是方法的局部变量、参数和返回值不会是成员变量
  • 使用虚线加箭头表示,虚线表示关系很浅,耦合少,箭头指向被依赖的类,表示需要这个类。

关联关系

关联关系比依赖关系更进一步,它可以描述为"Has a",即"拥有"关系。

上面依赖关系理解清楚后,关联关系就是一句话:被关联类的实例是本类的成员变量

比如还是上面代码,Engine的实例engineCar的成员变量,这时EngineCar的关系就是关联关系

还是一样逆向思维,假如我们用类图说明Car关联Engine如图(实线加箭头):

Car
Phone

上述UML图转换成代码后,Car类中就会生成一个Engine类型的成员变量,这是关键。

和依赖关系一起小节

  1. 关联关系比依赖关系更强,因为被关联的对象是本类成员变量,没有它,更没法使用。
  2. 生命周期角度来看,关联关系的类实例和本类的生命周期是一样的,而依赖关系只和其中方法的生命周期一样。
  3. 表示都是线加箭头,箭头的意思就是"找到"的意思,我需要找到这个类,才可以完成我自己的功能,而线的虚实就是关系的强弱

聚合关系和组合关系

聚合关系和组合关系是关联关系的细分类,即关联关系可以分为聚合关系和组合关系,其实这俩种关系不用分的如此仔细,在实际使用中就可以用关联关系来表示。

既然提及,就要说一下,组合关系比聚合关系更强,这里的思考角度就要从整体和部分之间的关系来说:

  • 聚合关系表示整体和部分之间的关系,比如学校和老师这俩种关系密切的类,我定义学校类时可以在其中定义一个老师列表的成员变量,同时定义老师类时也可以定义在职学校的成员变量。

    我们可以说这2个类是聚合关系,这里可以把学校看成整体,老师看成整体的一部分,但是老师可以脱离整体(学校)单独存在,他可以选择不在学校里教书,比如私教。

  • 组合关系比聚合关系更强一步就是上面最后一点不一样,组合从字面意思理解就是是组成的一部分,所以在组合关系中,部分对象不能单独存在,即这个部分不能脱离整体存在

    其实这个聚合和组合从抽象概念的意思上容易理解,比如身体和大脑的关系,大脑就无法单独存在。

说完了概念,就回到开头说的问题之一:太过抽象,那在代码中如何看出区别呢?因为聚合和组合都是关联关系的一种,而关联关系在代码中体现就是成员变量,所以这里关键就是成员变量的表现形式

首先是GooseGroup(雁群)和Goose(大雁)的聚合关系:

//雁群
public class GooseGroup {

    public Goose goose;

    public GooseGroup(Goose goose){
        this.goose = goose;
    }
}

用UML图如下:

GooseGroup
+ Goose goose
GooseGrooup(Goose goose)
Goose

这里用空心菱形加实线和箭头表示,即在关联关系上更进一步,那这个有什么特点呢?

上面代码中,通过构造函数把大雁传递进来,而大雁是可以单独存在的,这种情况下就是聚合关系。

我们接着看一下Goose(大雁)和Wing(翅膀)之间的组合关系:

public class Goose{

    //翅膀
    private Wing wing;

    public Goose(){
        this.wing = new Wing();
    }
}

用UML图如下:

Goose
-Wing wing
Goose(Wing wing)
Wing

这里用实心菱形加实线和箭头表示,即在聚合关系上更进一步,那这个有什么特点呢?

从上面代码可以看出,在构造函数中并没有从外面传递翅膀实例进来,这是因为从实际来看翅膀实例是没有意义的,从代码来看,创建Goose实例时就需要在其内部创建必要的成员变量Wing,这个和上面雁群有着本质区别,即Wing对于Goose来说更不可分。

实现关系

上面说的类关系都是局部变量(包括方法参数和返回值)和成员变量和其他类的关系,而实现关系就是Java中实现接口的意思,就是该类实现了接口中的抽象方法。

实现关系比较容易理解,直接看下面代码:

//飞翔接口
public interface IFly {

    void fly();

}
//实现接口
public class Goose implements IFly{
    @Override
    public void fly() {

    }

}

用类图表示如下:

IFly
fly()
Goose

这里使用虚线加三角箭头表示,其中我的理解是这样:前面例子中Car需要Engine实例,所以从Car指向Engine我需要这个实例,而本例中Goose实例创建也需要IFly类,但是从类角度来说,IFlyGoose的父类,所以这里的三角箭头类似找到父类的含义,而虚线则是比后面泛化的实现关系更弱一点。

小节:

  • 实现关系就是Java中实现接口的意思,表示子类和父类之间的关系。
  • 箭头表示需要这个类,而三角箭头表示找到父类的意思。

泛化关系

泛化其实就是我们平时代码中的继承关系,通过类的继承我们可以大量复用父类的功能,所以泛化关系是所有关系中最强的。

这里为什么叫做泛化呢?其实继承是我们从子类的角度来看的,而泛化表示一个通用的模型,他是从父类的角度来看的

所以泛化的关系更容易理解了:

//大雁继承至动物类
public class Goose  extends Animal{

    private Wing wing;

    public Goose(Wing wing){
        this.wing = wing;
    }
}

用类图表示如下:

Animal
Goose

这里用实线加三角箭头来表示,这里我的理解是上面实现关系的更进一步,所以用实线来表示。

到这里,我们已经介绍完了6种类之间的关系,以及他们用什么符号来表示,总结图表如下:

关系简介图形表示
依赖关系最小耦合的关系,表示为类和其局部变量的关系,比如方法参数、返回值,关系的生命周期也是方法的生命周期。虚线 + 箭头
关联关系表示类和成员变量之间的关系,关系的生命周期和类一样。实线 + 箭头
聚合关系关联关系的一种,注重整体和部分之间的关系,但是部分可以脱离整体存在。就比如学校和老师之间的关系。同时在代码中,虽然也是成员变量,部分的实例一般是通过构造参数传入,即可以单独存在。空心菱形 + 实线 + 箭头
组合关系关联关系的一种,强调是组合,也是整体和部分之间的关系,但是部分不能脱离整体存在。就比如大雁类和翅膀类,翅膀类是无法单独存在的。实心菱形 + 实线 + 箭头
实现关系即Java中实现接口的关系,是子类和父接口之间的关系。虚线 + 三角箭头
泛化关系即Java中继承父类的关系,是子类和父类之间的关系。实线 + 三角箭头

Mermaid使用

理解了上面6种关系后,尤其需要从代码角度来理解,还是非常好理解的。然后我们就可以使用工具来画出UML类图,这里具体的工具,我就不推荐了,我这里推荐一个使用markdown编辑器来绘图。

为什么我建议使用markdown呢?原因是使用一些画图工具很容易丢失原图文件,而使用一些MD编辑器绘制的UML图是和文本保存在一块的,不容易丢失也容易再次修改。

现在很多MD编辑器都使用了Mermaid工具,项目地址如下:

github.com/mermaid-js/…

简单使用

效率提升 | UML类图

默认效果如下图:

Animal
+int age
+String gender
+isMammal()
+mate()
Duck
+String beakColor
+swim()
+quack()
Fish
-int sizeInFeet
-canEat()
Zebra
+bool is_wild
+run()

而上面的UML类图的代码如下:

效率提升 | UML类图

所以现在我就来带大家来学习一下这个Mermaid中类图的使用。

Mermaid类图使用

定义类

UML提供了表示一个类信息的机制,包括类的属性和方法,以及一些额外的比如依赖、继承等关系,一个单独的类在UML中被分为3个部分:

  1. 顶部部分包含类的名字加粗和居中显示,第一个字母大写,可以标明该类类型,比如是接口还是枚举等。
  2. 中间部分包含类的属性,他们都是靠左排列,按类型 属性样式排列。
  3. 下面部分包含该类可执行的操作,就是指方法,同样靠左排列,按方法 类型样式排列。

比如下面代码:

效率提升 | UML类图 或者

效率提升 | UML类图

效果如图:

BankAccount
+String owner
+Bigdecimal owner
+deposit(amount)
+withdrawal(amount)

上面代码非常容易理解,也是使用class关键字来定义类,然后使用{}直接定义属性和方法,或者使用:定义属性和方法

同时,这里可以表明类的类型,主要有以下几种:

  • <<Interface>>表示类为接口。
  • <<Abstract>>表示类为抽象类。
  • <<Enumeration>>表示类为枚举类。

比如下面代码:

效率提升 | UML类图

定义一个接口:

«interface»
Shape
noOfVertices
draw()

再比如下面代码:

效率提升 | UML类图

定义了一个枚举类:

«enumeration»
Color
RED
BLUE
GREEN
WHITE
BLACK

可见性修饰符

我们在定义Java类时,可以对类的变量设置可见性修饰符,在Mermaid中,使用下面几个符号来简化可见性修饰符:

  • + 代表 Public
  • - 代表 Private
  • # 代表 Protected
  • ~ 代表 Package/Internal

这个就不举例了,上面的例子中,都有使用这些符号。

关系符号

类与类之间的关系在前面说类图关系我们已经说过了,相信大家多看几遍,也是很容易记住的,现在我们来看看在Mermaid中是如何表示的。

所有的关系和使用的符号以及效果如下表:

关系类型描述使用符号效果
依赖关系最弱的关系,表示用的关系,使用在类中的局部变量和返回类型,用虚线加箭头表示classA ..> classB效率提升 | UML类图
关联关系表示拥有的关系,使用在类中表示为成员变量,用实线加箭头表示classA --> classB效率提升 | UML类图
聚合关系关联关系的一种,表示部分和整体的关系,但是部分可以脱离整体存在,用空的菱形加实线和箭头表示classA o--> classB效率提升 | UML类图
组合关系比聚合关系更强一点,表示部分不能脱离整体存在,用实心菱形加实线和箭头表示classA *--> classB效率提升 | UML类图
实现关系表示代码中实现接口的关系,用虚线加三角箭头表示classA ..|> classB效率提升 | UML类图
泛化关系表示代码中的继承关系,用实线加三角箭头表示classA --|> classB效率提升 | UML类图

其实把UML图中各种关系搞明白和图示搞明白,在Mermaid中用的符号也非常好理解,即: -- 代表实线.. 表示虚线o 表示空菱形* 表示实菱形> 表示箭头,|> 表示三角箭头

添加额外说明

有时为了更好理解,可以在定义关系时,额外添加说明。比如下面代码:

效率提升 | UML类图

这里添加额外说明是实现关系,效果如图:

implement
classB
classA

再比如下面代码:

效率提升 | UML类图

这里额外添加说明是继承关系,效果如图:

extends
classB
classA

关系的多重性

这个是啥意思呢?比如前面所说的Car类和Engine类,就是一对一的关系,即一辆车就一辆引擎;但是比如GooseGroupGoose就是一对多的关系,所以这里可以使用符号来表示这种关系。

使用的符号和含义如下表:

符号含义
1仅有一个
0..10个或者1个
1..*1个或者多个
nn个,n>1
0 .. n0到n个,n>1
1 .. n1到n个,n>1

上面说起来比较抽象,比如下面代码:

效率提升 | UML类图

这里对关联关系做了更细一步的说明,即一对一、一对多等情况,效果如下:

1
*
1
1..*
many
Customer
Ticket
Student
Course
Galaxy
Star

这里也很容易理解,就不用赘述。

IDE插件

上面我们介绍了使用MD编辑器来快速绘制类图,可以在我们阅读源码时,快速构建各个类之间的关系,除了这种方法外,我们还可以使用Android Studio的插件来生成类图,可以快速帮助我们建立思维联系。

这里推荐使用一个插件:PlantUML,大家可以自己在插件市场中进行安装,安装重启后,我们来看看效果。

比如我这里以MagicIndicator库为例,点击整个Java代码包,右击->PlantUML Parser,经过短暂的解析过程后,就得到了如下的类图:

效率提升 | UML类图

部分截图:

效率提升 | UML类图

这里可以发现生成的类图特别大,但是内容特别全,对于我们看源码有极大帮助,可以快速建立类之间的关系。

总结

UML类图的使用,不仅可以帮助我们快速地记录和捋清楚类之间的关系,同时不同关系的区分也让我们更加理解类本身含义的设计。