合成/聚合复用原则CARP

7 阅读6分钟

合成/聚合复用原则是什么

合成/聚合复用原则就是在一个新的对象里面使用一些已有的对象,使之成为新对象的一部分,新的对象通过向这些对象的委派达到复用已有功能的目的。

合成和聚合的区别

合成和聚合均是关联的特殊种类。聚合用来表示“拥有”关系或者整体与部分的关系;而合成则用来表示一种强得多的“拥有”关系。在一个合成关系里,部分和整体的生命周期是一样的。一个合成的新的对象完全拥有对其组成部分的支配权,包括它们的创建和湮灭等。

更进一步来讲,一个合成的多重性(Multiplicity)不能超过 1,换言之,一个合成关系中的成分对象是不能与另一个合成关系共享的,一个成分对象在同一个时间内只能属于一个合成关系。如果一个合成关系湮灭了,那么所有的成分对象要么自己湮灭所有的成分对象(这种情况较为普遍),要么就得将这一责任交给别人(这种情况较为罕见)。

复用的基本种类

由于合成或聚合可以将已有的对象纳入到新对象中,使之称为新对象的一部分,因此新的对象可以调用已有对象的功能。这样做有下面的好处:

  • 新对象存取成分对象的唯一方法是通过成分对象的接口。
  • 这种复用是黑箱复用,因为成分对象的内部细节是新对象所看不见的。
  • 这种复用支持包装。
  • 这种复用所需的依赖较少。
  • 每一个新的类可以将焦点集中在一个任务上。
  • 这种复用可以在运行时间内动态进行,新对象可以动态地引用与成分对象类型相同的对象。

一般而言,如果一个角色得到了更多的责任,那么可以使用合成/聚合关系将新的责任委派到合适的对象。

当然,这种复用也有缺点,最主要的缺点就是通过使用这种复用建造的系统会有较多的对象需要管理。

通过继承达到复用的目的

合成/聚合作为复用的手段可以应用到几乎任何环境中去。而与合成/聚合不同的是,继承只能应用到很有限的一些环境中去。换言之,尽管集成是一种非常重要的复用手段,但是应用首先考虑使用合成/聚合,而不是继承。

继承的种类

继承是面向对象的语言特有的复用工具,而且是最容易被滥用的复用工具。这里讨论的继承,是指从一个 Java 类到另一个 Java 类的实现性继承,也就是实现继承,并不包括接口继承。

继承复用通过扩展一个已有对象的实现来得到新的功能,基类明显地捕获共同的属性和方法,而子类通过增加新的属性和方法来扩展超类的实现。继承是类型的复用。

在面向对象的设计理论早期,设计师十分热衷于继承,好像继承就是最好的复用手段。随着时间的推移和实践经验的积累,逐渐认识到了继承关系的缺点。

继承复用的优点

  • 新的实现较为容易,因为超类的大部分功能可以通过继承关系自动进入子类。
  • 修改或者扩展继承而来的实现较为容易。

继承复用的缺点

  • 继承复用破坏包装,因为继承将超类的实现细节暴露给子类。由于超类的内部细节常常是对子类透明的,因此这种复用是透明的复用,又称为“白箱”复用。
  • 如果超类的实现发生改变,那么子类的实现也不得不发生改变。因此,当一个基类发生改变时,这种改变会像水中投入石子引起的水波一样,将变化一圈又一圈地传导到一级又一级的子类,使设计师不得不相应地改变这些子类,以适应超类的变化。
  • 从超类继承而来的实现的静态的,不可能在运行时间内发生改变,因此没有足够的灵活性。

如何做到合成/聚合复用原则

区分“Has-A”与“Is-A”

“Is-A”表示一个类是另一个类的“一种”。而“Has-A”则不同,它表示某一个角色具有某一项责任。

导致错误地使用继承而不是合成/聚合的一个常见的原因是错误地把“Has-A”当做“Is-A”。“Is-A”代表一个类是另一个类的一种;“Has-A”代表一个类是另一个类的一个角色,而不是另一个类的一个特殊种类。

这就是说,当一个类是另一个类的角色时,不应当使用继承描述这种关系。

UML类图

继承复用类图

image.png

组合复用类图

image.png

示例代码

public class Multiplex {
    public static void main(String[] args) {
        Color color = new Red("red");
        Energy energy = new Electric("electric");

        Car car = new Car();
        car.setColor(color);
        car.setEnergy(energy);
        car.move();
    }
}

class Car {
    private Color color;
    private Energy energy;

    public void setColor(Color color) {
        this.color = color;
    }

    public void setEnergy(Energy energy) {
        this.energy = energy;
    }

    public void move() {
        System.out.println("A " + this.color.getColor() + " car is running by " + this.energy.getEnergy());
    }
}

interface Color {
    String getColor();
}

class Red implements Color {
    private String name;

    public Red(String name) {
        this.name = name;
    }

    @Override
    public String getColor() {
        return this.name;
    }
}

class White implements Color {

    private String name;

    public White(String name) {
        this.name = name;
    }

    @Override
    public String getColor() {
        return this.name;
    }
}

interface Energy {
    String getEnergy();
}

class Petrol implements Energy {
    private String name;

    public Petrol(String name) {
        this.name = name;
    }

    @Override
    public String getEnergy() {
        return this.name;
    }
}

class Electric implements Energy {
    private String name;

    public Electric(String name) {
        this.name = name;
    }

    @Override
    public String getEnergy() {
        return this.name;
    }
}

与其它设计原则的关联

与里氏代换原则联合使用

里氏代换原则是继承复用的基石。如果在任何使用 B 类型的地方都可以使用 S 类型,那么 S 类型才能称为 B 类型的子类型,而 B 类型才能称为 S 类型的基类型。

换言之,只有当每一个 S 在任何情况下都是一种 B 的时候,才可以将 S 设计成为 B 的子类。如果两个类的关系是 “Has-A” 关系而不是 “Is-A” 关系,这两个类一定违反里氏代换原则。

只有这两个类满足里氏代换原则,才有可能是 “Is-A” 关系。