UML建模与软件开发设计(六)——类图设计与类之间的关系

3.2.5.关联关系的分类

 然而,通过关联关系来描述类与类时还是比较抽象,有些关系的细节难以通过关联关系表达出来,比如类A与类B的角色定位、数量关系,关联方向等都描述得不够清晰准确(但你不能说仅仅通过关联关系描述类与类的关系是一种错误的表达方式,事实上,它是对的,只不过有时候描述得还不够清晰而已)。为了更加清晰地表达类与类之间的关联关系,UML又将关联关系细分为以下几个具体类型:

在这里插入图片描述
图3.2.5-1关联关系的分类

3.2.5.1.聚合关系

**聚合关系(Aggregation)**表示整体与部分的关系,是一种不稳定的包含关系,它描述了“has a”(即持有/拥有)的关系。在聚合关系中,成员对象是整体对象的一部分,但是成员对象可以脱离整体对象而独立存在。例如,学校与老师属于聚合关系,因为老师属于这所学校的职工,但该老师可以辞职去另一所学校任教;企业和职员是聚合关系,因为职员是某企业的成员之一,但假如这家企业倒闭了,却并不影响该职员去另一家企业任职;汽车与轮胎是聚合关系,因为轮胎属于汽车的一部分,但轮胎可以脱离汽车而独立存在;台式电脑与鼠标、键盘也是聚合关系,因为鼠标、键盘属于台式电脑的一部分,但鼠标、键盘可以单独拆出来组装到另一台电脑上;订单记录与订单中的商品属于聚合关系,如果订单被取消了,但订单中绑定的产品“解绑”后仍然可以售卖给其他消费者;模态框控件与模态框中的按钮控件是聚合关系,因为按钮控件可以脱离模态框而独立存在或应用到其他控件中。
聚合关系是关联关系的一种特例,是一种较强的关联关系,因此聚合关系也具有关联关系的导航性和多重性。
聚合关系的导航性:是指将一个类的实例聚合到另一个类的实例中(把类A的实例聚合到类B的实例中,还是把类B的实例聚合到类A的实例中);
聚合关系的多重性:类A聚合了一个类B的实例是单聚合,聚合了多个类B的实例是多聚合。
在UML中,聚合关系用带空心菱形的直线表示,并且空心菱形所在的一方表示整体,而另一方表示部分,即菱形从部分指向整体。例如,下面的类图描述了学校与老师之间的聚合关系:
在这里插入图片描述
图3.2.5.1-1学校与老师的聚合关系
在代码实现上,聚合关系与关联关系在语法上保持一致,因此只能从定义上理解二者逻辑关系的区别:

public class Teacher {
}
public class School {
  private List<Teacher> teachers;
}
再比如,台式电脑与鼠标、键盘之间的聚合关系的类图表示如下:

在这里插入图片描述
图3.2.5.1-2台式电脑与鼠标、键盘的聚合关系

3.2.5.2.组合关系

**组合关系(Composition)**也表示类之间整体和部分的关系,与聚合关系不同是,在组合关系中,部分不能脱离整体而独立存在,即整体对象可以控制成员对象的生命周期,一旦整体对象不存在,成员对象也将不存在,成员对象与整体对象之间具有同生共死的关系。例如,公司与部门是组合关系,如果公司倒闭了,也就没有部门了;鸟儿和翅膀是组合关系,翅膀不能脱离鸟儿而独立存在;国家与省份是组合关系…这些案例的共同特点在于部分不能脱离整体而独立存在。
组合关系是一种比聚合关系更强的关联关系,它 表示contains-a(包含)的关系,是一种强烈的包含关系。因此组合关系也具有关联关系的导航性和多重性。
组合关系的导航性:是指将一个类的实例组合到另一个类的实例中(把类A的实例组合到类B的实例中还是把类B的实例组合到类A的实例中);
组合关系的多重性:类A组合了一个类B的实例是单组合,组合了多个类B的实例是多组合。
在UML中,组合关系用带实心菱形的直线表示,并且实心菱形所在的一方表示整体,而另一方表示部分,即菱形从部分指向整体。例如,下面的类图描述了公司与部门之间的组合关系:
在这里插入图片描述
图3.2.5.2-1公司与部门的组合关系
在代码实现上,组合关系与聚合关系在语法上保持一致,因此只能从定义上理解二者逻辑关系的区别:

