掌握 C# 中的行为模式:职责链(Chain of Responsibility)、命令(Command)和观察者(Observer)模式

151 阅读9分钟

行为模式(Behavioral Patterns)专注于对象如何交互和通信,简化职责并创建更灵活的系统。在本文中,我们将深入探讨 C# 中的三个关键行为模式:职责链命令观察者模式,了解它们如何帮助您构建更具可维护性和可扩展性的系统。

为什么使用行为模式(Behavioral Patterns)?

行为模式解决了软件设计中的常见挑战,例如如何委派职责,如何使对象在不紧密耦合的情况下相互通信,以及如何保持系统架构的干净和灵活。这些模式简化了代码维护,减少了冗余,并使系统更容易扩展。


职责链(Chain of Responsibility)模式

职责链模式允许您将请求沿着处理链传递,每个处理者可以处理请求或将其传递给下一个处理者。这种模式帮助您在不修改现有代码的情况下添加新行为,因为每个处理者可以决定是否处理请求或传递给下一个。

使用场景

此模式常用于:

  • 日志框架:不同的日志级别由不同的日志处理器处理。
  • UI 事件处理:多个层次(例如按钮点击、表单事件)可以处理同一个操作。
  • 请求处理:在类似服务台的系统中,如果第一个处理者无法解决问题,请求会被升级。

它在 C# 中如何工作

假设您正在构建一个订购系统。首先,您希望对输入数据进行清理。随后,您添加了防止暴力破解的功能和缓存检查。每一步都可以实现为链中的单独处理者。

以下是一个简单的 C# 实现:

// 基础处理器类
abstract class RequestHandler
{
    protected RequestHandler nextHandler;

    public void SetNextHandler(RequestHandler handler)
    {
        nextHandler = handler;
    }

    public virtual void Handle(string request)
    {
        if (nextHandler != null)
        {
            nextHandler.Handle(request);
        }
    }
}

// 具体处理器:验证请求数据
class DataValidationHandler : RequestHandler
{
    public override void Handle(string request)
    {
        if (IsValid(request))
        {
            Console.WriteLine("请求数据验证通过。");
            base.Handle(request); // 传递给下一个处理器
        }
        else
        {
            Console.WriteLine("数据无效。请求被拒绝。");
        }
    }

    private bool IsValid(string request)
    {
        // 简单的验证逻辑
        return !string.IsNullOrEmpty(request);
    }
}

// 具体处理器:防止暴力破解
class BruteForceProtectionHandler : RequestHandler
{
    private Dictionary<string, int> failedAttempts = new Dictionary<string, int>();

    public override void Handle(string request)
    {
        if (IsAllowed(request))
        {
            Console.WriteLine("未检测到暴力破解。");
            base.Handle(request);
        }
        else
        {
            Console.WriteLine("请求因暴力破解检测而被阻止。");
        }
    }

    private bool IsAllowed(string request)
    {
        // 简单的暴力破解防护逻辑
        if (!failedAttempts.ContainsKey(request))
        {
            failedAttempts[request] = 0;
        }
        failedAttempts[request]++;
        return failedAttempts[request] <= 3; // 只允许 3 次尝试
    }
}

// 具体处理器:检查缓存
class CacheHandler : RequestHandler
{
    private Dictionary<string, string> cache = new Dictionary<string, string>();

    public override void Handle(string request)
    {
        if (cache.ContainsKey(request))
        {
            Console.WriteLine("返回缓存的响应。");
        }
        else
        {
            Console.WriteLine("处理请求并缓存响应。");
            cache[request] = "处理后的响应";
            base.Handle(request);
        }
    }
}

class Program
{
    static void Main()
    {
        // 设置职责链
        var validationHandler = new DataValidationHandler();
        var bruteForceHandler = new BruteForceProtectionHandler();
        var cacheHandler = new CacheHandler();

        validationHandler.SetNextHandler(bruteForceHandler);
        bruteForceHandler.SetNextHandler(cacheHandler);

        // 示例请求
        string request = "validRequest";

        // 通过职责链处理请求
        validationHandler.Handle(request);
    }
}

解释

在此示例中,每个请求会通过三个处理器:

  1. DataValidationHandler 确保请求数据有效。
  2. BruteForceProtectionHandler 检查暴力破解行为。
  3. CacheHandler 返回缓存结果,如果没有缓存,则处理并缓存新请求。

