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

2.4 访问权限
到目前为止我们已经运用了两个设计模式:我们用COMPOSITE来定义文件系统的结构,用PROXY来帮我们支持符号化链接。把我们讨论到现在的改动和其他一些改进合并起来,得到了如图2-4所示的体现了COMPOSITE模式和PROXY模式的类层次结构。

getName和getProtection用来返回节点的对应属性。Node基类为这些操作定义了默认的实现。streamIn用来把节点的内容写入文件系统,streamOut用来从文件系统读出节点的内容。(我们假设文件是按照简单的字节流来建模的,就像在Unix系统中那样。)streamIn和streamOut是抽象操作,这意味着基类声明了它们,但没有实现它们。因此它们的名字用斜体表示。getChild、adopt和orphan都有默认的实现,其目的是为了简化叶节点的定义。
说到叶节点,我们再来回顾一下:Node、File和Directory来自COMPOSITE模式。PROXY模式提供了Link类,它还指定了Node类,这个类我们原来就已经有了。因此,Node类是两个模式的交汇点。其他类只参与了Proxy模式或COMPOSITE模式,而Node类则参与了两个模式。这样的双重身份是Alexander所谓的“密集”复合模式的标志,其中两个或多个模式占据了系统中的同一个类“空间”。

密集度有它的好处,也有它的坏处。在相对较少的类中实现多个模式会让设计变得深奥,空间不大却意味深长,有点像一首诗。但另一方面,这样的密集度让我们联想起灵感匮乏的创造。

Richard Gabriel是这样说的[Gabriel95]:

在软件中,Alexandrian所说的密集度至少在一定程度上代表了低质量的代码——代码的每一部分都完成一件以上的任务。这样的代码就像是我们第一次编写的代码,它比正常的需要多占用了两三倍的内存空间。这样的代码就像是我们曾经在20世纪六七十年代编写的汇编语言代码。

说得好——“深奥的”代码不一定是好代码。事实上,Richard的担忧是另一个更大问题的症状:当一个模式被实现之后,它可能会丢失。这里有许多东西可以讨论,但我们得等一等——我们的文件系统正在向我们召唤呢!

※   ※   ※

在操作系统中,绝大多数用户级的命令都会通过某种方式对文件系统进行操作。因此,文件系统是计算机的信息仓库也就不足为奇了。随着操作系统的不断发展,这样一个重要组件必然会产生新的功能。

我们已经定义的类提供了少量功能。具体说来,Node类的接口只是把它所有子类支持的一些基本操作包括了进来。这些操作之所以基本,是因为它们不仅允许我们访问只有节点才能访问的信息,还允许我们执行只有节点才能执行的操作。

很自然,我们可能还想在这些类上执行其他一些操作。考虑一个用来统计文件中字数的操作。一旦我们认识到需要这样的操作,可能就想在Node基类中增加一个getWordCount操作。这是一件很糟糕的事情,因为我们最终至少得修改File类,而且可能还要修改其余的每个类。我们迫切地希望能避免修改已有的代码(可以理解为“向已有的代码中添加bug”)。但是我们没有必要恐慌,因为在基类中有流处理操作,文件系统的客户代码可以使用它们来检查文件中的文本。这样,我们就得以解脱,不必再对已有的代码进行修改了,因为客户代码可以通过已有的操作来实现字数统计。

事实上,我可以肯定地说,设计Node接口最主要的挑战在于找出一组最少的操作,客户代码可以通过这组操作来构建新功能而不受任何约束。另一种可供选择的方法是为了每个新功能而对Node及其子类进行改造,相比之下这种方法不但具有扩散性,而且容易出错。它还会使Node的接口发展成一个具有各种操作的大杂烩,并最终把Node对象的本质属性掩埋掉。所有的类会变得难以理解、难以扩展以及难以使用。因此,要定义一个简单有序的Node接口,把注意力集中在一组够用的基本操作上是关键。

但那些应该以不同的方式来处理不同的节点的操作该怎么办呢?我们怎样才能把它们放到Node类的外部呢?让我们以Unix的cat操作为例,它只是把文件的内容输出到标准输出设备上。但是,当我们将它用于目录的时候,它会报告无法输出节点的内容,也许是因为目录的文本表示不太好看吧。

