UML类间关系

类的关系

在UML类图中,常见的有以下几种关系:

  • 泛化/继承(Generalization)
  • 实现(Realization | Implementation)
  • 关联(Association)
  • 聚合(Aggregation)
  • 组合(Composition)
  • 依赖(Dependency)

泛化/继承(Generalization)

【泛化关系】:是一种继承关系,表示一般与特殊的关系。它指定了子类如何特化父类的所有特征和行为。例如:老虎是动物的一种,即有老虎的特性也有动物的共性。
【关系描述】:”is a” relationship
【箭头指向】:带空三角箭头的实线,箭头指向父类;
【示例解析】:Apple(子类) 继承了 Fruit(父类);

image-20240318113802885

实现(Realization | Implementation)

【实现关系】:是一种类与接口的关系,表示类是接口所有特征和行为的实现。
【箭头指向】:带三角箭头的虚线,箭头指向接口;
【示例解析】:BattleState(实现类) 实现了 State(接口 | 抽象类);

image-20240318134118035

组合(Composition)

【组合关系】:是 [强]整体与部分的关系,且部分不能离开整体而单独存在,不可分离 。如公司和部门是整体和部分的关系,没有公司就不存在部门。
组合关系是关联关系的一种,是比聚合关系还要强的关系,它要求普通的聚合关系中 代表整体的对象负责代表部分的对象的生命周期 。强耦合关系
【代码体现】:成员变量;
【箭头及指向】:带实心菱形的实线,菱形指向整体;
【示例解析】:将 部门 组合到 公司 中;

image-20240318134424429

聚合(Aggregation)

【聚合关系】:是整体与部分的关系,且部分可以离开整体而单独存在,可分离。如:车和轮胎是整体和部分的关系,轮胎离开车仍然可以存在。
聚合关系是关联关系的一种,是一种强关联关系;关联和聚合在语法上无法区分,必须考察具体的逻辑关系。
【关系描述】:”has a” relationship
【代码体现】:成员变量;
【箭头及指向】:带空心菱形的实心线,菱形指向整体;
【示例解析】:将 Wheel 聚合到 Car 中;

image-20240318134354221

关联(Association)

【关联关系】:是一种拥有的关系,它使一个类知道另一个类的属性和方法;关联具有导向性:即双向关系 或 单向关系。如:老师与学生,丈夫与妻子关联可以是双向的,也可以是单向的。
【代码体现】:成员变量;
【箭头指向】:带普通箭头的实心线,双向关联是有两个箭头 或 没有箭头的实线,单向的关联有一个箭头,指向被拥有者。
【示例解析】:老师与学生是双向关联,老师有多名学生,学生也可能有多名老师。但学生与某课程间的关系为单向关联,一名学生可能要上多门课程,课程是个抽象的东西他不拥有学生。
下图为自身关联:

image-20240318134318679

依赖(Dependency)

【依赖关系】:是一种使用的关系,即:一个类的实现需要另一个类的协助,所以要尽量不使用双向的依赖,会导致循环依赖。
【代码表现】:局部变量、方法的参数或者对静态方法的调用;
【箭头及指向】:带箭头的虚线,指向被使用者;
【示例解析】:BattleState(依赖者) 依赖 Weapon(被依赖者),即:BattleState类中使用到了Weapon类对象;
【使用方法】:

  • 类中使用到了另一个类的实例对象;
  • 类中的方法使用到了另一个类的实例对象;
  • 某类的实例作为另一个类的成员属性;
  • 某个类作为另一个类中方法的入参类型;
  • 某个类作为另一个类中方法的返回值类型;
image-20240318134458387

小结:

各种关系的强弱顺序:
泛化 = 实现 > 组合 > 聚合 > 关联 > 依赖


封装、继承、聚合

封装:封装的是属性,封:private  装:set、get

可以看做将属性和get/set方法捆绑的过程。

优点:
1、防止对封装数据进行未经授权的访问,提高安全性。使用者只能通过事先预定好的方法来访问数据,可以方便地加入控制逻辑,限制对属性的不合理操作;
2、有利于保证数据的完整性;
3、便于修改,增加代码的可维护性;
4、隐藏一个类的实现细节。

强内聚弱耦合 | 高内聚低耦合
一个类通常就是一个小的模块,我们应该让模块仅仅公开必须要让外界知道的内容,而隐藏其他一切内容。我们在进行程序的详细设计时,尽量避免一个模块直接修改或操作另一个模块的数据。
高内聚:让一个类功能尽量强大;
低耦合:如果多个类通信,尽量单线联系;

继承

子类继承父类的(非私有)属性、方法,不继承构造器,实现对类的复用;
所有的类的最根源的父类是Object。

