.NET 事件处理中使用模板方法设计模式






3.61/5 (15投票s)
2002年3月21日
4分钟阅读

110320
如何使用模板方法设计模式增强 .NET 事件处理
引言
Microsoft .NET 事件处理,与典型的面向对象框架一样,是使用众所周知的 _观察者_ 设计模式实现的。(参见书籍《设计模式》,Gamma 等人著,Addison-Wesley,1995 年,第 325-330 页)。本文描述了如何使用 _模板方法_ 设计模式来增强 .NET 事件处理。讨论和代码片段采用 C# 编写,但总结示例同时使用 C# 和 Visual Basic .NET 实现。
本文详细阐述了 Tomas Restrepo 在 2002 年 3 月《Visual Systems Journal》杂志上讨论的观点,该观点建立在 Microsoft 在 MSDN Library .NET 主题《类库开发人员设计指南》中推荐的事件处理实践之上。(参见子主题“事件使用指南”)。
最简单的事件处理策略就是引发一个事件,而不关心谁在处理它,或者不同的客户端是否需要以不同的方式处理它。
示例 - 简单的事件处理
考虑一个类 Supplier
,当它的 name
字段被设置时会引发一个事件,以及一个处理该事件的类 Client
。
public class Supplier
{
public Supplier() {}
public event EventHandler NameChanged;
public string Name
{
get { return name; }
set { name = value; OnNameChanged(); }
}
private void OnNameChanged()
{
// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
private string name;
}
public class Client
{
public Client()
{
// Register for supplier event
supplier = new Supplier();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}
public void TestEvent()
{
// Set the name - which generates an event
supplier.Name = "Kevin McFarlane";
}
private void supplier_NameChanged(object sender, EventArgs e)
{
// Handle supplier event
}
private Supplier supplier;
}
事件的客户端可以是 _外部的_ 和 _内部的_。
“外部”客户端是指消耗事件但与引发事件的类无关的客户端。换句话说,它不是事件类继承树的一部分。上面的 Client
类就是一个外部客户端。
“内部”客户端可以是事件引发类本身(如果它正在处理自己的事件)或事件引发类的子类。在这种情况下,上面概述的简单策略是不够的。客户端不能轻易地更改当事件被引发时会发生什么,或者处理事件时的默认行为是什么。
为了解决这个问题,在 .NET 《类库开发人员设计指南》中,Microsoft 推荐使用受保护的虚拟方法来引发每个事件。这为子类提供了通过重写来处理事件的途径。因此,在我们的示例中,OnNameChanged()
应该如下所示:
protected virtual void OnNameChanged()
{
// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
Microsoft 接着补充道:“派生类可以选择不在 OnEventName
的处理过程中调用基类。在 OnEventName
方法中不包含基类正确运行所需的任何处理,以此做好准备。”
问题就在这里。通常,OnNameChanged()
可以在引发事件之前执行一些默认处理。OnNameChanged()
的重写可能希望执行不同的操作。但为了确保外部客户端正常工作,它必须调用基类版本。如果它不调用基类版本,事件就不会为外部客户端引发。而且,它可能会忘记调用基类版本。忘记调用引发事件的基类版本会违反 Liskov(多态)替换原则:使用基类引用的方法必须能够使用派生类对象而无需了解它。幸运的是,这个问题有一个解决办法。
模板方法设计模式
模板方法设计模式的目的是将一个算法定义为固定的步骤序列,但允许一个或多个步骤可变。在我们的示例中,算法可以被视为由引发事件和响应事件组成。需要可变的部分是响应。因此,诀窍是将此与引发事件分离开来。我们将 OnNameChanged()
分为两个方法:InternalOnNameChanged()
和 OnNameChanged()
。InternalOnNameChanged()
调用 OnNameChanged()
来执行默认处理,然后引发事件。
private void InternalOnNameChanged()
{
// Derived classes may override default behaviour
OnNameChanged();
// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
protected virtual void OnNameChanged()
{
// Implement default behaviour here
}
现在 Name
属性的修改如下所示:
get { return name; }
set { name = value; InternalOnNameChanged(); }
这项技术的好处是:
- 基类实现中的一个关键步骤(在此例中是引发事件)不能被派生类通过不调用基类实现来规避。因此,可以可靠地服务外部客户端。
- 派生类可以安全地替换
OnNameChanged()
中基类的默认行为,而无需担心。
示例 - 模板方法设计模式事件处理
下面是一个同时使用 C# 和 Visual Basic .NET 实现的完整示例。它包含三个类:Supplier
、ExternalClient
和 InternalClient
。Supplier
引发一个事件。两个客户端类都消耗该事件。InternalClient
是 Supplier
的派生类。
ExternalClient
包含一个嵌入的 Supplier
引用。然而,这个引用是用 InternalClient
引用初始化的。因此,当 ExternalClient
注册 Supplier
事件时,这会调用 InternalClient
的 OnNameChanged()
重写。然后,事件将由 InternalClient
的 NameChanged()
和最后由 ExternalClient
的 NameChanged()
处理程序处理。
因此,产生的输出是:
InternalClient.OnNameChanged InternalClient.NameChanged ExternalClient.NameChanged
C# 实现
using System;
class Test
{
static void Main(string[] args)
{
ExternalClient client = new ExternalClient();
client.TestSupplier();
}
}
// Generates an event when its property is set.
public class Supplier
{
public Supplier() {}
public event EventHandler NameChanged;
public string Name
{
get { return name; }
set { name = value; InternalOnNameChanged(); }
}
// Internal clients, i.e., derived classes, can override default behaviour.
protected virtual void OnNameChanged()
{
// Implement default behaviour here
Console.WriteLine("Supplier.OnNameChanged");
}
// If internal clients (derived classes) override the default behaviour in OnNameChanged
// then external clients will still receive the event.
private void InternalOnNameChanged()
{
// Derived classes may override default behaviour
OnNameChanged();
// If there are registered clients raise event
if (NameChanged != null)
NameChanged(this, new EventArgs());
}
private string name;
}
// An "internal" client that handles the Supplier.NameChanged event
// but first overrides its default behaviour.
public class InternalClient : Supplier
{
public InternalClient()
{
NameChanged += new EventHandler(this.Supplier_NameChanged);
}
protected override void OnNameChanged()
{
// Override default behaviour of Supplier.NameChanged
Console.WriteLine("InternalClient.OnNameChanged");
}
private void Supplier_NameChanged(object sender, EventArgs e)
{
// Handle Supplier.NameChanged
Console.WriteLine("InternalClient.NameChanged");
}
}
// An "external" client that handles the Supplier.NameChanged event.
public class ExternalClient
{
public ExternalClient()
{
// Instantiate supplier as a reference to an InternalClient instance.
// This should trigger the InternalClient.OnNameChanged override
// when an event is raised.
supplier = new InternalClient();
supplier.NameChanged += new EventHandler(this.supplier_NameChanged);
}
public void TestSupplier()
{
// This should raise an event and it will be handled by
// the InternalClient and ExternalClient handlers.
supplier.Name = "Kevin McFarlane";
}
private void supplier_NameChanged(object sender, EventArgs e)
{
// Handle Supplier.NameChanged
Console.WriteLine("ExternalClient.NameChanged");
}
private Supplier supplier;
}
Visual Basic .NET 实现
Module Test
Sub Main()
Dim client As ExternalClient = New ExternalClient()
client.TestSupplier()
End Sub
End Module
' Generates an event when its property is set.
Public Class Supplier
Sub New()
End Sub
Public Event NameChanged As EventHandler
Public Property Name() As String
Get
Return mName
End Get
Set(ByVal Value As String)
mName = Value
InternalOnNameChanged()
End Set
End Property
' Internal clients, i.e., derived classes, can override default behaviour.
Protected Overridable Sub OnNameChanged()
' Implement default behaviour here
Console.WriteLine("Supplier.OnNameChanged")
End Sub
Private Sub InternalOnNameChanged()
' Derived classes may override default behaviour
OnNameChanged()
' Raise event for clients
RaiseEvent NameChanged(Me, New EventArgs())
End Sub
Private mName As String
End Class
' An "internal" client that handles the Supplier.NameChanged event
' but first overrides its default behaviour.
Public Class InternalClient
Inherits Supplier
Sub New()
End Sub
Protected Overrides Sub OnNameChanged()
' Override default behaviour of Supplier.NameChanged
Console.WriteLine("InternalClient.OnNameChanged")
End Sub
Private Sub Supplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs) _
Handles MyBase.NameChanged
' Handle Supplier.NameChanged
Console.WriteLine("InternalClient.NameChanged")
End Sub
End Class
' An "external" client that handles the Supplier.NameChanged event.
Public Class ExternalClient
Sub New()
' Instantiate Supplier as a reference to an InternalClient instance.
' This should trigger the InternalClient.OnNameChanged override
' when an event is raised.
mSupplier = New InternalClient()
' Register for Supplier.NameChanged event
AddHandler mSupplier.NameChanged, AddressOf mSupplier_NameChanged
End Sub
Public Sub TestSupplier()
' This should raise an event and it will be handled by
' the InternalClient and ExternalClient handlers.
mSupplier.Name = "Kevin McFarlane"
End Sub
Private Sub mSupplier_NameChanged(ByVal sender As Object, ByVal e As EventArgs)
' Handle Supplier.NameChanged
Console.WriteLine("ExternalClient.NameChanged")
End Sub
Private mSupplier As Supplier
End Class