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

存储资源管理器应用程序中的设计模式实现

starIconstarIconstarIconstarIcon
emptyStarIcon
starIcon

4.92/5 (175投票s)

2004年4月26日

10分钟阅读

viewsIcon

321191

downloadIcon

2289

一种用于设计和解释存储浏览器应用程序的设计模式方法。该应用程序用于探索计算机存储中的文件组成。

引言

关于 GoF 设计模式的许多信息已经发布,但大多数示例都为每个模式使用单独的插图,并且示例代码不是一个具有实际功能的应用程序。存储浏览器是一个小型应用程序,旨在协同使用多种设计模式。虽然本次提交的主要目的是分享代码而不是向读者教授设计模式,但应用程序设计本身的讨论是使用设计模式来解释的。

本文讨论了应用程序的结构、我使用的设计模式、它们的工作原理以及使用它们的优点。在我看来,设计模式值得学习。具备设计模式技能的开发人员可以通过识别模式来识别程序的意图和目的。具有复杂关系的类可以比开发人员只理解基本面向对象设计的情况下更好、更快地理解。设计模式可以用作设计师和代码阅读器的思维模型。

该应用程序用于探索计算机存储中的文件组成。使用的设计模式有:策略模式、观察者模式、适配器模式、模板方法模式、单例模式和包装器外观模式。前五个被称为 GoF 设计模式,最后一个是 POSA 模式(POSA 书籍第 2 卷)。

背景

这个应用程序的想法是当我迫切需要一个类似于 Windows 资源管理器的应用程序,它能向我显示存储中的文件大小组成时产生的。例如,我想知道在“程序文件”文件夹下,哪个文件夹使用了多大的空间,包括百分比数字。我还想知道我的 D 盘中 mp3 的总大小与我的 jpg 文件相比有多大。我想看到的信息应该像这样:

还有这个

我需要信息以图表形式呈现,以帮助我可视化这些数字。

功能

应用程序功能如下:

  • 一个 TreeView,其中包含表示文件系统文件夹的节点。可以选择这些文件夹以查看该路径下文件大小结构的信息。
  • 它显示按文件夹分组的文件大小信息。
  • 它显示按文件类型分组的文件大小信息。
  • 信息以列表视图、饼图和条形图的形式显示在右侧面板中。

应用程序中的设计模式

此应用程序中使用的设计模式如下图所示。

下面将讨论每种设计模式。

策略

意图:“定义一组算法,将每个算法封装起来,并使它们可以互换”(GoF)。

应用程序使用两种不同的算法来探索文件系统,这两种算法在两个策略类中实现:`FolderStrategy` 和 `FileTypeStrategy`(参见上图)。`FolderStrategy` 类实现了一种算法,该算法探索文件并汇总一个路径下不同文件夹中的文件大小。`FileTypeStrategy` 类实现了一种算法,该算法探索文件并汇总相同类型的文件大小。

现在假设我们想添加一个新算法。可以通过在 `ExporationStrategy` 类下创建一个新子类并将新算法代码放入 `Explore()` 方法中来完成。在运行时创建新类的实例并将其动态分配给 explorer 对象。当我们调用 `explore.Explore()` 方法时,算法就会运行。这就是策略模式的思想:封装并使算法可互换。实现如下:

public abstract class ExplorationStrategy
{
  public virtual void Explore (...){}
}

public class NewAlgorithm : ExplorationStrategy
{
  public override void Explore (...)
  {
    // the new algorithm

  }
}

public class StorageExplorerForm : System.Windows.Forms.Form
{
  // Somewhere in the initialization section

  ExplorationStrategy explorer;
  ExplorationStrategy folderStrategy = new FolderStrategy ();
  ExplorationStrategy fileTypeStrategy = new FileTypeStrategy ();
  ExplorationStrategy newStrategy = new NewAlgorithm ();

  // Somewhere else, an algorithm is selected 

  switch (...)
  {
    case 0: explorer = folderStrategy; break;
    case 1: explorer = fileTypeStrategy; break;
    case 2: explorer = newStrategy; break;
  }
  
  // Execute the algorithm

  explorer.Explore (...);
}    
            

观察者

意图:“定义对象之间的一对多依赖关系,以便当一个对象状态改变时,所有依赖对象都会被自动通知和更新”(GoF)。

在应用程序中,此模式在主题(具体的 `ExplorationStrategy` 对象)和观察者(具体的 `ExplorationObserver` 对象)之间创建了一种关系,以便当探索过程结束时,主题通知观察者,然后观察者显示结果(参见上图)。具体主题(`FolderStrategy` 或 `FileTypeStrategy`)通过调用 `OnFinish()` 方法通知观察者(`ListViewAdapter`、`PieChartAdapter` 和 `BarChartAdapter`),这反过来会生成一个事件。该事件被发送给观察者,然后由它们处理。这种关系在抽象类中定义,然后具体的类继承它。

