十四、觀察者模式與訪問者模式詳解

21.觀察者模式

21.1.課程目標

1、 掌握觀察者模式和訪問者模式的應用場景。

2、 掌握觀察者模式在具體業務場景中的應用。

3、 瞭解訪問者模式的雙分派。

4、 觀察者模式和訪問者模式的優、缺點。

21.2.內容定位

1、 有 Swing開發經驗的人群更容易理解觀察者模式。

2、 訪問者模式被稱為最複雜的設計模式。

21.3.觀察者模式

觀 察 者 模 式 ( Observer Pattern ) , 又叫發佈-訂閱( Publish/Subscribe ) 模式、模型-視圖 ( Model/View ) 模式、源-監聽器(Source/Listener) 模式或從屬者( Dependents ) 模式。定義一種一對多的依賴關係,一個主題對象可被多個觀察者對象同時監聽,使得每當主題對象狀態變化時,所 有依賴於它的對象都會得到通知並被自動更新。屬於行為型模式。

原文:Defines a one-to-many dependency relationship between objects so that each time an object' s state changes, its dependent objects are notified and automatically updated.

觀察者模式的核心是將觀察者與被觀察者解耦,以類似於消息/廣播發送的機制聯動兩者,使被觀察 者的變動能通知到感興趣的觀察者們,從而做出相應的響應。

21.4.應用場景

觀察者模式在現實生活應用也非常廣泛,比如:起床鬧鐘設置、APP角標通知、GPer生態圈消息通知、郵件通知、廣播通知、桌面程序的事件響應等(如下圖)。 APP角標通知

十四、觀察者模式與訪問者模式詳解

在軟件系統中,當系統一方行為依賴於另一方行為的變動時,可使用觀察者模式松耦合聯動雙方, 使得一方的變動可以通知到感興趣的另一方對象,從而讓另一方對象對此做出響應。觀察者模式適用於 以下幾種應用場景:

1、當一個抽象模型包含兩個方面內容,其中一個方面依賴於另一個方面;

2、其他一個或多個對象的變化依賴於另一個對象的變化;

3、實現類似廣播機制的功能,無需知道具體收聽者,只需分發廣播,系統中感興趣的對象會自動接收該廣播;

4、多層級嵌套使用,形成一種鏈式觸發機制,使得事件具備跨域(跨越兩種觀察者類型)通知。

下面來看下觀察者模式的通用UML類 圖 :

十四、觀察者模式與訪問者模式詳解

從 UML類圖中,我們可以看到,觀察者模式主要包含三種角色:

抽象主題(Subject) :指被觀察的對象(Observable ) 。該角色是一個抽象類或接口,定義了增 加、刪除、通知觀察者對象的方法;

具體主題(ConcreteSubject) :具體被觀察者,當其內狀態變化時,會通知已註冊的觀察者;

抽象觀察者(Observer) :定義了響應通知的更新方法;

具體觀察者(ConcreteObserver) :在得到狀態更新時,會自動做出響應。

21.5.觀察者模式在業務場景中的應用

當小夥伴們在GPer生態圈中提問的時候,如果有設置指定老師回答,對應的老師就會收到郵件通 知 ,這就是觀察者模式的一種應用場景。我們有些小夥伴可能會想到MQ ,異步隊列等,其實JDK本身 就提供這樣的APIO 我們用代碼來還原一下這樣一個應用場景,創建GPer類:

<code> /**
  * JDK提供的一種觀察者的實現方式,被觀察者
  */
 public class GPer extends Observable {
     private String name = "GPer生態圈";
     private static final GPer gper = new GPer();
 ​

     private GPer() {}
 ​
     public static GPer getInstance(){
         return gper;
    }
 ​
     public String getName() {
         return name;
    }
 ​
     public void publishQuestion(Question question){
         System.out.println(question.getUserName() + "在" + this.name + "上提交了一個問題。");
         setChanged();
         notifyObservers(question);
    }
 }/<code>

創建問題Question類:

<code> public class Question {
     private String userName;
     private String content;
 ​
     public String getUserName() {
         return userName;
    }
 ​
     public void setUserName(String userName) {
         this.userName = userName;
    }
 ​
     public String getContent() {
         return content;
    }
 ​
     public void setContent(String content) {
         this.content = content;
    }
 }/<code>

創建老師Teacher類:

<code> public class Teacher implements Observer {
 ​
     private String name;
 ​
     public Teacher(String name) {
         this.name = name;

    }
 ​
     public void update(Observable o, Object arg) {
         GPer gper = (GPer)o;
         Question question = (Question)arg;
         System.out.println("======================");
         System.out.println(name + "老師,你好!\\n" +
                         "您收到了一個來自" + gper.getName() + "的提問,希望您解答。問題內容如下:\\n" +
                         question.getContent() + "\\n" +
                         "提問者:" + question.getUserName());
    }
 }/<code>

客戶端測試代碼:

<code> public class Test {
     public static void main(String[] args) {
         GPer gper = GPer.getInstance();
         Teacher tom = new Teacher("Tom");
         Teacher jerry = new Teacher("Jerry");
         gper.addObserver(tom);
         gper.addObserver(jerry);
         //用戶行為
         Question question = new Question();
         question.setUserName("張三");
         question.setContent("觀察者模式適用於哪些場景?");
         gper.publishQuestion(question);
    }
 }/<code>

運行結果:

十四、觀察者模式與訪問者模式詳解

21.6.基於Guava API輕鬆落地觀察者模式

給大家推薦一個實現觀察者模式非常好用的框架。API使用也非常簡單,舉個例子,先引入maven 依賴包:

<code> <dependency>
     <groupid>com.google.guava/<groupid>
     <artifactid>guava/<artifactid>
     <version>18.0/<version>
 /<dependency>/<code>

創建偵聽事件 :

<code> public class PojoEvent {
 ​
     @Subscribe
     public void observer(Pojo pojo){
         System.out.println("執行PojoEvent方法,傳參為:" + pojo);
    }
 }/<code>
<code> public class VoEvent {
 ​
     @Subscribe
     public void observer(Vo arg){
    System.out.println("執行VoEvent方法,傳參為:" + arg);
    }
 }/<code>

客戶端測試代碼:

<code> public class Test {
     public static void main(String[] args) {
         EventBus eventBus = new EventBus();
         PojoEvent guavaEvent = new PojoEvent();
         VoEvent voEvent = new VoEvent();
         eventBus.register(guavaEvent);
         eventBus.register(voEvent);
         eventBus.post(new Pojo("Tom"));
    }
 }/<code>

運行結果:

十四、觀察者模式與訪問者模式詳解

21.7.使用觀察者模式設計鼠標事件響應API

下面再來設計一個業務場景,幫助小夥伴更好的理解觀察者模式。JDK源碼中,觀察者模式也應用 非常多。例如java.awt.Event就是觀察者模式的一種,只不過Java很少被用來寫桌面程序。我們自己 用代碼來實現一下,以幫助小夥伴們更深刻地瞭解觀察者模式的實現原理。首先,創建EventListener 接口 :

<code>/**
* 觀察者抽象
*/
public interface EventListener {
}/<code>

創建Event類:

<code>@Data
public class Event {
//事件源,動作是由誰發出的
private Object source;
//事件觸發,要通知誰(觀察者)
private EventListener target;
//觀察者給的回應
private Method callback;
//事件的名稱
private String trigger;
//事件的觸發事件
private long time;

public Event(EventListener target, Method callback) {
this.target = target;
this.callback = callback;
}
}/<code>

創建 EventContext 類 :

<code>/**
* 被觀察者的抽象
*/
public class EventContext {
protected Map<string> events = new HashMap<string>();

public void addLisenter(String eventType, EventListener target, Method callback){
events.put(eventType,new Event(target,callback));
}

public void addLisenter(String eventType, EventListener target){
try {
this.addLisenter(eventType, target, target.getClass().getMethod("on" + toUpperFirstCase(eventType), Event.class));
}catch (NoSuchMethodException e){

return;
}
}

private String toUpperFirstCase(String eventType) {
char [] chars = eventType.toCharArray();
chars[0] -= 32;
return String.valueOf(chars);
}

private void trigger(Event event){
event.setSource(this);
event.setTime(System.currentTimeMillis());

try {
if (event.getCallback() != null) {
//用反射調用回調函數
event.getCallback().invoke(event.getTarget(), event);
}
}catch (Exception e){
e.printStackTrace();
}
}

protected void trigger(String trigger){
if(!this.events.containsKey(trigger)){return;}
trigger(this.events.get(trigger).setTrigger(trigger));
}
}/<string>/<string>/<code>

創建 MouseEventType 接口 :

<code>public interface MouseEventType {
String ON_CLICK = "click";

String ON_MOVE = "move";
}/<code>

創建Mouse類 :

<code>/**
* 具體的被觀察者
*/
public class Mouse extends EventContext {
public void click(){
System.out.println("調用單擊方法");
this.trigger(MouseEventType.ON_CLICK);

}

public void move(){
System.out.println("調用移動方法");
this.trigger(MouseEventType.ON_MOVE);
}
}/<code>

創建回調方法MouseEventLisenter類 :

<code>/**
* 觀察者
*/
public class MouseEventLisenter implements EventListener {

public void onClick(Event e){
System.out.println("==========觸發鼠標單擊事件========\\n" + e);
}

public void onMove(Event e){

System.out.println("==========觸發鼠標移動事件========\\n" + e);
}
}/<code>

客戶端測試代碼:

<code>public class Test {
public static void main(String[] args) {
MouseEventLisenter lisenter = new MouseEventLisenter();
Mouse mouse = new Mouse();
mouse.addLisenter(MouseEventType.ON_CLICK,lisenter);
mouse.addLisenter(MouseEventType.ON_MOVE,lisenter);
mouse.click();
mouse.move();
}
}/<code>

運行結果:

十四、觀察者模式與訪問者模式詳解

21.8.觀察者模式在源碼中的應用

Spring 中 的 ContextLoaderListener 實 現 了 ServletContextListener 接 口 ServletContextListener 接口又繼承了 EventListener,在 JDK 中 EventListener 有非常廣泛的應用。

我們可以看一下源代碼,ContextLoaderListener:

<code>public class ContextLoaderListener extends ContextLoader implements ServletContextListener {

public ContextLoaderListener() {
}

public ContextLoaderListener(WebApplicationContext context) {
super(context);
}

@Override
public void contextInitialized(ServletContextEvent event) {
initWebApplicationContext(event.getServletContext());
}

@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}/<code>

ServletContextListener 接口源碼如下:

<code>public interface ServletContextListener extends EventListener {
void contextInitialized(ServletContextEvent var1);

void contextDestroyed(ServletContextEvent var1);
}/<code>

EventListener接口源碼如下:

<code> public interface EventListener {
 }/<code>

21.7.觀察者模式的優缺點

優點:

1、觀察者和被觀察者是松耦合(抽象耦合)的 ,符合依賴倒置原則;

