组合优于继承
本课属于 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 拥有移动行为)。组合让你可以在运行时替换行为,自由组合各种能力,并避免脆弱基类问题。
当确实存在层级关系时,使用继承。当你需要灵活性、希望在无关的类之间共享行为,或想要动态更改行为时,选择组合。
挑战
简单让我们构建一个通知系统,展示组合优于继承的优势。你不需要创建复杂的通知类层次结构,而是通过为 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 并打印结果
}
}
这节课包含一个小测验。开始学习即可作答,并记录你的进度。
面向对象编程中的所有课
自己练习: C# 在线编译器