对象与观察者之间的关系是通过将观察者的 `UpdateDisplay()` 方法注册到 `ExplorationStrategy.Finish` 事件来建立的,如下所示:

public abstract class ExplorationObserver
{
  public void SubscribeToExplorationEvent (ExplorationStrategy obj)
  {
    obj.Finish += new ExplorationFinishEventHandler (UpdateDisplay);
  }
}

在客户端(应用程序表单)的应用程序初始化期间,具体的观察者对象调用 `SubscribeToExplorationEvent()` 方法并将具体策略的实例作为参数传递。从那时起,主题-观察者关系就已建立。它显示如下:

// Initialization in the client: 

// Create the concrete subject

ExplorationStrategy folderStrategy = new FolderStrategy ();
ExplorationStrategy fileTypeStrategy = new FileTypeStrategy ();

// Create the concrete observer

ExplorationObserver pieChart = new PieChartAdapter ();

// Subscribe the concrete observer object to the concrete strategy object

pieChart.SubscribeToExplorationEvent (folderStrategy);
pieChart.SubscribeToExplorationEvent (fileTypeStrategy);

现在让我们看看这种模式的美妙之处。假设我们想修改应用程序以适应新的需求。除了在屏幕上显示探索结果外,我们还想将结果保存到文件中。为此,从 `ExplorationObserver` 类继承一个新类,并将保存结果到文件的代码放入 `UpdateDisplay()` 方法中。创建新类的实例,然后调用 `SubscribeToExplorationEvent()`,并将具体的 `ExplorationStrategy` 对象作为参数。完成。当应用程序运行时,信息将发送到显示器和文件。代码如下所示。

public class NewConcreteObserver : ExplorationObserver
{
  public override void UpdateDisplay (object o, ExplorationFinishEventArgs e)
  {
    Hashtable result = e.ExplorationResult;

    // Write the result to a file

  }  
}

// Somewhere, during the initialization in the client

...
ExplorationObserver fileSaver = new NewConcreteObserver ();
fileSaver.SubscribeToExplorationEvent (folderStrategy);
fileSaver.SubscribeToExplorationEvent (fileTypeStrategy);
    

观察者设计模式实际上始终用于事件驱动编程。这个想法常常在不知不觉中被使用。然而,如果我们想将它与其他模式(如适配器)结合使用,了解这些概念仍然很重要,这将在下一节中解释。

适配器

意图:“将一个类的接口转换为客户端期望的另一个接口。适配器使原本由于接口不兼容而无法协同工作的类能够协同工作”(GoF)。

现在是这样的情况:我希望探索结果显示在 .NET 的 `ListView` 类中。但是,我需要修改 `ListView` 的功能,使其能够在探索完成后接收自动通知。这意味着我需要将 `ListView` 类更改为具有 `ExplorationObserver` 类的签名,以便我可以覆盖当 `Finish` 事件发生时调用的 `UpdateDisplay()` 方法。不幸的是,我无法修改 .NET `ListView` 类。解决方案是创建一个新类,即 `ListViewAdapter`,它使用 `ListView` 的功能,同时仍然具有 `ExplorationObserver` 类的签名。这里适配器模式发挥了作用。`ListViewAdapter` 类是一个适配器。用 GoF 的术语来说:`ListViewAdapter` 类修改 `ListView` 类的接口,使其具有 `ExplorationStrategy` 所期望的 `ExplorationObserver` 接口。

适配器模式有两种形式:类适配器和对象适配器。类适配器通过创建继承自 `ListView` 和 `ExplorationObserver` 类的 `ListViewAdapter` 来实现。由于 C# 不支持多重继承,因此这是不可能的。实际上,如果 `ExplorationObserver` 是一个接口而不是一个抽象类,这就有可能实现。`ListViewAdapter` 然后可以实现该接口并继承自 `ListView` 类。但在这种情况下,`ExplorationObserver` 在类内部有一些实现,这使得它无法成为一个接口。

第二种形式,即在此应用程序中使用的对象适配器,使用对象组合(参见下图)。

`ListViewAdapter` 类继承 `ExplorationObserver` 类。为了拥有 `ListView` 功能,适配器创建了一个 `ListView` 对象,并在必要时使用该对象。这种方法的优点是 `ListView` 对象及其成员对 `ListViewAdapter` 的用户隐藏。代码如下所示:

public class ListViewAdapter : ExplorationObserver
{
  protected ListView listView;

  public ListViewAdapter()
  {
    // Create a ListView object and initialze it

    listView = new ListView();
    ...
  }
}

同样的方法也适用于 `PieChartAdapter` 和 `BarChartAdapter` 类。我再次假设我已经拥有这些图表类,并且不想修改它们。解决方案还是创建一个适配器类,该类继承自 `ExplorationObserver` 类,并在适配器内部创建图表对象(参见下图)。

模板方法

意图:“在一个操作中定义算法的骨架,将一些步骤延迟到子类中。模板方法让子类重新定义算法的某些步骤,而无需改变算法的结构”(GoF)。