2、分離了表示層(觀察者)和數據邏輯層(被觀察者 兒 並且建立了一套觸發機制,使得數據的變 化可以響應到多個表示層上;

3、實現了一對多的通訊機制,支持事件註冊機制,支持興趣分發機制,當被觀察者觸發事件時,只 有感興趣的觀察者可以接收到通知。

缺點:

1、 如果觀察者數量過多,則事件通知會耗時較長;

2、 事件通知呈線性關係,如果其中一個觀察者處理事件卡殼,會影響後續的觀察者接收該事件;

3、 如果觀察者和被觀察者之間存在循環依賴,則可能造成兩者之間的循環調用,導致系統崩潰。

21.8.思維導圖

十四、觀察者模式與訪問者模式詳解

22.訪問者模式

22.1.定義

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

原文:Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.

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

訪問者模式的核心是,解耦數據結構與數據操作,使得對元素的操作具備優秀的擴展性。可 以通過擴展不同的數據操作類型(訪問者)實現對相同元素集的不同的操作。

22.2.應用場景

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

十四、觀察者模式與訪問者模式詳解

當系統中存在類型數目穩定( 定 )的一類數據結構時,可以通過訪問者模式方便地實現對 該類型所有數據結構的不同操作,而又不會數據產生任何副作用(髒數據)。

簡言之,就是對集合中的不同類型數據(類型數量穩定)進行多種操作,則使用訪問者模式。 下面總結一下訪問者模式的適用場景:

1、 數據結構穩定,作用於數據結構的操作經常變化的場景;

2、 需要數據結構與數據操作分離的場景;

3、 需要對不同數據類型(元素)進行操作,而不使用分支判斷具體類型的場景。

首先來看下訪問者模式的通用UML類圖:

十四、觀察者模式與訪問者模式詳解

從 UML類圖中,我們可以看到,訪問者模式主要包含五種角色:

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

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

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

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

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

22.3.利用訪問者模式實現KPI考核的場景

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

由於CEO和 CTO對於不同員工的關注點是不一樣的,這就需要對不同員工類型進行不同的 處理。訪問者模式此時可以派上用場了。

<code>// 員工基類
public abstract class Employee {
public String name;
public int kpi; //員工KPI

public Employee(String name) {
this.name = name;
kpi = new Random().nextInt(10);
}

//接收訪問者的訪問
public abstract void accept(IVisitor visitor);
}/<code>

Employee類定義了員工基本信息及一個acceptQ方法,acceptQ方法表示接受訪問者的訪 問 ,由具體的子類來實現。訪問者是個接口,傳入不同的實現類,可訪問不同的數據。下面看 看工程師Engineer類的代碼:

<code>// 工程師類
public class Engineer extends Employee {
public Engineer(String name) {
super(name);
}

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

//考核指標是每年的代碼量
public int getCodeLines(){
return new Random().nextInt(10* 10000);
}
}/<code>

經理Manager類的代碼:

<code>public class Manager extends Employee {
public Manager(String name) {
super(name);
}

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

//考核的是每年新產品研發數量
public int getProducts(){
return new Random().nextInt(10);
}
}/<code>

工程師是考核的是代碼數量,經理考核的是產品數量,二者的職責不一樣。也正是因為有這 樣的差異性,才使得訪問模式能夠在這個場景下發揮作用。Employee. Engineer. Manager 這 3個類型就相當於數據結構,這些類型相對穩定,不會發生變化。

然後將這些員工添加到一個業務報表類中,公司高層可以通過該報表類的showReport()方 法查看所有員工的業績,具體代碼如下:

<code>public class BusinessReport {
private List<employee> employees = new LinkedList<employee>();

public BusinessReport() {
employees.add(new Manager("產品經理A"));
employees.add(new Engineer("程序員A"));
employees.add(new Engineer("程序員B"));
employees.add(new Engineer("程序員C"));
employees.add(new Manager("產品經理B"));
employees.add(new Engineer("程序員D"));
}


public void showReport(IVisitor visitor){
for (Employee employee : employees) {
employee.accept(visitor);
}
}
}/<employee>/<employee>/<code>

下面看看訪問者類型的定義,訪問者聲明瞭兩個visit()方法,分別對工程師和經理訪問,具 體代碼如下:

<code>public interface IVisitor {
void visit(Engineer engineer);

void visit(Manager manager);
}/<code>

首先定義一個IVisitor接 口 ,該接口有兩個visit()方 法 ,參數分別是Engineer. Manager , 也就是說對於Engineer和 Manager的訪問會調用兩個不同的方法,以此達到差異化處理的目 的。這兩個訪問者具體的實現類為CEOVisitor類和CTOVisitor類 ,代碼如下:

<code>public class CEOVistitor implements IVisitor {
public void visit(Engineer engineer) {
System.out.println("工程師" + engineer.name + ",KIP:" + engineer.kpi);
}

public void visit(Manager manager) {
System.out.println("經理:" + manager.name + ",KPI:" + manager.kpi + ",產品數量:" + manager.getProducts());
}
}/<code>

這就導致了 if-else邏輯的嵌套以及類型的強制轉換,難以擴展和維護,當類型較多時,這 個 ReportUtil就會很複雜。而使用訪問者模式,通過同一個函數對不同對元素類型進行相應對 處理,使結構更加清晰、靈活性更高。

再添加一個CTO的訪問者類:

<code>public class CTOVistitor implements IVisitor {
public void visit(Engineer engineer) {
System.out.println("工程師" + engineer.name + ",代碼行數:" + engineer.getCodeLines());
}

public void visit(Manager manager) {
System.out.println("經理:" + manager.name + ",產品數量:" + manager.getProducts());
}
}/<code>

重載的visitO方法會對元素進行不同的操作,而通過注入不同的訪問者又可以替換掉訪問者 的具體實現,使得對元素的操作變得更靈活,可擴展性更高,同時也消除了類型轉換.if-else等 “醜陋”的代碼。

下面是客戶端代碼:

<code>public class Test {
public static void main(String[] args) {
BusinessReport report = new BusinessReport();
System.out.println("==========CEO看報表===============");
report.showReport(new CEOVistitor());
System.out.println("==========CTO看報表===============");
report.showReport(new CTOVistitor());
}
}/<code>

運行結果如下:

十四、觀察者模式與訪問者模式詳解

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

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

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

22.4.從靜態分派到動態分派

變量被聲明時的類型叫做變量的靜態類型(Static Type), 有些人又把靜態類型叫做明顯類型 (Apparent Type); 而變量所引用的對象的真實類型又叫做變量的實際類型(Actual Type)o 比 如 :

<code>List list = null; 
list = new ArrayList();/<code>

聲明瞭一個變 量list ,它的靜態類型(也叫明顯類型)是 List ,而它的實際類型是ArrayList。 根據對象的類型而對方法進行的選擇,就是分派(Dispatch)。分派又分為兩種,即靜態分派和動 態分派。

1、靜態分派

靜態分派(Static Dispatch)就是按照變量的靜態類型進行分派,從而確定方法的執行版本, 靜態分派在編譯時期就可以確定方法的版本。而靜態分派最典型的應用就是方法重載'來看下面 這段代碼。

<code>public class Main {
public void test(String string){
System.out.println("string" + string);
}
public void test(Integer integer){
System.out.println("integer" + integer);
}

public static void main(String[] args) {
String string = "1";
Integer integer = 1;
Main main = new Main();
main.test(integer);
main.test(string);
}
}/<code>

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

2、動態分派

對於動態分派,與靜態相反,它不是在編譯期確定的方法版本,而是在運行時才能確定。而 動態分派最典型的應用就是多態的特性。舉個例子,來看下面的這段代碼。

<code>public interface Person {
void test();
}

public class Man implements Person {
public void test() {
System.out.println("男人");
}
}

public class WoMan implements Person {
public void test() {
System.out.println("女人");
}
}

public class Main {
public static void main(String[] args) {
Person man = new Man();
Person woman = new WoMan();

man.test();
woman.test();
}
}/<code>

這段程序輸出結果為依次打印男人和女人,然而這裡的test。方法版本,就無法根據Man 和 Woman的靜態類型去判斷了,他們的靜態類型都是Person接口,根本無從判斷。

顯然,產生的輸出結果,就是因為test。方法的版本是在運行時判斷的,這就是動態分派。

動態分派判斷的方法是在運行時獲取到Man和 Woman的實際引用類型,再確定方法的版 本 ,而由於此時判斷的依據只是實際引用類型,只有一個判斷依據,所以這就是單分派的概念, 這時我們的考量標準只有一個,即變量的實際引用類型。相應的,這說明Java是動態單分派的 語言。

22.5.訪問者模式中的偽動態雙分派

通過前面分析,我們知道Java是靜態多分派、動態單分派的語言。Java底層不支持動態的雙分派。但是通過使用設計模式,也可以在Java語言裡實現偽動態雙分派。在訪問者模式中使 用的就是偽動態雙分派。所謂動態雙分派就是在運行時依據兩個實際類型去判斷一個方法的運 行行為,而訪問者模式實現的手段是進行了兩次動態單分派來達到這個效果。

還是回到前面的KPI考核業務場景當中, BusinessReport類中的showReport()方 法 :

<code>public void showReport(IVisitor visitor) { 
for (Employee employee : employees) {
employee.accept(visitor);
\t}
}/<code>

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

分析accept()方法的調用過程

  1. 當調用accept()方法時,根 據 Employee的實際類型決定是調用Engineer還 是 Manager 的 accept()方法。
  2. 這時accept()方法的版本已經確定,假如是Engineer , 它的accept()方法是調用下面這 行代碼。
<code>public void accept(IVisitor visitor) { 
visitor .visit (this);
}/<code>

此時的th is是 Engineer類 型 ,所以對應的是IVisitor接口的visit(Engineer engineer)方 法 ,此時需要再根據訪問者的實際類型確定Visit()方法的版本,如此一來,就完成了動態雙分派 的過程。

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

而原本我們的做法,通常是傳入一個接口,直接使用該接口的方法,此為動態單分派,就像策略模式一樣。在這裡,showReport()方法傳入的訪問者接口並不是直接調用自己的visit()方 法 ,而是通過Employee的實際類型先動態分派一次,然後在分派後確定的方法版本里再進行 自己的動態分派。

注意:這裡確定accept (IVisitor visitor)方法是靜態分派決定的,所以這個並不在此次動態雙分派的範疇內,而且 靜態分派是在編譯期就完成的,所以accept (I Visitor vi si tor)方法的靜態分派與訪問者模式的動態雙分派並沒有任 何關係。動態雙分派說到底還是動態分派,是在運行時發生的,它與靜態分派有著本質上的區別,不可以說一次動態 分派加一次靜態分派就是動態雙分派,而且訪問者模式的雙分派本身也是另有所指。

而 this的類型不是動態確定的,你寫在哪個類當中,它的靜態類型就是哪個類,這是在編譯 期就確定的,不確定的是它的實際類型,請小夥伴也要區分開來。

22.6.訪問者模式在源碼中的應用

首先來看JDK的 NIO模塊下的Filevisitor,它接口提供了遞歸遍歷文件樹的支持。這個接口 上的方法表示了遍歷過程中的關鍵過程,允許你在文件被訪問、目錄將被訪問、目錄已被訪問、 發生錯誤等等過程上進行控制;換句話說,這個接口在文件被訪問前、訪問中和訪問後,以及 產生錯誤的時候都有相應的鉤子程序進行處理。

調 用 FileVisitor中的方法,會返回訪問結果FileVisitResult對 象 值 ,用於決定當前操作完 成後接下來該如何處理。FileVisitResult的標準返回值存放到FileVisitResult枚舉類型中:

FileVisitResult.CONTINUE : 這個訪問結果表示當前的遍歷過程將會繼續。

FileVisitResult.SKIP SIBLINGS :這個訪問結果表示當前的遍歷過程將會繼續,但是要 忽略當前文件/目錄的兄弟節點。

FileVisitResult.SKIP SUBTREE : 這個訪問結果表示當前的遍歷過程將會繼續,但是要 忽略當前目錄下的所有節點。

FileVisitResult.TERMINATE : 這個訪問結果表示當前的遍歷過程將會停止。

<code>public interface FileVisitor {
FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException;

FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException;

FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException;

FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException;
}
/<code>

通過它去遍歷文件樹會比較方便,比如查找文件夾內符合某個條件的文件或者某一天內所創 建的文件,這個類中都提供了相對應的方法。我們來看一下它的實現其實也非常簡單:

<code>public class SimpleFileVisitor implements FileVisitor {
protected SimpleFileVisitor() {
}

@Override
public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(dir);
Objects.requireNonNull(attrs);
return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
throws IOException
{
Objects.requireNonNull(file);
Objects.requireNonNull(attrs);

return FileVisitResult.CONTINUE;
}

@Override
public FileVisitResult visitFileFailed(T file, IOException exc)
throws IOException
{
Objects.requireNonNull(file);
throw exc;
}

@Override
public FileVisitResult postVisitDirectory(T dir, IOException exc)
throws IOException
{
Objects.requireNonNull(dir);
if (exc != null)
throw exc;
return FileVisitResult.CONTINUE;
}
}
/<code>

下面再來看訪問者模式在Spring中的應用, Spring loC中有個BeanDefinitionVisitor類 , 其中有一個visitBeanDefinitionO方 法 ,我們來看他的源碼:

<code>public class BeanDefinitionVisitor {

@Nullable
private StringValueResolver valueResolver;

public BeanDefinitionVisitor(StringValueResolver valueResolver) {
Assert.notNull(valueResolver, "StringValueResolver must not be null");
this.valueResolver = valueResolver;
}

protected BeanDefinitionVisitor() {
}

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());
}
}
}/<code>

我們看到在visitBeanDefinition方法中,分別訪問了其他的數據,比如父類的名字、自己 的類名、在 loC容器中的名稱等各種信息。

22.5.解釋器模式的優缺點

優點:

1、 解耦了數據結構與數據操作,使得操作集合可以獨立變化;

2、 擴展性好:可以通過擴展訪問者角色,實現對數據集的不同操作;

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

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

缺點:

1、無法增加元素類型:若系統數據結構對象易於變化,經常有新的數據對象增加進來,則訪問 者類必須增加對應元素類型的操作,違背了開閉原則;

2、具體元素變更困難:具體元素增加屬性,刪除屬性等操作會導致對應的訪問者類需要進行相 應的修改,尤其當有大量訪問者類時,修改範圍太大;

3、違背依賴倒置原則:為了達到" 區別對待〃,訪問者依賴的是具體元素類型,而不是抽象。

22.6.思維導圖

十四、觀察者模式與訪問者模式詳解

22.7.作業

1、用Guava API實現GPer社區提問通知的業務場景。


2、說說你理解的訪問者模式的精髓是什麼?


分享到:


相關文章: