UML中的各种关系
各种关系
名称 | 英文名称 | 符号 | 描述 | 实现方法 | 耦合强度 | 举例 | 关键词 | 备注 |
依赖 | dependency | 1.当类与类之间有使用关系 时就属于依赖关系;2.依赖 不具有“拥有关系”,而是 一种“相识关系”;3.一个 类的实现需要另一个类的协助 |
如果类A访问类B的属性或者方法, 或者类A负责实例化类B,具体地, 被依赖者B是依赖者A的:1.成员 函数返回值、2.形参、3.局部 变量、4.静态方法调用 |
★ | 学生使用电脑 驾驶员使用车 |
使用关系 | ||
关联 | association | 一种常识上的拥有关系,但 双方又在逻辑上各自独立 |
关联者的某数据成员是指向被关 联者的指针或引用 |
★★ | 老师和学生是 双向关联 |
拥有关系 has-a |
细分为:单向关联、 双向关联、 自身关联、多维关联 |
|
聚合 | aggregation | 是整体与部分的关系,且部分 可脱离整体而单独存在 |
同“关联” | ★★★ | 汽车和轮胎 | 整体与部分 单独存在 contains-a |
||
组合 | composition | 是整体与部分的关系,但部分 不能离开整体而单独存在 |
“整体类”的某数据成员存储 了“部分类”的实例 |
★★★★ | 人和头颅 | 整体与部分 不能单独存在 |
||
泛化 | generalization | 类与类之间的继承关系,一般 与特殊的关系,is a |
类继承 | ★★★★★ | 动物和猫 | 一般与特殊 is-a |
||
实现 | implementation | 类与接口之间的实现关系 | 接口实现 | ★★★★★ | 鸡和鸭都实现 下蛋接口 |
联系与区别
依赖与关联
- 实现不同:依赖者不会将被依赖者作为自己的数据成员,但关联会。
- 侧重点不同:依赖强调使用,关联强调拥有。
- 关系产生/结束时机不同:依赖关系是仅当使用关系发生(如类A的成员函数以类B的实例为参数)时而产生,伴随着使用结束而结束。关联关系当A中指向B的指针/引用被赋值时产生,当A的实例销毁时关系结束。
关联与聚合
- 聚合关系是关联关系的一种,是强的关联关系。
- 逻辑关系/层次不同:关联关系所涉及的两个类处在同一个层次上,而聚合关系中,两个类处于不同的层次上,一个代表整体,一个代表部分。
- 指向被关联/聚合者的指针/引用的初始化/赋值形式不同(这一点并非绝对):对于关联,通过A的成员函数( void A::setB(B *pB) )来为A中指向B的指针/引用赋值;对于聚合,通过带参的构造函数( A::A(B *pb) )来初始化A中指向B的指针/引用。
- 联和聚合在语法上无法区分,必须考察具体的逻辑关系。
聚合与组合
- 整体与部分的生命周期不同:对于聚合,整体与部分的生命周期相互独立;对于组合,整体与部分有相同的生命周期,其中代表整体的对象负责代表部分的对象的生命周期。
- 构造函数不同:聚合类的构造函数中包含另一个类的实例作为参数,组合类的构造函数包含另一个类的实例化。
- 信息的封装性不同:聚合关系中,客户端可以同时了解“整体类”与“部分类”;组合关系中,客户端只认识“整体类”,“部分类”被严密封装在“整体类中”,对客户端不可见。
举例说明
下图是设计模式中命令模式(Command Pattern)的UML类图和C++实现,其中代码“改编”自这个开源项目。不熟悉命令模式的读者请自行学习。
1 #include <iostream> 2 3 class Receiver 4 { 5 public: 6 void Action() 7 { 8 std::cout << "Receiver: execute action" << std::endl; 9 } 10 // ... 11 }; 12 13 class Command 14 { 15 public: 16 virtual ~Command() {} 17 virtual void Execute() = 0; 18 19 protected: 20 Command() {} 21 }; 22 23 class ConcreteCommand : public Command 24 { 25 public: 26 ConcreteCommand(Receiver *r) : receiver(r) {} 27 28 ~ConcreteCommand() 29 { 30 if (receiver) 31 { 32 delete receiver; 33 } 34 } 35 36 void Execute() 37 { 38 receiver->Action(); 39 } 40 41 private: 42 Receiver *receiver; 43 }; 44 45 class Invoker 46 { 47 public: 48 void SetCommand(Command *c) 49 { 50 command = c; 51 } 52 53 void ExecuteCommand() 54 { 55 if (command) 56 { 57 command->Execute(); 58 } 59 } 60 61 private: 62 Command *command = nullptr; 63 }; 64 65 int main() 66 { 67 ConcreteCommand command(new Receiver()); 68 69 Invoker invoker; 70 invoker.SetCommand(&command); 71 invoker.ExecuteCommand(); 72 73 return 0; 74 }
下面结合代码进行分析:
- 依赖:这里的 main 函数相当于 Client , main 函数中仅仅创建并使用了 Receiver 、 ConcreteCommand 、 Invoker 的实例,相当于将它们的实例作为局部变量,因此 main 函数( Client )依赖了这三个类。
- 关联: Invoker 类中有指向 Command 或其子类的指针,该指针通过成员函数 void SetCommand(Command *c) 被赋值,因此 Invoker 关联了 Command 。
- 聚合: ConcreteCommand 类中有指向 Receiver 的指针,该指针通过构造函数 ConcreteCommand(Receiver *r) 在 ConcreteCommand 实例被创建的同时被初始化,因此,
ConcreteCommand
聚合了Receiver
。
需要特别说明的是,受GoF的《Design Patterns: Elements of Reusable Object-Oriented Software》误导,几乎所有资料的UML类图都将 Ivoker 和 Command 画成了聚合关系,而将 ConcreteCommand 和 Receiver 画成了关联关系(正好跟上图相反),这显然是不对的。理由如下:
- “绑定”关系的牢固性不同:
- Invoker 和 Command 通过 void Invoker::SetCommand(Command *c) 建立关系,并且,通过多次调用该函数,可以对 Invoker::command 这个成员变量赋不同的值,即 Invoker 实例可以“绑定”不同的 Command 实例,这种“绑定”是不稳定的,可更换的。
- 反观 ConcreteCommand ,它通过自身的构造函数 ConcreteCommand(Receiver *r) 与 Receiver 实例“绑定”,并且 ConcreteCommand 实例一旦构造完成,它所“绑定”的 Receiver 实例就不能换成其它 Receiver 实例了。显然, ConcreteCommand 和 Receiver 之间的“绑定”关系,要比 Invoker 和 Command 之间的“绑定”关系牢固得多。
- 关系层次不同:
- 普通成员函数 void Invoker::SetCommand(Command *c) 暗示 Invoker 和 Command 之间是同一个层次上的“平等”关系。
- 构造函数 ConcreteCommand(Receiver *r) 暗示 ConcreteCommand 和 Receiver 之间是整体与部分的关系。
参考