Object类下的通用方法(所有类都有这些方法):

  • clone克隆:创建一个和自己一样的对象,深度克隆,但是如果是嵌套的对象,每一层对象类都需要实现Cloneable接口并重写Object的clone()方法,比较繁琐,这种场景推荐使用序列化和反序列化;
  • toString:变成字符串
  • finalize:垃圾回收,他是最后一道防线轮询堆地址是否有栈在调用。      
  • wait:让当前线程等候
  • notify:唤醒被等待的线程
  • getClass:得到当前对象的运行时类
  • hashCode:这是当前对象的hash码。让当前对象唯一,便于查找。
  • notifyAll:唤醒全部线程。
  • equals:判断堆的值是否相等。

Java中继承为单继承,可多实现。

聚合|组合(为了不使用继承,组合聚合复合原则)

组合(composite)、聚合(aggregation):如果想复用一个类,除了继承以外,还可以把该类当做另一个类的组成部分,从而允许新类直接复用该类的public方法,不管是继承还是组合、聚合,都允许在新类中直接复用旧类的方法。
组合、聚合 是直接把旧类对象作为新类的属性嵌入,用于实现新类的功能,通常需要在新类里使用private修饰符嵌入该类对象。 并在构造器中实现对该对象成员变量的实例化。
组合和聚合从复合原则上是一样的,都是将复用类作为一个另一个类的组成部分。不同之处在于组合的限制更严格,复用类的生命周期追随调用类的生命周期,复用类对象无法单独存在,当调用类对象消失时复用类也要消失;而聚合中复用类可以单独存在。在实际开发中多使用聚合,此处使用聚合为例。

封装的实现

常见的封装有两种实现方式:

  • 继承
  • 聚合

继承有封装的作用,通过子类的public方法进行暴露。如果父类中的属性和方法很多,使用继承会导致子类过多的暴露父类的方法,包括不需要暴露的内容,这样就降低了封装性。此时可以考虑使用静态内部类,将待继承的父类收敛到子类中,在外部类中定义一个静态内部类来继承目标父类,并在外部类中创建静态内部类的对象,用于执行具体的操作,而外部类只定义必须暴露的public方法,这样就有效提升了外部类的封装性,暴露的方法都是必须要暴露的,没有多余不受控制的public方法。
阅读JDK源码,其实发现很多源码都是这么玩的,都是定义一个静态内部类去继承目标父类,然后在外部类中使用该静态内部类对象继承的方法,确实是高度封装外部类的好玩法!!!
其实对于继承父类并保证高封装性的平衡有两种方式或者说两种场景:

  1. 单个类需要使用目标父类的方法:类中定义静态内部类来继承目标父类;
  2. 多个类需要使用目标父类的方法:单独定义一个类用于继承目标父类,提高复用性;

Demo

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
/**
* LinkedHashLRUCache中定义的方法的底层实现都是LinkedHashMap,没有直接选择继承LinkedHashMap类进行方法暴露的原因如下一段文档注释.
* This class is not thread-safe class.
*
* @param <K>
* @param <V>
*/
public class LinkedHashLRUCache<K, V> implements LRUCache<K, V> {

private final int limit;
private final InternalLRUCache<K, V> internalLRUCache;

public LinkedHashLRUCache(int limit) {
Preconditions.checkArgument(limit > 0, "The limit big than zero.");
this.limit = limit;
this.internalLRUCache = new InternalLRUCache<>(limit);
}

@Override
public void put(K key, V value) {
this.internalLRUCache.put(key, value);
}

@Override
public V get(K key) {
return this.internalLRUCache.get(key);
}

@Override
public void remove(K key) {
this.internalLRUCache.remove(key);
}

@Override
public int size() {
return this.internalLRUCache.size();
}

@Override
public void clear() {
this.internalLRUCache.clear();
}

@Override
public String toString() {
return internalLRUCache.toString();
}

/**
* 此处定义了一个内部类来继承LinkedHashMap,而不是在外部类上直接继承LinkedHashMap,因为如果在外部类上直接继承LinkedHashMap,
* 那么调用方就可以看见LinkedHashMap的所有方法,这样就降低了代码的封装性,我只希望调用者看见我定义的public方法.所以考虑使用聚合
* 方式引入LinkedHashMap的子类对象,通过自定义的public方法进行暴露.
* 又因为这个LinkedHashMap的子类只在当前类中被访问,所以就直接定义成当前类的静态内部类,更能体现良好的封装性.而没有定义成单独类.
* 如果有很多地方就要聚合LinkedHashMap的子类,那么就要考虑将其定义为一个单独的外部类,方便调用.
*
* @param <K>
* @param <V>
*/
private static class InternalLRUCache<K, V> extends LinkedHashMap<K, V> {

private final int limit;

public InternalLRUCache(int limit) {
super(16, 0.75f, true);
this.limit = limit;
}

@Override
protected boolean removeEldestEntry(Map.Entry<K, V> eldest) {
return size() > limit;
}
}
}

上述是针对一个类进行封装,如果有多个外部类需要进行封装,考虑将继承了父类的静态内部类单独定义成一个基类,然后让所有的外部类都继承这个基类,这样可以提升多个外部类的封装性。