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






4.94/5 (19投票s)
C# 中传播器设计模式的可重用实现,这是著名的观察者设计模式的潜在更强大的替代方案。

引言
本文提供了 C# 中传播器设计模式的可重用实现。传播器设计模式是一种用于更新依赖网络中对象的模式。当状态更改需要通过对象网络推送时,它非常有用。状态更改由对象本身表示,该对象通过传播器网络传输。通过将状态更改封装为对象,传播器变得松散耦合。
我已将传播器模式用于一个复杂的 GUI 应用程序,其中用户界面的不同组件需要保持同步。这个 GUI 也非常可配置,因此拥有松散耦合的 GUI 组件对我来说肯定是有利的。
演示应用程序由一个名为 DemoForm
的表单和两个控件组成:FontControl
和 ColorControl
。DemoForm
、FontControl
和 ColorControl
都显示一个具有相同字体和颜色的字符串。正如名称所示,FontControl
可以更改字符串的字体,ColorControl
可以更改字符串的颜色。DemoForm
有一个重置按钮,用于将字体和颜色重置为原始值。这三个组件中的字体和颜色通过传播器设计模式保持同步。依赖网络显示在下方。

如果您想立即开始使用 Propagator
类,请跳过接下来的两节,直接跳转到“使用代码”部分。
背景
传播器可以看作是观察者设计模式的改进版本。在观察者设计模式中,主题通知其观察者更改。在传播器设计模式中,不存在主题和观察者之间的区别。依赖网络中的每个对象都是一个 Propagator
,一个 Propagator
可以有 0 个或更多依赖的 Propagator
。通过设置 Propagator
之间正确的依赖关系,可以创建依赖对象网络。
在我的设计中,我决定将每个状态更改封装在一个对象中。这类似于命令设计模式,其中命令封装了要在应用程序中执行的操作。状态更改可以由任何实体创建,并通过依赖的 Propagator
网络发送。每个 Propagator
都可以选择响应状态更改,但它们总是会将其传递给依赖的 Propagator
。
状态更改对象只使用一次然后被处置。它保留了一个已观察到状态更改的 Propagator
列表。这可以防止状态更改永远循环网络。
Propagator
之间的依赖可以是双向的。例如,如果网络表示父子关系,则状态更改可以由子级或父级发起。为了优化状态更改的传播,发送的 Propagator
作为参数给出。通过这种方式,接收的 Propagator
不会将状态更改直接发送回发送方。
以下是我的 Propagator
实现特点的快速概述
- 状态更改直接通过依赖图推送
- 支持循环依赖图
- 支持双向依赖
- 依赖图的深度优先迭代
- 当抛出异常时,依赖图的迭代将停止
设计
可重用 Propagator
类的类图显示在下方

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()
方法的第一个参数是依赖的传播器。第二个参数指示依赖是否是双向的。例如,以下代码在 propagatorA
和 propagatorB
之间创建双向依赖。
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
来实现。状态更改 enum
和 ColorChange
类显示在下方。请注意,状态更改 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
。与 ColorControl
和 FontControl
一样,它包含一个 Propagator
对象。在 DemoForm
构造函数中,ColorControl
和 FontControl
的 Propagator
被使用 AddDependent()
方法添加为双向依赖项。这将 ColorControl
、FontControl
和 DemoForm
连接在一个网络中。无论何时在任何 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 日 - 初始版本