C#中级45、什么是组合优于继承

我们应该通过在其他对象中包含对象来重用代码,而不是通过一个对象继承另一个对象。

"组合优于继承"(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 嵌入(匿名组合)

现代软件设计趋势:少用继承,多用组合 + 接口 + 依赖注入。


⚠️ 注意:不是完全不用继承!

继承仍有其价值:

  • 定义 类型层次 (如异常体系:ExceptionIOException
  • 模板方法模式(固定算法骨架)
  • 标记接口或抽象基类(如 Stream

🔸 关键区别
继承用于"定义是什么",组合用于"定义能做什么"。


✅ 总结

对比项 继承 组合
关系 is-a has-a / uses-a
耦合度 高(紧耦合) 低(松耦合)
灵活性 编译时固定 运行时可变
扩展性 差(类爆炸) 好(插件式)
测试难度
适用场景 严格的类型分类 行为复用、功能组装

🧠 记住
"继承让你获得能力,但组合让你选择能力。"

在 C# 开发中,优先问自己:"我是在描述一种新类型,还是在组装已有功能?"

如果是后者------果断选择 组合

问题

仅仅使用组合的问题是什么

如果我们决定不完全使用继承,我们使定义确实处于是关系中的类型变得更加困难。即一个类型是另一个类型的关系。

什么是转发方法

转发方法表面类型之间关系非常密切,

相关推荐
二川bro33 分钟前
数据可视化进阶:Python动态图表制作实战
开发语言·python·信息可视化
我是唐青枫37 分钟前
一文理解 C#.NET Tuples:从基础到高级应用
c#·.net
q***2511 小时前
java进阶1——JVM
java·开发语言·jvm
while(1){yan}1 小时前
线程的状态
java·开发语言·jvm
豐儀麟阁贵1 小时前
8.3 Java常见的异常类
java·开发语言
lzh200409191 小时前
【C++STL】List详解
开发语言·c++
q***44811 小时前
Java进阶10 IO流
java·开发语言
济宁雪人2 小时前
Java安全基础——文件系统安全
java·开发语言·安全
Charles_go2 小时前
C#中级46、什么是模拟
开发语言·oracle·c#