設計模式-行爲模式之Visitor

更多請移步我的博客

意圖

Visitor是行爲模式的一種,允許你在不改變要操作對象類的情況下定義一個新操作。

問題

你的團隊正在開發一款地理信息結構地圖的app。圖的節點不僅表示城鎮也有其他諸如景點,行業等信息。節點間通過道路關聯。在引擎中,每個節點都是一個對象,他們的類型由他們自己的類來表示。

你接到一個任務,要把地圖導出爲XML。乍看之下很容易實現。你需要爲每個類型的節點添加一個導出方法,然後遍歷地圖併爲每個節點執行導出方法。這個方法不僅簡單而且優雅,因爲你可以使用多態來避免和具體的節點類耦合。

但不幸的是,系統架構師不允許修改已存在的node類。這些代碼已經在生產環境,沒有人希望冒着風險修改他。

另外,他質疑節點類中的XML導出是否有意義。這些類的主要工作是和地理數據協作。導出行爲放在這裏看起來很不合適。

還有另一個拒絕的原因。在此之後,市場部門的人可能會要求你導出其他格式或添加其他一些奇怪的功能。這會迫使你再次修改那些珍貴的代碼。

解決

Visitor模式建議你把新的行爲放在一個單獨的類中,而不是把它繼承在已存在的類中。關聯到對象的行爲,不會被對象本身調用。對象被作爲visitor對象的方法參數傳遞。

對於不同類型的對象,行爲的代碼可能有點不同。因此,visitor類必須爲不同類型的參數提供不同的行爲方法。

class ExportVisitor implements Visitor is
    method doForCity(City c) { ... }
    method doForIndustry(Industry f) { ... }
    method doForSightSeeing(SightSeeing ss) { ... }
    // ...

但是,我們怎麼爲整個地圖調用這些方法呢?這些方法有不同的簽名,這不允許我們使用多態。爲了找到合適的方法來執行給定的對象,我們需要檢查它的類。這聽起來就是個噩夢。

foreach (Node node : graph)
    if (node instanceof City)
        exportVisitor.doForCity((City) node);
    if (node instanceof Industry)
        exportVisitor.doForIndustry((Industry) node);
    // ...
}

