淺談 C++17 里的 Visitor 模式
一、Visitor Pattern
訪問(wèn)者模式是一種行為模式,允許任意的分離的訪問(wèn)者能夠在管理者控制下訪問(wèn)所管理的元素。訪問(wèn)者不能改變對(duì)象的定義(但這并不是強(qiáng)制性的,你可以約定為允許改變)。對(duì)管理者而言,它不關(guān)心究竟有多少訪問(wèn)者,它只關(guān)心一個(gè)確定的元素訪問(wèn)順序(例如對(duì)于二叉樹(shù)來(lái)說(shuō),你可以提供中序、前序等多種訪問(wèn)順序)。

1、組成
Visitor 模式包含兩個(gè)主要的對(duì)象:Visitable 對(duì)象和 Vistor 對(duì)象。此外,作為將被操作的對(duì)象,在 Visitor 模式中也包含 Visited 對(duì)象。
一個(gè) Visitable 對(duì)象,即管理者,可能包含一系列形態(tài)各異的元素(Visited),它們可能在 Visitable 中具有復(fù)雜的結(jié)構(gòu)關(guān)系(但也可以是某種單純的容納關(guān)系,如一個(gè)簡(jiǎn)單的 vector)。Visitable 一般會(huì)是一個(gè)復(fù)雜的容器,負(fù)責(zé)解釋這些關(guān)系,并以一種標(biāo)準(zhǔn)的邏輯遍歷這些元素。當(dāng) Visitable 對(duì)這些元素進(jìn)行遍歷時(shí),它會(huì)將每個(gè)元素提供給 Visitor 令其能夠訪問(wèn)該 Visited 元素。
這樣一種編程模式就是 Visitor Pattern。
2、接口
為了能夠觀察每個(gè)元素,因此實(shí)際上必然會(huì)有一個(gè)約束:所有的可被觀察的元素具有共同的基類 Visited。
所有的 Visitors 必須派生于 Visitor 才能提供給 Visitable.accept(visitor&) 接口。
namespace hicc::util {
struct base_visitor {
virtual ~base_visitor() {}
};
struct base_visitable {
virtual ~base_visitable() {}
};
template<typename Visited, typename ReturnType = void>
class visitor : public base_visitor {
public:
using return_t = ReturnType;
using visited_t = std::unique_ptr<Visited>;
virtual return_t visit(visited_t const &visited) = 0;
};
template<typename Visited, typename ReturnType = void>
class visitable : public base_visitable {
public:
virtual ~visitable() {}
using return_t = ReturnType;
using visitor_t = visitor<Visited, return_t>;
virtual return_t accept(visitor_t &guest) = 0;
};
} // namespace hicc::util
3、場(chǎng)景
以一個(gè)實(shí)例來(lái)說(shuō),假設(shè)我們正在設(shè)計(jì)一套矢量圖編輯器,在畫布(Canvas)中,可以有很多圖層(Layer),每一圖層包含一定的屬性(例如填充色,透明度),并且可以有多種圖元(Element)。圖元可以是 Point,Line,Rect,Arc 等等。
為了能夠?qū)嫴祭L制在屏幕上,我們可以有一個(gè) Screen 設(shè)備對(duì)象,它實(shí)現(xiàn)了 Visitor 接口,因此畫布可以接受 Screen 的訪問(wèn),從而將畫布中的圖元繪制到屏幕上。
如果我們提供 Printer 作為觀察者 ,那么畫布將能夠把圖元打印出來(lái)。
如果我們提供 Document 作為觀察者,那么畫布將能夠把圖元特性序列化到一個(gè)磁盤文件中去。
如果今后需要其它的行為,我們可以繼續(xù)增加新的觀察者,然后對(duì)畫布及其所擁有的圖元進(jìn)行類似的操作。
4、特點(diǎn)
- 如果你需要對(duì)一個(gè)復(fù)雜對(duì)象結(jié)構(gòu) (例如對(duì)象樹(shù)) 中的所有元素執(zhí)行某些操作, 可使用訪問(wèn)者模式。
- 訪問(wèn)者模式將非主要的功能從對(duì)象管理者身上抽離,所以它也是一種解耦手段。
- 如果你正在制作一個(gè)對(duì)象庫(kù)的類庫(kù),那么向外提供一個(gè)訪問(wèn)接口,將會(huì)有利于用戶無(wú)侵入地開(kāi)發(fā)自己的 visitor 來(lái)訪問(wèn)你的類庫(kù)——他不必為了自己的一點(diǎn)點(diǎn)事情就給你
issue/pull request。 - 對(duì)于結(jié)構(gòu)層級(jí)復(fù)雜的情況,要善于使用對(duì)象嵌套與遞歸能力,避免反復(fù)編寫相似邏輯。
請(qǐng)查閱
canva,layer,group的參考實(shí)現(xiàn),它們通過(guò)實(shí)現(xiàn)drawable和vistiable<drawable> 的方式完成了嵌套性的自我管理能力,并使得 accept() 能夠遞歸地進(jìn)入每一個(gè)容器中。
5、實(shí)現(xiàn)
我們以矢量圖編輯器的一部分為示例進(jìn)行實(shí)現(xiàn),采用了前面給出的基礎(chǔ)類模板。
drawable 和 基礎(chǔ)圖元
首先做 drawable/shape 的基本聲明以及基礎(chǔ)圖元:
namespace hicc::dp::visitor::basic {
using draw_id = std::size_t;
/** @brief a shape such as a dot, a line, a rectangle, and so on. */
struct drawable {
virtual ~drawable() {}
friend std::ostream &operator<<(std::ostream &os, drawable const *o) {
return os << '<' << o->type_name() << '#' << o->id() << '>';
}
virtual std::string type_name() const = 0;
draw_id id() const { return _id; }
void id(draw_id id_) { _id = id_; }
private:
draw_id _id;
};
#define MAKE_DRAWABLE(T) \
T(draw_id id_) { id(id_); } \
T() {} \
virtual ~T() {} \
std::string type_name() const override { \
return std::string{hicc::debug::type_name<T>()}; \
} \
friend std::ostream &operator<<(std::ostream &os, T const &o) { \
return os << '<' << o.type_name() << '#' << o.id() << '>'; \
}
//@formatter:off
struct point : public drawable {MAKE_DRAWABLE(point)};
struct line : public drawable {MAKE_DRAWABLE(line)};
struct rect : public drawable {MAKE_DRAWABLE(rect)};
struct ellipse : public drawable {MAKE_DRAWABLE(ellipse)};
struct arc : public drawable {MAKE_DRAWABLE(arc)};
struct triangle : public drawable {MAKE_DRAWABLE(triangle)};
struct star : public drawable {MAKE_DRAWABLE(star)};
struct polygon : public drawable {MAKE_DRAWABLE(polygon)};
struct text : public drawable {MAKE_DRAWABLE(text)};
//@formatter:on
// note: dot, rect (line, rect, ellipse, arc, text), poly (triangle, star, polygon)
}
為了調(diào)試目的,我們重載了 '<<' 流輸出運(yùn)算符,而且利用宏 MAKE_DRAWABLE 來(lái)削減重復(fù)性代碼的鍵擊輸入。在 MAKE_DRAWABLE 宏中,我們通過(guò) hicc::debug::type_name<T>() 來(lái)獲得類名,并將此作為字符串從 drawable::type_name() 返回。
出于簡(jiǎn)化的理由基礎(chǔ)圖元沒(méi)有進(jìn)行層次化,而是平行地派生于 drawable。
復(fù)合性圖元和圖層
下面聲明 group 對(duì)象,這種對(duì)象包含一組圖元。由于我們想要盡可能多的遞歸結(jié)構(gòu),所以圖層也被認(rèn)為是一種一組圖元的組合形式:
namespace hicc::dp::visitor::basic {
struct group : public drawable
, public hicc::util::visitable<drawable> {
MAKE_DRAWABLE(group)
using drawable_t = std::unique_ptr<drawable>;
using drawables_t = std::unordered_map<draw_id, drawable_t>;
drawables_t drawables;
void add(drawable_t &&t) { drawables.emplace(t->id(), std::move(t)); }
return_t accept(visitor_t &guest) override {
for (auto const &[did, dr] : drawables) {
guest.visit(dr);
UNUSED(did);
}
}
};
struct layer : public group {
MAKE_DRAWABLE(layer)
// more: attrs, ...
};
}
在 group class 中已經(jīng)實(shí)現(xiàn)了 visitable 接口,它的 accept 能夠接受訪問(wèn)者的訪問(wèn),此時(shí) 圖元組 group 會(huì)遍歷自己的所有圖元并提供給訪問(wèn)者。
我們還可以基于 group class 創(chuàng)建 compound 圖元類型,它允許將若干圖元組合成一個(gè)新的圖元元件,兩者的區(qū)別在于,group 一般是 UI 操作中的臨時(shí)性對(duì)象,而 compound 圖元能夠作為元件庫(kù)中的一員供用戶挑選和使用。
默認(rèn)時(shí) guest 會(huì)訪問(wèn) visited const & 形式的圖元,也就是只讀方式。
圖層至少具有 group 的全部能力,所以面對(duì)訪問(wèn)者它的做法是相同的。圖層的屬性部分(mask,overlay 等等)被略過(guò)了。
畫布 Canvas
畫布包含了若干圖層,所以它同樣應(yīng)該實(shí)現(xiàn) visitable 接口:
namespace hicc::dp::visitor::basic {
struct canvas : public hicc::util::visitable<drawable> {
using layer_t = std::unique_ptr<layer>;
using layers_t = std::unordered_map<draw_id, layer_t>;
layers_t layers;
void add(draw_id id) { layers.emplace(id, std::make_unique<layer>(id)); }
layer_t &get(draw_id id) { return layers[id]; }
layer_t &operator[](draw_id id) { return layers[id]; }
virtual return_t accept(visitor_t &guest) override {
// hicc_debug("[canva] visiting for: %s", to_string(guest).c_str());
for (auto const &[lid, ly] : layers) {
ly->accept(guest);
}
return;
}
};
}
其中,add 將會(huì)以默認(rèn)參數(shù)創(chuàng)建一個(gè)新圖層,圖層順序遵循向上疊加方式。get 和 [] 運(yùn)算符能夠通過(guò)正整數(shù)下標(biāo)訪問(wèn)某一個(gè)圖層。但是代碼中沒(méi)有包含圖層順序的管理功能,如果有意,你可以添加一個(gè) std::vector<draw_id> 的輔助結(jié)構(gòu)來(lái)幫助管理圖層順序。
現(xiàn)在我們來(lái)回顧畫布-圖層-圖元體系,accept 接口成功地貫穿了整個(gè)體系。
是時(shí)候建立訪問(wèn)者們了
screen 或 printer
這兩者實(shí)現(xiàn)了簡(jiǎn)單的訪問(wèn)者接口:
namespace hicc::dp::visitor::basic {
struct screen : public hicc::util::visitor<drawable> {
return_t visit(visited_t const &visited) override {
hicc_debug("[screen][draw] for: %s", to_string(visited.get()).c_str());
}
friend std::ostream &operator<<(std::ostream &os, screen const &) {
return os << "[screen] ";
}
};
struct printer : public hicc::util::visitor<drawable> {
return_t visit(visited_t const &visited) override {
hicc_debug("[printer][draw] for: %s", to_string(visited.get()).c_str());
}
friend std::ostream &operator<<(std::ostream &os, printer const &) {
return os << "[printer] ";
}
};
}
hicc::to_string 是一個(gè)簡(jiǎn)易的串流包裝,它做如下的核心邏輯:
template<typename T>
inline std::string to_string(T const &t) {
std::stringstream ss;
ss << t;
return ss.str();
}
test case
測(cè)試程序構(gòu)造了微型的畫布以及幾個(gè)圖元,然后示意性地訪問(wèn)它們:
void test_visitor_basic() {
using namespace hicc::dp::visitor::basic;
canvas c;
static draw_id id = 0, did = 0;
c.add(++id); // added one graph-layer
c[1]->add(std::make_unique<line>(++did));
c[1]->add(std::make_unique<line>(++did));
c[1]->add(std::make_unique<rect>(++did));
screen scr;
c.accept(scr);
}
輸出結(jié)果應(yīng)該類似于這樣:
--- BEGIN OF test_visitor_basic ---------------------- 09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::rect#3> 09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#2> 09/14/21 00:33:31 [debug]: [screen][draw] for: <hicc::dp::visitor::basic::line#1 --- END OF test_visitor_basic ---------------------- It took 2.813.753ms
二、Epilogue
Visitor 模式有時(shí)候能夠被迭代器模式所代替。但是迭代器常常會(huì)有一個(gè)致命缺陷而影響了其實(shí)用性:迭代器本身可能是僵化的、高代價(jià)的、效率低下的——除非你做出了最恰當(dāng)?shù)脑O(shè)計(jì)時(shí)選擇并實(shí)現(xiàn)了最精巧的迭代器。 它們兩者都允許用戶無(wú)侵入地訪問(wèn)一個(gè)已知的復(fù)雜容器的內(nèi)容。
到此這篇關(guān)于淺談 C++17 里的 Visitor 模式的文章就介紹到這了,更多相關(guān) C++17 里的 Visitor 模式內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
- Java 的雙重分發(fā)與 Visitor 模式實(shí)例詳解
- C#設(shè)計(jì)模式之Visitor訪問(wèn)者模式解決長(zhǎng)隆歡樂(lè)世界問(wèn)題實(shí)例
- 實(shí)例講解iOS應(yīng)用的設(shè)計(jì)模式開(kāi)發(fā)中的Visitor訪問(wèn)者模式
- 學(xué)習(xí)php設(shè)計(jì)模式 php實(shí)現(xiàn)訪問(wèn)者模式(Visitor)
- Java設(shè)計(jì)模式之訪問(wèn)模式(Visitor者模式)介紹
- php設(shè)計(jì)模式 Visitor 訪問(wèn)者模式
- C++的命名空間詳解
- C++基于OpenCV實(shí)現(xiàn)手勢(shì)識(shí)別的源碼
- C++內(nèi)存模型和名稱空間詳解
相關(guān)文章
C語(yǔ)言實(shí)現(xiàn)經(jīng)典掃雷小游戲完整代碼(遞歸展開(kāi)?+?選擇標(biāo)記)
這篇文章主要介紹了C語(yǔ)言小項(xiàng)目之掃雷游戲帶遞歸展開(kāi)?+?選擇標(biāo)記效果,本代碼中,我們用字符?!?來(lái)標(biāo)識(shí)雷,文中附有完整代碼,需要的朋友可以參考下2022-05-05
C++實(shí)現(xiàn)校園運(yùn)動(dòng)會(huì)報(bào)名系統(tǒng)
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)校園運(yùn)動(dòng)會(huì)報(bào)名系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2018-10-10
C/C++實(shí)現(xiàn)內(nèi)存泄漏檢測(cè)詳解
這篇文章主要為大家詳細(xì)介紹了c++進(jìn)行內(nèi)存泄漏檢測(cè)的方法,幫助大家更好的理解和學(xué)習(xí)使用c++,感興趣的朋友可以了解下,希望能夠給你帶來(lái)幫助2023-02-02
C++實(shí)現(xiàn)拓?fù)渑判颍ˋOV網(wǎng)絡(luò))
這篇文章主要為大家詳細(xì)介紹了C++實(shí)現(xiàn)拓?fù)渑判颍闹惺纠a介紹的非常詳細(xì),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下2020-04-04
C語(yǔ)言編程銀行ATM存取款系統(tǒng)實(shí)現(xiàn)源碼
這篇文章主要為大家介紹了C語(yǔ)言編程銀行ATM存取款系統(tǒng)實(shí)現(xiàn)的源碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進(jìn)步2021-11-11

