设计原则 - 合成复用原则

合成复用原则:类之间尽量使用合成/集合的方式,其次是使用继承

含义

  • 合成复用原则(Composite/Aggregate Reuse Principle,CARP)是指尽量使用对象组合(has-a)、聚合(contanis-a) ​,而不是继承关系达到软件复用的目的。可以使系统更加灵活,降低类与类之间的耦合度,一个类的变化对其他类造成的影响相对较少

  • 继承我们叫做白箱复用,相当于把所有的实现细节暴露给子类。组合/聚合也称之为黑箱复用,对类以外的对象是无法获取到实现细节的

核心思想

  • 优先使用聚合、组合的方式,而不是使用继承

实现方式

合成复用原则是通过将已有的对象纳入新对象中,作为新对象的成员对象来实现的,新对象可以调用已有对象的功能,从而达到复用

复用的基本方式

通常类的复用可分为继承复用(使用继承实现代码的复用性)和合成复用(使用组合或者聚合实现代码的复用性)两种

  1. 组合/聚合(合成复用)

    • 聚合与组合的区别:

      • 聚合是用来表示拥有关系或者整体与部分的关系
      • 组合则是表示一种强得多的拥有关系,在组合里,部分与整体的生命周期是一样的。一个组合的新的对象完全拥有对其组成部分的支配权,包括它们的创建和销毁等。一个组合关系中的成分对象是不能与另一个组合关系共享的。一个组成部分在同一个时间内只能属于一个组合关系
  2. 继承(继承复用)

复用两种方式的区别

  1. 组合/聚合:是将已有的对象纳入到新对象中,使之成为新对象的一部分

    • 优点

      1. 对象间的耦合度低,组合或者聚合本身就比继承的耦合度低。当我们真正去使用组合或者聚合复用时,我们可以在类的成员位置声明抽象父类或者父接口,这样,我们就能动态地去传递该抽象类或者父接口的子类对象了
      2. 维持了类的封装性,因为成员对象的内部细节是对新对象不可见的,也就是说新对象不知道成员对象里面的具体的实现,但是可以调用其功能,所以这种复用又被称为黑箱复用
      3. 复用的灵活性高,这种复用可以在运行时动态进行,新对象可以动态地引用与成分对象类型相同的对象,也就是说如果我们要给成员变量进行赋值,那么我们就可以在程序运行的时候才对其进行赋值了。若成员位置声明的是抽象的类或者接口,则我们就可以传递该接口或者该类的子类对象了
    • 缺点

      1. 通过使用这种方式复用建造的系统会有较多的对象需要管理
  2. 继承:是面向对象特有的复用工具,而且也最容易被滥用。继承复用通过扩展一个已有对象的实现来得到新的功能

    • 优点

      1. 简单、易实现,因为父类的大部分功能都可以通过继承关系自动进入子类、且修改或扩展继承而来的实现也较为容易
    • 缺点

      1. 破坏了类的封装性,因为继承会将父类的实现细节暴露给子类,也就是说子类可以直接去继承父类中的功能,这样,子类就可以将父类中的功能给覆盖掉了,所以,父类对子类是透明的。其实,这种复用我们又可称为白箱复用
      2. 子类与父类的耦合度高,父类的实现的任何改变都会导致子类的实现发生变化,修改父类就会形成连锁反应,这不利于类的扩展与维护
      3. 限制了复用的灵活性,从父类继承而来的实现是静态的,这是因为在编译时就已经定义好了,所以在运行时是不可能发生变化的。我们总不能说,在程序运行过程中,来解除子类和父类的继承关系吧!况且,这也是无法实现的,所以我们才说继承限制了复用的灵活性
    • 说明:如果使用继承,则必须严格遵循里氏替换原则。合成复用原则同里氏替换原则相辅相成的,两者都是开闭原则的具体实现规范。

案例

汽车分类

汽车按照动力源可分为电动车、汽油车…;按颜色可以分为黑色、白色、红色…;同时考虑上面两种方式,可组合的汽车就有很多了

如果使用继承复用的话,那么设计出来的UML类图就是

继承复用

继承复用

从以上类图中可以看到,首先定义了一个顶层父类,即汽车类,而该类下面又有两个子类,一个是汽油汽车类,一个是电动汽车类。汽油汽车类下面又有三个子类,黑、红、白颜色的汽油汽车类;电动汽车类也是一样,黑、红、白颜色的电动汽车类。