即使給定的編程語言支持重載(比如:Java或者C#),方法的重載也不會有幫助。因爲無法事先知道給定節點的精確類,即使使用了重載也不一定能正確找到執行方法。

但是Visitor迷失對整個問題有一個解決方案。它使用Double Dispatch技術來保持多態性。如果我們把確定正確visitor方法的工作委託給我們傳遞給visitor的對象會怎麼樣?這些對象自己知道自己的類,所以他們可以挑選一個合適的方法。

// Client code
foreach (Node node : graph)
    node.accept(exportVisitor);

// City
class City is
    method accept(Visitor v) is
        v.doForCity(this);
    // ...

// Industry
class Industry is
    method accept(Visitor v) is
        v.doForIndustry(this);
    // ...

我必須承認,我們必須改變node類。但是至少這個改動很小,並且具有可擴展性。

我們接下來要爲所有的個性visitor抽離出通用的接口。現在,如果你需要給程序添加一個新的行爲,你要做的僅僅是增加一個visitor類。所有已存在的類仍然可以不受影響的很的工作。

真實世界的類比

保險代理

想象下一個剛入行的保險員,迫切需要新的客戶。他隨機訪問附近的鄰居,爲他們提供服務。但是不同類型的鄰居,需要不同的保險服務。

  • 在住宅,他兜售醫療保險。

  • 在銀行,他兜售防盜保險。

  • 在公司,他兜售自然災害險。

結構

structure

  1. Visitor爲所有類型的visitor聲明瞭通用的接口。他聲明瞭一系列把Context Components當作參數的參觀方法。這些方法的名字在支持重載的語言中可以一樣,但是參數類型不能一樣。

  2. Concrete Visitor實現通用接口描述的操作。每個具體的visitor都表示一個獨立的行爲。

  3. Component聲明瞭一個用來接收Visitor參數的方法。這個方法以Visitor接口作爲參數。

  4. Concrete Component實現這個驗收方法。這個方法的目的是爲了用來爲當前組件重定向到一個正確的visitor方法。

  5. Client表示一個集合或者其他複雜對象(比如,一個Composite樹)。Client通常不知道其組件的具體類別。

僞代碼

在這個例子中,Visitor模式將XML導出添加到幾何圖形的層次結構中。

// A complex hierarchy of components.
interface Shape is
    method move(x, y)
    method draw()
    method accept(v: Visitor)

// It is crucial to implement the accept() method in every
// single component, not just a base class. It helps the
// program to pick a proper method on the visitor class in
// case if a given component's type is unknow.
class Dot extends Shape is
    // ...
    method accept(v: Visitor) is
        v.visitDot(this)

class Circle extends Dot is
    // ...
    method accept(v: Visitor) is
        v.visitCircle(this)

class Rectangle extends Shape is
    // ...
    method accept(v: Visitor) is
        v.visitRectangle(this)

class CompoundShape implements Shape is
    // ...
    method accept(v: Visitor) is
        v.visitCompoundShape(this)


// Visitor interface must have visiting methods for the
// every single component. Note that each time you add a new
// class to the component history, you will need to add a
// method to the visitor classes. In this case, you might
// consider avoiding visitor altogether.
interface Visitor is
    method visitDot(d: Dot)
    method visitCircle(c: Circle)
    method visitRectangle(r: Rectangle)
    method visitCompoundShape(cs: CompoundShape)

// Concrete visitor adds a single operation to the entire
// hierarchy of components. Which means that if you need to
// add multiple operations, you will have to create
// several visitor.
class XMLExportVisitor is
    method visitDot(d: Dot) is
        Export dot's id and center coordinates.

    method visitCircle(c: Circle) is
        Export circle's id, center coordinates and radius.

    method visitRectangle(r: Rectangle) is
        Export rectangle's id, left-top coordinates, width and height.

    method visitCompoundShape(cs: CompoundShape) is
        Export shape's id and the list of children ids.


// Application can use visitor along with any set of
// components without checking their type first. Double
// dispatch mechanism guarantees that a proper visiting
// method will be called for any given component.
class Application is
    field allShapes: array of Shapes

    method export() is
        exportVisitor = new XMLExportVisitor()

        foreach shape in allShapes
            shape.accept(exportVisitor)

如果你不知道爲什麼這裏需要accapt方法,你需要了解一下二次分派。在Java 8之後,接口允許有默認實現,所以本例子可以利用重載和default實現的更加精簡。

適用性

  • 當你需要對複雜對象結構(例如樹)的所有元素執行操作時,並且所有元素都是異構的。

    Visitor模式允許你爲一系列不同類型的對象執行一個操作。

  • 當你需要能夠在一個複雜的對象結構上運行幾個不相關的行爲,但是你不想用這些行爲的代碼來“阻塞”結構的類。

    Visitor模式允許你從一堆構成對象結構的類中提取和統一相關的行爲,並將其集成到一個visitor類中。這些轉型與虛擬在不同的app中重用這些類,而不用關心和它不相關的行爲。

  • 當一個新行爲只對現有層次結構中的某些類有意義。

    Visitor模式允許你製作一個特殊的visitor類實現某些對象的行爲,但不爲其他對象。

如何實現

  1. 爲程序中的具體組件創建Visitor接口並且聲明一個“visiting”方法。

  2. 在組件的基類中添加抽象的accept方法。

  3. 具體的組件實現抽象的accept方法。他們必須把請求重定向到適合當前組件類的特定visitor方法。

  4. 組件層級結構只需要關心Visitor接口。這樣visitor就不必和具體的組件耦合。

  5. 對於每個新行爲,創建一個新的Concrete Visitor類並實現所有的訪問方法。

  6. 客戶端創建visitor對象,並把他們當作參數傳給組建的accept方法。

優點

  • 簡化類在複雜的對象結構上添加新的操作。

  • 將相關的行爲移動到一個類中。

  • visitor可以在對對象結構的工作過程中積累狀態。

缺點

  • 如果組件的層次經常改變,不適合使用該模式。

  • 違反組件的封裝。

和其他模式的關係

  • Visitor模式像加強版的Command模式,它可以在任何類型的對象上執行一個操作。

  • Visitor可以對整個Composite樹應用一個操作。

  • Visitor可以和Iterator模式協作來遍歷複雜的數據結構,並對所有元素執行一些操作,即使它們有不同的類型。

小結

Visitor模式的結構比較簡單,其中比較巧妙的是“二次分派”技術,這裏不做展開,大家可自行問度娘或者谷哥。給個簡單的例子,自行體會下

參考

翻譯整理自:https://refactoring.guru/design-patterns/visitor

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章