由于cat的行为取决于节点的类型,看起来似乎有必要在基类中定义一个操作,并让File和Directory以不同的方式来实现该操作。因此我们最终还得修改已有的类。

有没有别的方法?假设我们坚持不把这个功能放到Node类中,而要把它放到客户代码中。那么看来除了引入向下转型来让客户代码判断节点的类型之外,我们没有什么其他的选择。

void Client::cat (Node* node) {
  Link*l;

  if (dynamic_cast<File*>(node)) {
    node->streamOut(cout);  // stream out contents
  } else if (dynamic_cast<Directory*>(node)) {
    cerr << "Can't cat a directory." << endl;

  } else if (l = dynamic_cast<Link*>(node)) {
    cat(l->getSubject());  // cat the link's subject
  }
}

向下转型似乎又是难以避免的了。而且,它使客户代码变得更加复杂。没错,我们是故意不把功能放到Node类中,而要把功能放到客户代码中的。但是除了功能本身,我们还增加了类型测试和条件分支,这合起来就构成了对方法的二次分派。

如果说把功能放到Node类中令人反感,那么使用向下转型就令人恶心了。但是,在我们为了避免向下转型而不假思索地将cat()操作弄到Node及其子类中之前,让我们来看一看VISITOR模式,这个设计模式为我们提供了第三种选择。它的意图如下:

表示一个用来处理某对象结构中各个元素的操作。VISITOR让我们无需修改待处理元素的类,就可以定义新的操作。

模式的动机部分讨论了一个编译器,这个编译器用抽象语法树来表示程序。它所面临的问题是支持一组各式各样的分析器,比如类型检查、精美的打印以及代码生成,而不需要对实现抽象语法树的类进行修改。这个编译器问题和我们的问题相似,唯一的不同之处在于我们要处理的是文件系统结构,而不是抽象语法树,而且我们想要对文件系统结构执行完全不同的操作。(但话又说回来,也许精美地打印一个目录的结构还能沾得上边。)无论如何,操作本身并不重要,重要的是把操作从Node类中分离出来,但又无需引入向下转型和额外的条件分支。

VISITOR只要在它的“Element”参与者中加入一个操作,就可以达到这一目的。这个操作在我们的Node类中如下所示。

virtual void accept(Vistor&) = 0;

accept让一个“Visitor”对象访问一个指定的节点。Visitor对象封装了要对节点执行的操作。所有的Element具体子类实现accept的方式不仅简单,而且看起来也完全相同。

void File::accept (Visitor& v)   { v.visit(this); }
void Directory::accept (Visitor& v) { v.visit(this); }
void Link::accept (Visitor& v)   { v.visit(this); }

所有这些实现看起来完全相同,但它们实际上是不同的——在每个实现中,this的类型是不一样的。上述实现暗示了Visitor的接口看起来应该像下面这样。

class Visitor {
public:
  Visitor();
  void visit(File*);
  void visit(Directory*);
  void visit(Link*);
};

这里最有意思的特性是,当一个节点的accept操作调用Visitor对象的visit时,它同时向Visitor表明了自己的类型。然后,被调用的Visitor操作可以根据节点的类型对它进行相应的处理。

void Visitor::visit (File*f) {
  f->streamOut(cout);
}

void Visitor::visit (Directory* d) {
  cerr << "Can't cat a directory." << endl;
}

void Visit::visitor (Link*l) {
  l->getSubject()->accept(**this);
}

最后一个操作需要做些解释。它调用了getSubject(),这个操作返回该符号化链接指向的节点,也就是它的Subject⑤。我们不能直接把Subject的内容打印出来,因为它可能是一个目录。相反,我们让它接受一个Visitor对象,就像我们对Link类本身所做的那样。这使得Visitor能够根据Subject的类型来做相应的处理。Visitor会通过这种方式挨个访问任意数量的链接,直到最终到达一个文件或目录,这时它就终于可以做些有用的事情了。

因此,现在我们只要创建一个Visitor并让节点接受它,就可以对任何节点执行cat操作。

Visitor cat;
node->accept(cat);

节点反过来调用Visitor,这个调用会根据节点的实际类型(File、Directory或Link)被解析成与之对应的visit操作,从而得到相应的处理。结果是Visitor无需进行类型测试,就可以把cat之类的功能打包在单个类中。