使用继承复用产生了很多子类。如果现在又有一个新的动力源了,例如氢能汽车,那么我们又要在以上类图中添加一个新的子类,而且,我们还得添加更多的子类,因为氢能汽车也得按颜色来划分。

继承复用-新增氢能源汽车类

继承复用-新增氢能源汽车类

代码

public abstract class Car {
    
    
    public abstract void run();
}
// 汽油、氢能都类似
public class ElectricCar extends Car {
    
    
    @Override
    public void run() {
    
    
        System.out.println(" - 电动车");
        //System.out.println(" - 汽油车");
    }
}
// 红、黑、白色电动车类似
public class RedElectricCar extends ElectricCar {
    
    
    public void go() {
    
    
        System.out.print("红色");
        run();
    }
}
// 红、黑、白色汽油车类似
public class RedPetrolCar extends PetrolCar {
    
    
    public void go() {
    
    
        System.out.print("红色");
        run();
    }
}

Main

public static void main(String[] args) {
    
    
    BlackPetrolCar blackPetrolCar = new BlackPetrolCar();
    blackPetrolCar.go();
}

在这里插入图片描述

案例改进:

继承复用改为合成复用

继承复用

继承复用

合成复用

合成复用

首先,来看上面类图中右边的部分,在这一部分,我们定义了一个颜色接口,而且该接口有三个子实现类,一个是Black类、White类、Red类;在以上类图中左边的部分,我们定义了一个顶层父类,即汽车类,而且该类还组合进了Color接口,也就是说,到时候我们在去创建汽车对象的时候,就需要给它传递具体的颜色的子类对象了。此外,该汽车类还有两个子类,一个是汽油汽车类,一个是电动汽车类。

代码

public interface Color {
    
    
    void colorKind();
}
public abstract class Car {
    
    
    public abstract void run();

    Color color;

    public Color getColor() {
    
    
        return color;
    }

    public void setColor(Color color) {
    
    
        this.color = color;
    }
}
public class ElectricCar extends Car {
    
    
    @Override
    public void run() {
    
    
        System.out.println(" - 电动车");
    }
}
public class White implements Color{
    
    
    @Override
    public void colorKind() {
    
    
        System.out.println("白色");
    }
}

Main

public static void main(String[] args) {
    
    
	ElectricCar electricCar = new ElectricCar();
    White white = new White();
    electricCar.setColor(white);
    electricCar.getColor().colorKind();//白色
    electricCar.run();//电动汽车
}

在这里插入图片描述

如果要是现在多了一种动力源,例如氢能汽车,UML类图就是下面这样

合成复用

合成复用

只需要在汽车类下新增子类即可

案例二

假设有A、B两个类。B类需要复用A类的方法

通过合成复用原则来实现上述案例,不再展示继承复用

方式一

使用简单依赖。在B类中添加方法,方法入参使用A类

// 测试类
public class Test {
    
    
  public static void main(String[] args) {
    
    
    B b = new B();
    b.methodB(new A());
  }
}

// A类
class A {
    
    
  public void methodA() {
    
    
    System.out.println("A类的方法执行了。");
  }
}

// B类
class B {
    
    
  public void methodB(A a) {
    
    
    System.out.println("B类的方法执行了。");
    a.methodA();
  }
}

方式二

把A类聚合到B类中。在B类中添加属性,类型为A类,通过set方法进行初始化

// 测试类
public class Test2 {
    
    
  public static void main(String[] args) {
    
    
    B b = new B();
    System.out.println("使用聚合的执行结果:");
    b.setA(new A());
    b.methodB();
  }
}

// A类
class A {
    
    
  public void methodA() {
    
    
    System.out.println("A类的方法执行了。");
  }
}

// B类
class B {
    
    
  private A a;
  public A getA() {
    
    
    return a;
  }
  public void setA(A a) {
    
    
    this.a = a;
  }
  public void methodB() {
    
    
    System.out.println("B类的方法执行了。");
    this.a.methodA();
  }
}

方式三

把A类组合到B类中。在B类中添加属性,类型为A类,并直接new出来

public class Test3 {
    
    
  public static void main(String[] args) {
    
    
    B b = new B();
    System.out.println("使用组合的执行结果:");
    b.methodB();
  }
}

class A {
    
    
  public void methodA() {
    
    
    System.out.println("A类的方法执行了。");
  }
}

class B {
    
    
  private A a = new A();
  public void methodB() {
    
    
    System.out.println("B类的方法执行了。");
    this.a.methodA();
  }
}

猜你喜欢

转载自blog.csdn.net/qq_42700109/article/details/132836907