《设计模式沉思录》—第2章2.1节基础

第2章 运用模式进行设计
设计模式沉思录
如果想体验一下运用模式的感觉,那么最好的方法就是运用它们。对我来说,最大的挑战在于找到一个所有人都能够理解的示例。人们对自己的问题最感兴趣,如果某些人对某个示例越感兴趣,这个示例往往就越具体。问题在于,这样的示例所涉及的问题往往太过晦涩,对于没有相关领域背景的人来说难以理解。

层级文件系统(hierarchical file system)是每个计算机用户都熟悉的东西,就让我们来看看该如何设计它。我们不会关心诸如I/O缓冲和磁盘扇区管理之类的底层实现问题,我们要关心的是设计一个让应用程序开发人员使用的编程模型——文件系统的API。在大多数的操作系统中,这样的API通常包含大量的过程调用和一些数据结构,但对扩展性的支持却很少或根本没有。我们的设计将完全是面向对象的而且是可扩展的。

我们首先集中讨论这个设计最重要的两方面,以及用来对这两方面进行处理的模式。然后我会在这个示例的基础上展示其他模式是如何解决设计问题的。本章的目的并不是要为如何应用模式规定一个严格的流程,也不是要展示设计文件系统的最佳方法,而是要鼓励读者自己应用模式。用得越多,看得越多,你在处理模式的时候就会感到越轻松。最终,你将慢慢地学会应用模式所需的精湛技艺:属于你自己的技艺。

2.1 基础
从用户的角度来看,无论文件有多大,目录结构有多复杂,文件系统都应该能够对它们进行处理。文件系统不应该对目录结构的广度或深度施加任何限制。从程序员的角度来看,文件结构的表示方法不仅应该容易处理,而且应该容易扩展。

假设我们正在实现一个用来列出一个目录中文件的命令。我们编写的用来得到一个目录的名字的代码与用来得到一个文件的名字的代码相比,应该没有区别,也就是说,同样的代码应该能够同时处理这两种情况。换句话说,在请求目录的名字和文件的名字时,应该能够以相同的方式处理。这样得到的代码将会更易于编写和维护。我们还想在不重新实现部分系统的前提下,加入新的文件类型(比如符号化链接)。

因此,一开始有两件事情非常清楚:一是文件和目录是这个问题域(problem domain)的关键元素,二是我们需要一种方式,能够让我们在完成设计之后再为这些元素引入特别的版本。一种显而易见的设计方法是用对象来表示这些元素。

我们如何实现图2-1所示的结构呢?有两种对象,这意味着我们需要两个类——一个用来表示文件,另一个用来表示目录。我们还想以同样的方式处理文件和目录,这意味着它们必须有一个共同的接口。更进一步说,这意味着这两个类必须派生自一个共同的(抽象)基类,我们称之为Node。最后,我们还知道目录中包含文件。

所有这些约束基本上已经替我们把类的层次结构定义出来了。

Class Node {
public:
  // declare common interface here
protected:
  Node();
  Node(const Node&);
};
Class File : public Node {
public:
  File();
  // redeclare common interface here
};
Class Directory : public Node {
public:
  Directory();
  // redeclare common interface here
private:
  list<Node*> _nodes;
};

另一个需要斟酌的问题与共同接口的组成有关。哪些操作既能够适用于文件又能够适用于目录呢?

文件和目录有各种各样的共同属性,比如名字、大小、保护属性等。每个属性可以有相应的操作来访问和修改它的值。以相同的方式来处理那些对文件和目录都有明确意义的操作是很简单的事情。但是,想以相同的方式来处理那些不能明确适用于两者的操作时,问题就随之而来。

举个例子,用户经常执行的一项操作就是列出一个目录中的所有文件。这意味着Directory需要一个接口来枚举它的子节点。下面这个简单的接口用来返回第n个子节点。

virtual Node* getChild(int n);

由于一个目录既可能包含File对象,也可能包含Directory对象,因此getChild必须返回一个Node*。这个返回值的类型衍生出一个重要的结果:它强制我们不仅要在Directory类中定义getChild,而且还要在Node类中定义该接口。为什么?因为我们想要能够列出子目录的子节点。实际上,用户经常想要访问文件系统结构的下一层。除非不用强制转换就能用getChild的返回值来调用getChild,否则是无法通过一种静态的、类型安全的方式来完成这个操作的。因此,和属性操作一样,getChild是我们想要同时用在文件和目录上的操作。

同时,getChild也是允许我们以递归的方式来定义Directory的操作的关键。假设Node声明了一个size操作,这个操作返回该目录树(及其子树)所占用的总字节数。Directory可以这样定义自己的这个操作:依次调用它所有子节点的size操作,将所有的返回值相加,得到的总和就是自己的返回值。