public class Dept {
}
public class Company {
  private List<Dept> depts;
}

再比如,下面的类图描述了国家与省份之间的组合关系:
在这里插入图片描述

3.2.5.3.关联关系与聚合关系的联系与区别

(1)联系:聚合关系是一种特殊的关联关系,代码实现与普通的关联关系保持一致;
(2)区别:聚合关系比普通的关联关系的关联性要更强,聚合关系强调整体与部分的关联关系,而普通的关联关系中,类与类之间的地位一般是平等的;此外,在UML类图中,关联关系使用一根没有箭头的实线表示,而聚合关系则使用一根空菱形且菱形指向整体的实线来表示。

3.2.5.4.聚合关系与组合关系的联系与区别

(1)联系:聚合关系与组合关系都属于整体与部分的关联关系;它们的代码实现是一致的;
(2)区别:在聚合关系中,“部分”能脱离“整体”独立存在,部分”的生命周期与“整体”的生命周期无关,而在组合关系中,“部分”不能脱离整体而独立存在,“部分”的生命周期与“整体”的生命周期有关;此外,在UML类图中,关聚合关系使用一根空菱形且菱形指向整体的实线来表示,而组合关系用一根实心菱形且菱形指向整体的实线来表示。
(3)判断聚合关系和组合关系的依据:
 第一步:判断两个类之间是否存在整体和部分的关联关系,若不存在则是普通的关联关系,否则进行第二步判断;
 第二步:在整体与部分的关联关系中,如果“部分”可以从“整体”拆分出去而形成一个独立的事物(拆分后不需要考虑“整体”能否独立工作,但要确保“部分”可独立工作),那么就是聚合关系,反之则是组合关系。

3.2.5.5.有向关联

3.2.5.5.1.有向关联的基本概念

有时候,两个对象不是整体与部分的关联关系(既不是聚合关系,也不是组合关系),这两个对象都是独立的个体,或者说它们的地位或等级是一样的,但是又要产生关联,那么只能是普通的关联关系。例如,老师与学生,老师与学生都是独立的个体,他们的地位或等级是一样的,此时我们可以通过导航性来具体描述这类事物间的关联关系。我们把从导航性的角度来描述事物关系的关联关系称为有向关联。

3.2.5.5.2.有向关联的分类

根据导航性,可将关联关系分为有向关联分为单向关联和双向关联。

3.2.5.5.2.1.单向关联

只在一个方向上可以导航的关联关系称为单向关联(Unidirection Association)。在UML中,单向关联用一条带箭头的实线来表示,并且箭头指向成员属性对应的类型。例如,产品与计量单位(计量单位是产品的成员属性,但产品不可能是计量单位的成员属性,所以产品与计量单位构成单向关联关系。此外,产品与计量单位也存在聚合关系)、客户与收货地址(不存在聚合关系)、人与身份证(存在聚合关系)都是单向关联。例如,下面的类图描述了客户与收货地址之间的单向关联关系:
在这里插入图片描述
图3.2.5.5.2.1-1客户与收货地址之间的单向关联
在代码实现上,单向关联与普通的关联关系在语法上保持一致:

public class Address {
}
public class Customer {
  private Address address;
}

3.2.5.5.2.1.1.自关联

特别地,我们把指向对象本身的单向关联称为自关联。也就是说,在自关联中,类的成员属性是该类本身。自关联通常应用于链表、树等数据结构的结点类:
在这里插入图片描述
图3.2.5.5.2.1.1-1自关联
对应的Java代码片段如下所示。

