聊一聊UML中类之间的关联关系

前言

在软件工程中,类图为一种静态的结构图、最常用的UML图,描述了所构建系统的类、接口以及它们之间的静态结构和关系,即类图中包含从用户的客观世界模型中抽象出来的类、类的内部结构和类与类之间的关系,简化了人们对系统的理解。

如我们所知所有的面向对象语言都离不开类的概念,理解了程序中类的设计也就理解了程序的一半。针对面向对象语言中的各种类之间的关联关系,主要包含以下几种关系:依赖、关联、聚合、组合、实现、继承(泛化)。耦合度依次递增,关于耦合度,可以理解为当一个类变化时,对另一个类的影响,如果耦合度越低,影响越小,反之耦合度越高,对有关系的另一个类的影响越大。

UML中箭头的方向

知道对方信息时才能指向对方,如下以继承关系为例:

  • 定义子类时需要通过extends关键字指向父类
  • 子类一定是知道父类定义的,但父类并不知道子类的定义
  • 只有知道对方信息时才能指向对方
  • 所以箭头方向是从类指向

各关联关系对应的UML箭头类型如下:

关联关系 继承(Extends) 实现(Implements) 关联(Association) 依赖(Dependence) 组合(Composition) 聚合(Aggregation)
UML箭头 image.png image.png image.png image.png image.png image.png

泛化(继承) 与 实现

泛化(继承)关系(实线)

泛化(generalization)关系时指一个类(子类、子接口)继承另外一个类(称为父类、父接口)的功能,并可以增加它自己新功能的能力,继承是类与类或者接口与接口最常见的关系,在Java中通过关键字extends来表示。 image.png

实现关系(虚线)

实现(Implements)是指一个class实现interface接口(一个或者多个),表示类具备了某种能力,实现是类与接口中最常见的关系,在Java中通过implements关键字来表示。 image.png

依赖 与 关联

依赖关系(虚线)

  • 临时用一下,表示一种使用关系,一个类需要借助另一个类来实现功能
  • 一般是一个类使用另一个类作为参数(型参、局部变量等)使用,或作为返回值

依赖(dependency)关系也是表示类与类之间的连接,表示一个类依赖于另外一个类的定义,依赖关系时是单向的。简单理解就是类A使用到了类B,这种依赖具有偶然性、临时性,是非常弱的关系。但是类B的变化会影响到类A。举个例子,如人要喝水,水有解渴的功能,则人与水的关系就是依赖,人喝好水之后,与水的关系就解除了,因此是一种弱的连接。在代码层面,为类B作为参数被类A在某个方法中使用。

在java中,依赖表现为:局部变量,方法中的参数和对静态方法的调用。 image.png

关联关系(实线)

  • 表示一个类对象和另一个类对象有关联
  • 通常是一个类中有另一个类对象作为属性

关联(association)关系表示类与类之间的连接,它使得一个类知道另外一个类的属性和方法。

关联可以使用单箭头表示单向关联,使用双箭头或者不使用箭头表示双向关联,不建议使用双向关联,关联有两个端点,每个端点可以有一个基数,表示这个关联的类可以有几个实例。

  • 0..1 表示可以有0个或者1个实例
  • 0..* 表示对实例的数目没有限制
  • 1     表示只能有一个实例
  • 1..* 表示至少有一个实例

关联关系体现的是两个类,或者类与接口之间的强依赖关系,这种关系很强烈,比依赖更强,不是偶然性的,也不是临时性的,而是一种长期性,相对平等的关系,表现在代码层面,为被关联的类B以类属性的形式出现在类A中,也可能是关联类A引用了被关联类B的全局变量。具体点来说比如学生和老师,这里只以一个学生有一个老师为例。

在Java中,关联关系是使用实例变量来实现的

image.png

聚合 与 组合

聚合关系(空心)

  • 整体和局部的关系,两者有着独立的生命周期,是has a 关系

聚合(aggregation)是关联关系的特例,聚合是整个与个体的关系,即has-a关系,此时整体和部分是可以分离的,他们具有各自的生命周期,部分可以属于多个对象,也可以被多个对象共享;比如汽车和轮子,公司与员工的关系;在代码层面聚合与关联是一致的,只能从语义上来区分。 image.png

聚合关系也是使用实例变量来实现的,在java语法上区分不出关联和聚合,关联关系中类处于一个层次,而聚合则明显的在两个不同的层次。

组合关系(实心)

  • 整体局部的关系,和聚合关系相比,关系更加强烈,两者有相同的生命周期,contains-a关系

组合(compostion)也是关联关系的一种特例,体现的是一种contain-a关系,比聚合更强,是一种强聚合关系。它同样体现整体与部分的关系,但此时整体与部分是不可分的,整体生命周期的结束也意味着部分生命周期的结束,反之亦然。如大脑和人类、公司和部门的关系。 image.png

组合与聚合几乎完全相同,唯一区别就是对于组合,部分不可脱离整体单独存在,其生命周期应该是一致的。

总结:

上述逐一介绍了UML中类与类、类与接口、接口与接口的之间的常见关联关系:泛化(继承)实现关联依赖组合聚合 的基本概念和简单UML类图中对应的箭头表示。

但单单了解这些概念还是不够的,重要的是要学以致用,了解什么场景下应该使用什么关联关系?对应的关联关系会带来什么实质性的好处?

关联、依赖、聚合、组合之间的区别?

关联、依赖、聚合、组合的关系很容易搞混。依赖关系是一种弱关联,描述的是类之间的依赖性,只要一个类使用到另一个类就可以把这种关系看成是依赖;而聚合和组合都是关联的一种形式。

共同之处:
当对象A和对象B之间存在关联、依赖、聚合或者组合关系时,对象A都有可能调用对象B的方法。

区别之处:
关联关系: 对于两个相对独立的对象A和B,当一个对象A的实例与B的实例存在固定的对应关系时,这两个对象之间为关联关系。代码中表现为在A中定义了B类型的属性。

依赖关系: 对于两个相对独立的对象A和B,当一个对象A负责注入对象B的实例,或者调用对象B提供的服务时,这两个对象之间主要体现为依赖关系。代码中的表现即将B类型的参数传入A的方法中,而不是在A中定义B类型的属性。

聚合、组合与关联在代码层面并没有明显的区别,需要结合实际的业务环境来判断它们之间的关系。同样的两个类,处在不同的业务环境中,可能它们的关系也不相同。

如何理解组合优于继承,多用组合少用继承?

在面向对象编程中,有一条非常经典的设计原则,那就是:组合优于继承,多用组合少用继承。
这句话怎么理解呢?至于组合与继承,其实通过上面的常见关联关系介绍我们也有所了解两者是不同纬度的关系,如果我们能够清晰明白界限也就不会有这个顾虑了,但是可能往往我们日常码代码时没有太多时间考虑细致的边界问题,会第一反应采用继承的方式去实现代码的复用,减少重复代码,久而久之,就凸显出了组合更好的处理代码复用的问题,所以就有了组合优于继承,其实组合也并不是完美的,继承也并非一无是处

继承的优劣:

  1. 继承是面向对象的三大特性之一,用来表示类之间的 is-a 关系,可以解决代码复用的问题。
  2. 继承虽有诸多作用,但随着类的继承层次会越来越深、继承关系会越来越复杂。而这种层次很深、很复杂的继承关系问题也就表现出来了:
    • 一方面,会导致代码的可读性变差。
      因为我们要搞清楚某个类具有哪些方法、属性,必须阅读父类的代码、父类的父类的代码……一直追溯到最顶层父类的代码。
    • 另一方面,破坏了类的封装特性,将父类的实现细节暴露给子类。
      子类的实现依赖父类的实现,两者高度耦合,一旦父类代码修改,就会影响所有子类的逻辑。

而使用组合意味着要做更细粒度的类的拆分。这也就意味着,我们要定义更多的类和接口。类和接口的增多也就或多或少地增加代码的复杂程度和维护成本。

组合与继承的区别和联系:

  1. 在继承关系中,破坏了类的封装特性,将父类的实现细节暴露给子类。
    子类的实现依赖父类的实现,两者高度耦合,如果父类代码修改,就会影响所有子类的逻辑,这样就可能导致子类行为的不可预知性。

  2. 组合是通过对现有的对象进行拼装(组合)产生新的、更复杂的功能。
    因为在对象之间,各自的内部细节是不可见的。

  3. 继承在写代码的时候就要指名具体继承哪个类,所以,在编译期就确定了关系。
    组合,在写代码的时候可以采用面向接口编程。所以,类的组合关系一般在运行期确定。

  4. 组合是在组合类和被包含类之间的一种松耦合关系,而继承则是父类和子类之间的一种紧耦合关系。

  5. 当选择使用组合关系时,在组合类中包含了外部类的对象,组合类可以调用外部类必须的方法;
    而使用继承关系时,父类的所有方法和变量都被子类无条件继承,子类不能选择。

如何判断该用组合还是继承?

  1. 如果类之间的继承结构稳定(不会轻易改变),继承层次比较浅(比如,最多有两层继承关系),继承关系不复杂,我们就可以大胆地使用继承。
    反之,系统越不稳定,继承层次很深,继承关系复杂,我们就尽量使用组合来替代继承。

  2. 除此之外,还有一些设计模式会固定使用继承或者组合。比如,装饰者模式(decorator pattern)、策略模式(strategy pattern)、组合模式(composite pattern)等都使用了组合关系,而模板模式(template pattern)使用了继承关系。

  3. 有一些特殊的场景要求我们必须使用继承。如果你不能改变一个函数的入参类型,而入参又非接口,为了支持多态,只能采用继承来实现。

之所以很多人都在说“多用组合少用继承”,只是因为,长期以来,我们过度使用继承。我们应该控制好它们的副作用、发挥它们各自的优势,在不同的场合下,恰当地选择使用继承还是组合,这才能帮助我们写出高质量的代码。

猜你喜欢

转载自juejin.im/post/7106134610341281799
今日推荐