long Directory::size() {
  long total = 0;
  Node* child;

  for (int i = 0; child = getChild(i); ++i) {
    total += child->size();
  }

  return total;
}

目录和文件的例子说明了COMPOSITE模式最关键的几个方面:它产生的树结构可以支持任意的复杂度,它还规定了如何以统一的方式来处理这些树结构中的对象。COMPOSITE模式的意图部分对这些方面进行了描述:

将对象组织成一个树结构来表示“部分—整体”的层次结构,给客户一种统一的方式来处理这些对象,无论这些对象是内部节点(internal node)还是叶节点(leaf)。

适用性部分描述了我们应该在以下场合使用COMPOSITE模式。

我们想要表示对象的“部分—整体”层次结构。
我们想让用户能够忽略复合对象和单个对象之间的区别。用户将以统一的方式来处理复合结构中的所有对象。
该模式的结构部分用一个经过修改的OMT①图的形式,描绘了典型的COMPOSITE类结构。之所以说它是典型,我的意思只是它代表了我们(GoF)所见过的类的最为常见的组织方式。它并不能代表最终得到的各个类及其关系,这是因为有时候受到某种设计或实现的影响,我们必须采取一些折中,这种情况下得到的接口可能会有所不同。(COMPOSITE模式同样对这些内容进行了阐述。)

图2-2展示了COMPOSITE模式涉及的各个类,以及这些类之间的静态关系。我们的Node类相当于Component,它是一个抽象基类。File类相当于子类Leaf,而Directory类则相当于子类Composite。从Composite指向Component的箭头线表明Composite包含了Component类型的实例。箭头前面的实心圆圈表示多于一个实例;如果没有实心圆圈,则表示有且仅有一个实例。箭头线尾部的菱形表示Composite聚合了它的子实例,这也意味着删除一个Composite会同样删除它的子实例。它还意味着所有的Component没有被共享,因此确保了树结构。COMPOSITE模式的参与者和协作部分对各个类之间的静态关系和动态关系分别进行了解释。

COMPOSITE的效果部分总结了使用该模式的好处和坏处。好处是,COMPOSITE支持任意复杂度的树结构。这个特性产生的直接结果就是对客户代码隐藏了节点的复杂度:他们无法辨别出他们正在处理的Component到底是一个Leaf还是一个Composite,事实上他们也没有必要去辨别,这使得客户代码更加独立于Component的代码。客户代码也变得更加简单,因为它能够以统一的方式来处理Leaf和Composite。客户代码再也不需要根据Component的实际类型来决定要执行许多代码分支中的哪一个分支。最好的是,我们可以添加新的Component类型而无须修改已有的代码。
但是COMPOSITE的坏处在于它可能会产生这样的系统:系统中每个对象的类与其他对象的类看起来都差不多。由于显著的区别只有在运行的时候才会显现出来,因此这会使代码难以理解,即便我们知道类的具体实现也无济于事。此外,如果在一个比较低的层次运用该模式,或者运用该模式时的粒度太细,那么对象的数量可能会多得让系统负担不起。

正如读者可能已经猜到的那样,COMPOSITE模式的实现部分讨论了在实现该模式时会面临的许多问题。

为了提高性能,应该在何时以及何处对信息进行高速缓存;
Component类应该分配多少存储空间;
在存储子节点的时候,应该使用什么数据结构;
是否应该在Component类中声明那些用来添加和删除子节点的操作;
等等。
在开发我们的文件系统时,我们将努力解决这些问题中的一部分,以及许多其他问题。

时间: 2024-09-16 21:02:47

《设计模式沉思录》—第2章2.1节基础的相关文章

《设计模式沉思录》—第1章1.1节对模式的十大误解

第1章 介绍设计模式沉思录在阅读本书之前,如果读者还没有听说过一本名叫<设计模式>(Design Patterns: Elements of Reusable Object-Oriented Software [GoF95])的书,那么现在正好可以去找一本来读.如果读者听说过该书,甚或自己还有一本但却从来没有实际研读过,那么现在也正好应该好好研读一下. 如果你仍然在继续往下阅读,那么我会假设你不是上述两种人.这意味着你对模式有大致的了解,特别是对23个设计模式有一定的了解.你至少需要具备这样的

《设计模式沉思录》目录—导读

版权声明设计模式沉思录Authorized translation from the English language edition, entitled Pattern Hatching: Design Patterns Applied, 9780201432930 by John Vlissides, published by Pearson Education, Inc., publishing as Addison-Wesley Professional. Copyright 1998

《设计模式沉思录》—第2章2.7节多用户文件系统的保护