public class Node {
  private Node node;
}

3.2.5.5.2.2.双向关联

在两个方向上都可以导航的关联称为双向关联(Bidirection Association)。在UML中,双向关联用一条没有箭头的实线来表示(即关联关系的默认表示)。例如,客户与产品之间是典型的双向关联,客户可以购买产品,卖出的产品也总会与对应的客户关联;班级与学生可认为是双向关联,一个班级包括了若干学生,而学生总会有某个班级与之对应;购房者与房产也构成了一个双向关联关系。例如,下面的类图描述了客户与产品之间的双向关联关系:
在这里插入图片描述
图3.2.5.5.2.2-1双向关联
对应的Java代码片段如下所示。

public class Product {
  private Customer customer;
}
public class Customer {
  private List<Product> products;
}

3.2.5.6.多重性关联与数据表间关系

导航性和多重性是描述事物关联关系的两个不同维度。因此除了从导航性来描述事物的关联关系外,我们更多的是从多重性这个维度去描述具有(普通)关联关系特征的事物。多重性关联又称重数性关联(Multiplicity Association),它用来描述两个关联对象在数量上的对应关系或者表示一个类的对象与另一个类的对象所关联的个数。例如,人与身份证属于多重性关联中的一对一关联关系,每个人只有一个唯一的有效身份证件;班级与学生构成多重性关联且属于一对多的关联关系,因为一个班级由多个学生组成,反之,学生与班级构成一对一的关联关系,因为一个学生只能对应一个特定的班级,因此班级与学生构成双向多重性关联关系;老师与学生之间构成多重性关联且属于多对多的关系,一个老师可以带多个学生,而一个学生可以学多个老师的课程,其他常见的多对多关联关系的还有:订单与产品种类,一个订单包含了多种不同的商品,而同一种商品同时也可能对应多个不同的订单;学生与课程,一个学生可以选择多门课程学习,一门课程也可以被多名学生选择;用户与角色(权限角色),一个用户可对应多个不同的权限角色,不同的权限角色拥有不同的访问权限,一个角色也可以授权给多个不同的用户…
在UML中,对象之间的多重性关联可以直接在关联直线的基础上用一个数字或一个数字范围表示:
在这里插入图片描述
在这里插入图片描述

3.2.5.6.1.一对一关联关系及一对一表间关系设计

例如,下面的类图描述了人与身份证的一对一关联关系:
在这里插入图片描述
图3.2.5.6-1人与身份证的一对一关联关系
对应的Java片段如下所示。

public class IDCard {
}
public class Person {
  private IDCard idCard;
}
在关系型数据库中,表与表之间的关系分为一对一、一对多和多对多,这体现了[关系型]数据库的核心设计思想。本章节介绍一对一表间关系的表结构设计。首先,两张独立的表一般是无法直接产生关联关系的,为了让两张表在逻辑上产生关联关系,必须借助非业务字段让两张表产生联系,一种通用的设计方案是在这两个表的其中一个表中添加一个外键(关于外键约束的话题争论将会在数据库设计一章中提及),使得外键字段值等于另一张表的主键字段值,确保这两张表在逻辑上产生相应的关联关系,我们把具有此外键的表被称为主表的从表或子表,以外键作为主键的表称为主表或父表。外键除了可以让两张独立的表产生关联关系外,还以用来消除数据冗余,以便保持数据的完整性、统一性。通常,我们可将从表的外键抽象为一个实体类型,因此在单向的一对一关系中,一般将关联有引用类型属性的类设计为从表,将另一个类设计为主表。例如,在人与身份证的关联关系中,可将身份证设计为主表,人设计为从表(主表和从表通常是根据业务需要决定,与表的主、次顺序无关)。人与身份证的表结构如下所示。