这使得系统灵活且易于扩展——每个处理器只关注一个职责,您可以通过添加新的处理器来引入新行为。

常见陷阱

  • 过于复杂的职责链:过长的处理链可能变得难以调试或管理。确保每个处理器的职责明确。

命令(Command)模式

命令模式将请求封装为对象,允许它们被排队、记录或撤销。请求的发送者无需了解接收者的任何信息,也不必关心请求将如何处理。

何时使用

  • 任务调度:在任务/作业调度器系统中,每个任务都可以表示为一个命令。调度器触发命令,命令可以表示生成报告、发送电子邮件或数据处理等操作。调度器无需知道每个命令的具体细节,它只需按顺序或根据计划执行它们。
  • 撤销/重做机制:如果您正在开发带有撤销/重做功能的系统(如需要跟踪更改的财务系统或内容管理系统),每个操作都可以封装为一个命令。例如,命令可以封装数据库操作,存储命令可以允许回滚或重新执行这些操作。
  • 审计日志:命令可以用于记录复杂操作。例如,每个命令在执行业务逻辑前可以记录其详细信息。这在需要审计操作的系统中(如金融系统中的交易)非常有用,而无需修改核心逻辑。
  • 事务处理:如果系统处理一系列业务操作,每个操作可以封装在命令中。事务管理器可以按顺序执行它们,确保每个操作是原子性的。如果某个命令失败,它可以停止链条,回滚已成功的命令,或重新尝试。

它在 C# 中如何工作

以下是一个在事务处理场景中使用命令模式的示例:

// 命令接口
public interface ICommand
{
    void Execute();
}

// 转账操作的具体命令
public class TransferMoneyCommand : ICommand
{
    private string _fromAccount;
    private string _toAccount;
    private decimal _amount;

    public TransferMoneyCommand(string fromAccount, string toAccount, decimal amount)
    {
        _fromAccount = fromAccount;
        _toAccount = toAccount;
        _amount = amount;
    }

    public void Execute()
    {
        // 转账的业务逻辑
        Console.WriteLine($"正在从 {_fromAccount} 转账 {_amount}{_toAccount}。");
        // 添加与数据库或外部系统交互的逻辑
    }
}

// 生成报告的具体命令
public class GenerateReportCommand : ICommand
{
    private string _reportType;

    public GenerateReportCommand(string reportType)
    {
        _reportType = reportType;
    }

    public void Execute()
    {
        // 生成报告的业务逻辑
        Console.WriteLine($"正在生成 {_reportType} 报告。");
        // 添加报告生成的逻辑
    }
}

// 调用者类,用于执行命令
public class CommandInvoker
{
    private List<ICommand> _commands = new List<ICommand>();

    public void AddCommand(ICommand command)
    {
        _commands.Add(command);
    }

    public void ExecuteAll()
    {
        foreach (var command in _commands)
        {
            command.Execute();
        }
        _commands.Clear();
    }
}

class Program
{
    static void Main()
    {
        // 创建命令
        var transferCommand = new TransferMoneyCommand("AccountA", "AccountB", 1000);
        var reportCommand = new GenerateReportCommand("Monthly");

        // 调用者执行命令
        var invoker = new CommandInvoker();
        invoker.AddCommand(transferCommand);
        invoker.AddCommand(reportCommand);

        // 执行所有命令
        invoker.ExecuteAll();
    }
}

解释:

  • CommandInvoker 充当调用者,触发一系列后端命令(如转账、生成报告)。
  • 每个 ICommand 实现表示一个具体的后端操作,如转账或生成报告。
  • 调用者无需了解每个命令的具体细节,它只执行命令,使得系统灵活且易于扩展。

最佳实践

  • 将相关命令进行分组,简化代码。
  • 实现撤销功能,通过存储执行过的命令历史记录来实现。

观察者(Observer)模式

观察者模式定义了对象之间的一对多关系,当一个对象(主体)的状态发生变化时,所有依赖它的对象(观察者)都会收到更新通知。这在需要将一个对象的状态反映到多个对象时非常有用。

何时使用

  • UI 组件:当数据发生变化时,UI 组件需要刷新。
  • 事件驱动系统:在这种系统中,组件需要动态响应变化。

它在 C# 中如何工作

以下是使用观察者模式实现的基本发布-订阅模型:

using System;
using System.Collections.Generic;

