C# 中的设计模式示例
简单的示例说明了设计模式
引言
设计模式是一些在编写程序时常见的思想。完整的列表和它们的定义可以在这里找到。该链接提供了非常好的描述和示例,所以我不会在这里重复定义。我认为设计模式不容易理解,而且有很多示例需要记住。为了应用设计模式来解决问题,你需要理解并记住这些模式,这样你才能在问题中看到它们。我连名字都记不住一半;我无法准确背出任何一个模式的定义;而且我不确定是否完全理解了设计模式的定义。然而,一个简单的例子帮助了我,使得模式更容易理解和记忆。我想分享它,并希望它也能帮助到其他人。
背景
我几年前参加了 NetObjectives 的培训。我不记得培训中的很多定义或原则,但他们的例子一直留在我的脑海里。这是一个简单的问题,但它包含了几个模式。
例子是这样的:我们需要为两种形状:矩形和圆形设计类。我们需要在不同的设备上绘制它们:控制台和打印机。形状可以移动和放大。将来,我们可能需要支持其他形式的转换,例如旋转和扭曲。转换形状后,我们需要能够撤销它。我们需要支持更复杂的形状,这些形状由更简单的形状组成。
这个例子中的模式
我们首先将矩形和圆形抽象为 Shape。如果 Shape 知道如何在打印机和控制台上绘制,那么我们就需要不同版本的 Shape。所以,最好将 Shape 与绘图设备分开。我们也把控制台和打印机抽象为绘图设备。这实际上是使用了桥接模式 (Bridge pattern),因为它解耦了 Shape 和 Drawing 的实现。在下面的代码片段中,IShape
保存了对 IDrawing
对象的引用。Shape 有两个实现:Circle 和 Rectangle。Drawing 有两个实现:V1 和 V2。
/*
IShape------------->IDrawing
/ \ / \
Circle Rectangle V1 V2
*/
abstract class IShape
{
protected IDrawing drawing;
abstract public void Draw();
}
abstract class IDrawing
{
abstract public void DrawLine();
abstract public void DrawCircle();
}
class Circle:IShape
{
override public void Draw(){drawing.DrawCircle();}
}
class Rectangle:IShape
{
override public void Draw(){drawing.DrawLine(); drawing.DrawLine();}
}
class V1Drawing:IDrawing
{
override public void DrawLine(){}
override public void DrawCircle(){}
}
class V2Drawing:IDrawing
{
override public void DrawLine(){}
override public void DrawCircle(){}
}
假设 `Shapes` 的数量是固定的,但是会有新的 `Transformations` 类型。为了支持新的 `Transformations` 类型,我们可以使用访问者模式 (Visitor pattern) 来使 `Shapes` 类面向未来。在下面的代码片段中,`Shape` 类定义了操作接口 `Accept(ITransform)`,这样新的转换类型就可以在不修改 `Shape` 类的情况下对 `shapes` 进行操作。
/*
IShape---------->ITransform
/ \ / \
Circle Rectangle Move Expand
*/
abstract class IShape
{
abstract public void Accept(ITransform transform);
}
abstract class ITransform
{
abstract public void TransformCircle(Circle c);
abstract public void TransformRectangle(Rectangle rect);
}
class Circle : IShape
{
override public void Accept(ITransform transform)
{
transform.TransformCircle(this);
}
}
class Rectangle : IShape
{
override public void Accept(ITransform transform)
{
transform.TransformRectangle(this);
}
}
class Move : ITransform
{
override public void TransformCircle(Circle c) { }
override public void TransformRectangle(Rectangle rect) { }
}
class Expand : ITransform
{
override public void TransformCircle(Circle c) { }
override public void TransformRectangle(Rectangle rect) { }
}
如果我们想 `undo` 转换,我们可以使用备忘录模式 (Memento pattern),在这种模式下,我们可以存储 `Shape` 对象的状态而不暴露其内部状态。
//Memento--->IShape
interface IMemento
{
void ResetState();
}
class IShape
{
public abstract IMemento GetMemento();
}
如果一个形状由其他形状的集合组成,我们可以使用组合模式 (Composite pattern),这样我们就可以以相同的方式处理复杂形状。
/* IShape
/ | \
Circle | Rectangle
|
CompositeShape 1--->* IShape
*/
class CompositeShape : IShape
{
List<IShape> list = new List<IShape>();
}
如果你对一个对象调用 `MembewiseClone()`,你只会得到一个浅拷贝。书《C# 3.0 Design Patterns》提供了一个 `DeepCopy` 的解决方案。它将对象 `serializes` 到一个 `MemoryStream` 中,然后 `Deserializes` 回一个克隆的对象。
[Serializable]
class IShape
{
public IShape DeepCopy()
{
using (MemoryStream m = new MemoryStream())
{
BinaryFormatter f = new BinaryFormatter();
f.Serialize(m, this);
m.Seek(0, SeekOrigin.Begin);
IShape ret = (IShape)f.Deserialize(m);
ret.Drawing = drawing;
return ret;
}
}
}
C# 和 .NET 库中的设计模式示例
C# 和 .NET 库中有许多示例。例如
适配器模式
Streams
是用于读写字节序列的工具。如果你需要读写 `string` 或字符,你需要创建一个 `Reader/Writer` 类。幸运的是,所有的 `Reader`/`Writer` 类都可以从一个 `stream` 对象构建。所以,我认为 `Reader`/`Writer` 类是适配器,它们将字节数组接口转换为 `string`/`char` 接口。
Reader/Writer-->Stream
BinaryReader/Writer{ReadChars,ReadString}
TextReader/Writer{ReadLine,ReadBlock}
|-StreamReader/Writer
|-StringReader/Writer
Stream{Read(byte[]), Write(byte[], Seek, Position)
|
|-NetworkStream(socket){DataAvailable}
|-FileStream(path){ Lock/Unlock/GetACL,IsAsync}
|-MemoryStream (byte[]){GetBuffer(), WriteTo(stream),ToArray()}
我认为“装箱 (boxing)”也是一个适配器。它将值类型转换为引用类型。
装饰器模式 (Decorator Pattern)
当你需要 `encryption` 或 `compression`,或者需要在网络流上添加缓冲区时,你可以使用装饰器 (Decorator) 模式。`bufferedStream`、`CryptoStream` 和 `DeflateStream` 是其他流的装饰器。它们在不改变原始 `stream` 接口的情况下,为现有的 `stream` 添加了额外的功能。
Stream
|
|- BufferedStream(stream,buffersize), for network
|- CryptoStream: for encryption
|- DeflateStream: for compression
享元模式
为了节省空间,`String` 类持有对 `Intern` 对象的引用。所以,如果两个 `string` 具有相同的字面量,它们共享相同的存储空间。它使用“共享”来高效地支持大量细粒度的对象,因此它使用了享元模式 (Flyweight pattern)。
String s3 = String.Intern(s2);
对象池模式 (Object Pool Pattern)
创建线程是昂贵的。你可以调用 `Threadpool.QueueUserWorkitem()`,它使用线程池来更好地利用系统资源。所以,我认为线程池是对象池模式的一个很好的例子。
观察者模式
观察者模式 (Observer pattern) 定义了一个一对多的依赖关系,这样当一个对象的状态发生变化时,它的所有依赖项都会得到通知。C# 中的 `event` 就是为此目的而设计的。一个事件可以被订阅
UnhandledExceptionEventHandler handler =
(object sender, UnhandledExceptionEventArgs v) => {
Console.WriteLine(
"sender={0}, arg={1}, exception={2}, v.IsTerminating={3}",
sender, v, v.ExceptionObject, v.IsTerminating);
};
AppDomain.CurrentDomain.UnhandledException += handler;
throw new Exception("hiii");
要取消订阅,你只需要调用 event-=
。
AppDomain.CurrentDomain.UnhandledException -= handler;
你也可以定义自己的事件
static public event EventHandler<EventArgs> breakingNews;
if(breakingNews!=null)
breakingNews("NBC", EventArgs.Empty);
如果你不想检查事件是否为 `null`,只需添加一个虚拟的订阅者
breakingNews += delegate { };
迭代器模式
迭代器 (Iterator) 提供了一种访问聚合对象内部元素的方式。C# 中有 `foreach` 关键字,这使得迭代器 (Iterator) 非常容易。对我来说,理解 `Enumerable` 和 `Enumerator` 是很困难的。更令人困惑的是,为什么我必须为 `IEnumerable` 和 `IEnumerable<T>` 定义 `GetEnumerator`。所以,我只是试着记住这个例子
class IntEnumerable:IEnumerable<int>
{
public IEnumerator<int> GetEnumerator()
{
yield return 1;
yield return 2;
}
IEnumerator IEnumerable.GetEnumerator()
{
return this.GetEnumerator();
}
}
//Then you use the iterator like this:
foreach(int i in new IntEnumerable())Console.WriteLine(i);
单例模式
在 C# 中创建单例的最简单方法是使用 `static` 变量。
class Singleton
{
private static Singleton instance = new Singleton();
public static Singleton GetInstance()
{
return instance;
}
}
有些人使用双重检查锁定 (double-checked-lock) 来创建线程安全的惰性初始化单例。然而,它的实现非常微妙,以至于有些人认为它是反模式:[阅读此文]。
Netobjectives 有一种方法,我认为这是创建线程安全惰性初始化单例的最佳方式。注意,`static` 构造函数会禁用 `BeforeFieldInit` 标志(参见此链接)。内部类 `Nested` 避免了在调用 Singleton 的其他 `static` 字段或方法时过早初始化。
class Singleton
{
private Singleton() { }
public static Singleton GetInstance()
{
return Nested.instance;
}
private class Nested
{
internal static Singleton instance = new Singleton();
//static constructor to prevent beforefieldInit
// see http://www.yoda.arachsys.com/csharp/beforefieldinit.html
static Nested() {}
}
}
Using the Code
示例代码是用 C# 3.0 编写的。
结论
我确定我错过了许多模式,并且可能在以上示例中误解了一些。如果属实,请告诉我,以便进行更正。
我从 NetObjectives 的培训中获得的示例中受益匪浅。我觉得这次培训令人大开眼界,我学到了很多东西。几年后,我仍然记得的唯一事情是他们关于 Shape、Drawing 和 Transform 的例子。如果你不熟悉设计模式,我希望这个例子能激发你阅读维基百科或书籍,或者参加 NetObjectives 的培训。如果你参加过他们的培训或熟悉设计模式,那么这些例子可能会帮助你快速回顾设计模式。
我学到的一件事是,如果我的代码有很多复制粘贴,或者有很多 `switch`/`case` 语句,那么我可能需要考虑使用一些设计模式。
我认为设计模式的思想与数据库规范化类似。在数据库规范化过程中,我们将一个大表分解成几个小表,这样表的总行数就少了很多,并且在一个小表中的修改不会影响其他表。如果我们能够分离/隔离问题中的概念,那么我们很可能已经在应用设计模式的思想了,即使我们不记得模式的名称。
历史
- 2009年2月23日:初次发布
- 2009年3月6日:文章更新
- 修复了一个损坏的链接
- 修复了代码片段的编译错误
- 移除了部分示例