65.9K
CodeProject 正在变化。 阅读更多。
Home

C# 中的 Propagator - 观察者设计模式的替代方案

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.94/5 (19投票s)

2009 年 7 月 14 日

CPOL

9分钟阅读

viewsIcon

59246

downloadIcon

389

C# 中传播器设计模式的可重用实现,这是著名的观察者设计模式的潜在更强大的替代方案。

Propagator Demo Form

引言

本文提供了 C# 中传播器设计模式的可重用实现。传播器设计模式是一种用于更新依赖网络中对象的模式。当状态更改需要通过对象网络推送时,它非常有用。状态更改由对象本身表示,该对象通过传播器网络传输。通过将状态更改封装为对象,传播器变得松散耦合。

我已将传播器模式用于一个复杂的 GUI 应用程序,其中用户界面的不同组件需要保持同步。这个 GUI 也非常可配置,因此拥有松散耦合的 GUI 组件对我来说肯定是有利的。

演示应用程序由一个名为 DemoForm 的表单和两个控件组成:FontControlColorControlDemoFormFontControlColorControl 都显示一个具有相同字体和颜色的字符串。正如名称所示,FontControl 可以更改字符串的字体,ColorControl 可以更改字符串的颜色。DemoForm 有一个重置按钮,用于将字体和颜色重置为原始值。这三个组件中的字体和颜色通过传播器设计模式保持同步。依赖网络显示在下方。

Demo Form Dependency Graph

如果您想立即开始使用 Propagator 类,请跳过接下来的两节,直接跳转到“使用代码”部分。

背景

传播器可以看作是观察者设计模式的改进版本。在观察者设计模式中,主题通知其观察者更改。在传播器设计模式中,不存在主题和观察者之间的区别。依赖网络中的每个对象都是一个 Propagator,一个 Propagator 可以有 0 个或更多依赖的 Propagator。通过设置 Propagator 之间正确的依赖关系,可以创建依赖对象网络。

在我的设计中,我决定将每个状态更改封装在一个对象中。这类似于命令设计模式,其中命令封装了要在应用程序中执行的操作。状态更改可以由任何实体创建,并通过依赖的 Propagator 网络发送。每个 Propagator 都可以选择响应状态更改,但它们总是会将其传递给依赖的 Propagator

状态更改对象只使用一次然后被处置。它保留了一个已观察到状态更改的 Propagator 列表。这可以防止状态更改永远循环网络。

Propagator 之间的依赖可以是双向的。例如,如果网络表示父子关系,则状态更改可以由子级或父级发起。为了优化状态更改的传播,发送的 Propagator 作为参数给出。通过这种方式,接收的 Propagator 不会将状态更改直接发送回发送方。

以下是我的 Propagator 实现特点的快速概述

  • 状态更改直接通过依赖图推送
  • 支持循环依赖图
  • 支持双向依赖
  • 依赖图的深度优先迭代
  • 当抛出异常时,依赖图的迭代将停止

设计

可重用 Propagator 类的类图显示在下方

Propagator Classes

IPropagator 接口提供了添加和删除依赖传播器的方法。它还具有 Process() 方法,该方法允许 StateChange 对象通过依赖 Propagator 网络传输。IPropagator 接口显示在下方

public interface IPropagator
{
    void Process(StateChange stateChange);
    void Process(StateChange stateChange, StateChangeOptions options);
    void Process(StateChange stateChange, StateChangeOptions options, IPropagator sender);
    void AddDependent(IPropagator dependent, bool biDirectional);
    void RemoveDependent(IPropagator dependent, bool biDirectional);
    void RemoveAllDependents(bool biDirectional);
}

AddDependent() 方法的第一个参数是依赖的传播器。第二个参数指示依赖是否是双向的。例如,以下代码在 propagatorApropagatorB 之间创建双向依赖。

IPropagator propagatorA = new Propagator();
IPropagator propagatorB = new Propagator();
propagatorA.AddDependent(propagatorB, true);

要删除特定的依赖项,请调用 RemoveDependent()。此方法有一个参数,指示是否应双向删除依赖项。还有一个 RemoveAllDependents() 方法可以删除所有依赖项。同样,此方法也有一个选项可以双向删除依赖项。但是,如果依赖项是单向的,则 Remove() 方法不会抛出异常。

Process() 方法有几个重载。第一个重载是您代码中要调用的那个。它将负责更新当前的 Propagator 并通知依赖的 Propagator。第二个重载有一个 StateChangeOptions enum 参数,它控制状态更改的处理方式。它定义如下

[Flags]
public enum StateChangeOptions
{
    // If set, the invoked propagator will be updated.
    Update = 1 << 0,

    // If set, dependents will be notified and asked to update.
    Notify = 1 << 1,

    // If set, the invoked propagator will be updated and 
    // dependents will be notified and asked to update.
    UpdateAndNotify = Update | Notify
}

