訪問者模式(學習筆記)
1. 意圖
表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作
2. 動機
假如你的團隊開發了一款能夠使用巨型影象中地理資訊的應用程式。影象中的每個節點既能代表複雜實體(例如一座城市),也能代表更精細的物件(例如工業區和旅遊景點等)。如果節點代表的真實物件之間存在公路,那麼這些節點就會相互連線。在程式內部,每個節點的型別都由其所屬的類來表示,每個特定的節點則是一個物件。
一段時間後,接到了實現將影象匯出到XML檔案中的任務。這些工作最初看上去非常簡單。你計劃為每個節點類新增匯出函式,然後遞迴執行影象中每個節點的匯出函式。解決方案簡單且優雅:使用多型機制可以讓匯出方法的呼叫程式碼不會和具體的節點類相耦合。但s是系統架構師拒絕批准對已有節點類進行修改。他認為這些程式碼已經是產品了,不想冒險對其進行修改,因為修改可能會引入潛在的缺陷。
此外,他還質疑在節點類中包含匯出XML檔案的程式碼是否有意義。這些類的主要工作是處理地理資料,匯出XML檔案的程式碼放在這裡並不合適。還有另一個原因,那就是在此項任務完成後,營銷部門很有可能會要求程式提供匯出其他型別檔案的功能,或者提出其他奇怪的要求。這樣你很可能會被迫再次修改這些重要但脆弱的類。
訪問者模式建議將新行為放入一個名為訪問者的獨立類中,而不是試圖將其整合到已有類中。現在,需要執行操作的原始物件將作為引數被傳遞給訪問者中的方法,讓方法能訪問物件所包含的一切必要資料。
如果現在該操作能在不同類的物件上執行會怎麼樣呢?比如在我們的示例中,各節點類匯出XML檔案的實際實現很可能會稍有不同。因此,訪問者類可以定義一組(而不是一個)方法,且每個方法可接收不同型別的引數,如下所示:
class ExportVisitor implements Visitor is
method doForCity(City c) { ... }
method doForIndustry(Industry f) { ... }
method doForSightSeeing(SightSeeing ss) { ... }
// ...
問題是,我們該如何呼叫這些方法呢?可以發現,這些方法的簽名各不相同,因此不能使用多型機制。為了可以挑選出能夠處理特定物件的訪問者方法,我們需要對它的類進行檢查,如下面程式碼所示:
foreach (Node node in graph)
if (node instanceof City)
exportVisitor.doForCity((City) node)
if (node instanceof Industry)
exportVisitor.doForIndustry((Industry) node)
// ...
}
可否使用方法地過載呢?依然不行,因為我們無法提前知曉節點物件所屬的類,所以過載機制無法執行正確的方法
訪問者模式可以解決這個問題。它使用了一種名為雙分派(在選擇一個方法的時候,不僅僅要根據訊息接收者(receiver)的執行時型別(Run time type),還要根據引數的執行時型別(Run time type)。這裡的訊息接收者其實就是方法的呼叫者。具體來講就是,對於訊息表示式a.m(b),雙分派能夠按照a和b的實際型別為其繫結對應方法體)的技巧,在不使用累贅的條件語句地情況下,也可以執行正確的方法。與其讓客戶端來選擇呼叫正確版本的方法,不如將物件放在各個節點中,並將訪問者物件作為引數傳給該物件。由於該物件知曉其自身的類,因此能更自然地在訪問者中選出正確的方法。它們會 “接收” 一個訪問者並告訴其應執行的訪問者方法
// 客戶端程式碼
foreach (Node node in graph)
node.accept(exportVisitor)
// 城市
class City is
method accept(Visitor v) is
v.doForCity(this)
// ...
// 工業區
class Industry is
method accept(Visitor v) is
v.doForIndustry(this)
// ...
我們最終還是修改了節點類,但畢竟改動很小,而且在後續進一步新增行為時無需再次修改程式碼。現在,如果我們抽取出所有訪問者的通用介面,所有已有的節點都能與我們在程式中引入的任何訪問者互動。如果需要引入與節點相關的某個行為,你只需要實現一個新的訪問者類即可
3. 適用性
- 一個物件結構(如物件樹)包含很多類物件,它們有不同的介面,而你想對這些物件實施一些依賴於其具體類的操作
- 需要對一個物件結構中的物件進行很多不同並且不相關的操作,而你想避免讓這些操作汙染這些類物件。Visitor使得你可以將相關操作集中起來定義在一個類中。當物件結構被很多應用共享時,用Visitor模式讓每個應用僅包含需要用到的操作
- 當某個行為僅在類層次結構中的一些類中有意義,而在其他類中沒有意義時,可使用該模式
4. 結構
5. 效果
1. 訪問者模式使得易於增加新的操作(開閉原則)
2. 訪問者集中相關的操作而分離無關的操作(單一職責原則)
3. 增加新的ConcreteElement類很困難 Visitor模式使得難以增加新的Element的子類。每新增一個新的ConcreteElement都要在Visitor中新增一個新的抽象操作,並在每一個ConcreteVisitor類中實現相應的操作。所以在使用訪問者模式時考慮的關鍵問題是系統的哪個部分會經常變化,是作用於物件結構上的演算法還是構成該結構的給個物件的類。如果總是有新的ConcreteElement類加進來,Visitor類層次將變得難以維護
4. 在訪問者同某個元素進行互動時,它們可能沒有訪問元素私有成員變數和方法的必要許可權
6. 程式碼實現
在本例中,我們希望將一系列幾何形狀匯出為 XML 檔案。重點在於我們不希望直接修改形狀程式碼,或者至少能確保最小程度的修改。
shapes/Shape.java: 通用形狀介面
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:23 */ public interface Shape { void move(int x, int y); void draw(); String accept(Visitor visitor); }
shapes/Dot.java: 點
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:24 */ public class Dot implements Shape{ private int id; private int x; private int y; public Dot() { } public Dot(int id, int x, int y) { this.id = id; this.x = x; this.y = y; } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } @Override public String accept(Visitor visitor) { return visitor.visitDot(this); } public int getX() { return x; } public int getY() { return y; } public int getId() { return id; } }
shapes/Circle.java: 圓形
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:25 */ public class Circle extends Dot{ private int radius; public Circle(int id, int x, int y, int radius) { super(id, x, y); this.radius = radius; } @Override public String accept(Visitor visitor) { return visitor.visitCircle(this); } public int getRadius() { return radius; } }
shapes/Rectangle.java: 矩形
package visitor.shapes; import visitor.visitor.Visitor; /** * @author GaoMing * @date 2021/7/26 - 17:26 */ public class Rectangle implements Shape{ private int id; private int x; private int y; private int width; private int height; public Rectangle(int id, int x, int y, int width, int height) { this.id = id; this.x = x; this.y = y; this.width = width; this.height = height; } @Override public String accept(Visitor visitor) { return visitor.visitRectangle(this); } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } public int getId() { return id; } public int getX() { return x; } public int getY() { return y; } public int getWidth() { return width; } public int getHeight() { return height; } }
shapes/CompoundShape.java: 組合形狀
package visitor.shapes; import visitor.visitor.Visitor; import java.util.ArrayList; import java.util.List; /** * @author GaoMing * @date 2021/7/26 - 17:27 */ public class CompoundShape implements Shape{ public int id; public List<Shape> children = new ArrayList<>(); public CompoundShape(int id) { this.id = id; } @Override public void move(int x, int y) { // move shape } @Override public void draw() { // draw shape } public int getId() { return id; } @Override public String accept(Visitor visitor) { return visitor.visitCompoundGraphic(this); } public void add(Shape shape) { children.add(shape); } }
visitor/Visitor.java: 通用訪問者介面
package visitor.visitor; import visitor.shapes.Circle; import visitor.shapes.CompoundShape; import visitor.shapes.Dot; import visitor.shapes.Rectangle; /** * @author GaoMing * @date 2021/7/26 - 17:24 */ public interface Visitor { String visitDot(Dot dot); String visitCircle(Circle circle); String visitRectangle(Rectangle rectangle); String visitCompoundGraphic(CompoundShape cg); }
visitor/XMLExportVisitor.java: 具體訪問者,將所有形狀匯出為 XML 檔案
package visitor.visitor; import visitor.shapes.*; /** * @author GaoMing * @date 2021/7/26 - 17:28 */ public class XMLExportVisitor implements Visitor{ public String export(Shape... args) { StringBuilder sb = new StringBuilder(); sb.append("<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "\n"); for (Shape shape : args) { sb.append(shape.accept(this)).append("\n"); } return sb.toString(); } public String visitDot(Dot d) { return "<dot>" + "\n" + " <id>" + d.getId() + "</id>" + "\n" + " <x>" + d.getX() + "</x>" + "\n" + " <y>" + d.getY() + "</y>" + "\n" + "</dot>"; } public String visitCircle(Circle c) { return "<circle>" + "\n" + " <id>" + c.getId() + "</id>" + "\n" + " <x>" + c.getX() + "</x>" + "\n" + " <y>" + c.getY() + "</y>" + "\n" + " <radius>" + c.getRadius() + "</radius>" + "\n" + "</circle>"; } public String visitRectangle(Rectangle r) { return "<rectangle>" + "\n" + " <id>" + r.getId() + "</id>" + "\n" + " <x>" + r.getX() + "</x>" + "\n" + " <y>" + r.getY() + "</y>" + "\n" + " <width>" + r.getWidth() + "</width>" + "\n" + " <height>" + r.getHeight() + "</height>" + "\n" + "</rectangle>"; } public String visitCompoundGraphic(CompoundShape cg) { return "<compound_graphic>" + "\n" + " <id>" + cg.getId() + "</id>" + "\n" + _visitCompoundGraphic(cg) + "</compound_graphic>"; } private String _visitCompoundGraphic(CompoundShape cg) { StringBuilder sb = new StringBuilder(); for (Shape shape : cg.children) { String obj = shape.accept(this); // Proper indentation for sub-objects. obj = " " + obj.replace("\n", "\n ") + "\n"; sb.append(obj); } return sb.toString(); } }
Demo.java: 客戶端程式碼
package visitor; import visitor.shapes.*; import visitor.visitor.XMLExportVisitor; /** * @author GaoMing * @date 2021/7/26 - 17:23 */ public class Demo { public static void main(String[] args) { Dot dot = new Dot(1, 10, 55); Circle circle = new Circle(2, 23, 15, 10); Rectangle rectangle = new Rectangle(3, 10, 17, 20, 30); CompoundShape compoundShape = new CompoundShape(4); compoundShape.add(dot); compoundShape.add(circle); compoundShape.add(rectangle); CompoundShape c = new CompoundShape(5); c.add(dot); compoundShape.add(c); export(circle, compoundShape); } private static void export(Shape... shapes) { XMLExportVisitor exportVisitor = new XMLExportVisitor(); System.out.println(exportVisitor.export(shapes)); } }
執行結果
<?xml version="1.0" encoding="utf-8"?> <circle> <id>2</id> <x>23</x> <y>15</y> <radius>10</radius> </circle> <?xml version="1.0" encoding="utf-8"?> <compound_graphic> <id>4</id> <dot> <id>1</id> <x>10</x> <y>55</y> </dot> <circle> <id>2</id> <x>23</x> <y>15</y> <radius>10</radius> </circle> <rectangle> <id>3</id> <x>10</x> <y>17</y> <width>20</width> <height>30</height> </rectangle> <compound_graphic> <id>5</id> <dot> <id>1</id> <x>10</x> <y>55</y> </dot> </compound_graphic> </compound_graphic>
7. 與其他模式的關係
- 可以將訪問者模式視為命令模式的加強版本,其物件可對不同類的多種物件執行操作
- 可以使用訪問者對整個組合模式樹執行操作
- 可以同時使用訪問者和迭代器模式來遍歷複雜資料結構,並對其中的元素執行所需操作,即使這些元素所屬的類完全不同。而迭代器能夠訪問的所有元素都有一個共同的父類
8. 已知應用
使用示例:訪問者不是常用的設計模式,因為它不僅複雜,應用範圍也比較狹窄
這裡是 Java 程式庫程式碼中該模式的一些示例:
javax.lang.model.element.AnnotationValue 和 AnnotationValueVisitor
javax.lang.model.element.Element 和 ElementVisitor
javax.lang.model.type.TypeMirror 和 TypeVisitor
java.nio.file.FileVisitor 和 SimpleFileVisitor
javax.faces.component.visit.VisitContext 和 VisitCallback