模板方法本身是类中的一个方法,它调用其他原始方法,以便当子类覆盖原始方法时,派生类中的模板方法产生不同的结果。现在让我们看看此应用程序中的模板方法实现(参见下图)。

图表中的模板方法是 `OnPaint()` 方法。在 `OnPaint()` 内部已经定义了一些绘制图表的步骤。这些步骤包括初始化 Graphics 对象、绘制图例和绘制图表图像的过程。如果我们要继承这个类以成为不同类型的图表,例如 `PieChart` 类和 `BarChart` 类,那么在 `OnPaint()` 方法内部,将绘制图表操作转换为 `DrawChart()` 方法调用。在 `Chart` 类本身中,`DrawChart()` 是一个空方法。在继承自 `Chart` 类的 `PieChart` 类中,`DrawChart()` 包含绘制饼图图像的实现,而在 `BarChart` 中,`DrawChart()` 绘制条形图图像。我们可以对绘制图例、标题以及绘制图表过程中涉及的每个步骤做同样的事情。代码如下所示:

public abstract class Chart : Control
{
  // OnPaint is a template method. It contains steps 

  // necessary to draw a complete chart

  public OnPaint ()
  {
    ...
    DrawLegend (...);
    DrawChart (...);
  }

  protected abstract void DrawChart (...)
  protected virtual void DrawLegend (...) { // Draw legend }

}

public abstract class PieChart : Chart
{
  protected virtual void DrawChart (...) 
  {
    // Draw the pie chart image

  }
}

public abstract class BarChart : Chart
{
  protected virtual void DrawChart (...) 
  {
    // Draw the bar chart image

  }
}
  

我使用与 `ListViewAdapter` 类相同的方法。它有两个派生类:`FolderListView` 和 `FileTypeListView`。这两个类在显示信息时步骤非常相似,除了设置图标的步骤。`FolderListView` 设置一个表示文件夹图像的单个图标,而 `FileTypeListView` 从 `ImageList` 中的一系列图像中选择图标。由于这种差异,该步骤被延迟到派生类中(参见下图)。

使用模板方法的关键是在基类中尽可能通用地定义算法的骨架,并将可能变化的步骤延迟到派生类。此外,在基类中,我们需要为延迟的步骤创建一些默认实现,以防派生类不想覆盖这些步骤。一旦我们有了好的模板和默认实现,我们就可以轻松地创建各种派生类。

单例

意图:“确保一个类只有一个实例,并提供一个全局访问点” [GoF]。

在此应用程序中,有两个类各自只需要一个实例即可运行:`FileIcons` 类和 `IconReader` 类。`FileIcon` 类用于从 Windows Shell 中检索和缓冲 Shell 图标。此D类表示系统中图标的集合。从定义中我们知道,它只需要一个正在应用程序中运行的类实例。`IconReader` 类用于从系统中检索图标而不进行缓冲。它调用 Shell API 函数。我们也只需要此类的一个实例,因此它被实现为单例。

MSDN 提供了一个如何在 .NET 中使用 C# 实现单例的示例。在此应用程序中,.NET 单例的实现如下:

sealed class FileIcons
{
  ...

  // By making it private, the class cannot be explicitly created

  private FileIcons () {}

  // This is the statement that ensures only one instance exists

  public static readonly FileIcons Instance = new FileIcons();

  public int GetIconIndex (...)
  {
    ...
  }
}

要使用 `FileIcons` 类,我们只需使用 `FileIcons.Instance` 属性,而无需创建该类,如下所示:

int iconIndex = FileIcons.Instance.GetIconIndex (...);

包装器外观模式

意图:“将现有非面向对象 API 提供的功能和数据封装在更简洁、健壮、可移植、可维护和高内聚的面向对象类接口中”(POSA 第 2 卷)。

要从 Windows 操作系统访问 shell 图标,我们必须调用 Shell API 和 Win API 函数。函数调用涉及严格使用数据结构和复杂参数。为了封装这种复杂性,我创建了一个 `IconReader` 类。POSA 指出这与外观模式不同,因为外观模式封装了类之间的关系复杂性,而包装器外观模式封装了非面向对象函数。此外,这种模式的目的更具体。例如,用于从 Windows Shell 检索图标的 Shell API 函数是 `SHGetFileInfo()` 函数。这个函数可以完成很多工作,而不仅仅是图标检索。因为 `IconReader` 类专门用于与图标相关的工作,所以我只创建了特定于它的方法,它们是 `GetSmallFileTypeIcon()` 方法(用于检索表示文件扩展名的图标)和 `GetClosedFolderIcon()`(用于检索表示关闭文件夹的图标)。图标检索还需要调用 Win32 API `DestroyIcon()` 函数。这种模式背后的思想是:不要关注过程背后的复杂性,只要它能提供健壮且内聚的功能,就使用所有必要的资源,并且只向类用户呈现内聚的接口。

© . All rights reserved.