目录
组合优于继承(合成复用原则)
“组合优于继承”是一个面向对象编程的设计原则,它建议我们在需要复用代码的时候,尽量使用组合(has-a)的方式,而不是继承(is-a)的方式。组合是指一个类包含另一个类的对象作为自己的属性,而继承是指一个类直接从另一个类派生出来,拥有其所有的属性和方法。
关键点
将系统的功能划分为独立的模块或组件:
将系统划分为多个独立的、可复用的模块或组件,每个模块或组件负责一个清晰的功能。
通过组合实现模块间的关系:
使用组合关系将独立的模块或组件组合在一起,形成更复杂的功能。这种组合关系可以通过成员变量、方法参数或者构造函数来实现。
避免使用继承带来的僵化结构:
相比于继承关系,合成复用原则避免了通过继承产生的紧耦合和静态的结构。通过组合关系,模块或组件可以更加灵活地组合在一起,提供更好的扩展性和适应性。
优先使用对象组合而不是类继承:
当需要在一个类中使用另一个类的功能时,优先考虑通过对象组合的方式实现,而不是通过类继承。这样可以降低系统的耦合性,并且使得模块之间的关系更加灵活
组合(Composition)相对于继承(Inheritance)具有以下优势
更灵活的代码结构:
组合允许对象之间的动态组合,而不是通过继承的静态关系。通过组合,可以在运行时决定对象之间的关系,并根据需要进行组合或解除组合。这种灵活性使得代码结构更加可扩展和适应变化。
松耦合的关系:
继承创建了强耦合的关系,子类与父类之间高度依赖。这意味着如果父类发生变化,子类可能会受到影响。相比之下,组合关系更松散,各个对象之间通过接口进行交互,可以独立于彼此进行修改和演化。
避免类爆炸问题:
继承层次结构中的类数量可能会快速增长,导致类的数量爆炸式增加。这使得维护和理解代码变得困难。相比之下,组合关系不会导致类的数量增加,因为对象之间的组合是动态的,可以灵活地构建各种组合。
更好的代码可读性和可维护性:
继承的层次结构可能会导致代码的复杂性增加,因为子类继承了父类的所有属性和方法。这使得代码的理解和维护变得困难。组合关系更简洁明了,每个对象都只负责自己的职责,代码结构更加清晰,易于理解和维护。
更好的设计原则遵循:
组合关系更符合设计原则中的一些重要概念,如单一责任原则和开放封闭原则。通过组合,每个对象都有明确的职责和功能,可以更好地划分模块和组件,使系统更加灵活、可扩展和可维护。
尽管继承在某些情况下仍然有其合理的用途,但组合相对于继承提供了更灵活、松耦合和可维护的代码结构。通过合理运用组合关系,可以实现更好的代码设计和更适应变化的系统架构。
组合和继承的区别
假设我们要设计一个汽车(Car)类和一个发动机(Engine)类。如果我们使用继承的方式,那么我们可以让汽车类继承发动机类,这样汽车就拥有了发动机的属性和方法。但是这样做有几个问题:
首先,这样违反了“is-a”的逻辑关系。汽车并不是一种发动机,它们之间没有本质上的层次结构。
其次,这样导致了汽车和发动机之间的紧密耦合。如果发动机类发生了变化,比如增加了一个新的属性或方法,那么汽车类也必须跟着改变。这样会增加代码的复杂度和维护成本。
再次,这样限制了汽车和发动机之间的灵活性。如果我们想让汽车换一个不同类型或品牌的发动机,那么我们就必须重新定义一个新的汽车子类来继承那个发动机类。这样会导致代码冗余和浪费。
// 使用继承的方式
public class Engine
{
public string Type { get; set; }
public int HorsePower { get; set; }
public void Start()
{
// Start the engine
}
public void Stop()
{
// Stop the engine
}
}
public class Car : Engine // Car is not an engine!
{
public string Model { get; set; }
public string Color { get; set; }
public void Drive()
{
// Drive the car
}
}
// 如果想换一个不同的发动机,就要定义一个新的子类
public class CarWithElectricEngine : ElectricEngine // Car is not an electric engine!
{
public string Model { get; set; }
public string Color { get; set; }
public void Drive()
{
// Drive the car
}
}
如果我们使用组合的方式,那么我们可以让汽车类包含一个发动机类的对象作为自己的属性,这样汽车就可以使用发动机的属性和方法。这样做有几个好处:
首先,这样符合了“has-a”的逻辑关系。汽车有一个发动机,它们之间是一种组合关系。
其次,这样降低了汽车和发动机之间的耦合。如果发动机类发生了变化,那么汽车类不需要跟着改变。只要发动机类提供了相同的接口,汽车类就可以正常工作。
再次,这样提高了汽车和发动机之间的灵活性。如果我们想让汽车换一个不同类型或品牌的发动机,那么我们只需要改变汽车类中包含的发动机对象的引用即可。这样可以实现运行时的动态变化。
// 使用组合的方式
public class Engine
{
public string Type { get; set; }
public int HorsePower { get; set; }
public void Start()
{
// Start the engine
}
public void Stop()
{
// Stop the engine
}
}
public class Car
{
public string Model { get; set; }
public string Color { get; set; }
public Engine Engine { get; set; } // Car has an engine
public void Drive()
{
// Drive the car
Engine.Start(); // Use the engine's method
}
}
// 如果想换一个不同的发动机,只要改变引用即可
public class Example
{
public static void Main()
{
Car car = new Car();
car.Model = "Toyota";
car.Color = "Red";
car.Engine = new Engine(); // Use a normal engine
car.Engine.Type = "Gasoline";
car.Engine.HorsePower = 200;
car.Drive(); // Drive the car with a normal engine
car.Engine = new ElectricEngine(); // Use an electric engine
car.Engine.Type = "Electric";
car.Engine.HorsePower = 150;
car.Drive(); // Drive the car with an electric engine
}
}
组合应用
// Shape类,表示各种形状
class Shape {
public void draw() {
// 绘制形状的共享代码
}
}
// 矩形模块
class Rectangle {
private Shape shape; // 组合关系
public Rectangle() {
shape = new Shape();
}
public void drawRectangle() {
shape.draw();
// 绘制矩形的特定代码
}
}
// 圆形模块
class Circle {
private Shape shape; // 组合关系
public Circle() {
shape = new Shape();
}
public void drawCircle() {
shape.draw();
// 绘制圆形的特定代码
}
}
// 三角形模块
class Triangle {
private Shape shape; // 组合关系
public Triangle() {
shape = new Shape();
}
public void drawTriangle() {
shape.draw();
// 绘制三角形的特定代码
}
}
// 测试代码
public class DrawingApp {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.drawRectangle();
Circle circle = new Circle();
circle.drawCircle();
Triangle triangle = new Triangle();
triangle.drawTriangle();
}
}
在上述示例中,我们通过使用组合关系将具体的形状模块与通用的形状绘制代码进行了组合。每个具体形状模块都拥有一个形状实例,并通过调用该实例的draw()方法来绘制形状。这样,我们实现了形状的复用和绘制功能的组合。
使用合成复用原则,我们可以灵活地扩展系统,例如添加新的形状模块,而不需要修改现有的代码。同时,这种设计也减少了形状模块之间的耦合,使系统更加灵活和可维护。
设计建议
面向接口编程:
使用接口定义模块或组件的功能,而不是具体的实现类。通过面向接口编程,可以降低模块之间的依赖性,提高代码的灵活性和可替换性。
组合优先:
在设计中,优先考虑使用对象组合来构建系统,而不是过度依赖类继承。对象组合更灵活,可以根据需要动态地组合不同的对象,而类继承在一定程度上限制了系统的扩展性。
单一责任:
确保每个模块或组件具有清晰的单一责任。每个模块应专注于完成特定的功能,并且在模块内部进行细分,避免功能的耦合和冗余。
封装变化:
识别系统中容易发生变化的部分,并将其封装起来。这样,在变化发生时,只需要修改变化的部分,而不影响其他部分的功能。
松耦合&高内聚:
模块之间应保持松耦合关系,降低它们之间的依赖性。同时,模块内部应 该保持高内聚,即模块的各个部分相互关联并协同工作,完成特定的任务。
可插拔性和可扩展性:
通过合成复用原则,设计具有可插拔性和可扩展性的系统。模块之间的组合关系可以根据需要进行修改和替换,从而方便地扩展系统功能。
避免过度设计:
在应用合成复用原则时,要避免过度设计和过度抽象。只有在确实需要复用和组合的情况下才使用合成复用原则,避免不必要的复杂性和额外的开发成本。
根据这些建议可以帮助设计出更灵活、可扩展和易维护的系统,充分利用合成复用原则的优势。
然而,具体的实践方法和技术选择还需要根据具体的项目需求和技术环境进行评估和决策。
常见的反模式
在日常的软件设计中,可能会出现一些违反合成设计原则的反模式。
下面是总结的一些常见的反模式:
过度复杂的组合层次: 当系统中存在过多的组合层次时,可能导致代码复杂性增加、理解困难和维护成本提高。应避免无谓的组合和过度嵌套。
过度抽象和泛化: 为了实现复用和灵活性,有时可能过度抽象和泛化代码,增加了系统的复杂性和理解难度。要确保抽象的层次适当,符合实际需求,并避免不必要的抽象。
破坏封装性: 合成复用原则强调模块的封装和独立性,但在实践中可能会破坏模块的封装性,直接访问或修改模块内部的成员。这会导致模块之间的耦合增加,降低系统的可维护性和可扩展性。
过度依赖于具体实现: 有时为了方便和快速实现功能,可能会直接依赖于具体实现而不是抽象接口。这样会导致代码的灵活性和可替换性降低,增加了耦合性。
缺乏正确的接口设计: 在应用合成复用原则时,正确设计接口是至关重要的。如果接口设计不合理,可能会导致模块之间的协作困难、接口冗余或接口不稳定等问题。
忽视模块的单一责任原则: 每个模块应该具有单一的责任,但在实践中可能会忽视这一原则,导致模块的功能过于复杂、耦合性增加和难以维护。
以上反模式可能会导致代码质量下降、可维护性差和系统设计的灵活性降低。在应用合成复用原则时,需要警惕这些反模式,并根据具体情况进行适当的权衡和设计决策,以确保系统的健壮性和可维护性。
软件工程:合成复用原则,组合优于继承
设计原则之组合优先继承