// 观察者接口
public interface ISubscriber
{
    void Update(string message);
}

// 具体的订阅者类:接收电子邮件通知
public class EmailSubscriber : ISubscriber
{
    private string _email;

    public EmailSubscriber(string email)
    {
        _email = email;
    }

    public void Update(string message)
    {
        Console.WriteLine($"向 {_email} 发送邮件: {message}");
    }
}

// 主体接口
public interface IEmailAlertSystem
{
    void Subscribe(ISubscriber subscriber);
    void Unsubscribe(ISubscriber subscriber);
    void NotifySubscribers(string message);
}

// 具体的主体类:电子邮件通知系统
public class EmailAlertSystem : IEmailAlertSystem
{
    private List<ISubscriber> _subscribers = new List<ISubscriber>();

    public void Subscribe(ISubscriber subscriber)
    {
        _subscribers.Add(subscriber);
        Console.WriteLine("已添加订阅者。");
    }

    public void Unsubscribe(ISubscriber subscriber)
    {
        _subscribers.Remove(subscriber);
        Console.WriteLine("已移除订阅者。");
    }

    public void NotifySubscribers(string message)
    {
        foreach (var subscriber in _subscribers)
        {
            subscriber.Update(message);
        }
    }
}

// 示例用法
class Program
{
    static void Main(string[] args)
    {
        // 创建电子邮件通知系统(发布者)
        var alertSystem = new EmailAlertSystem();

        // 创建订阅者(观察者)
        var subscriber1 = new EmailSubscriber("user1@example.com");
        var subscriber2 = new EmailSubscriber("user2@example.com");

        // 订阅通知系统
        alertSystem.Subscribe(subscriber1);
        alertSystem.Subscribe(subscriber2);

        // 通知订阅者一条重要通知
        alertSystem.NotifySubscribers("重要更新:新产品发布!");

        // 移除一个订阅者
        alertSystem.Unsubscribe(subscriber1);

        // 再次通知订阅者
        alertSystem.NotifySubscribers("提醒:不要错过明天的产品发布活动!");
    }
}

解释:

  • IEmailAlertSystem:定义了管理订阅者并通知它们的接口。
  • EmailAlertSystem:一个实现发布者的具体类。它持有一个订阅者列表并发送通知。
  • EmailSubscriber:表示接收电子邮件通知的订阅者。当接收到通知时,它会发送包含提供信息的电子邮件。

每个订阅者可以动态添加或移除,当发布者通知它们时,它们都会收到更新。

最佳实践

  • 避免内存泄漏,确保观察者在不再需要时解除订阅。
  • 在某些情况下,使用弱引用,防止观察者持有对主体的强引用。

其他值得注意的行为模式

  • 策略(Strategy)模式:此模式允许定义一系列算法并使它们可以互换。常见的用例是动态切换不同的排序算法。在 C# 中,这通常通过接口或委托实现,由于内置语言功能可以优雅地处理类似需求,因此策略模式作为独立模式的应用不太常见。

  • 迭代器(Iterator)模式:迭代器模式提供了一种遍历集合元素的方法,而无需暴露其内部结构。在 C# 中,迭代器通过 foreachIEnumerable 深入集成在语言中,因此在典型应用代码中手动实现此模式并不常见。

  • 访问者(Visitor)模式:访问者模式允许向一组对象添加新操作,而无需修改它们的类。虽然它在扩展功能方面非常强大,但它要求对现有的类层次结构进行修改,这可能会引入不必要的复杂性。除非有强烈的需求在现有对象结构中增加灵活性,否则通常避免使用此模式。

  • 模板方法(Template Method)模式:此模式定义了算法的框架,并将某些步骤留给子类实现。在 C# 中,继承和抽象类通常提供了类似的功能,许多人更倾向于使用接口或委托的灵活性,因此在现代开发中该模式的使用较少。

  • 状态(State)模式:状态模式允许对象在其内部状态发生变化时改变其行为。虽然它在 UI 系统或游戏开发中很有用,但与观察者或命令模式相比,它的应用更为专门化。


结论

行为模式是管理对象之间交互的强大方式,使您的 C# 应用程序更加灵活且易于维护。通过理解何时以及如何应用这些模式,您可以构建可扩展且易于扩展的系统。

在继续您的编程之旅时,考虑结合多种模式来解决更大的架构挑战。