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

C# 中的设计模式示例

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.08/5 (24投票s)

2009年2月23日

CPOL

6分钟阅读

viewsIcon

230344

downloadIcon

3702

简单的示例说明了设计模式

引言

设计模式是一些在编写程序时常见的思想。完整的列表和它们的定义可以在这里找到。该链接提供了非常好的描述和示例,所以我不会在这里重复定义。我认为设计模式不容易理解,而且有很多示例需要记住。为了应用设计模式来解决问题,你需要理解并记住这些模式,这样你才能在问题中看到它们。我连名字都记不住一半;我无法准确背出任何一个模式的定义;而且我不确定是否完全理解了设计模式的定义。然而,一个简单的例子帮助了我,使得模式更容易理解和记忆。我想分享它,并希望它也能帮助到其他人。

背景

我几年前参加了 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日:文章更新
    • 修复了一个损坏的链接
    • 修复了代码片段的编译错误
    • 移除了部分示例
© . All rights reserved.