軟體設計GoF23種設計模式中最為複雜最難理解的訪問者模式詳解

訪問者模式(Visitor Pattern)是一種將資料結構和資料操作分離的設計模式。是指封裝一些作用於某種資料結構中的各種元素的操作,它可以在不改變資料結構的前提下定義作用於這些元素的新的操作。

訪問者模式被稱為最複雜的設計模式,並且使用頻率不高,設計模式的作者也評價為:大多情況下,你不需要使用訪問者模式,但是一旦需要使用它時,那就真的需要使用了。訪問者模式的基本思想是,針對系統中擁有固定型別數的物件結構(元素),在其內提供一個accept()方法用來接受訪問者物件的訪問。不同的訪問者對同一元素的訪問內容不同,使得相同的元素集合可以產生不同的資料結果。accept()方法可以接收不同的訪問者物件,然後在內部將自己(元素)轉發到接收到的訪問者物件的visit()方法內。訪問者內部對應型別的visit()方法就會得到回撥執行,對元素進行操作。也就是透過兩次動態分發(第一次是對訪問者的分發accept()方法,第二次是對元素的分發visit()方法),才最終將一個具體的元素傳遞到一個具體的訪問者。如此一來,就解耦了資料結構與操作,且資料操作不會改變元素狀態。

一、訪問者模式的應用場景

訪問者模式在生活場景中也是非常當多的,例如每年年底的KPI考核,KPI考核標準是相對穩定的,但是參與KPI考核的員工可能每年都會發生變化,那麼員工就是訪問者。我們平時去食堂或者餐廳吃飯,餐廳的選單和就餐方式是相對穩定的,但是去餐廳就餐的人員是每天都在發生變化的,因此就餐人員就是訪問者。

軟體設計GoF23種設計模式中最為複雜最難理解的訪問者模式詳解

軟體設計GoF23種設計模式中最為複雜最難理解的訪問者模式詳解

訪問者模式的核心是,解耦資料結構與資料操作,使得對元素的操作具備優秀的擴充套件性。可以透過擴充套件不同的資料操作型別(訪問者)實現對相同元素的不同的操作。簡而言之就是對集合中的不同型別資料(型別數量穩定)進行多種操作,則使用訪問者模式。

訪問者模式的應用場景適用於以下幾個場景:

資料結構穩定,作用於資料結構的操作經常變化的場景;

需要資料結構與資料操作呢分離的場景;

需要對不同資料型別(元素)進行操作,而不使用分支判斷具體型別的場景。

訪問者模式主要包含五種角色:

抽象訪問者(Visitor):介面或抽象類,該類地冠以了對每一個具體元素(Element)的訪問行為visit()方法,其引數就是具體的元素(Element)物件。理論上來說,Visitor的方法個數與元素(Element)個數是相等的。如果元素(Element)個數經常變動,會導致Visitor的方法也要進行變動,此時,該情形並不適用訪問者模式;

具體訪問者(ConcreteVisitor):實現對具體元素的操作;

抽象元素(Element):接囗或抽象類,定義了一個接受訪問者訪問的方法accept()表示所有元素型別都支援被訪問者訪問;

具體元素(ConcreteElement):具體元素型別,提供接受訪問者的具體實現。通常的實現都為:visitor。visit(this);

結構物件(ObjectStructure):內部維護了元素集合,並提供方法接受訪問者對該集合所有元素進行操作。

1。1 利用訪問者模式實現公司KPI考核

每到年底,公司的管理層就要開始評定員工一年的工作績效了,管理層有CEO和CTO,那麼CEO關注的是工程師的KPI和經理的KPI以及新產品的數量,而CTO關心的是工程師的程式碼量、經理的新產品數量。

由於CEO和CTO對於不同員工的關注點是不一樣的,這就需要對不同的員工型別進行不同的處理。此時訪問者模式就派上用場了。下面來看下具體的程式碼實現,首先建立員工Employee類:

public abstract class Employee { private String name; private int kpi; public Employee(String name) { this。name = name; this。kpi = new Random()。nextInt(10); } /** * 接收訪問者的訪問 * @param visitor */ public abstract void accept(IVisitor visitor);}

Employee類的accept()方法表示接受訪問者的訪問,由具體的子類實現。

訪問者是一個介面,傳入不同的實現類,可以訪問不同的資料。

分別建立工程師Engineer類和經理Manager類:

public class Engineer extends Employee { public Engineer(String name) { super(name); } @Override public void accept(IVisitor visitor) { visitor。visit(this); } public int getCodeLines() { return new Random()。nextInt(10 * 10000); }}

public class Manager extends Employee { public Manager(String name) { super(name); } @Override public void accept(IVisitor visitor) { visitor。visit(this); } public int getPrducts() { return new Random()。nextInt(10); }}

工程師考核的是程式碼數量,經理考核的是產品數量,二者的職責不一樣。也正是因為有這樣的差異性,才使得訪問模式能夠在這個場景下發揮作用。將這些員工新增到一個業務報表類中,公司高層可以透過該報表類的showReport()方法檢視所有員工的績效,建立BusinessReport類:

public class BusinessReport { private List employeeList = new LinkedList<>(); public BusinessReport() { employeeList。add(new Engineer(“工程師1”)); employeeList。add(new Engineer(“工程師2”)); employeeList。add(new Engineer(“工程師3”)); employeeList。add(new Engineer(“工程師4”)); employeeList。add(new Manager(“產品經理1”)); employeeList。add(new Manager(“產品經理2”)); } /** * * @param visitor 公司高層,如CEO,CTO */ public void showReport(IVisitor visitor) { for(Employee employee : employeeList) { employee。accept(visitor); } }}

定義訪問者型別,建立介面IVisitor,訪問者聲明瞭兩個visit()方法,分別針對工程師和經理,程式碼如下:

public interface IVisitor { void visit(Engineer engineer); void visit(Manager manager);}

具體訪問者CEOVisitor和CTOVisitor類:

public class CEOVisitor implements IVisitor { @Override public void visit(Engineer engineer) { System。out。println(“工程師:” + engineer。name + “, KPI:” + engineer。kpi); } @Override public void visit(Manager manager) { System。out。println(“經理:” + manager。name + “, KPI:” + manager。kpi + “, 新產品數量” + manager。getPrducts() ); }}

public class CTOVisitor implements IVisitor { @Override public void visit(Engineer engineer) { System。out。println(“工程師:” + engineer。name + “, 程式碼數量:” + engineer。getCodeLines()); } @Override public void visit(Manager manager) { System。out。println(“經理:” + manager。name + “, 新產品數量” + manager。getPrducts() ); }}

測試main方法:

public static void main(String[] args) { BusinessReport businessReport = new BusinessReport(); businessReport。showReport(new CEOVisitor()); businessReport。showReport(new CTOVisitor());}

執行結果如下:

軟體設計GoF23種設計模式中最為複雜最難理解的訪問者模式詳解

在上述的案例中,Employee扮演了Element角色,而Engineer和Manager都是ConcreteElement;CEOVisitor和CTOVisitor都是具體的Visitor物件;而BusinessReport就是ObjectStructure。

訪問者模式最大的優點就是增加訪問者非常容,我們從程式碼中可以看到,如果要增加一訪問者,只要新實現一個訪問者介面的類,從而達到資料物件與資料操作相分離的效果。如果不實用訪問者模式而又不想對不同的元素進行不同的操作,那麼必定需要使用if-else和型別轉換,這使得程式碼唯以升級維護。

我們要根據具體情況來評估是否適合使用訪問者模式,例如,我們的物件結構是否足夠穩定是否需要經常定義新的操作,使用訪問者模式是否能最佳化我們的程式碼而不是使我們的程式碼變得更復雜。

1。2 從靜態分派到動態分派

變數被宣告時的型別叫做變數的靜態型別(Static Type),有些人把靜態型別叫做明顯型別(Apparent Type);而變數所引用的物件的真是型別又叫做變數的實際型別(Actual Type)。

