本节书摘来自异步社区《设计模式沉思录》一书中的第2章,第2.4节访问权限,作者【美】John Vlissides,更多章节内容可以访问云栖社区“异步社区”公众号查看。

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);
}

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

  1. 设计模式沉思录读后感2

    暑假在家待了8天,回来就不出意外的开始了我新的代码生活,这个学期任务是文件管理和数据挖掘,据说挺有挑战的哦,不过我喜欢,有难度才能学到更多东西嘛. 回家后基本上没看什么书,就在回家的火车上看了几个设计 ...

  2. 关于《设计模式》与《设计模式沉思录》中提到的“常露齿嘻笑的猫”(Cheshire Cat)的说明

    最近在看GoF的<设计模式>,在此之前看了John Vlissides的<设计模式沉思录>,在"沉思录"P42页脚注中,作者提到 "在C++中这样 ...

  3. 关于《设计模式》与《设计模式沉思录》中提到的“常露齿嘻笑的猫”(Cheshire Cat)的说明...

    最近在看GoF的<设计模式>,在此之前看了John Vlissides的<设计模式沉思录>,在"沉思录"P42页脚注中,作者提到 "在C++中这样 ...

  4. 设计模式沉思录读后感

    引言:前段时间参加一个大学生服务外包比赛,一个月类疯狂的碼代码亚,终于在完成一个小型的web项目,接下来听老师分配,要学习一下文件目录管理,给了我一本<设计模式沉思录>,让这几天先看一遍, ...

  5. 《设计模式沉思录》分享

    书籍信息 书名: 设计模式沉思录 原作名: Pattern Hatching: Design Patterns Applied 豆瓣评分: 8.6分(78人评价) 内容简介 本书作者是设计模式的开山鼻 ...

  6. 【读书笔记】设计模式沉思录

    当我在china-pub上看到这本书的预售消息后就下订单了,不知道多少日子之后收到了china-pub的邮件.不过书到手后我好失望,怎么才140页~~~忽悠谁呢. 抱着浓缩的都是精华的心态,我看下去了 ...

  7. 设计模式沉思录:一 资源池

    文章首发链接:https://mp.weixin.qq.com/s/pUjm_u6xaoFreK_36qITcg 公众号:程序员架构进阶 一 摘要 在工作中,经常会看到或者用到池化技术,例如数据库连接 ...

  8. 设计模式沉思录 - 读书笔记(XMind)

    注:后面会不定期,以XMind的方式发布一些读书笔记. 目标:书还要是越读越薄才行!

  9. 设计模式调优-性能设计沉思录(10)

    JAVA调优系列文章 JVM调优全面探讨-性能设计沉思录(1)_luozhonghua2000的博客-CSDN博客 JVM GC回收和内存分配优化-性能设计沉思录(2)_luozhonghua2000 ...

最新文章

  1. SA区坏道数据恢复的经历
  2. linux系统中apache虚拟目录配置
  3. python 分数序列求和公式_Python分数序列求和,编程练习题实例二十四
  4. mysql搭建测试环境的步骤_如何搭建测试环境
  5. VMware安装Centos7过程
  6. Paddle.js PaddleClas 实战 ——『寻物大作战』AI 小游戏
  7. 逆势而上的技术:图神经网络学习来了!
  8. 2006年软件500强
  9. C++高手总结的编程规律
  10. OpenCV windows 上安装
  11. 高并发架构系列:Kafka、RocketMQ、RabbitMQ的优劣势比较
  12. 电影海王真的好看吗|我爬取了9000条影评,得出的结论是
  13. android 屏幕亮度代码,android 设置系统屏幕亮度
  14. Python解决数字棒球游戏
  15. 字节跳动基于ClickHouse优化实践之“高可用”
  16. linux注销系统有几种方法,怎么注销Linux子系统
  17. CANVAS LMS开源系统
  18. setClickable,setEnabled,setFocusable 的区别
  19. 微信小程序有什么商业价值?
  20. 微信小程序:(更新)云开发微群人脉

热门文章

  1. secp256r1 c语言程序,区块链中的数学-secp256k1点压缩和公钥恢复原理
  2. linux系统安装klocwork,linux下klocwork的使用
  3. plc维修入门与故障处理实例_13个浮筒液位计维修实例助你快速解决现场故障问题...
  4. Vue的三个点es6知识,扩展运算符表达含义
  5. ef mysql 中文乱码,mysql解決中文亂碼問題
  6. java声明接口_为什么必须用Java声明接口?
  7. linux c 数组拷贝,C++对数组进行复制 - osc_8iux0cyz的个人空间 - OSCHINA - 中文开源技术交流社区...
  8. @enableautoconfiguration注解作用_如何让代码变“高级”-Spring组合注解提升代码维度(这么有趣)...
  9. nandflash移植程序_韦东山鸿蒙移植01-移植RTOS需要做的事
  10. ios并发会造成什么问题_女生月经不调会引起什么并发症?