2.7 多用户文件系统的保护我们已经讨论了如何给我们正在设计的文件系统添加简单的单用户保护.前面提到我们会将这个概念扩展到多用户环境,在这个环境中许多用户共享同一个文件系统.无论是配以中枢文件系统的传统分时系统,还是当代的网络文件系统,对多用户的支持都是必不可少的.即使那些为单用户环境所设计的个人计算机操作系统(如OS/2和Windows NT),现在也已经支持多用户.无论是什么情况,多用户支持都给文件系统保护这一问题增加了难度. 我们将再一次采用最简易的设计思路,效仿Unix系统的多用户保护机

《设计模式沉思录》—第2章2.8节小结

2.8 小结我们已经将模式应用于文件系统设计的各个方面.COMPOSITE模式的贡献在于定义了递归的树状结构,打造出了文件系统的主干.PROXY对主干进行了增强,使它支持符号化链接.VISITOR为我们提供了一种手段,使我们能够以一种得体的.非侵入性的方式来添加与类型相关的新功能. TEMPLATE METHOD在基本层面(即单个操作层面)为文件系统的保护提供了支持.对于单用户保护来说,我们只需要该模式就足够了.但为了支持多用户,我们还需要更多的抽象来支持登录.用户以及组.SINGLETON在两

《设计模式沉思录》—第2章2.3节“但是应该如何引入代用品呢?”

2.3 "但是应该如何引入代用品呢?"很高兴你能提出这个问题,因为我们现在正打算添加一个新的功能--符号化链接(symbolic link,它在Mac Finder中被称为别名,在Windows 95中被称为快捷方式).符号化链接基本上是对文件系统中另一个节点的引用.它是该节点的"代用品"(surrogate),它不是节点本身.如果删除符号化链接,它会消失但不会影响到它所引用的节点. 符号化链接有自己的访问权限,这个访问权限与它引用的节点的访问权限可能是不同的.但是

《设计模式沉思录》—第2章2.4节访问权限

2.4 访问权限到目前为止我们已经运用了两个设计模式:我们用COMPOSITE来定义文件系统的结构,用PROXY来帮我们支持符号化链接.把我们讨论到现在的改动和其他一些改进合并起来,得到了如图2-4所示的体现了COMPOSITE模式和PROXY模式的类层次结构. getName和getProtection用来返回节点的对应属性.Node基类为这些操作定义了默认的实现.streamIn用来把节点的内容写入文件系统,streamOut用来从文件系统读出节点的内容.(我们假设文件是按照简单的字节流来建

《设计模式沉思录》—第2章2.2节孤儿、孤儿的收养以及代用品

2.2 孤儿.孤儿的收养以及代用品现在让我们深入研究一下在我们的文件系统中运用COMPOSITE模式可能会得到什么样的结果.我们首先考察在设计Node类的接口时必须采取的一个重要折中,接着会尝试给刚诞生的设计增加一些新功能. 我们使用了COMPOSITE模式来构成文件系统的主干.这个模式向我们展示了如何用面向对象的方法来表示层级文件系统的基本特征.这种模式通过继承和组合来将它的关键参与者(Component.Composite及Leaf类)联系在一起,从而支持任意大小和复杂度的文件系统结构.它同

《设计模式沉思录》—第2章2.5节关于VISITOR的一些警告

2.5 关于VISITOR的一些警告在使用VISITOR模式之前,有两件事情需要考虑. 首先问一下自己,被访问的类层次结构是否稳定?拿我们的例子来说,我们是否会经常定义新的Node子类,还是说这种情况很少见?增加一种新的Node类型可能会迫使我们仅仅为了增加一个相应的visit操作而修改Visitor类层次结构中所有的类. 如果所有的visitor对新的子类不感兴趣,而且我们已经定义了一个与visitNode等价的操作来在默认情况下进行合理的处理,那么就不存在问题.但是,如果只有一种类型的vis

《设计模式沉思录》—第2章2.6节单用户文件系统的保护

2.6 单用户文件系统的保护经常使用计算机的人大都有过丢失重要数据的惨痛经历,起因可能只是一个不巧的语法错误,也可能是鼠标点偏了,或者只是深夜脑子突然不好使.在正确的时间删除一个错误的文件是一种常见的灾难.另一种情况是无意的编辑--在不经意间修改了一个不应该修改的文件.虽然一个高级文件系统会具备撤销功能,可以从这些不幸的事件中恢复,但我们通常更希望防患于未然.可悲的是,大多数文件系统给我们另一种不同的选择:预防或后悔⑥. 目前我们将集中精力讨论对文件系统对象(即节点)的删除和修改操作进行保护.之