淺談 C++17 里的 Visitor 模式
一、Visitor Pattern
訪問者模式是一種行為模式,允許任意的分離的訪問者能夠在管理者控制下訪問所管理的元素。訪問者不能改變對象的定義(但這并不是強制性的,你可以約定為允許改變)。對管理者而言,它不關(guān)心究竟有多少訪問者,它只關(guān)心一個確定的元素訪問順序(例如對于二叉樹來說,你可以提供中序、前序等多種訪問順序)。
1、組成
Visitor
模式包含兩個主要的對象:Visitable
對象和 Vistor
對象。此外,作為將被操作的對象,在 Visitor 模式中也包含 Visited 對象。
一個 Visitable
對象,即管理者,可能包含一系列形態(tài)各異的元素(Visited
),它們可能在 Visitable
中具有復雜的結(jié)構(gòu)關(guān)系(但也可以是某種單純的容納關(guān)系,如一個簡單的 vector)。Visitable 一般會是一個復雜的容器,負責解釋這些關(guān)系,并以一種標準的邏輯遍歷這些元素。當 Visitable
對這些元素進行遍歷時,它會將每個元素提供給 Visitor 令其能夠訪問該 Visited 元素。
這樣一種編程模式就是 Visitor Pattern
。
2、接口
為了能夠觀察每個元素,因此實際上必然會有一個約束:所有的可被觀察的元素具有共同的基類 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、場景
以一個實例來說,假設(shè)我們正在設(shè)計一套矢量圖編輯器,在畫布(Canvas
)中,可以有很多圖層(Layer
),每一圖層包含一定的屬性(例如填充色,透明度),并且可以有多種圖元(Element
)。圖元可以是 Point
,Line
,Rect
,Arc
等等。
為了能夠?qū)嫴祭L制在屏幕上,我們可以有一個 Screen
設(shè)備對象,它實現(xiàn)了 Visitor
接口,因此畫布可以接受 Screen
的訪問,從而將畫布中的圖元繪制到屏幕上。
如果我們提供 Printer
作為觀察者 ,那么畫布將能夠把圖元打印出來。
如果我們提供 Document
作為觀察者,那么畫布將能夠把圖元特性序列化到一個磁盤文件中去。
如果今后需要其它的行為,我們可以繼續(xù)增加新的觀察者,然后對畫布及其所擁有的圖元進行類似的操作。
4、特點
- 如果你需要對一個復雜對象結(jié)構(gòu) (例如對象樹) 中的所有元素執(zhí)行某些操作, 可使用訪問者模式。
- 訪問者模式將非主要的功能從對象管理者身上抽離,所以它也是一種解耦手段。
- 如果你正在制作一個對象庫的類庫,那么向外提供一個訪問接口,將會有利于用戶無侵入地開發(fā)自己的 visitor 來訪問你的類庫——他不必為了自己的一點點事情就給你
issue/pull request
。 - 對于結(jié)構(gòu)層級復雜的情況,要善于使用對象嵌套與遞歸能力,避免反復編寫相似邏輯。
請查閱
canva
,layer
,group
的參考實現(xiàn),它們通過實現(xiàn)drawable
和vistiable
<drawable
> 的方式完成了嵌套性的自我管理能力,并使得 accept() 能夠遞歸地進入每一個容器中。
5、實現(xiàn)
我們以矢量圖編輯器的一部分為示例進行實現(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)試目的,我們重載了 '<<' 流輸出運算符,而且利用宏 MAKE_DRAWABLE
來削減重復性代碼的鍵擊輸入。在 MAKE_DRAWABLE
宏中,我們通過 hicc::debug::type_name<T>()
來獲得類名,并將此作為字符串從 drawable::type_name()
返回。
出于簡化的理由基礎(chǔ)圖元沒有進行層次化,而是平行地派生于 drawable
。
復合性圖元和圖層
下面聲明 group
對象,這種對象包含一組圖元。由于我們想要盡可能多的遞歸結(jié)構(gòu),所以圖層也被認為是一種一組圖元的組合形式:
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)實現(xiàn)了 visitable
接口,它的 accept
能夠接受訪問者的訪問,此時 圖元組 group
會遍歷自己的所有圖元并提供給訪問者。
我們還可以基于 group class
創(chuàng)建 compound
圖元類型,它允許將若干圖元組合成一個新的圖元元件,兩者的區(qū)別在于,group
一般是 UI 操作中的臨時性對象,而 compound
圖元能夠作為元件庫中的一員供用戶挑選和使用。
默認時 guest 會訪問 visited const &
形式的圖元,也就是只讀方式。
圖層至少具有 group
的全部能力,所以面對訪問者它的做法是相同的。圖層的屬性部分(mask
,overlay
等等)被略過了。
畫布 Canvas
畫布包含了若干圖層,所以它同樣應該實現(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 將會以默認參數(shù)創(chuàng)建一個新圖層,圖層順序遵循向上疊加方式。get 和 [] 運算符能夠通過正整數(shù)下標訪問某一個圖層。但是代碼中沒有包含圖層順序的管理功能,如果有意,你可以添加一個 std::vector<draw_id>
的輔助結(jié)構(gòu)來幫助管理圖層順序。
現(xiàn)在我們來回顧畫布-圖層-圖元體系,accept
接口成功地貫穿了整個體系。
是時候建立訪問者們了
screen 或 printer
這兩者實現(xià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
是一個簡易的串流包裝,它做如下的核心邏輯:
template<typename T> inline std::string to_string(T const &t) { std::stringstream ss; ss << t; return ss.str(); }
test case
測試程序構(gòu)造了微型的畫布以及幾個圖元,然后示意性地訪問它們:
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é)果應該類似于這樣:
--- 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ù)脑O(shè)計時選擇并實現(xiàn)了最精巧的迭代器。 它們兩者都允許用戶無侵入地訪問一個已知的復雜容器的內(nèi)容。
到此這篇關(guān)于淺談 C++17 里的 Visitor 模式的文章就介紹到這了,更多相關(guān) C++17 里的 Visitor 模式內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
C語言實現(xiàn)經(jīng)典掃雷小游戲完整代碼(遞歸展開?+?選擇標記)
這篇文章主要介紹了C語言小項目之掃雷游戲帶遞歸展開?+?選擇標記效果,本代碼中,我們用字符?!?來標識雷,文中附有完整代碼,需要的朋友可以參考下2022-05-05C++實現(xiàn)拓撲排序(AOV網(wǎng)絡(luò))
這篇文章主要為大家詳細介紹了C++實現(xiàn)拓撲排序,文中示例代碼介紹的非常詳細,具有一定的參考價值,感興趣的小伙伴們可以參考一下2020-04-04C語言編程銀行ATM存取款系統(tǒng)實現(xiàn)源碼
這篇文章主要為大家介紹了C語言編程銀行ATM存取款系統(tǒng)實現(xiàn)的源碼示例,有需要的朋友可以借鑒參考下,希望能夠有所幫助,祝大家多多進步2021-11-11