比如:

List list = null;list = new ArrayList();

上面的程式碼聲明瞭一個list,它的靜態型別(也叫明顯型別)是List,而它的實際型別是ArrayList。根據物件的型別而對方法進行的選擇,就是分派(Dispatch)。分派又分為兩種,即動態分派和靜態分派。

1。2。1 靜態分派

靜態分派(Static Dispatch)就是按照變數的靜態型別進行分派,從而確定方法的執行版本,靜態分派在編譯時期就可以確定方法的版本。而靜態分配最經典的就是方法過載,請看下面的這段程式碼:

public class StaticDispatch { public void test(String string) { System。out。println(“string”); } public void test(Integer integer) { System。out。println(“integer”); } public static void main(String[] args) { String string = “1”; Integer integer = 1; StaticDispatch staticDispatch = new StaticDispatch(); staticDispatch。test(string); staticDispatch。test(integer); }}

在靜態分派判斷的時候,我們根據多個判斷依據(即引數型別和個數)判斷出了方法的版本,那麼這個就是多分派的概念,因為有一個以上的考量標準。所以

Java語言是靜態多分派語言。

1。2。2 動態分派

動態分派,與靜態相反,它不是在編譯期間確定方法的版本,而是在執行時確定的。Java是動態單分派語言。

1。2。3 訪問者模式中的偽動態雙分派

透過前面分析,我們知道Java是靜態多分派、動態單分派的語言。Java底層不支援動態的雙分派。但是透過使用設計模式,也可以在Java語言裡實現偽動態雙分派。在訪問者模式中使用的就是偽動態雙分派。所謂動態雙分派就是在執行時依據兩個實際型別去判斷一個方法的執行行為,而訪問者模式實現的手段是進行了兩次動態單分派來達到這個效果。 還是回到前面的公司KPI考核業務場景當中,BusinessReport類中的showReport()方法:

/** * * @param visitor 公司高層,如CEO,CTO */public void showReport(IVisitor visitor) { for(Employee employee : employeeList) { employee。accept(visitor); }}

這裡就是依據Employee和IVisitor兩個實際型別決定了showReport()方法的執行結果從而決定了accept()方法的動作。

分析accept()方法的呼叫過程 1、當呼叫accept()方法時,根據Employee的實際型別決定是呼叫Engineer還是Manager的accept()方法。

2、這時accept()方法的版本已經確定,假如是Engineer,它的accept()方去是呼叫下面這行程式碼。

public void accept(IVisitor visitor) { visitor。visit(this);}

此時的this是Engineer型別,所以對應的IVisitor介面的visit(Engineer enginner)方法,此時需要再根據訪問者的實際型別確定visit()方法的版本,這樣一來,就完成了動態分派的過程。

以上的過程就是透過兩次動態雙分派,第一次對accept()方法進行動態分派,第二次訪問者的visit()方法進行動態分派,從而到達了根據兩個實際型別確定一個方法的行為結果。

二、訪問者模式在原始碼中的體現

2。1 NIO中的FileVisitor介面

JDK中的NIO模組下的FileVisitor介面,它提供遞迴遍歷檔案樹的支援。來看下原始碼:

public interface FileVisitor { /** * Invoked for a directory before entries in the directory are visited。 * *

If this method returns {@link FileVisitResult#CONTINUE CONTINUE}, * then entries in the directory are visited。 If this method returns {@link * FileVisitResult#SKIP_SUBTREE SKIP_SUBTREE} or {@link * FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS} then entries in the * directory (and any descendants) will not be visited。 * * @param dir * a reference to the directory * @param attrs * the directory‘s basic attributes * * @return the visit result * * @throws IOException * if an I/O error occurs */ FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs) throws IOException; /** * Invoked for a file in a directory。 * * @param file * a reference to the file * @param attrs * the file’s basic attributes * * @return the visit result * * @throws IOException * if an I/O error occurs */ FileVisitResult visitFile(T file, BasicFileAttributes attrs) throws IOException; /** * Invoked for a file that could not be visited。 This method is invoked * if the file‘s attributes could not be read, the file is a directory * that could not be opened, and other reasons。 * * @param file * a reference to the file * @param exc * the I/O exception that prevented the file from being visited * * @return the visit result * * @throws IOException * if an I/O error occurs */ FileVisitResult visitFileFailed(T file, IOException exc) throws IOException; /** * Invoked for a directory after entries in the directory, and all of their * descendants, have been visited。 This method is also invoked when iteration * of the directory completes prematurely (by a {@link #visitFile visitFile} * method returning {@link FileVisitResult#SKIP_SIBLINGS SKIP_SIBLINGS}, * or an I/O error when iterating over the directory)。 * * @param dir * a reference to the directory * @param exc * {@code null} if the iteration of the directory completes without * an error; otherwise the I/O exception that caused the iteration * of the directory to complete prematurely * * @return the visit result * * @throws IOException * if an I/O error occurs */ FileVisitResult postVisitDirectory(T dir, IOException exc) throws IOException;}

這個介面上面定義的方法表示了遍歷檔案的關鍵過程,允許在檔案被訪問、目錄被訪問、目錄已被訪問、放生錯誤過程中進行控制整個流程。呼叫介面中的方法,會返回訪問結果FileVisitResult物件值,用於決定當前操作完成後接下來該如何處理。FileVisitResult的標準返回值存放到列舉型別中:

public enum FileVisitResult { /** * Continue。 When returned from a {@link FileVisitor#preVisitDirectory * preVisitDirectory} method then the entries in the directory should also * be visited。 */ //當前的遍歷過程將會繼續 CONTINUE, /** * Terminate。 */ //表示當前的遍歷過程將會停止 TERMINATE, /** * Continue without visiting the entries in this directory。 This result * is only meaningful when returned from the {@link * FileVisitor#preVisitDirectory preVisitDirectory} method; otherwise * this result type is the same as returning {@link #CONTINUE}。 */ //當前的遍歷過程將會繼續,但是要忽略當前目錄下的所有節點 SKIP_SUBTREE, /** * Continue without visiting the siblings of this file or directory。 * If returned from the {@link FileVisitor#preVisitDirectory * preVisitDirectory} method then the entries in the directory are also * skipped and the {@link FileVisitor#postVisitDirectory postVisitDirectory} * method is not invoked。 */ //當前的遍歷過程將會繼續,但是要忽略當前檔案/目錄的兄弟節點 SKIP_SIBLINGS;}

2。2 Spring中的BeanDefinitionVisitor類

在Spring的Ioc中有個BeanDefinitionVisitor類,它有一個visitBeanDefinition()方法,看下原始碼:

public void visitBeanDefinition(BeanDefinition beanDefinition) { visitParentName(beanDefinition); visitBeanClassName(beanDefinition); visitFactoryBeanName(beanDefinition); visitFactoryMethodName(beanDefinition); visitScope(beanDefinition); if (beanDefinition。hasPropertyValues()) { visitPropertyValues(beanDefinition。getPropertyValues()); } if (beanDefinition。hasConstructorArgumentValues()) { ConstructorArgumentValues cas = beanDefinition。getConstructorArgumentValues(); visitIndexedArgumentValues(cas。getIndexedArgumentValues()); visitGenericArgumentValues(cas。getGenericArgumentValues()); }}

在其方法中分別訪問了其它的資料,比如父類的名字、自己的類名、在Ioc容器中的名稱等各種資訊。

三、訪問者模式的優缺點

優點

解耦了資料結構與資料操作,使得操作集合可以獨立變化;

擴充套件性好:可以透過擴充套件訪問者角色,實現對資料集的不同操作;

元素具體型別並非單一,訪問者均可操作;

各角色職責分離,符合單一職責原則。

缺點

無法增加元素型別:若系統資料結構物件另於變化,經常有新的資料物件增加進來,則訪問者類必須增加對應元素型別的操作,違背了開閉原則。