(1)身份证信息表
在这里插入图片描述
(2)人员信息表
在这里插入图片描述

3.2.5.6.2.一对多关联关系及一对多表间关系设计

下面的类图描述了班级与学生的多重性关联关系:
在这里插入图片描述
图3.2.5.6-2班级与学生的双向多重性关联关系
对应的Java代码片段如下所示。

public class Classes {
  private List<Student> students;
}
public class Student {
  private Classes classes;
}
在上面的双向一对多的表间关系中,班级代表“一”,而学生代表“多”。那么外键应该设计到班级信息表中还是应该设计到学生信息表呢?我们假定应设计到班级信息表中,如下所示:

(1)创建学生信息表
在这里插入图片描述
(2)创建班级信息表
在这里插入图片描述
注意到,由于外键fstu_id被设计在班级信息表中,导致班级信息表记录中的班级名称重复,使得t_classes表的fid已失去唯一性的意义(因为主键的职责就是保证数据唯一性),因此上述设计降低了t_classes表的可读性,让 t_classes表的数据变得难以维护、数据结构不够清晰。现在我们将外键设计到t_student表中:
(1)创建班级信息表
在这里插入图片描述
(2)创建学生信息表
在这里插入图片描述
我们注意到,将外键设计到学生信息表中后,学生信息表和班级信息表的数据分布比较均匀,提升了数据的可读性,显然这种设计比之前的设计更为合理一些。因此,外键应该设计到一对多关系中的“多”方中。

3.2.5.6.3.多对多关联关系及多对多表间关系设计

下面的类图描述了老师与学生的多重性关联关系:
在这里插入图片描述
图3.2.5.6-3老师与学生的双向多重性关联关系
从逻辑表达上来说,上述这样的类图设计是没有问题的,然而根据这样的类图设计出的数据表在一定程度上降低了数据的可读性。在前面,我们已经介绍了一对多表间关系的处理,外键应该设计到一对多关系中的“多”方中,那么这种方案对于多对多表间关系是否同样适用呢?根据上述类图原型,若采用“一对多表间关系”的方式进行设计,那么数据表设计如下所示:
(1)将学生信息表设计为主表
在这里插入图片描述
(2)将老师信息表作为从表,其中fstu_id为外键,字段值为学生信息表的主键:
在这里插入图片描述
我们注意到,在从表t_teacher中,fid = 1001、1003和fid = 1002、1004所对应的老师名字、身份证都相同,因此,由于外键fstu_id的存在,使得t_teacher表的fid失去唯一性的意义了(因为主键原则就是保证数据唯一性),这一设计降低了t_teacher表的可读性,使得t_teacher表的数据变得难以维护、数据结构不够清晰,最关键的是,这种设计无法很好地描述多对多表间关系,例如上述的老师信息表无法描述同一个老师给多个学生上课这种业务,原因是主键字段fid不能像学生信息表那样作为某个老师的唯一ID标识描述,即fid已经失去了业务意义(我们可以增加一个fteacher_id业务字段来解决这个问题)。因此,“一对多表间关系”的表设计方案无法适用于“多对多表间关系”。那么怎么办呢?为了确保“多对多表间关系”中的表数据都具有可读性,这里就需要借助“中间关系表”来完成了,具体设计思路如下:

设计一张中间关系表,中间表的字段分别为学生信息表的主键、老师信息表的主键,同时多对多中的表都不增加外键,而是通过中间表完成多对多表间的关系映射。可见,在多对多表间关系中,不再使用一对一和一对多关系中的外键维护表间的关联关系,而是通过中间关系表来维护多对多关系中表的关联关系。

该设计思路可以用下图来描述:
在这里插入图片描述
中间关系表的设计原理在于将多对多的关系转换为两个一对多的关系,即表1与中间关系表构成一对多的关系,表2与中间关系表构成一对多的关系,这样就从原的[表1:表2]的[m:n]转换为[表1:中间关系表]的[1:m]和[表2:中间关系表]的[1:m]。这是因为中间关系表的一组主键字段可以设计为中间关系表的联合主键,用来确定一组唯一的关联关系(相当于“一对多”中的“一”),利用该原理,可以推广到若干个多对多的表间关系的处理。
通过上述分析,我们可以在学生类和老师类中增加一个中间关系类—课程类,学生类和老师类与课程类的关联关系类图如下所示:
在这里插入图片描述
图3.2.5.6.3-1学生类与课程类的一对多关联关系
在这里插入图片描述
图3.2.5.6.3-2老师类与课程类的一对多关联关系

完整的双向多重性关联关系类图如下所示:

在这里插入图片描述
注意到,学生类与课程类、老师类与课程类均符合一对多的类图结构特点,因此学生类与课程类、老师类与课程类均构成了一对多关联关系。
对应的Java代码如下所示。

package com.yzh.demo.uml.mulitiassociation.biddirrection;

import java.util.List;

/**
 * @description:学生类
 * @author: yzh
 * @date: 2022-06-07 5:10
 */
public class Student {

  private Long id;

  private String name;

  private List<Course> courses;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public List<Course> getCourses() {
    return courses;
  }

  public void setCourses(List<Course> courses) {
    this.courses = courses;
  }
}

package com.yzh.demo.uml.mulitiassociation.biddirrection;

import java.util.List;

/**
 * @description:老师类
 * @author: yzh
 * @date: 2022-06-07 5:10
 */
public class Teacher {

  private Long id;

  private String name;

  private List<Course> courses;

  public Long getId() {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getName() {
    return name;
  }

  public void setName(String name) {
    this.name = name;
  }

  public List<Course> getCourses() {
    return courses;
  }

  public void setCourses(List<Course> courses) {
    this.courses = courses;
  }
}

package com.yzh.demo.uml.mulitiassociation.biddirrection;

/**
 * @description:课程类,充当中间关系类角色
 * @author: yzh
 * @date: 2022-06-07 5:11
 */
public class Course {

  private Student student;

  private Teacher teacher;

  public Student getStudent() {
    return student;
  }

  public void setStudent(Student student) {
    this.student = student;
  }

  public Teacher getTeacher() {
    return teacher;
  }

  public void setTeacher(Teacher teacher) {
    this.teacher = teacher;
  }
}

package com.yzh.demo.uml.mulitiassociation.biddirrection;

/**
 * @description: 客户端测试类
 * @author: yzh
 * @date: 2022-06-07 5:12
 */
public class Client {

  public static void main(String[] args) {
    Student student = new Student();
    // 1.保存学生信息
    saveStudent(student);
    Teacher teacher = new Teacher();
    // 2.保存老师信息
    saveTeacher(teacher);
    // 3.保存课程信息,记录学生与老师的关联关系
    saveCourse(student, teacher);
  }

  private static void saveStudent(Student student) {
    System.out.println("保存学生信息...");
  }

  private static void saveTeacher(Teacher teacher) {
    System.out.println("保存老师信息...");
  }

  private static void saveCourse(Student student, Teacher teacher) {
    Course course = new Course();
    course.setStudent(student);
    course.setTeacher(teacher);
    System.out.println("保存课程信息...");
  }

}
//~output:
保存学生信息...
保存老师信息...
保存课程信息...
根据上述的分析,学生信息表、老师信息表和课程信息表设计如下:

(1)创建学生信息表
在这里插入图片描述
(2)创建老师信息表
在这里插入图片描述
(3)创建课程信息表。该表用来充当中间关系表,数据表记录的作用是使得对应的数据表产生关联关系的依据,我们不妨将学生信息表主键联合老师信息表的主键设计为课程信息表的联合主键,也可以单独设计一个fid字段作为课程信息表的主键:
在这里插入图片描述
与之前的方案相比,学生信息表和老师信息表数据的可读性和易维护性大大提升。

3.3.依赖关系

依赖(Dependency)关系是一种使用关系,也就是在某个类中使用了另一个类的实例或该实例的方法,这便是依赖关系。在UML中,依赖关系用带箭头的虚线表示,且箭头的方向为:由依赖的一方指向被依赖的另一方。
依赖关系通常通过以下3种方式来实现:

 将一个类的对象作为另一个类中方法的参数;
在这里插入图片描述
图3.3-1类A依赖类B的methodB()方法
对应的Java代码片段如下:

public class A {
  public void methodA(B b){
    b.methodB();
  }
}
public class B {
  public void methodB(){}
}

 在一个类中将另一个类的对象作为其局部变量;例如,通过new关键字创建对象、常量的引用等等。
在这里插入图片描述
图3.3-2类A依赖类B实例的methodB()方法
对应的Java代码片段如下:

public class A {
  public void methodA(){
    B b = new B();
    b.methodB();
  }
}
public class B {
  public void methodB(){}
}

 在一个类的方法中调用另一个类的静态方法或使用super/this关键字调用方法。
在这里插入图片描述
图3.3-3类A依赖类StringUtils的isEmpty()方法
对应的Java代码片段如下:

public class A {
  public void methodA(String str){
    if(StringUtils.isEmpty(str)) {
      // ...
    }
  }
}

3.4.泛化关系

泛化关系(Generalization)也就是继承关系,用于描述父类与子类之间的关系,父类又称基类或超类,子类又称作派生类。在UML中,泛化关系用带空心三角形的实线表示。
在泛化关系中,超类中需要被子类继承的属性、方法通常都会使用保护类型访问修饰符(protected)修饰,在UML中用符号“#”表示“protected”。一般而言,在泛化关系的类图中,子类不需要显式地声明继承自父类的方法,此时只需要显式地声明自身新增的方法,但如果子类重写了父类的某个方法,那么必须在子类类图中显式地声明该方法,以体现子类重写了父类的方法。例如:

在这里插入图片描述
图3.4-1泛化关系
在类图3.4-1中,该泛化关系表示类SubForm继承自类AbstractForm,由于method1()方法属于保护类型方法,因此,实际上在子类SubForm中包含了自身定义的method2()方法和从父类继承到的method1()方法。对应的Java代码片段如下:

public class AbstractForm {
  protected void method1(){}
}
public class SubForm extends AbstractForm {
  public void method2() {
    this.method1();
  }
}
注意:若子类重写了父类的method()方法,但仍然希望在当前子类的其他方法中调用父类的method()方法,可以通过super关键字调用父类的method()方法,若使用this或省略this调用method()方法,则表示调用的是当前子类中重写的method()方法。例如:
public class AbstractForm {
  protected void method1(){
  }
  protected void method(){
    System.out.println("AbstractForm.method");
  }
}
public class SubForm extends AbstractForm {
  public void method2() {
    this.method1();
    super.method();
    this.method();
  }
  @Override
  public void method() {
    System.out.println("SubForm.method");
  }
  public static void main(String[] args) {
    SubForm form = new SubForm();
    form.method2();
  }
}
//~output:
AbstractForm.method
SubForm.method

3.5.接口与实现关系

接口和实现类之间存在一种实现(Realization)关系,在这种关系中,实现类实现了接口所有的抽象方法。在Java中,通过关键字implements来表示实现关系。而在UML中,类与接口之间的实现关系用带空心三角形的虚线来表示,通常实现类所实现的接口的抽象方法,都需要在实现类中显式声明出来,以体现实现类实现了接口对应的抽象方法。例如:

在这里插入图片描述
图3.5-1实现关系
对应的Java代码片段如下所示。

public interface IInterface {
  void method();
}
public class ImplClass implements IInterface {
  @Override
  public void method() {
  }
}
  • 0
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值