合成复用原则详解篇(附图解及源码实例)

来源:mikechen的互联网架构

通过合成复用,我们可以更加优雅地实现代码复用。

合成复用原则解决了继承在代码复用时的相关问题。

本文,主要全面详解合成复用原则的概念、背景、作用、UML、示例、应用,以及组合和继承的选型思路。

设计模式的七大原则已全部完结,结合一起看,更易融会贯通:

1. 微信面试:说说里氏替换原则

2. 精通接口隔离原则

3. 依赖倒置原则就看这篇

4. 3 分钟吃透开闭原则

5. 单一职责原则让代码质量提升100倍

6. 通俗理解迪米特法则

01
  合成复用原则是什么?

合成复用原则(CRP),又称为组合/聚合复用原则,英文全称 Composite Reuse Principle。

合成复用原则要求在实现代码复用时,尽量先使用组合或聚合等关联关系,其次考虑使用继承关系

合成复用原则的核心思想是:

  • 将不同的类、模块或组件组合在一起,来创建新的类或对象。

  • 尽量不通过继承已有的类来获得需要的功能。

 

02  合成复用原则的由来

在早期的面向对象编程中,代码复用主要用继承。

通过继承,一个类可以从另一个类派生,获得其属性和方法。

但继承也有一些缺点:

  • 紧耦合(Tight Coupling):继承引入了强耦合,使得子类依赖于父类的内部实现细节。当父类发生变化时,子类会受到影响随之变化。


  • 继承层次复杂性:随着继承层次的增加,代码的复杂性也会增加,继承层次的扩展和维护会很复杂。


  • 白箱复用:继承会将父类的实现细节暴露给子类,父类对子类是透明的,破坏了类的封装性。


  • 限制了复用的灵活性:从父类继承而来的实现是静态的,在编译时已经定义,所以在运行时不可能发生变化。


为了解决继承带来的问题,合成复用原则应运而生。

通过合成复用,我们可以更加优雅地实现代码复用。

组合的优点:

  • 低耦合性:代码之间的关联性较小,减少了类之间的紧耦合关系。


  • 黑箱复用:维持了类的封装性,成分对象的实现细节对新对象不可见。


  • 复用灵活度更高:由于不受继承关系的限制,可以适应不同的情况和需求。


  • 确保了每个组件的单一职责原则:当一个组件需要进行更改时,只需要关注特定的组件,不需要影响整个继承层次。


03  组合与继承如何选型

组合并没有完全替代继承,组合与继承分别适用于不同的场景。

组合通常用于:

  • 需求不断变化的场景中,同时不受继承关系的限制。


  • 在类中使用其他类的功能,但不用继承它们的所有属性和方法。


继承通常用于:扩展现有类的功能。

是否使用继承,需要遵循里氏替换原则Coad 法则

3.1  里氏替换原则

里氏替换原则要求子类对象必须能够替换其父类对象,子类应该保持与父类一致的接口和行为,不应该改变或破坏继承来的约定。

关于里氏替换原则的介绍,推荐看这篇:

此处我们重点了解 Coad 法则。

3.2  Coad 法则

Coad 法则明确了继承的具体使用条件:

  • 子类是父类的特殊种类,而不是父类的一个角色。仅 is-a 关系适合继承,而 has-a 关系应使用组合来描述。


  • 不会出现需要将子类替换成另一个子类的情况。如果不能确定将来是否需要这种替换,就不应使用继承。


  • 子类应扩展而不是替代父类的责任。如果子类需要大规模替代父类的行为,那么这个类不应是父类的子类。


  • 只有在分类学意义上才可以使用继承,不要从工具类继承。


当 Coad 法则中的条件全部被满足时,才应当使用继承关系。

这里要特别注意:

滥用继承较为常见的错误是将 has-a 视为 is-a。

在这里,is-a 表示一种类是另一种类的一种类型,而 has-a 表示一个类是另一个类的一个组成部分,不是另外一个类的特殊种类。

例如:

“人”是一个类型,“老师”、“组长”、“员工”是“人”的子类,如图:

一个人可以同时拥有多个角色,例如,一个人可以同时是“老师”、“组长”、“员工”。

但是,按照继承的设计逻辑,如果一个人是雇员,就不可能是经理或学生,这显然是不合理的。

这种设计将“角色”的等级结构和“人”的等级结构混淆了。

正确的设计:

构建一个抽象类“角色”,让“人”可以同时拥有多个“角色”(组合),“老师”、“组长”、“员工”是“角色”的子类。

另外,只有两个类满足里氏替换原则时,才可能是 is-a 关系。

也就是说,如果两个类是 has-a 关系,但是设计成了继承,就必然违反里氏替换原则。


04  合成复用原则的实现示例

假设:

在一个汽车分类管理程序中,汽车有两种分类方式:

  • 按动力源分类:汽油汽车、电动汽车等;


  • 按颜色分类:白色汽车、黑色汽车和红色汽车等。


如果使用继承,就需要同时考虑这两种分类,将会产生6个组合。

这样的方式会带来两个问题:

  • 导致子类过多。


  • 任何一个分类发生变更,都要修改源代码,违背了开闭原则。


图例:

代码:





































































































public abstract class Car{     abstract void run();}

public class ElectricCar extends Car {
   @Override    void run() {        System.out.println("电动汽车");    }
}

public class PetrolCar extends Car {
   @Override    void run() {        System.out.println("汽油汽车");    }
}

public class BlackElectricCar extends ElectricCar{
   public void appearance(){        System.out.print("黑色");        super.run();    }}

public class BlackPetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("黑色");        super.run();    }}

public class RedElectricCar extends ElectricCar {    public void appearance(){        System.out.print("红色");        super.run();    }}

public class RedPetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("红色");        super.run();    }}

public class WhiteElectricCar extends ElectricCar{
   public void appearance(){        System.out.print("白色");        super.run();    }}

public class WhitePetrolCar  extends PetrolCar{    public void appearance(){        System.out.print("白色");        super.run();    }}


public class Test {
   public static void main(String[] args) {        RedElectricCar redElectricCar = new RedElectricCar();        redElectricCar.appearance();//红色电动汽车    }
}

采用组合方式:

  • 先把颜色 Color 抽象为接口,有白色,黑色,红色三个颜色实现类;


  • 再将 Color 对象组合在汽车 Car 类中。


最终,只需要生成 5 个类,就可以实现上述功能。

并且,后续分类有任何变更,只需要增加实现类,不要去修改源代码。

图例:

代码:























































































public abstract class Car{     abstract void run();     Color color;     public Color getColor() {       return color;     }     public void setColor(Color color) {        this.color = color;     }}

public interface Color {      void colorKind();}

public class ElectricCar extends Car {
   @Override    void run() {        System.out.println("电动汽车");    }
}

public class PetrolCar extends Car {
   @Override    void run() {        System.out.println("汽油汽车");    }
}

public class White implements Color{
   @Override    public void colorKind() {        System.out.println("白色");    }
}

public class Black implements Color{
   @Override    public void colorKind() {        System.out.println("黑色");    }
}

public class Red implements Color{
   @Override    public void colorKind() {        System.out.println("红色");    }
}

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

 

总结  

以上,就是对合成复用原则的概念、背景、作用、UML、示例、应用,以及组合和继承的选型思路的全面详解。


请使用浏览器的分享功能分享到微信等