第三个 Process() 重载有第三个参数,用于指定发送方 Propagator。此重载由 Propagator 类使用,以使状态更改的传递更有效。如果依赖是双向的,此参数将防止状态更改直接返回到发送方。

StateChange 类表示要通过依赖网络传播的状态更改。StateChangeTypeID 有助于识别状态更改的类型。StateChange 类维护一个已观察到状态更改的 Propagator 列表。这可以防止状态更改永远通过循环图。

Propagator 类实现了 IPropagator。此外,它实现了一个用于处理状态更改的调度机制。Propagator 定义了一个用于处理状态更改的委托,并维护一个从 StateChangeTypeID 到此类处理程序方法的字典。状态更改处理程序通过 AddHandler() 方法注册。

Using the Code

在开始使用传播器之前,您应该确定项目中的哪些类需要成为依赖图的一部分。有关依赖图的示例,请参阅“简介”部分。

下面的代码展示了一个包含 Propagator 的最小类。Propagator 为了调试目的用名称实例化。Propagator 可以通过属性访问。请注意,此属性的返回类型是 IPropagator 接口而不是 Propagator 类。这可以防止任何外部对象向此 Propagator 添加状态更改处理程序。

// Example class with propagator.
public class ClassWithPropagator
{
    // Propagator object.
    private Propagator _propagator = new Propagator("SimpleExample");

    // Constructor.
    public ClassWithPropagator()
    {
    }

    // Propagator.
    public IPropagator Propagator
    {
        get
        {
            return _propagator;
        }
    }
}

下面的代码展示了如何通过其 Propagator 链接两个对象的示例。请注意,第二个参数表示依赖是双向的。对于单向依赖,请为此参数传入 false

ClassWithPropagator p1 = new ClassWithPropagator();
ClassWithPropagator p2 = new ClassWithPropagator();
p1.Propagator.AddDependent(p2.Propagator, true);

如果状态更改具有关联数据,则必须从 StateChange 派生一个类,才能通过网络发送关联数据。确保给您的状态更改类一个唯一的 ID。这可以通过定义所有状态更改 ID 的 enum 来实现。状态更改 enumColorChange 类显示在下方。请注意,状态更改 ID 作为 public const 字段可用,并且它传递给基类构造函数。

// Enum to ensure that state changes have a unique ID.
public enum DemoStateChanges
{
    ColorChangeID,
    FontChangeID
}
    
// Represents a change of color.
public class ColorChange : StateChange
{
    // ID of state change.
    public const int ID = (int) DemoStateChanges.ColorChange;

    // New color.
    private Color _color;

    // Constructor.
    public ColorChange(Color color)
        : base(ID) // Pass ID to base class.
    {
        _color = color;
    }

    // Get new color.
    public Color Color
    {
        get
        {
            return _color;
        }
    }
}

下一步是定义状态更改的处理程序。这些处理程序通过调用 AddHandler() 方法添加到 Propagator 类中,该方法带有状态更改类型的 ID 和具有以下签名的委托

void Handler(StateChange stateChange);

添加状态更改处理程序的好地方是在构造函数中。

下面的代码显示了示例代码中的 ColorControl。在构造函数中,为颜色和字体更改添加了两个状态更改处理程序。当颜色更改时,调用 HandleColorChange 方法;当字体更改时,调用 HandleFontChange 方法。这些方法被定义为 private 并相应地更新用户界面。如果您查看 HandleColorChange(),您会看到 StateChange 对象被转换为 ColorChange 对象。通过这种方式,可以检索新颜色。

ColorControl 的另一个有趣方法是 buttonSelectColor_Click()。此方法显示 .NET ColorDialog,让用户选择颜色。如果用户单击“确定”,则会创建一个新的 ColorChange 对象并将其传递给 Process() 方法。此方法确保执行当前 Propagator 的状态更改处理程序,之后会通知其他 Propagator 状态更改。

// Control for choosing color.
public partial class ColorControl : UserControl
{
    private Color _currentColor = Color.Black;
    private Propagator _propagator = new Propagator("ColorControl");

    // Constructor.
    public ColorControl()
    {
        InitializeComponent();

        // Add state change handlers.
        _propagator.AddHandler(ColorChange.ID, HandleColorChange);
        _propagator.AddHandler(FontChange.ID, HandleFontChange);
    }

    // Access to propagator.
    public IPropagator Propagator
    {
        get
        {
            return _propagator;
        }
    }

    // Select new color.
    private void buttonSelectColor_Click(object sender, EventArgs e)
    {
        ColorDialog colorDialog = new ColorDialog();
        colorDialog.Color = _currentColor;

        DialogResult result = colorDialog.ShowDialog();
        if (result == DialogResult.OK)
        {
            // Create state change object for color change.
            ColorChange colorChange = new ColorChange(colorDialog.Color);
            
            // Handle and notify dependent Propagators.
            _propagator.Process(colorChange);
        }
    }

