我们应该通过在其他对象中包含对象来重用代码,而不是通过一个对象继承另一个对象。
"组合优于继承"(Composition over Inheritance) 是面向对象设计中的一条 核心原则,它主张:
"当你想复用代码或扩展功能时,优先使用'包含对象'(组合),而不是'继承父类'。"
🎯 为什么提出这个原则?
❌ 继承的问题(尤其在深度继承树中)
| 问题 | 说明 |
|---|---|
| 紧耦合 | 子类与父类强绑定,父类一改,所有子类可能崩溃 |
| 脆弱的基类问题(Fragile Base Class) | 父类的微小改动可能导致子类行为异常 |
| 继承爆炸 | 多个维度变化 → 子类数量指数级增长(如:带牛奶/糖/奶油的咖啡) |
| 违反单一职责 | 父类承担过多职责,子类被迫继承不需要的功能 |
| 无法在运行时改变行为 | 继承是编译时静态绑定 |
💡 继承表达的是 "is-a" 关系(狗 is-a 动物),但很多场景其实是 "has-a" 或 "uses-a"。
✅ 组合的优势
组合 = 一个对象持有另一个对象的引用,并委托它完成部分工作。
| 优势 | 说明 |
|---|---|
| 松耦合 | 只依赖接口,不依赖具体实现 |
| 灵活扩展 | 运行时可动态更换组件 |
| 避免类爆炸 | N 个功能只需 N 个组件类,而非 2^N 个子类 |
| 符合开闭原则 | 新增功能只需新增组件,不修改现有代码 |
| 更易测试 | 可轻松注入 Mock 对象 |
💡 组合表达的是 "has-a" 关系(汽车 has-a 引擎)。
🔍 经典案例对比:鸟类飞行行为
❌ 错误方式:用继承
abstract class Bird
{
public virtual void Fly() => Console.WriteLine("Flying...");
}
class Sparrow : Bird { } // 麻雀会飞
class Ostrich : Bird
{
// 鸵鸟不会飞!但被迫继承 Fly()
public override void Fly() => throw new NotSupportedException("鸵鸟不会飞!");
}
- 问题 :鸵鸟"是鸟",但不该有
Fly()方法! - 违反 Liskov 替换原则(LSP):子类不能完全替代父类。
✅ 正确方式:用组合 + 策略模式
// 飞行行为接口
interface IFlyable
{
void Fly();
}
class WingFlying : IFlyable
{
public void Fly() => Console.WriteLine("用翅膀飞...");
}
class CannotFly : IFlyable
{
public void Fly() => Console.WriteLine("我不会飞");
}
// 鸟类持有飞行行为(组合)
class Bird
{
private readonly IFlyable _flyBehavior;
public Bird(IFlyable flyBehavior)
{
_flyBehavior = flyBehavior;
}
public void PerformFly() => _flyBehavior.Fly();
}
// 使用
var sparrow = new Bird(new WingFlying());
sparrow.PerformFly(); // 用翅膀飞...
var ostrich = new Bird(new CannotFly());
ostrich.PerformFly(); // 我不会飞
✅ 行为可插拔!未来加"火箭飞行"只需新增
RocketFlying类。
🧩 组合 vs 继承:何时用哪个?
| 场景 | 推荐方式 | 原因 |
|---|---|---|
| 表达 "is-a" 且行为一致 | 继承 | 如 Dog is-a Animal,且所有动物都能 Eat() |
| 表达 "has-a" 或行为可变 | 组合 | 如 Car has-a Engine,引擎可换 |
| 多个独立变化维度 | 组合 | 如 UI 控件:边框 + 背景 + 滚动条 |
| 需要运行时切换行为 | 组合 | 如游戏角色换武器、换技能 |
| 复用代码但不想继承全部 | 组合 | 避免继承不需要的方法 |
📌 经验法则 :
如果两个类之间不是严格的"种类"关系(比如"企鹅是一种鸟"但不会飞),就不要用继承。
💡 C# 中组合的常见实现方式
1. 直接持有字段(最简单)
public class Car
{
private Engine _engine = new ElectricEngine(); // 组合
public void Start() => _engine.Start();
}
2. 通过构造函数注入(推荐,支持 DI)
public class OrderService
{
private readonly IEmailSender _emailSender;
public OrderService(IEmailSender emailSender) // ← 组合 + 依赖注入
{
_emailSender = emailSender;
}
}
3. 装饰器模式(组合的高级形式)
var stream = new CryptoStream(
new BufferedStream(
new FileStream("data.txt", FileMode.Open)
)
);
// 每一层都是对下一层的"装饰"(组合)
🌐 现代框架如何体现"组合优于继承"?
| 框架 | 实践 |
|---|---|
| ASP.NET Core | 中间件管道(Middleware)是组合链,而非继承 |
| Entity Framework | 通过配置和依赖注入组合行为,而非继承 DbContext 扩展 |
| WPF/MAUI | UI 控件通过组合(Panel 包含 Button)构建界面,而非继承 |
| Go 语言 | 根本不支持继承! 全靠 struct 嵌入(匿名组合) |
✅ 现代软件设计趋势:少用继承,多用组合 + 接口 + 依赖注入。
⚠️ 注意:不是完全不用继承!
继承仍有其价值:
- 定义 类型层次 (如异常体系:
Exception→IOException) - 模板方法模式(固定算法骨架)
- 标记接口或抽象基类(如
Stream)
🔸 关键区别 :
继承用于"定义是什么",组合用于"定义能做什么"。
✅ 总结
| 对比项 | 继承 | 组合 |
|---|---|---|
| 关系 | is-a | has-a / uses-a |
| 耦合度 | 高(紧耦合) | 低(松耦合) |
| 灵活性 | 编译时固定 | 运行时可变 |
| 扩展性 | 差(类爆炸) | 好(插件式) |
| 测试难度 | 高 | 低 |
| 适用场景 | 严格的类型分类 | 行为复用、功能组装 |
🧠 记住 :
"继承让你获得能力,但组合让你选择能力。"
在 C# 开发中,优先问自己:"我是在描述一种新类型,还是在组装已有功能?"
如果是后者------果断选择 组合!
问题
仅仅使用组合的问题是什么
如果我们决定不完全使用继承,我们使定义确实处于是关系中的类型变得更加困难。即一个类型是另一个类型的关系。
什么是转发方法
转发方法表面类型之间关系非常密切,