Menu
CoddyTech

组合优于继承

本课属于 Coddy C# 学习旅程的“面向对象编程”单元。第 41 课,共 70 课。

继承会让类之间紧密耦合。当 Car 继承自 Vehicle 时,对 Vehicle 的任何更改都可能导致 Car 出错。组合提供了一种更灵活的替代方案:类不继承行为,而是包含提供这些行为的对象。

考虑一个可以移动和攻击的游戏角色。使用继承时,你可能会创建复杂的层级结构。使用组合时,你只需为角色添加所需的组件:

public class MovementBehavior
{
    public void Move() => Console.WriteLine("Moving...");
}

public class AttackBehavior
{
    public void Attack() => Console.WriteLine("Attacking!");
}

public class Character
{
    private MovementBehavior movement = new MovementBehavior();
    private AttackBehavior attack = new AttackBehavior();
    
    public void Move() => movement.Move();
    public void Attack() => attack.Attack();
}

关键区别在于它们的关系。继承表示“是一种”(Dog 是一种 Animal),而组合表示“拥有”(Character 拥有移动行为)。组合让你可以在运行时替换行为,自由组合各种能力,并避免脆弱基类问题。

当确实存在层级关系时,使用继承。当你需要灵活性、希望在无关的类之间共享行为,或想要动态更改行为时,选择组合。

challenge icon

挑战

简单

让我们构建一个通知系统,展示组合优于继承的优势。你不需要创建复杂的通知类层次结构,而是通过为 NotificationSender 提供可互换的行为组件来组合它。

你将把代码分成 3 个文件:

  • Behaviors.cs:在 Notifications 命名空间中创建两个行为类,分别处理发送通知的不同环节:
    • FormattingBehavior:包含一个 Format(string message) 方法,该方法会在消息开头加上 [ALERT] ,并在末尾加上 [END]
    • DeliveryBehavior:包含一个 Deliver(string channel, string message) 方法,该方法会返回一个符合 Sending via {channel}: {message} 格式的字符串
  • NotificationSender.cs:在 Notifications 命名空间中创建一个 NotificationSender 类,使用组合来整合各种行为。发送器应满足以下要求:
    • 将 FormattingBehavior 和 DeliveryBehavior 的实例都作为私有字段
    • 包含一个 Channel 属性(string),用于保存投递渠道名称
    • 包含一个接收渠道名称的构造函数
    • 包含一个 Send(string message) 方法:先使用格式化行为处理消息,再使用投递行为发送消息,并返回最终结果
  • Program.cs:在主文件中,使用输入的渠道创建一个 NotificationSender,然后发送输入的消息。打印结果,展示组合后的行为如何协同工作。

你将收到两个输入:

  • 投递渠道名称(例如,Email、SMS)
  • 要发送的消息

打印对通知发送器调用 Send() 的结果。

例如,如果输入是 Email 和 Server is down,输出应为:

Sending via Email: [ALERT] Server is down [END]

注意,NotificationSender 不继承任何类,它只是“拥有”格式化和投递行为。这种组合方式意味着,你可以轻松替换任一行为,而无需更改发送器类,也可以在完全不同的类中重复使用这些行为!

亲手试试

using System;
using Notifications;

class Program
{
    public static void Main(string[] args)
    {
        // 读取输入
        string channel = Console.ReadLine();
        string message = Console.ReadLine();

        // TODO:使用 channel 创建一个 NotificationSender
        
        // TODO:发送 message 并打印结果
        
    }
}
quiz icon自我测试

这节课包含一个小测验。开始学习即可作答,并记录你的进度。

面向对象编程中的所有课

自己练习: C# 在线编译器