Monday, May 19, 2014

Visitor, Ch10, Modern C++ Design



這章的焦點焦點放在 visitor pattern 上面,因為對於這個 design pattern 不太熟,所以先來複習一下。

需求和動機

首先,在 visitor 架構中有一組多形的資料結構,需要支援一些操作。這應該很常見,像是線上商店可能會把書籍和音樂條列在一起,顯示的圖示不同,點選以後分別可以試閱或者試聽。因為要條列在一起,所以 Book & Music 要繼承同一個父類別,並且提供不同的 display() & preview() 實作。這樣使得資料和功能都寫在同一個 class,但這兩者應該分開,因為資料很少變動,可是功能經常修改或增減。

Visitor pattern 可以把功能從資料抽出來,把每個功能變成一個 Visitor class,而資料提供一個轉接的 virtual accept(Visitor&),只要傳入不同的 Visitor 就可以執行不同的功能。以線上商店的例子來說,Book::accept(DisplayVisitor) 會顯示書籍的圖示,而 Music::accept(PreviewVisitor) 會播放試聽的音樂。如果要修改書籍的圖示,單獨修改 DisplayVisitor 不會影響原有的資料,也不會影響 PreviewVisitor 的功能。

Cylic Dependency




上圖中,Visitee->AbstractVisitor->A 三者間有循環依賴的關係,雖然只是 depend by name,通常使用 forward declaration 就可以。問題是,如果增加一個 C 繼承 Visitee,並且針對 C 提供對應的 virtual AbstractVisitor::visit(C&),那麼所有繼承 Visitee 的 class 像是 A & B 都要重新編譯,因為AbstractVisitor 增加了新的 virtual function。

不管重新編譯的範圍有多大,可能都是很嚴重的問題,因為繼承的基本精神是「相同界面,不同實作」。上圖的 A & B 可能在兩個不同 library 裡面,根本看不到 source code,如果需要重新編譯,就是很大的 API change。

Acyclic Visitor



上圖把原本的 visit() 獨立出來,讓 A & B 各自使用各自的 Visitor,避免互相依賴。在實作裡, A::accept(AbstractVisitor& v) 因為參數提供的型別訊息不足,所以需要用 dynamic_cast<Visitor<A>&>(v) 以後才能呼叫 visit(*this)。當然,也是因為用更抽象的參數,才能避免 cyclic dependency。

比較兩張架構圖,可以發現 acyclic visitor 把型別檢查從編譯延遲到執行,以便得到 A & B 獨立的好處。假設 ConcreteVisitor 在新增 C 的時候沒有實作 Visit(C&),那麼 cyclic visitor 會 compile error,但 acyclic visitor 要到執行才會出錯。另外一個問題是 dynamic_cast 的效率,比起原來直接呼叫 virtual function 可能要多數十倍。

Summary

用 template 寫出 Visitor & Visitee 的骨架並不難,只要定義 pure virtual function 抽象界面。比較特別的地方有兩個:首先是 A & B 各自的 accept() 實作,每個要呼叫各自對應的 visit(),這章使用 macro 來示範,也可以用 curiously-recursive-template。另一個地方,是在決定 cyclic/acyclic 的時候盡可能減少 Visitee, A, B, ConcreteVisitor 的改動。這可以透過 typedef + template specialization 達成,只需修改兩個 headers,其他的實作都只要重新編譯就可以了。

No comments: