在前面的几篇文章中介绍了Visitor模式,这个模式理解起来可能有些复杂,并且用处可能也没有那么广泛,但在有些场景下使用它可能会起到意想不到的效果。并且它是一个绝佳的领略“双分发”概念的模式,前文对比了它和工厂方法模式的优缺点。
Visitor模式的一大缺点就是依赖问题,在定义Visitor接口的时候,就必须要知道所有的子类类型。在一些编译链接型语言如C++中,Visitor类和子类就会出现相互依赖的情况,当然,通过前向声明可以解决这个问题。另外一个问题就是在不同的子类中有很多“制式”代码。比如所有的子类必须实现基类中的Accept(Visitor&)方法,并且方法的实现都是一模一样的,都是调用该参数的Visitor方法并传入指向对象本身的引用。
幸好在C++ 11中引入了可变参数模板,使用可变参数模板以及CRTP技术,可以极大简化Visitor模式中的一些实现方面的问题。在这一过程中顺便学习和了解Moden C++的一些强大的功能,这些功能是如此的特别以至于它跟C#有很大的不同。
使用可变参数模板解决相互依赖问题
在前文中,一个典型的Visitor模式如下:
可以看到,抽象的基类Visitor,里面定义了所有对子类WordFile、PdfFile、PPTFile等的Visit方法。而在各个子类中都必须实现Accept(Visitor&)方法,这就是一个典型的相互依问题。在C++中,必须首先定义Visitor类,在定义Visitor类之前,必须前向声明WordFile、PdfFile、PPTFile,在Visitor类中仅使用了这几个类的引用,因为引用本身就是一个指针,它的大小是确定的,所以只需要定义前向声明即可。
得益于可变参数模板,我们可以定义一些这些类型的参数包,然后利用循环递归继承来展开,从而可以简化Visitor类里面接口的生成。代码如下:
template <typename... Types>
class Visitor;
template <typename T>
class Visitor<T>
{
public:
virtual void visit(const T &visitable) = 0;
};
template <typename T, typename... TList>
class Visitor<T, TList...> : private Visitor<TList...>
{
public:
using Visitor<TList...>::visit;
virtual void visit(const T &visitable) = 0;
};
代码非常简洁明了。这就是一个典型的可变参数包递归继承展开。有两个地方需要特别强调,在接受两个泛型参数的Visitor模板类中,它私有继承自接受一个参数包Visitor模板类。在Visitor模板类内部,首先使用using引入了私有基类中的visit方法(子类中使用using声明引入基类成员名称,详见《C++ Primer 5th,Page 546》)并避免了隐藏基类的同名方法,同时定义了自己的接受类型为T的visit方法。
现在要实现右图中的Visitor基类以及里面的三个虚方法,只需要使用如下声明即可。
using MyVisitor = Visitor<PdfFile, PPTFile, WordFile>;
Visitor<PdfFile,PPTFile,WordFile>会在编译阶段展开为以下的代码:
class Visitor
{
public:
virtual void visit(const PdfFile &visitable) = 0;
virtual void visit(const PPTFile &visitable) = 0;
virtual void visit(const PPTFile &visitable) = 0;
};
将来,如果要增加一个新的子类型,比如XLSFile,只需要在泛型模板参数里面增加一个即可,比如:
using MyNewVisitor = Visitor<PdfFile, PPTFile, WordFile, XLSFile>;
是不是非常的清新自然,它解决了相互依赖的问题,还使得代码更加清晰。
移除子类中的死板代码
经过前面的这部分优化之后,接下来就需要实现各个子类,根据Visitor的“套路”,各个子类必须实现Accept接口。标准的实现如下:
class PdfFile;
class PPTFile;
class WordFile;
class BaseFile
{
public:
using MyVisitor = Visitor<PdfFile, PPTFile, WordFile>;
virtual void Accept(MyVisitor &visitor);
}
class PdfFile : public BaseFile
{
public:
void Accept(BaseFile::MyVisitor &v)
{
v.visit(*this);
}
};
class PPTFile : public BaseFile
{
public:
void Accept(BaseFile::MyVisitor &v)
{
v.visit(*this);
}
};
class WordFile : public BaseFile
{
public:
void Accept(BaseFile::MyVisitor &v)
{
v.visit(*this);
}
};
在BaseFile中定义了Accept(MyVisitor&)虚方法,每个子类中都必须实现这个方法,但各个子类中的实现都十分“雷同”,都是直接调用参数v本身的visit方法,并传入指向当前对象的指针,最终目的是通过this指针来指向当前对象,从而达到“第二次分发”的目的。
看到这里面的重复代码,就让人想到了“Bad Smell",有没有一种办法,将这些子类的Accept实现,提升到一个基类里面,并且在基类里面能够提前知道子类的类型呢?
要能在基类中提前知道子类的类型,可以使用CRTP这种技巧,将子类作为基类的模板参数。
假设我们的文件还有一个构造函数,传入文件路径,在这个需求的基础上,代码实现如下。首先定义一个实现普通的基类。
class ResourceFile
{
private:
std::string filePath;
public:
using MyVisitor = Visitor<PdfFile, PPTFile, WordFile>;
ResourceFile(const std::string &path) : filePath(path) {}
ResourceFile(std::string &&path) noexcept : filePath(std::move(path)) {}
std::string GetPath() const
{
return filePath;
}
virtual void accept(MyVisitor &visitor) = 0;
};
这个基类与之前定义的BaseFile区别不大,不过是增加了几个构造函数。接下来定义一个模板类继承自以上的ResouceFile类,用T来表示当前的类型,然后重写accept虚方法,并将this指针动态转换为T类型。
template <class T>
class ResourceFileBase : public ResourceFile
{
public:
ResourceFileBase(const std::string &path) : ResourceFile(path) {}
void accept(MyVisitor &_visitor) override
{
_visitor.visit(*(static_cast<T *>(this)));
}
};
注意,这里的this表示指向当前对象的指针,在强制转换为指向T的指针之后,还需要取指针的地址,从而得到引用,因为visit方法接受的是引用。
现在,各子类只需要继承自这个ResourceFileBase,并且提供自身类型作为模板参数即可。
class PdfFile : public ResourceFileBase<PdfFile>
{
public:
PdfFile(const std::string &path) : ResourceFileBase(path)
{
}
};
class PPTFile : public ResourceFileBase<PPTFile>
{
public:
PPTFile(const std::string &path) : ResourceFileBase(path)
{
}
};
class WordFile : public ResourceFileBase<WordFile>
{
public:
WordFile(const std::string &path) : ResourceFileBase(path)
{
}
};
可以看到,这里把每个子类需要实现的accept方法都提取到ResourceFileBase里了,子类可以专注自身的逻辑。相比原始的Visitor模式,这种方法虽然多了一次静态类型转换,但减少了一部分的代码。
Visitor的各种子类区别不大,需要实现Visitor接口,这里继承自ResouceFile里面声明的MyVisitor类型:
class CompressorVisitor : public ResourceFile::MyVisitor
{
public:
void visit(const PdfFile &v) // override
{
std::cout << "compress pdf (" << v.GetPath() << ")." << std::endl;
}
void visit(const WordFile &v) // override
{
std::cout << "compress word (" << v.GetPath() << ")." << std::endl;
}
void visit(const PPTFile &v) // override
{
std::cout << "compress ppt (" << v.GetPath() << ")." << std::endl;
}
};
class ExtractorVisitor : public ResourceFile::MyVisitor
{
public:
void visit(const PdfFile &v) // override
{
std::cout << "extract pdf (" << v.GetPath() << ")." << std::endl;
}
void visit(const WordFile &v) // override
{
std::cout << "extract word (" << v.GetPath() << ")." << std::endl;
}
void visit(const PPTFile &v) // override
{
std::cout << "extract ppt (" << v.GetPath() << ")." << std::endl;
}
};
测试代码如下:
using FileCollection = std::vector<std::unique_ptr<ResourceFile>>;
int main()
{
FileCollection fc;
fc.push_back(std::make_unique<PdfFile>("my pdf file.pdf"));
fc.push_back(std::make_unique<PPTFile>("my ppt file.pdf"));
fc.push_back(std::make_unique<WordFile>("my word file.pdf"));
ExtractorVisitor extractor;
CompressorVisitor compressor;
for (auto &i : fc)
{
i->accept(extractor);
i->accept(compressor);
}
return 0;
};
输出结果如下:
extract pdf (my pdf file.pdf).
compress pdf (my pdf file.pdf).
extract ppt (my ppt file.pdf).
compress ppt (my ppt file.pdf).
extract word (my word file.pdf).
compress word (my word file.pdf).
总结
本文首先总结了传统的Visitor访问者模式在实现中的两个缺点:Visitor对各待访问类型的双向依赖,以及各待访问类型在实现Visitor中的Accept方式时的雷同代码。C++ 11中的可变参数模板通过使用循环递归继承的方式解决了第一个问题,通过模板编程以及CRTP的方式,将各待访问子类中的实现Visitor的Accept相同提取到了基类中,从而简化了子类的实现。通过以上实现能够帮助我们在加深对Visitor模式理解的同时,体会到Modern C++的可变参数模板这一新特性所带来的编程效率提升。
- https://www.yycoding.xyz/post/2024/6/15/inspect-visitor-pattern-from-double-dispatch-perspective
- https://www.yycoding.xyz/post/2021/1/22/introduction-to-design-patterns-of-visitor-pattern
- https://stackoverflow.com/questions/11796121/implementing-the-visitor-pattern-using-c-templates?rq=3
- https://blog.csdn.net/haokan123456789/article/details/138679600
- https://blog.csdn.net/wmy19890322/article/details/121427697
- https://www.cnblogs.com/dragonxyl/p/15292612.html
- https://www.artima.com/articles/cooperative-visitor-a-template-technique-for-visitor-creation
- https://www.vishalchovatiya.com/double-dispatch-visitor-design-pattern-in-modern-cpp/
- https://www.foonathan.net/2017/12/visitors/
- https://gist.github.com/tomas789/7844152
- https://rextester.com/TXVI24042
- https://stackoverflow.com/questions/16868129/how-to-store-variadic-template-arguments
- https://coliru.stacked-crooked.com/view?id=14bdf12311c879e751e7f78238955459-a73d80d9462cfb550c0755cfd96ceec5