动机

在软件构建过程中,由于需求的改变,某些类层次结构中常常需要增加新的行为(方法),如果直接在基类中做这样的更改,将会给子类带来很繁重的变更负担,甚至破坏原有设计

  • 不是一个类需要更改行为,而是一整个类层次结构,从基类开始,到所有的子类都需要添加方法
  • 而且,往往是你已经部署之后,还需要新增行为

如何在不更改类层次结构的前提下,在运行时根据需要透明地位类层次结构上的各个类动态添加新的操作,从而避免以上问题

代码

原始代码

  1. class Element
  2. {
  3. public:
  4. virtual void Func1() = 0;
  5. virtual ~Element(){}
  6. };
  7. class ElementA : public Element
  8. {
  9. public:
  10. void Func1() override {
  11. //...
  12. }
  13. };
  14. class ElementB : public Element
  15. {
  16. public:
  17. void Func1() override {
  18. //...
  19. }
  20. };

对于Element、ElementA、ElementB这个类层次结构
在你部署发布以后,由于某些需求的变更,你需要再添加某个方法,比如Element::Func2()

一个直观的方法是直接在代码中添加,比如

  1. class Element
  2. {
  3. public:
  4. virtual void Func1() = 0;
  5. virtual void Func2(int data) = 0; //新增
  6. virtual ~Element(){}
  7. };
  8. class ElementA : public Element
  9. {
  10. public:
  11. void Func1() override {
  12. //...
  13. }
  14. virtual void Func2(int data) override{ //新增
  15. //...
  16. }
  17. };
  18. class ElementB : public Element
  19. {
  20. public:
  21. void Func1() override {
  22. //...
  23. }
  24. virtual void Func2(int data) override{ //新增
  25. //...
  26. }
  27. };

访问器模式

这么做有一些问题

  1. 违反开闭原则:对修改关闭,对扩展开放
  2. 程序已经部署发布,你需要重新编译一份,交给客户。这样的代价是很高的
  3. 下次需求再变更,你又需要类似的添加代码,编译,再次与客户对接交付

那么如何在不更改源代码的情况下,完成扩展一个方法呢?

  • 在起初设计Element类层次结构的时候,你预料到之后可能会添加某些行为,但不知道具体是什么,你就可以使用Visitor设计模式,先为Element添加可扩展性,方便今后的扩展

最开始做的设计

  1. class Visitor;
  2. class Element
  3. {
  4. public:
  5. virtual void accept(Visitor& visitor) = 0; //第一次多态辨析
  6. virtual ~Element(){}
  7. };
  8. class ElementA : public Element
  9. {
  10. public:
  11. void accept(Visitor &visitor) override {
  12. //将自己传进去,并调用处理自己的方法
  13. visitor.visitElementA(*this);
  14. }
  15. };
  16. class ElementB : public Element
  17. {
  18. public:
  19. void accept(Visitor &visitor) override {
  20. visitor.visitElementB(*this); //第二次多态辨析
  21. }
  22. };
  23. class Visitor{
  24. public:
  25. virtual void visitElementA(ElementA& element) = 0;
  26. virtual void visitElementB(ElementB& element) = 0;
  27. virtual ~Visitor(){}
  28. };

在将来,需求变更的时候,需要为Element添加方法时,就可以这样写

  1. //扩展的方法Func1
  2. class Fun1Visitor : public Visitor{
  3. public:
  4. void visitElementA(ElementA& element) override{
  5. cout << "Fun1Visitor is processing ElementA" << endl;
  6. }
  7. void visitElementB(ElementB& element) override{
  8. cout << "Fun1Visitor is processing ElementB" << endl;
  9. }
  10. };
  11. //扩展的方法Func2
  12. class Fun2Visitor : public Visitor{
  13. public:
  14. void visitElementA(ElementA& element) override{
  15. cout << "Fun2Visitor is processing ElementA" << endl;
  16. }
  17. void visitElementB(ElementB& element) override{
  18. cout << "Fun2Visitor is processing ElementB" << endl;
  19. }
  20. };
  21. void main()
  22. {
  23. //客户端使用扩展的第二个方法,即Fun2Visitor
  24. Fun2Visitor visitor;
  25. ElementB elementB;
  26. elementB.accept(visitor);
  27. ElementA elementA;
  28. elementA.accept(visitor);
  29. }

其中,elementB.accept(visitor)执行了两次多态辨析(double dispatch、两次派遣)

  1. 第一次多态辨析:Element::accpet 分发到 ElementB::accept
  2. 第二次多态辨析:Visitor::visitElementB 分发到 Fun2Visitor::visitElementB

模式定义

表示一个作用于某对象结构中的各元素的操作。使得可以在不改变(稳定)各元素的类的前提下定义(扩展)作用于这些元素的新操作(变化)。——《设计模式》GoF

结构

image.png
缺点:Element类层次结构也是要稳定的。这也是Visitor设计模式的一个大前提

  1. 如果新增了一个ElementC,Visitor基类要跟着改变。你就要去更改Visitor类层次结构的代码,那么此时,你又违反了开闭原则
  2. 这个前提条件不容易满足,很苛刻

要点总结

要点一

Visitor模式通过所谓的双重分发(double dispatch)来实现在不更改(不在编译时添加新的操作)Element类层次结构的前提下,在运行时透明底为类层次结构上的各个类动态添加新的操作(支持变化)。

要点二

所谓双重分发即Visitor模式中间包括了两个多态分发(注意其中的多态机制):第一个为accept方法的多态辨析;第二个为visitElementX方法的多态辨析。

要点三

Visitor模式的最大缺点在于扩展类层次结构(增加新的Element子类,比如新增一个ElementC),会导致Visitor类的改变。Visitor类改变,你再去修改,会违反开闭原则。
因此Visitor模式适用于以下情况

  • Element类层次结构稳定
  • Element类层次结构的操作经常面临频繁改动

正是因为这前提条件太苛刻了,所以这个设计模式应用的很少。
一旦使用,Visitor是非常重的。所谓重,就是Visitor要为Element整个类层次结构实现对应的类与方法。

在用设计模式的时候有两个极端

  1. 你假设所有代码都是稳定的,那根本没有必要引入设计模式
  2. 你假设所有代码都是变化的,那根本没有一个设计模式能满足你
  3. 设计模式是一种相对的。代码里有稳定的部分、有变化的部分,才能使用设计模式

所以在使用设计模式的时候,你需要判断出哪个部分是稳定的,哪个部分是变化的,才能正确的选择适用你的设计模式。

要点四

在C#中,有一个语言特性:扩展方法,可以代替此设计模式
这也呼应了这句话:“设计模式是为了弥补语言设计的不足而诞生的”