把cat操作封装到Visitor中非常漂亮,但如果想对节点执行cat之外的操作,看起来我们还是得修改已有的代码。假设我们想要实现另一个命令,这个命令用来列出一个目录中所有子节点的名字,它和Unix中的ls命令相似。此外,如果节点是一个目录,那么应该给输出添加“/”后缀,如果节点是一个符号化链接,那么应该给输出添加“@”后缀。

我们需要把“访问Node的权限”授予给另一个类似于Visitor的类,但我们不想再给Node基类增加另一个accept操作。事实上我们也不必那样做。任何Node对象都可以接受任何类型的Visitor对象。只不过我们目前只有一种类型的Visitor。但在Visitor模式中,Visitor实际上是一个抽象类。

class Visitor {
public:
  virtual ~Visitor() { }

  virtual void visit(File*) = 0;
  virtual void visit(Directory*) = 0;
  virtual void visit(Link*) = 0;

protected:
  Visitor();
  Visitor(const Visitor&);
};

我们为每一个新功能从Visitor派生一个子类,并根据每种可访问的节点的类型来实现相应的visit操作。例如,CatVisitor子类会像前面所讲的那样实现所有操作。我们还可以定义SuffixPrinterVisitor,用它来为节点打印正确的后缀。

class SuffixPrinterVisitor : public Visitor {
public:
  SuffixPrinterVisitor() { }
  virtual ~SuffixPrinterVisitor() { }

  virtual void visit(File*)   { }
  virtual void visit(Directory*) { cout << "/"; }
  virtual void visit(Link*)   { cout << "@"; }
};

我们可以在实现了ls命令的客户代码中使用SuffixPrinterVisitor。

void Client::ls (Node* n) {
  SuffixPrinterVisitor suffixPrinter;
  Node* child;

  for (int i=0; child = n->getChild(i); ++i) {
    cout << child->getName();
    child->accept(suffixPrinter);
    cout << endl;
  }
}

一旦给Node类增加了accept(Visitor&)操作,我们就获得了对节点的访问权。此后无论我们要给Visitor定义多少子类,我们都再也不需要修改Node类及其派生类了。

之前我们使用了函数重载,这样Visitor的操作就可以使用相同的名字。另一种可选的方法是将节点的类型信息嵌入到visit操作的名字中。

class Visitor {
public:
  virtual ~Visitor() { }

  virtual void visitFile(File*) = 0;
  virtual void visitDirectory(Directory*) = 0;
  virtual void visitLink(Link*) = 0;

protected:
  Visitor();
  Visitor(const Visitor&);
};

对这些操作的调用会变得更加清晰一些,也更冗长一些。

void File::accept (Visitor& v)   { v.visitFile(this); }
void Directory::accept (Visitor& v) { v.visitDirectory(this); }
void Link::accept (Visitor& v)   { v.visitLink(this); }

如果存在一种合理的默认处理方法,而且Visitor的子类往往只覆盖(override)所有操作中的一小部分,那么这种做法还有另一个显著的好处。当我们使用重载的时候,子类必须覆盖所有的函数,否则我们常常使用的C++编译器可能会抱怨我们对虚拟重载函数的选择性覆盖隐藏了基类中的一个或多个操作。当我们给Visitor操作以不同的名字时,我们就避开了这个问题。然后子类就可以重新定义操作的一个子集,而不会受到C++编译器的限制。

基类的各个操作可以为每种类型的节点实现默认处理方法。当默认处理方法适用于两种或多种类型时,我们可以把公共功能放到一个“全能”(catch-all)的visitNode(Node*)操作中,供其他操作在默认情况下调用。

void Visitor::visitNode (Node* n) {
  // common default behavior
}

void Visitor::visitFile (File*f) {
  Visitor::visitNode(f);
}

void Visitor::visitDirectory (Directory* d) {
  Visitor::visitNode(d);
}

void Visitor::visitLink (Link*l) {
  Visitor::visitNode(l);
}
时间: 2024-10-23 15:02:38

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

《设计模式沉思录》—第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.3节“但是应该如何引入代用品呢?”

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

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

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

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