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

2.2 孤儿、孤儿的收养以及代用品
现在让我们深入研究一下在我们的文件系统中运用COMPOSITE模式可能会得到什么样的结果。我们首先考察在设计Node类的接口时必须采取的一个重要折中,接着会尝试给刚诞生的设计增加一些新功能。

我们使用了COMPOSITE模式来构成文件系统的主干。这个模式向我们展示了如何用面向对象的方法来表示层级文件系统的基本特征。这种模式通过继承和组合来将它的关键参与者(Component、Composite及Leaf类)联系在一起,从而支持任意大小和复杂度的文件系统结构。它同时使客户能够以统一的方式来处理文件和目录(以及文件系统中可能出现的任何其他东西)。

正如我们已经看到的那样,统一性的关键在于为文件系统中的对象提供一个共同的接口。到目前为止我们的设计中已经有了三种对象类:Node、File和Directory。我们已经解释了需要在Node基类中定义那些对文件和目录都有明确意义的操作。用来获取和设置节点的名字和保护属性的操作就属于这一类。我们还解释了,虽然有一个用来访问子节点的操作(getChild)乍一看对File对象并不合适,但为什么我们仍然需要把它放在共同的接口中。现在让我们来考虑其他一些看上去更没有什么共通之处的操作。

※   ※   ※

小孩子们是从哪里来的?虽然这听起来像是一个早熟的5岁小孩问的问题,但是我们仍然需要知道。(我猜在任何场合下这都是个不错的问题。)在一个Directory对象能够枚举它的子节点之前,它必须通过某种方式获得子节点。但是从哪里获得呢?

显然不是从它自己身上。把一个目录可能包含的每个子节点创建出来不应该是目录的责任,这样的事情应该由文件系统的用户来控制。让文件系统的用户来创建文件和目录并把它们放到相应的地方,这才是比较合理的做法。这意味着Directory对象将会收养(adopt)子节点,而不是创建子节点。因此,Directory需要一个接口来收养子节点。类似下面的接口就可以:

virtual void adopt(Node* child);

当客户代码调用一个目录对象的adopt函数时,就等于是明确地把管理这个子节点的责任转交给该目录对象。责任意味着所有权:当一个目录对象被删除的时候,这个子节点也应该被删除。这就是Directory和Node类之间(在图2-2中用菱形表示)的聚合关系的本质。

现在,如果客户代码可以让一个目录对象承担管理一个子节点的责任,那么应该还有一个函数来解除(relinquish)这份责任。因此我们还需要另外一个接口:

virtual void orphan(Node* child);

在这里“orphan”并不意味着它的父目录已经死了——被删除了,它只不过意味着该目录对象不再是这个子节点的父目录。这个子节点仍将继续存在,也许它马上就会被另一个节点收养,也许它会被删除。

这和统一性有什么关系?为什么我们不能把这些操作只定义在Directory中?

好吧,假设我们就是这样定义的。现在考虑一下客户代码如何实现改变文件系统结构的操作。一个用来创建新目录的用户级命令就是此类客户代码的一个例子。这个命令的用户界面无关紧要,我们可以假设它只不过是一个命令行界面,类似Unix的mkdir命令。mkdir有一个参数,用来传入待创建目录的名字,如下面所示:

mkdir newsubdir
事实上,用户可以在名字前面加上任何有效的路径。

mkdir subdirA/subdirB/newsubdir
只要subdirA和subdirB已经存在而且是目录而不是文件,那么这条命令就应该能够正确执行。更概括地说,subdirA和subdirB应该是Node子类的实例,而且可以有子节点。如果这一点不成立,那么用户应该得到一条错误消息。

我们怎么实现mkdir呢?首先,我们假设mkdir能够找出当前的目录是什么,也就是说它能得到一个与用户的当前目录相对应的Directory对象②。给当前目录增加一个新目录只不过是小事一桩:先创建一个Directory实例,然后调用当前目录对象的adopt函数,并将新目录作为参数传入。

Directory* current;
// ...
current->adopt(new Directory("newsubdir"));

就是这么简单。但一般情况下传给mkdir的不仅仅只是一个名字,而是一个路径,我们应该怎样处理这种情况呢?

事情从这里开始变得有些棘手了。mkdir必须

(1)找到subdirA对象(若该对象不存在则报告一个错误);

(2)找到subdirB对象(若该对象不存在则报告一个错误);

(3)让subdirB收养newsubdir对象。

第1点和第2点涉及对当前目录的子节点进行遍历,以及对subdirA(如果它存在的话)的子节点进行遍历,其目的是为了找到代表subdirB的节点。

在mkdir实现的内部,可能会有一个递归函数,该函数以路径作为它的参数。

void Client::mkdir (Directory* current, const string& path) {
  string subpath = subpath(path);

  if (subpath.empty()) {
    current->adopt(new Directory(path));
  } else {
    string name = head(path);
    Node* child = find(name, current);

    if (child) {
      mkdir(child, subpath);
    } else {
      cerr << name << " nonexistent." << endl;
    }
  }
}

