前言
UML(Unified Modeling Language 统一建模语言)是用来对软件密集系统进行可视化建模的一种语言。
简单来说,UML 类图就是用来表示一个软件系统中各个类的特征及相互关系的图。这里说的软件系统可能很庞大,是一个实实在在的复杂软件架构(如 Android 系统),也可能只是某种抽象表示(比如设计模式)。
下面就以 Java 语言为例,看看如何用 UML 类图表示 Java 中类、接口及其复杂的关系。
类
类使用包含其 类名,属性,方法名及其参数并且用分割线分隔的长方形表示。在长方形中属性及方法名又有如下规则。
类中成员变量表示
可见性 名称:类型 [ = 默认值 ]
其中可见性用+,-,* 三种符合表示,意义如下。
可见性 | 含义 |
---|---|
+ | public |
- | private |
* | protected |
~ | package private |
类中方法表示
可见性 名称(参数列表) [ : 返回类型]
方法中可见性的含义和成员变量一致,只是名称由属性名变成了方法名。
假设现有类 Student.java
1 | public class Student { |
那么他所对应的 UML 类图如下:
接口 & 抽象类
通过上述信息,已经阐述了 UML 表示 Java Class 所需要的所有基础知识,剩下的接口、抽象类包括抽象方法、静态类、抽象方法和枚举类只是在其基础上扩展,对类的定义有一些稍微的差异而已。
上图简单展示了 Java 集合类之间的关系,下面详细的梳理一下如何用 UML 图描述类之间的关系。
类之间的关系
在 UML 中,类(这里泛指 Java 中的类和接口)之间的关系可以用以下 6 种形态表达。
- 继承(泛化): 子类和父类用带空心三角的直线表示,箭头指向父类;
- 实现: 就是接口的实现类和接口之间的关系,用带空心三角的虚线表示,箭头指向接口;
- 依赖:就是一个类中包含另一个类的对象,用带箭头的虚线表示,箭头指向另一个对象对应的类;
- 关联:一个类和另一个类有联系,用带箭头的直线表示,箭头指向被包含类;
- 聚合:整体和部分的关系,部分可以脱离整体存在;用带空心菱形的直线表示,箭头指向代表部分的那个类;
- 组合:整体和部分的关系,部分受整体的影响,整体不存在时部分也将消失,用带实心菱形的直线表示,箭头指向代表部分的那个类;
为了加深印象,下面通过图示再来详细阐述一下以上的内容。
继承(泛化)
这个最好理解了,比如 Android 中的 View 树。 这里需要注意的是,接口之间也是可以有继承关系的
用带空心三角的直线表示,箭头指向基类
实现
同样以 Android 中 View 和 RecyclerView 为例,他们各自实现了不同的接口 。
用带空心三角的虚线表示,箭头指向接口
依赖
依赖关系是类与类之间最弱的关系,依赖可以简单的理解一个类使用了另一个类。同时被依赖的类,如果发生了变化会影响依赖他的类。
这个最好的解释就是 View 的 draw 方法了, 在整个 View 类中,只是需要在 draw 方法中依赖 Canvas 进行绘制操作。onTouchEvent() 的参数 MotionEvent 也是类似。对 View 来说只是依赖这两个类完成一些关键操作,同时 MotiveEvent 有不同的 event 时,View 也需要发生相应的变化。
用带箭头的虚线表示,箭头指向当前类依赖的类
总的来说,依赖关系就是在某个类的方法中需要另一个类,但这种依赖关系不一定表现为形参,具体细节可参考设计模式之 UML 类图。当然,这里遵循设计模式中『依赖注入』的理念,使用形参去注入另外一个类(这里的类泛指类和接口)来实现依赖应该是更好的选择。
关联
Association
关联关系的定义为:对于两个相对独立的对象,当一个对象的实例与另一个对象的一些特定实例存在固定的对应关系时,这两个对象之间为关联关系。关联关系表示一个类和另一类有联系,是一种包含关系。
关联关系分为单向关联和双向关联:在 Java 中,
- 单向关联表现为: 类 A 当中使用了 类 B,其中类 B 是作为类 A 的> 成员变量。
- 双向关联表现为: 类 A 当中使用类 B 作为成员变量,同时类 B 中也使用了类 A 作为成员变量。
这里就需要了解一下,关联和依赖的区别了。在依赖关系中,被依赖的类只是临时被拿来用一下,和当前类只是一种偶然的关系,而在关联关系中,所有被关联的类,和当前类都是有必然联系的类。
这种关系通常使用类的属性表达,比如 Android 中的 ViewGroup 的 LayoutParams 类。
关联关系用带箭头的直线表示,箭头指向当前类关联的类
每个 ViewGroup 必然会有他的 LayoutParams 类,来补充 ViewGroup 的具体信息。
聚合(Aggregation)
聚合关系是关联关系的一种,耦合度强于关联。表示 has-a 的关系,是一种不稳定的包含关系。
这种场景也是比较常见的。比如在为了方便实现 RecyclerView 中的点击事件,一般会在其 Adapter 类中注入一个接口,通过回调的方式来实现点击事件的监听。
1 | public class MyAdapter extends RecyclerView.Adapter { |
这是一种典型的 has-a 关系。
用带空心菱形的直线表示,菱形从局部指向整体
组合
组合关系也是一种部分和整体的关系,但是部分存活周期受到整体的影响,若整体不存在则部分也将不存在。
这个就很好理解,我们平时写的 Activity 或 Fragment 和其内部成员的关系就是这样,Acitivity 或 Fragment 内的各种 View,Presenter,ViewModel 对象,一旦这个 Activity 或 Fragment 销毁了,那么这些内容基本也就没有存在的意义了。
用用带实心菱形的直线表示,菱形从局部指向整体
组合关系的另一个例子就是自定义 View 的实现。
比如下面一个自定义闹钟的实现 ClockView(节选部分,完整源码可参考ClockView) 中;用 Paint,TextPaint,RectF,ValueAnimator 组合起来,完成了这个自定义 View ,但是同时当这个自定义不存在时,这些类就没什么存在的意义了。
1 | public class ClockView extends View { |
总结
- 上述六种类的关系,按照 依赖>关联>聚合>组合>继承>实现 的顺序,他们的耦合度依次增强。这就是为什么设计模式中,提倡我们依赖注入的原因,同时我们也可以看到组合的耦合度是低于继承的,所以在继承一个父类之前,我们可以优先考虑一下组合的可能性。
- 结合上调耦合度关系,及组合、聚合和关联的定义,我们可以知道,在代码层面这三种关系其实是非常相似的,我们很难直接从代码分析出类之间的 关联、聚合、组合这三种关系来,需要结合其具体的语义才能分辨。
- 关于关联,聚合、组合更友好的解释。
聚合和组合的关系,表示一个类对象持有另外的类对象作为自己的一个属性.
关联仅仅表示一个对象跟另外的对象发生了通信, 没有持有它作为对象.
所以人们说: 关联描述的是方法层次上的交互, 而聚合和组合是描述属性层次上的交互!
For Example: 邮递员和你的交互, 属于关联, 因为他只是拜访你, 给你送一封信.
邮递员和邮局的关系,属于聚合或者组合{暂且只区分关联关系}.
原因是: 你打电话给邮递员送信,邮递员不一定听你的, 而邮局就不同了, 它要自己的
邮递员送信, 邮递员敢不听话吗?
这就是关联跟其它两种方法的区别, 你没有持有对方做为自己的属性, 用一次,算别人给你
的恩赐, 但你不可能肆意指挥它
聚合和组合的关系:
他们都将对象持有为属性. 但这是不同的.
比如你买的书, 和你借的书. 现在都放在家里. 但概念是不同的, 你买的书,可以随便处理,
鬼划糊涂,但图书馆的借来的书,你能这么干吗? 聚合:大家都可持有,但不可完全占有,组合:
私有财产.