类与类之间的 6 种关系

1. 前言

2016 年 8 月我去面试 Android 的时候,遇到一个 Java 大牛,问过我这个问题。我确实不知道类与类之间竟然会有 6 种关系。面试后,虽然查了一下,但是依然不理解:类与类之间这 6 种关系是怎么总结出来的。

这篇文字就用来介绍类与类之间的 6 种关系,如何使用 UML 表示类与类之间的 6 种关系,如何更好地记忆 6 种关系的表示。

2. 正文

2.1 继承(generalization)

这个大家都不陌生,可以说是非常熟悉了。使用继承,我们能够以现有类为基础(获取它的全部可继承的成员),通过添加或修改一些成员来创建新类。源类常被称为基类、超类或者父类,导出类常被称为继承类或者子类。以经典的几何形的例子来说明:

在上面的图中,Shape 类是父类,而 CircleSquareTriangle 是三个子类。我们使用空心三角箭头+实线,来表示子类与父类之间的继承关系。需要注意的是,箭头是由子类指向父类的。

通过继承,子类获得了父类的全部可继承的成员;但是,子类难道仅仅是为了获取父类的全部可继承的成员而已吗?

显然不是,如果仅仅那样,何不直接使用父类呢?

实际上,子类通常会和父类产生差异,有两种办法可以实现。

第一种产生差异的办法是直接在子类中添加新的方法(这些新方法并不是父类的方法),这样就扩展了接口,这种情况下子类和父类是 is-like-a(像是一个)关系。
第二种产生差异的办法是改变现有父类的方法的行为(这被称为覆盖 override),这种情况下子类和父类是 is-a(是一个)关系。替代原则适用于这种情况。

从这里,我们也可以了解到,单说继承关系是 is-a 的关系,是不准确的。

2.2 实现(realization)

这是类与接口之间的关系。注意类实现接口,需要用到的是 implements 关键字。下边使用 Java 编程思想中的例子说明:

代码如下:

interface Instrument {
    void play();
    String what();
    void adjust();
}

class Wind implements Instrument {
    @Override
    public void play() {
        System.out.println("Wind " + "play()");
    }
    @Override
    public String what() {
        return "Wind what()";
    }
    @Override
    public void adjust() {
        System.out.println("Wind " + "adjust()");
    }
}

class Percussion implements Instrument {
    @Override
    public void play() {
        System.out.println("Percussion " + "play()");
    }
    @Override
    public String what() {
        return "Percussion what()";
    }
    @Override
    public void adjust() {
        System.out.println("Percussion " + "adjust()");
    }
}

class Stringed implements Instrument {
    @Override
    public void play() {
        System.out.println("Stringed " + "play()");
    }
    @Override
    public String what() {
        return "Stringed what()";
    }
    @Override
    public void adjust() {
        System.out.println("Stringed " + "adjust()");
    }
}

对应的 UML 类图如下:

在上图中,WindPercussionStringed 类实现了 Instrument 接口。在图中,使用空心三角箭头+虚线来表示它们之间的实现关系。

到这里,我们知道:继承关系是空心三角箭头+实线表示,实现关系是空心三角箭头+虚线表示。记忆方法:继承一般可以获取父类的实现好的实实在在的方法,所以用实线;实现只是从父类获取了方法的声明,父类并没有实现,这不实在,所以用虚线。

2.3 依赖(dependency)

依赖关系是一种使用关系。

依赖关系在代码中如何体现呢?

比如如果在类 A 中,通过 new 关键字创建了另一个类 B 的实例, 那么独立于类 B 去使用或者测试类 A 是不行的。 这时,类 A 和类 B 就存在 dependency。 类 A 被叫做 dependant(依赖他人生活的人),类 B 被叫做 dependency(依赖)。 A dependant depends on its dependencies,在这里, 类 A 依赖于类 B。依赖可以理解为耦合。

查询 dependency 的解释是:(尤指过分或不利的)依靠,依赖。我们从解释中,可以看到在代码中出现依赖时是不好的,而依赖确实是有缺点的:代码复用性差,难以测试,难以维护。

依赖的分类:类依赖,接口依赖,方法/属性依赖,直接或间接的依赖。

在 UML 图中,使用箭头+虚线来表示。
在这里插入图片描述

2.4 关联(association)

表示两个不相关的对象之间的关系。两个单独的类通过它们的对象发生关联。两个类是不相关的,也就是说一个类可以单独存在在另外一个类不存在的情况下。可以是一对一、一对多、多对一、一对一的关系。

在 UML 图中,使用箭头加实线来表示。

2.5 聚合(aggregation)

聚合是关联的一种。如果组合是动态发生的,通常被称为聚合(aggregation)。

聚合是一种弱的“拥有”关系,称为“has-a” 关系,它体现的是 A 对象包含 B 对象,但 B 对象并不是 A 对象的一部分。比如车队里包含有多个车,人群里包含有多个人,雁群里包含多只大雁。它只是一种单方向的关系,比如车队包含车,但车并不包含车队。


聚合关系使用空心菱形+实线表示。

2.6 组合(composition)

使用现有的类合成新的类,这就是组合(composition)。

组合经常被视为“contains-a”(拥有)关系,组合是一种强的“拥有”关系,体现的是严格的部分和整体的关系,部分和整体的生命周期是一样的(整体离不开部分,部分也离不开整体)。比如汽车拥有引擎,人拥有腿。在 UML 图中,使用实心菱形+实线来表示组合关系。

组合非常直观,只需要在新类中产生现有类的对象,这样新的类就由现有类的对象所组成。组合只是复用了现有代码的功能,而不是现有代码的形式。

使用代码表示如下:

public class Car {
    private Engine mEngine;
}

class Engine {
}

对应的 UML 类图:

上图中的数字表示 1 个 Car 对应着 1 个Engine

public class Person {
    private Leg mLeg;
}

class Leg {
}

对应的 UML 图:

上图中的数字表示 1 个 Person 对应着 2 个Leg

3. 最后

简单地介绍了一下类与类之间的 6 种关系,希望能够帮助到大家。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章