这里head和subpath是字符串处理例程。head返回路径中的第一个名字,而subpath则返回剩余的部分。find操作在一个目录中根据指定的名字查找对应的子节点。

Node* Client::find (const string& name, Directory* current) {
  Node* child = 0;

  for (int i=0; child = current->getChild(); ++i) {
    if (name == child->getName()) {
      return child;
    }
  }
  return 0;
}

值得注意的是,由于getChild返回的是Node,因此find也必须返回Node。这并没有什么不合理的地方,因为子节点既可以是一个Directory也可以是一个File。但是如果仔细地阅读代码,就会发现这个小小的细节对Client::mkdir有着致命的影响——Client::mkdir是无法通过编译的。

让我们再看一下对mkdir的递归调用。传给它的是Node,而不是所需的Directory。问题在于,当我们深入访问文件系统的层级时,我们并不知道一个子节点到底是文件还是目录。一般来说,只要客户代码不关心这种区别,这就是一件好事。但在目前的情况下,看起来我们确实需要关心这种区别,因为只有Directory才定义了用来收养子节点和遗弃子节点的接口。

但我们真地需要关心这一点吗?或者更进一步说,客户代码(mkdir命令)需要关心这一点吗?不一定。它的任务是要么创建一个新目录,要么向用户报告错误。因此让我们假设,只是假设一下,我们对所有的Node类都以统一的方式来处理adopt和orphan。

好了,好了。我知道你在想,“天啊!这些操作对File之类的叶节点来说毫无意义。”但这样的假设是不是切合实际呢?如果今后有人想定义一种新的类似垃圾箱(说得更准确一些,是回收站)的叶节点,它可以销毁它收养的所有子节点,那么这种情况该怎么处理?如果想在叶节点收养子节点时产生一条错误消息,那么这种情况又该怎么处理?我们很难证明adopt对叶节点来说绝无意义,orphan同样也是如此。

另一方面,有人可能会争辩说一开始就没有必要把File类和Directory类分开——所有的东西都应该是Directory。这样的论点是合理的,但是从实现的角度来说,它存在一些问题。一般来说,Directory对象中的许多内容对大多数文件来说是不必要的,比如用来存储子节点的数据结构、用来对子节点信息进行高速缓存以提高性能的数据结构,等等。经验表明,在许多应用程序中,叶节点的数量通常要比内部节点的数量多得多。这也是为什么COMPOSITE模式要把Leaf和Composite类分开的原因。

让我们来看一看,如果我们不仅仅只在Directory类中定义adopt和orphan,而是在所有的Node类中定义adopt和orphan,那将发生什么情况。我们让这些操作在默认的情况下产生错误消息。

virtual void Node::adopt (Node*) {
  cerr << getName() << " is not a directory." << endl;
}

virtual void Node::orphan (Node* child) {
  cerr << child->getName() << " not found." << endl;
}

虽然这些并不一定是最好的错误消息,但是应该足以让读者领会其中的含义。除了产生错误消息之外,这些操作还可以抛出异常,或者什么也不做——我们有许多选择。现在无论在什么情况下,Client::mkdir都可以完美地执行③。同时请注意,这种方法不需要对File类做任何改动。当然,我们必须修改Client::mkdir,在参数中用Node来代替Directory

void Client::mkdir (Node* current, cosnt string& path) {
  // ...
}
关```
键在于:虽然看起来我们不应该以统一的方式来处理adopt和orphan操作,但这样做实际上是有好处的,至少在这个应用程序中如此。另一种最有可能的选择是引入某种形式的向下转型,让客户来确定节点的类型。

void Client::mkdir (Directory* current, const string& path) {
  string subpath = subpath(path);

  if (subpath.empty()) {
    current->adopt(new Directory(path));

  } else {
    string name = head(path);
    Node* node = find(name, current);

    if (node) {
      Directory child = dynamic_cast>(node);
      if (child) {
        mkdir(child, subpath);
      } else {
        cerr << getName() << " is not a directory."
           << endl;
      }
    } else {
      cerr << name << " nonexistent." << endl;
    }
  }
}

想必你已经注意到dynamic_cast引入了额外的检查和分支。为了能够处理用户在path中指定了无效目录名的情况,这样做是必需的。这个例子同时说明了不统一性会让客户代码变得更加复杂。
时间: 2024-10-06 09:47:38

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

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

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

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

第2章 运用模式进行设计设计模式沉思录如果想体验一下运用模式的感觉,那么最好的方法就是运用它们.对我来说,最大的挑战在于找到一个所有人都能够理解的示例.人们对自己的问题最感兴趣,如果某些人对某个示例越感兴趣,这个示例往往就越具体.问题在于,这样的示例所涉及的问题往往太过晦涩,对于没有相关领域背景的人来说难以理解. 层级文件系统(hierarchical file system)是每个计算机用户都熟悉的东西,就让我们来看看该如何设计它.我们不会关心诸如I/O缓冲和磁盘扇区管理之类的底层实现问题,我

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

版权声明设计模式沉思录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.5节关于VISITOR的一些警告

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

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

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