合成复用原则

本文深入探讨了合成复用原则在面向对象设计中的应用,强调了尽量使用对象组合而非继承来实现复用的重要性。通过比较组合与继承的优缺点,提出了在不同场景下选择复用方式的原则,并通过实例展示了如何使用关联关系实现系统复用,避免了通过继承带来的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

合成复用原则又称为组合/聚合复用原则(Composition/Aggregate Reuse Principle, CARP),其定义如下:

合成复用原则(Composite Reuse Principle, CRP):尽量使用对象组合,而不是继承来达到复用的目的。

合成复用原则就是在一个新的对象里通过关联关系(包括组合关系和聚合关系)来使用一些已有的对象,使之成为新对象的一部分;新对象通过委派调用已有对象的方法达到复用功能的目的。简言之:复用时要尽量使用组合/聚合关系(关联关系),少用继承。
在面向对象设计中,可以通过两种方法在不同的环境中复用已有的设计和实现:
1、通过组合/聚合关系(首先考虑)
2、通过继承
组合/聚合优缺点:
优点:
由于组合或聚合关系可以将已有的对象(也可称为成员对象)纳入到新对象中,使之成为新对象的一部分,因此新对象可以调用已有对象的功能,这样做可以使得成员对象的内部实现细节对于新对象不可见,所以这种复用又称为“黑箱”复用,相对继承关系而言,其耦合度相对较低,成员对象的变化对新对象的影响不大,可以在新对象中根据实际需要有选择性地调用成员对象的操作。合成复用可以在运行时动态进行,新对象可以动态地引用与成员对象类型相同的其他对象。
缺点:
通过这种方式复用建造的系统会有较多的对象需要管理
继承优缺点:
优点:
新的实现较为容易,因为基类的大部分功能可以通过继承关系自动进入派生类,修改或扩展继承而来的实现较为容易。有效使用继承会有助于对问题的理解,降低复杂度。滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。
缺点:
通过继承来进行复用的主要问题在于继承复用会破坏系统的封装性,因为继承会将基类的实现细节暴露给子类,由于基类的内部细节通常对子类来说是可见的,所以这种复用又称“白箱”复用,如果基类发生改变,那么子类的实现也不得不发生改变;从基类继承而来的实现是静态的,不可能在运行时发生改变,没有足够的灵活性。
选择原则:
1、一般而言,如果两个类之间是“Has-A”的关系应使用组合或聚合,如果是“Is-A”关系可使用继承。"Is-A"是严格的分类学意义上的定义,意思是一个类是另一个类的"一种";而"Has-A"则不同,它表示某一个角色具有某一项责任。
2、永远不会出现需要将子类换成另一个类的子类的情况,也就是说,一个子类所继承的基类是固定的,所谓的固定是指以后不会换成继承别的基类。
3、子类应具有扩展父类的责任,而不仅是重写(Override)或注销掉(使某些方法失效)父类的行为,如果子类需要大量重写父类的行为,那么这个子类不应当成为这个父类的子类。
4、只有在分类学角度上有意义,才可以使用继承,不要继承工具类。
使用:

在使用继承时,需要严格遵循里氏代换原则,有效使用继承会有助于对问题的理解,降低复杂度,而滥用继承反而会增加系统构建和维护的难度以及系统的复杂度,因此需要慎重使用继承复用。下面通过一个简单实例来加深对合成复用原则的理解:
Sunny软件公司开发人员在初期的CRM系统设计中,考虑到客户数量不多,系统采用MySQL作为数据库,与数据库操作有关的类如CustomerDAO类等都需要连接数据库,连接数据库的方法getConnection()封装在DBUtil类中,由于需要重用DBUtil类的getConnection()方法,设计人员将CustomerDAO作为DBUtil类的子类,初始设计方案结构如下图所示:

随着客户数量的增加,系统决定升级为Oracle数据库,因此需要增加一个新的OracleDBUtil类来连接Oracle数据库,由于在初始设计方案中CustomerDAO和DBUtil之间是继承关系,因此在更换数据库连接方式时需要修改CustomerDAO类的源代码,将CustomerDAO作为OracleDBUtil的子类,这将违反开闭原则。【当然也可以修改DBUtil类的源代码,同样会违反开闭原则。】

根据合成复用原则,我们在实现复用时应该多用关联,少用继承。因此在本实例中我们可以使用关联复用来取代继承复用,重构后的结构如图所示:

CustomerDAO和DBUtil之间的关系由继承关系变为关联关系,采用依赖注入的方式将DBUtil对象注入到CustomerDAO中,可以使用构造注入,也可以使用Setter注入。如果需要对DBUtil的功能进行扩展,可以通过其子类来实现,如通过子类OracleDBUtil来连接Oracle数据库。由于CustomerDAO针对DBUtil编程,根据里氏代换原则,DBUtil子类的对象可以覆盖DBUtil对象,只需在CustomerDAO中注入子类对象即可使用子类所扩展的方法。例如在CustomerDAO中注入OracleDBUtil对象,即可实现Oracle数据库连接,原有代码无须进行修改,而且还可以很灵活地增加新的数据库连接方式。
### 合成复用原则的代码实现 合成复用原则提倡使用对象组合而非继承来构建新功能。这种方式可以提高系统的灵活性并减少不必要的复杂度。 #### 示例场景描述 假设有一个图形编辑器应用程序,其中涉及不同类型的形状(圆形、矩形)。传统做法可能是创建多个具体形状类分别继承自一个通用`Shape`基类。然而按照合成复用原则,则应考虑让各个组件之间建立关联而不是层次结构。 下面是一个基于此理念的具体案例: ```cpp // 定义接口 ShapeInterface 而不是抽象类 class ShapeInterface { public: virtual void draw() const = 0; }; // Circle 和 Rectangle 类实现了该接口 class Circle : public ShapeInterface { private: int radius_; public: explicit Circle(int r):radius_(r){} void draw() const override { std::cout << "Drawing a circle with radius " << radius_ << "\n"; } }; class Rectangle : public ShapeInterface { private: int width_, height_; public: Rectangle(int w, int h) :width_(w),height_(h){}; void draw() const override { std::cout << "Drawing a rectangle of size ("<< width_ << "," << height_ <<")\n";} }; // Composite class that holds multiple shapes and can also be treated as one shape. class ShapesGroup : public ShapeInterface{ private: std::vector<ShapeInterface*> members_; // Composition relationship public: ~ShapesGroup(){ for(auto member : members_) delete member; } void addMember(ShapeInterface* s){ members_.push_back(s); } void removeMember(ShapeInterface* s){ auto it = std::find(members_.begin(),members_.end(),s); if(it != members_.end()){ delete *it; members_.erase(it); } } void draw()const override{ for(const auto &member : members_) member->draw(); } }; ``` 上述例子展示了如何利用组合方式替代传统的继承机制[^1]。这里并没有直接从某个特定类型派生出其他实体;相反地,通过将不同的部件组装在一起形成更大的整体——即复合体(`ShapesGroup`),从而达到相同的效果。这样做不仅简化了程序逻辑而且增强了模块间的独立性和可维护性。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值