    // Handle color change event.
    private void HandleColorChange(StateChange stateChange)
    {
        ColorChange colorChange = stateChange as ColorChange;
        if (colorChange != null)
        {
            Color newColor = colorChange.Color;

            // Set color of example text.
            labelExample.ForeColor = newColor;

            // Set background of color panel.
            panelColor.BackColor = newColor;

            // Remember the current color.
            _currentColor = newColor;
        }
    }

    // Handle font change event.
    private void HandleFontChange(StateChange stateChange)
    {
        FontChange fontChange = stateChange as FontChange;
        if (fontChange != null)
        {
            // Set font of example text.
            labelExample.Font = fontChange.Font;
        }
    }
}

DemoForm 包含一个 ColorControl 和一个 FontControl。与 ColorControlFontControl 一样,它包含一个 Propagator 对象。在 DemoForm 构造函数中,ColorControlFontControlPropagator 被使用 AddDependent() 方法添加为双向依赖项。这将 ColorControlFontControlDemoForm 连接在一个网络中。无论何时在任何 Propagator 上调用 Process(),状态更改都将传播到所有其他 Propagator

DemoForm 通过更新其示例字符串标签来响应颜色和字体的更改。DemoForm 本身也从其 Initialize() 方法发送状态更改。此方法发送带有默认值的字体和颜色状态更改。Initialize() 方法在 DemoForm() 构造函数中调用,以初始化依赖网络。当按下“重置”按钮时,也会调用此方法。

public partial class DemoForm : Form
{
    private Propagator _propagator = new Propagator("DemoForm");

    // Constructor.
    public DemoForm()
    {
        InitializeComponent();

        // Add font and color controls as dependents.
        _propagator.AddDependent(fontControl.Propagator, true);
        _propagator.AddDependent(colorControl.Propagator, true);

        // Add font and color change handlers.
        _propagator.AddHandler(FontChange.ID, HandleFontChange);
        _propagator.AddHandler(ColorChange.ID, HandleColorChange);

        // Initialize font and color.
        Initialize();
    }
            
    // Reset font and color.
    private void buttonReset_Click(object sender, EventArgs e)
    {
        Initialize();
    }

    // Initialize font and color.
    private void Initialize()
    {
        ColorChange colorChange = new ColorChange(Color.Blue);
        _propagator.Process(colorChange);

        Font font = new Font("Arial", 14);
        FontChange fontChange = new FontChange(font);
        _propagator.Process(fontChange);
    }

    // Handle color change event.
    public void HandleColorChange(StateChange stateChange)
    {
        ColorChange colorChange = stateChange as ColorChange;
        if (colorChange != null)
        {
            // Set color of example text.
            labelExample.ForeColor = colorChange.Color;
        }
    }

    // Handle font change event.
    public void HandleFontChange(StateChange stateChange)
    {
        FontChange fontChange = stateChange as FontChange;
        if (fontChange != null)
        {
            // Set font of example text.
            labelExample.Font = fontChange.Font;
        }
    }
}

何时使用传播器

当您想解耦组件同时使它们与应用程序的当前状态保持同步时,Propagator 非常有用。如果状态更改的数量可能随时间变化,或者某些组件只对某些状态更改感兴趣,那么解耦特别有用。

如果状态更改仅在特定组件中感兴趣,则直接方法调用更有效且更有意义。例如,在示例项目中,在 ColorControl 中使用 Propagator 只是为了更新标签是没有意义的。一般规则是,如果状态更改可能在整个依赖网络中都感兴趣,请使用 Propagator 基础设施。如果它仅对“本地”或“附近”对象感兴趣,请使用更直接的方法。

Propagator 直接将更改推送到依赖网络中。如果需要对更改进行被动响应,则黑板设计模式可能更有用。

关注点

为了使我的 GUI 中的不同组件保持同步,我最初使用了一个带有父控制器和子控制器的“Controller”类(只是一个名称!;-))。我遇到的问题是父控制器和子控制器的层次结构:状态更改要么向上要么向下传播,但有时它需要双向传播。我还遇到了状态更改在整个树中来回传播不止一次的问题。我不得不提出复杂的解决方案。例如,在发送状态更改时,您必须指定方向,例如“ToParent”或“ToCurrentAndChildren”。

我偶然发现了一篇名为“Propagator: A Family of Patterns”的旧文章,然后茅塞顿开。将 GUI 组件视为一个没有父/子层次结构的依赖对象网络要容易得多。我使状态更改对象足够智能,以避免无限循环,并且“发送方检查”也使图迭代效率很高。我很高兴最终得到了一个更简单、更强大的东西!

历史

  • 2009 年 7 月 13 日 - 初始版本
© . All rights reserved.