类间关系及UML表示
文章目录
类间关系是理解面向对象设计原则基础和设计模式的基础,因此笔者整理了一番类间关系及其UML表示相关的知识,也作为个人学习的总结。可以说比较详细了吧,其中,也写了一些实例来帮助概念的理解。
1.依赖(dependency)关系
对于两个相对独立的对象,当一个对象负责构造另一个对象的实例,或者依赖另一个对象的服务时,这两个对象之间主要体现为依赖关系。
可以简单的理解为,一个类A使用到了另一个类B,而这种使用关系是具有偶然性的、临时性的、非常弱的,但是B类的变化会影响到A。
例如:充电电池通过充电器来充电,自行车通过打气筒来充气,人借助螺丝刀拧螺丝,人借用船过河,艺术家鉴赏艺术品,警察抓小偷,学生读书,某人买车。
表现在代码层面,为类B作为参数被类A在某个方法中使用,例如:局部变量,方法中的参数,对静态方法的调用等。
class B{}
class A{
//作为方法中的参数
f1(B b)
//作为返回值
B f2()
//局部变量
f3(){B b = new B();}
}
例如:
class Car
{
// Do something
}
class Person
{
void move(Car car);
}
说明:人的快速移动需要有车的协助, 但是这种依赖是比较弱的, 就是人也可以不用车而用其他工具, 注意:与关联不同的是人不必拥有这辆车,而只要使用就行。
UML表示方法:虚线箭头,类A指向类B。
依赖关系的分类
依赖关系可分为单向依赖、自身依赖、双向依赖。以下举实际例子分别说明。
(1)单向依赖
一只老鼠在苹果园中吃苹果,老鼠每吃一个苹果,就根据该苹果的所含能量增加一些体重。结合上边给出的模型,设计老鼠和苹果类。
//苹果类
class Apple {
private int power;
Apple(int e){
this.power = e;
}
int getEnergy(){
return power;
}
}
//老鼠类
class Mouse {
private int weight;
Mouse(int weight){
this.weight = weight;
}
public int getWeight(){
return weight;
}
void eat(Apple apple){
weight+=apple.getEnergy();
}
}
拓展:
或:
(2)自身依赖
例子:
一个游戏中有很多怪物(Monster),怪物之间可能要发生战斗(fight),每场战斗都是一个怪物与另一怪物之间的一对一战斗。每个怪物都有自己的速度(Speed)、生命值(hitpoint)、攻击力值(damage)和防御力值(defense);战斗时,两个怪物依次攻击对方,即怪物a首先攻击怪物b,然后轮到怪物b攻击怪物a,之后,怪物a再次攻击怪物b,…,直到一方生命值为0;战斗时,由速度快的一方首先发起攻击;若速度一样,比较生命值,由高者首先攻击;若生命值也相等,比较攻击力,由高者首先攻击;若攻击力还相等,比较防御力,由高者首先攻击;若四项都相等,则选择任一方首先攻击;怪物A攻击怪物B时,会给怪物B造成伤害,使得怪物B的生命值降低,降低值为:2*A的攻击力-B的防御力,最小为1。请根据你对上述描述的理解,定义并实现怪物类Monster,成员的设计可以任意,但要求该类至少有一个成员函数fight,用来描述与另外一个怪物进行战斗的过程。不必考虑怪物的生命值减少至0后如何处理。
怪物类:
class Monster {
private int speed;//速度
private int hitpoint;//生命值
private int damage;//攻击力
private int defense;//防御值
Monster(int spd,int hit,int dam,int def){
this.speed = spd;
this.hitpoint = hit;
this.damage = dam;
this.defense = def;
}
//进行战斗
boolean fight(Monster other){
if(priorTo(other)){ //若由己方攻击
if(attacked(other) == 0){//敌方生命为0时战斗胜利,返回true
return true;
}
}
//由敌方发起,轮流攻击,直至一方生命值为0
while(true){
if(other.attacked(this) == 0){
return false;
}
if (attacked(other) == 0){
return true;
}
}
}
//比较优先级,若四项相同则由调用方攻击
boolean priorTo(Monster other){
if(speed != other.speed){
return speed > other.speed;
}
if (hitpoint != other.hitpoint) {
return hitpoint > other.hitpoint;
}
if (damage != other.damage) {
return damage > other.damage;
}
if (defense != other.defense) {
return defense > other.defense;
}
return true;
}
//攻击,返回被攻击方的生命值
int attacked(Monster other){
int harm = damage * 2 - other.defense; //攻击实际伤害
if(harm < 1){
harm = 1;
}
other.hitpoint -= harm; //生命值降低
if(other.hitpoint < 0){
other.hitpoint = 0;//生命值最低为0
}
return other.hitpoint;
}
}
测试类:
class TestMonster {
public static void main(String [] args){
Monster a = new Monster(10,200,7,8);
Monster b = new Monster(10,150,8,7);
if(a.fight(b)){
System.out.println("A win !");
}else{
System.out.println("B win !");
}
}
}
扩展:
或:
(3)双向依赖
警察抓住犯人,犯人类中指明被哪个警察抓,这是一个双向依赖。
class Police {
private int award;
Police(){
award = 0;
}
void catchPerson(Person person){};
}
class Person {
Person(){};
void beCatched(Police police){};
}
2.关联(association)关系
对于两个相对独立的对象,当一个对象的实例与另一个对象的一些特定实例存在固定的对应关系时,这两个对象之间为关联关系。
关联关系体现的是两个类、或者类与接口之间语义级别的一种强依赖关系,比如我和我的朋友;这种关系比依赖更强、不存在依赖关系的偶然性、关系也不是临时性的,一般是长期性的,而且双方的关系一般是平等的。
例如:客户和订单(1:N),公司和员工(1:N),主人和汽车(1:N),师傅和徒弟(1:N),丈夫和妻子(1:1),飞机和航班(1:N),学生和课程(N:N)。
class B {...}
class A {
B b;
afun() { b.bfun(); }
}
关联关系的分类
关联关系可以分为单向关联,自身关联和双向关联。
UML表示方法:实线箭头,类A指向类B,表示单向关联。如果使用双箭头或不使用箭头表示双向关联。
(1)单向关联
单向关联是指只有某一方拥有另一方的引用,这样只有拥有对方者可以调用对方的公共属性和方法。
如下面代码:
class Husband
{
private int money;
public void goShopping();
};
class Wife
{
CHusband pHuband;
};
上面代码中妻子拥有丈夫,可以使用对方的属性,比如钱,可以让对方做能做的事,比如购物。
- 单向关联实例:
学生管理程序中学生和宿舍。每个学生的信息除了包括姓名、学号等之外,还要有宿舍信息。宿舍信息包括几号楼,第几层,几号房间,以及住了哪几个学生等信息。
class Student
{
private Dorm mpDorm;
Student(Dorm aDorm){ }
int DormFloor(){
return mpDorm.floor( );
}
}
class Dorm {}
-
扩展:
class Parent{}
class A {
A{};
Parent p;
void func( );
}
(2)自身关联
自身关联是指拥有一个自身的引用。
例:链表中的节点类
class Node{
private int data;
private Node node;
Node(int data){
this.data = data;
}
}
(3)双向关联
双向关联是指双方都拥有对方的引用,都可以调用对方的公共属性和方法。
class Husband
{
private Wife wife;
}
class Wife
{
private Husband huband;
}
上面代码中丈夫和妻子是比较公平的关系,都可以使用对方公共的属性。
3.聚合/聚集(aggregation)关系
当对象B被加入到对象A中,成为对象A的组成部分时,对象A和对象B之间为聚合关系。
聚合是关联关系的一种特例。聚合指的是整体与部分之间的关系,体现的是整体与部分、拥有的关系,即has-a的关系,此时整体与部分之间是可分离的,可以具有各自的生命周期,部分可以属于多个整体对象,也可以为多个整体对象共享。
例如:自行车和车把、响铃、轮胎,汽车和引擎、轮胎、刹车装置,计算机和主板、CPU、内存、硬盘,航母编队和航母、驱护舰艇、舰载机、潜艇,课题组和科研人员。
与关联关系的比较:
-
在代码层面,聚合和关联关系是一致的,只能从语义级别来区分。
-
关联关系中两个类是处于相同的层次,而聚合关系中两个类是处于不平等的层次,一个表示整体,一个表示部分。
class B {...} class A { B b; .....}
UML表示方法:尾部为空心菱形的实线箭头(也可以没箭头),类A指向类B。
聚合示例:
学生宿舍管理程序中宿舍和学生。宿舍有多个学生,宿舍建立时,可以指定,也可不指定对应的多个学生,并且以后可以随时增删学生。
class Dorm {
private int maxCount;
Student []students;
Dorm(Student[] s,int count){
maxCount = count;
students = new Student[maxCount];
for (int i = 0; i <count ; i++) {
students[i] = s[i];
}
}
void addStudent(Student s , int index){
if(students[index] == null)
students[index] = s;
}
void removeStudent( int index){
students[index] = null;
}
}
扩展
简单聚合有多种扩展。
-
扩展1:如果篮和水果
-
扩展2:如防盗门和锁
-
扩展3:如防盗门和报警器
-
扩展4
-
扩展5
4.组合/合成(composition)关系
组合也是关联关系的一种特例,体现的是一种contains-a的关系,这种关系比聚合更强,也称为强聚合;同样体现整体与部分间的关系,但此时整体与部分是不可分的,整体的生命周期结束也就意味着部分的生命周期结束。整体类负责部分类对象的生存与消亡。
例如:公司和部门,人和大脑、四肢,窗口和标题栏、菜单栏、状态栏。
与关联、聚合的区别:
在代码层面,组合和关联关系是一致的,只能从语义级别来区分。
组合跟聚合几乎相同,唯一的区别就是“部分”不能脱离“整体”单独存在,就是说, “部分”的生命期不能比“整体”还要长。
class Company
{
Department department[n];
}
class Department
{
// Do something
}
上边的例子中,部门是部分,公司是整体,部门不能脱离公司而存在,一旦公司破产了,部门也就不存在了,它们具有相同的生命周期。
UML表示方法:尾部为实心菱形的实现箭头(也可以没箭头),类A指向类B
5.泛化(generalization)关系
泛化是一种一般与特殊、一般与具体之间关系的描述,具体描述建立在一般描述的基础之上,并对其进行了扩展。
比如狗是对动物的具体描述,一般把狗设计为动物的子类。
在代码层面,泛化是通过继承实现的。
class Animal{}
class Dog extends Animal{}
UML表示方法:空心三角形箭头的实线,子类指向父类
6.实现(realization)关系
实现是一种类与接口的关系,表示类是接口所有特征和行为的实现。从广义上来说,类模板和模板类也是一种实现关系。
在代码层面,实现一般通过类实现接口来描述的。
Interface IAnimal{}
class Animal implements IAnimal
{
// Do something
}
UML表示方法:空心三角形箭头的虚线,实现类指向接口。
泛化与实现的区别
泛化和实现的区别就在于子类是否继承了父类的实现,如有继承则关系为泛化,反之为实现。
总结
上述几种关系所表现的强弱程度依次为:泛化/实现>组合>聚合>关联>依赖,因此在程序设计的过程中,应根据业务需求选择尽可能使用关联程度弱的设计。