1. 程式人生 > >訪問者模式 Visitor 行為型 設計模式(二十七)

訪問者模式 Visitor 行為型 設計模式(二十七)

訪問者模式 Visitor image_5c2481f3_6073 《俠客行》是當代作家金庸創作的長篇武俠小說,新版電視劇《俠客行》中,開篇有一段獨白:  茫茫海外,傳說有座俠客島,島上賞善罰惡二使,每隔十年必到中原武林,向各大門派下發放賞善罰惡令, 強邀掌門人赴島喝臘八粥,拒接令者,皆造屠戮,無一倖免,接令而去者,杳無音訊,生死未僕,俠客島之行,已被視為死亡之旅。” 不過話說電視劇,我總是覺得老版的好看。

意圖

表示一個作用於某物件結構中的各元素的操作。 它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。

意圖解析

我們以程式碼描述一下《俠客行》中的這個場景 假定
賞善罰惡二使,一個叫做張三,一個叫做李四,面對一眾掌門 張三負責賞善,對好人賞賜,壞人他不處理; 相反,李四負責罰惡,好人不處理,對壞人懲罰

俠客行程式碼示例

定義了 “掌門人”介面
package visitor.俠客行;
public interface 掌門人 {

}
“掌門人”有兩種型別 沒做過壞事的掌門做過壞事的掌門
package visitor.俠客行;

public class 沒做過壞事的掌門 implements 掌門人 {

}
package visitor.俠客行;

public class 做過壞事的掌門 implements 掌門人 {

}
定義了 俠客島,俠客島管理維護“江湖的掌門人”,使用List 提供了掌門人的新增方法  “add掌門人(掌門人 某掌門)” 定義了“賞善罰惡(String 處理人)”方法,用於賞善罰惡,接受引數為處理人   如果是賞善大使張三,他會賞賜好人,不管壞人 如果是罰惡大使李四,他會懲罰壞人,不管好人
package visitor.俠客行;
 
import
java.util.ArrayList; import java.util.List; public class 俠客島 { private List<掌門人> 掌門人List = new ArrayList<>(); public void add掌門人(掌門人 某掌門) { 掌門人List.add(某掌門); } public void 賞善罰惡(String 處理人) { if (處理人.equals("張三")) { for (掌門人 某掌門X : 掌門人List) { if (某掌門X instanceof 沒做過壞事的掌門) { System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門"); } else if (某掌門X instanceof 做過壞事的掌門) { System.out.println("壞掌門, 張三: 不管做過壞事的掌門"); } System.out.println(); } } else if (處理人.equals("李四")) { for (掌門人 某掌門X : 掌門人List) { if (某掌門X instanceof 沒做過壞事的掌門) { System.out.println("好掌門, 李四: 不管沒做過壞事的掌門"); } else if (某掌門X instanceof 做過壞事的掌門) { System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門"); } System.out.println(); } } } }

 

測試程式碼 image_5c2481f3_37bc   上面的測試程式碼中,我們創造了俠客島的“賞善罰惡二使” 並且將幾個“掌門人”交於他們處理 列印結果分別展示了對於這幾個“掌門人”,張三和李四的不同來訪,產生的不同結果   如果我們想增加來訪者怎麼辦?比如這次是龍木島主親自出島處理,好人賞賜,壞人直接處理怎麼辦? 我們可以直接新增賞善罰惡方法的處理邏輯,如下圖所示,新增加了一個else if 可以通過測試程式碼看到結果 image_5c2481f3_7af6   如果有些掌門人既沒有做什麼好事,也沒有做什麼壞事怎麼處理?也就是新增一種掌門人? 你會發現,所有的判斷的地方,也還是都需要新增加一個else if ...... ̄□ ̄|| 因為 上面的示例,使用的是兩層判斷邏輯,每一層都跟具體的型別有關係!!! 不管是增加新的來訪者,還是增加新的種類的成員,都不符合開閉原則,而且判斷邏輯複雜混亂   上面的過程在程式世界中, 也會經常出現。 實際開發中,經常用到集合框架 集合框架中也經常會儲存不同的型別(此處指的是不同的最終型別,如果擡槓,還不都是Object    ̄□ ̄||) 比如多個不同的子類,像上面示例中的好掌門和壞掌門,都是掌門人型別,但是具體子型別不同。 對於集合中的元素,可能會有不同的處理操作 比如上面示例中的,張三和李四的到來,處理肯定不一樣,沒幹過壞事的和幹過壞事的處理也不一樣 比如去體檢,不同的專案的醫生會有不同的行為操作,你和跟你一起排隊體檢的人也不一樣,但是你還是你,他還是他     在上面的《俠客行》的示例中,我們使用了 雙重判斷來確定下面兩層問題: 一層是來訪者是誰? 另外一層是當前的掌門人是什麼型別? 如果有X種來訪者,Y種類型掌門人,怕是要搞出來X*Y種組合了,所以才會邏輯複雜,擴充套件性差 所以,那麼 根本問題就是靈活的確定這兩個維度,來訪者和當前型別 ,進而確定具體的行為,對吧?   再回頭審視一下《俠客行》的示例,對於訪問者,有張三、李四、龍木島主,還可能會有其他人, 顯然,我們應該 嘗試將訪問者進行抽象,張三,李四,龍木島主,他們都是具體的訪問者。 而且,而且,而且, 他們都會訪問不同型別的掌門人,既然是訪問  不同型別掌門人 也就是方法名一樣,型別不一樣? 這不就是 方法過載

新版程式碼示例

掌門人相關角色不變
package visitor.新版俠客行;
public interface 掌門人 {
}

package visitor.新版俠客行;
public class 沒做過壞事的掌門 implements 掌門人 {
}


package visitor.新版俠客行;
public class 做過壞事的掌門 implements 掌門人 {
}
新增加訪問者角色,訪問者既可能訪問好人,也可能訪問壞人,使用方法的過載在解決  方法都是拜訪,有兩種型別的過載版本
package visitor.新版俠客行;
public interface 訪問使者 {
  void 拜訪(做過壞事的掌門 壞人);
  void 拜訪(沒做過壞事的掌門 好人);
}
張三負責賞善,當他訪問到好人時,賞賜,壞人不處理
package visitor.新版俠客行;
 
public class 張三 implements 訪問使者 {
    @Override
    public void 拜訪(沒做過壞事的掌門 好人) {
        System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門");
    }
     
    @Override
    public void 拜訪(做過壞事的掌門 壞人) {
        System.out.println("壞掌門, 張三: 不管做過壞事的掌門");
    }
}
李四負責罰惡,訪問到好人時不處理,遇到壞人時,就懲罰!
package visitor.新版俠客行;
 
public class 李四 implements 訪問使者 {
 
    @Override
    public void 拜訪(沒做過壞事的掌門 好人) {
        System.out.println("好掌門, 李四: 不管沒做過壞事的掌門");
    }

    @Override
    public void 拜訪(做過壞事的掌門 壞人) {
        System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門");
    }
}
引入了訪問使者角色,我們就不需要對使者進行判斷了 藉助了使者的多型性,不管是何種使者都有訪問不同型別掌門人的方法 所以可以去掉了一層邏輯判斷,程式碼簡化如下
package visitor.新版俠客行;
 
import java.util.ArrayList;
import java.util.List;
 
public class 俠客島 {
  private List<掌門人> 掌門人List = new ArrayList<>();
   
  public void add掌門人(掌門人 某掌門) {
    掌門人List.add(某掌門);
  }
   
  public void 賞善罰惡(訪問使者 使者) {
      for (掌門人 某掌門X : 掌門人List) {
         if (某掌門X instanceof 沒做過壞事的掌門) {
             使者.拜訪((沒做過壞事的掌門)某掌門X);
         } else if (某掌門X instanceof 做過壞事的掌門) {
             使者.拜訪((做過壞事的掌門)某掌門X);
         }
         System.out.println();
      }
  }
}
測試程式碼也稍作調整 定義了兩個訪問者,傳遞給“賞善罰惡”方法
package visitor.新版俠客行;
 
public class Test {
 
public static void main(String[] args){
 
    俠客島 善善罰惡二使 = new 俠客島();
     
    善善罰惡二使.add掌門人(new 做過壞事的掌門());
    善善罰惡二使.add掌門人(new 沒做過壞事的掌門());
    善善罰惡二使.add掌門人(new 沒做過壞事的掌門());
    善善罰惡二使.add掌門人(new 做過壞事的掌門());
     
    訪問使者 張三 = new 張三();
    訪問使者 李四 = new 李四();
     
    善善罰惡二使.賞善罰惡(李四);
    善善罰惡二使.賞善罰惡(張三);
    }
}

 

image_5c2481f3_6b9   可以看到,《新版俠客行》和老版本的功能的一樣的,但是程式碼簡化了 而且,最重要的是能夠很方便的擴充套件使者,比如我們仍舊增加“龍木島主”這一訪客。
package visitor.新版俠客行;
public class 龍木島主 implements 訪問使者 {
    @Override
    public void 拜訪(做過壞事的掌門 壞人) {
        System.out.println("龍木島主,懲罰壞人");
    }
    @Override
    public void 拜訪(沒做過壞事的掌門 好人) {
        System.out.println("龍木島主,賞賜好人");
    }
}
新增加了"龍木島主“訪客後,客戶端可以直接使用了,不需要修改”俠客島“的程式碼了 測試程式碼增加如下兩行,檢視下面結果 image_5c2481f3_110a   但是如果增加新的掌門人型別呢? 因為我們仍舊有具體型別的判斷,如下圖所示 image_5c2481f3_1f7e 所以,想要增加新的掌門人,又完蛋了   ̄□ ̄||   而且,現在的判斷邏輯也還是交織著,複雜的。 對於訪問者的判斷,我們藉助於多型以及方法的過載,去掉了一層訪問者的判斷 通過多型可以將請求路由到真實的來訪者,通過方法過載,可以呼叫到正確的方法   如果能把這一層的if else if判斷也去掉,是不是就可以靈活擴充套件掌門人了呢? 使者只知道某掌門X,但是他最終的具體型別,是不知道的 所以,沒辦法直接呼叫拜訪方法的,因為我們的確沒有這種引數型別的方法  image_5c2481f3_959 ps:有人覺得“拜訪”方法的型別使用 掌門人  不就好了麼 但是對於不同的具體型別有不同的行為,那你在“拜訪”方法中還是少不了要進行判斷,只是此處判斷還是“拜訪”方法內判斷的問題)   前面的那段if else if判斷邏輯,訪問的方法都是  使者.拜訪,只不過具體型別不同 image_5c2481f4_73d5 但是如何確定型別?問題也就轉換為”到底怎麼判斷某掌門X的型別“或者”到底誰知道某掌門X的型別“ 那誰知道他的型別呢? 如果不借助外力,比如 instanceof 判斷的話,還有誰知道? 某掌門X 他自己知道!!!他自己知道!!! 所以,如果是在  某掌門X自己內部的方法,就可以獲取到this了,這就是當前物件的真實型別 把這個型別在回傳給來訪使者不就可以了麼 所以 給掌門人定義一個“ 接受拜訪”方法,不管何種型別的掌門人,都能夠接受各種訪客的拜訪 接受拜訪(訪問使者 賞善罰惡使者){ 賞善罰惡使者.拜訪(this);  

最新版俠客行程式碼示例

說起來有點迷惑,我看看程式碼 《最新版俠客行》 掌門人都增加了”接受拜訪“的方法
package visitor.最新版本俠客行;
public interface 掌門人 {
void 接受拜訪(訪問使者 賞善使者);
}

 

package visitor.最新版本俠客行;
public class 沒做過壞事的掌門 implements 掌門人 {
  @Override
  public void 接受拜訪(訪問使者 賞善罰惡使者) {
    賞善罰惡使者.拜訪(this);
  }
}

 

package visitor.最新版本俠客行;
public class 做過壞事的掌門 implements 掌門人 {
  @Override
  public void 接受拜訪(訪問使者 賞善罰惡使者) {
    賞善罰惡使者.拜訪(this);
  }

}
訪問使者相關角色與《新版俠客行》中一樣
package visitor.最新版本俠客行;
public interface 訪問使者 {
    void 拜訪(做過壞事的掌門 壞人);
    void 拜訪(沒做過壞事的掌門 好人);
}
 
 
package visitor.最新版本俠客行;
public class 張三 implements 訪問使者 {
    @Override
    public void 拜訪(沒做過壞事的掌門 好人) {
        System.out.println("好掌門, 張三: 賞賜沒做過壞事的掌門");
    }
    @Override
    public void 拜訪(做過壞事的掌門 壞人) {
        System.out.println("壞掌門, 張三: 不管做過壞事的掌門");
    }
}
 
package visitor.最新版本俠客行; public class 李四 implements 訪問使者 { @Override public void 拜訪(沒做過壞事的掌門 好人) { System.out.println("好掌門, 李四: 不管沒做過壞事的掌門"); } @Override public void 拜訪(做過壞事的掌門 壞人) { System.out.println("壞掌門, 李四: 懲罰做過壞事的掌門"); } }
此時的俠客島輕鬆了,不再需要來回的判斷型別了
package visitor.最新版本俠客行;
 
import java.util.ArrayList;
import java.util.List;
public class 俠客島 {
    private List<掌門人> 掌門人List = new ArrayList<>();
    public void add掌門人(掌門人 某掌門) {
        掌門人List.add(某掌門);
    }
    public void 賞善罰惡(訪問使者 使者) {
        for (掌門人 某掌門X : 掌門人List) {
            某掌門X.接受拜訪(使者);
            System.out.println();
        }
    }
}
image_5c2481f4_4529 從結果看跟上一個版本一樣 但是很顯然,我們的俠客島輕鬆了   接下來我們看一下新增加訪客和新增加掌門人的場景 擴充套件龍木島主
package visitor.最新版本俠客行;

public class 龍木島主 implements 訪問使者 {
@Override
public void 拜訪(做過壞事的掌門 壞人) {
System.out.println("龍木島主,懲罰壞人");
}

@Override
public void 拜訪(沒做過壞事的掌門 好人) {
System.out.println("龍木島主,賞賜好人");
}
}
測試程式碼如下,顯然因為拜訪使者的抽象,才得以能夠更好的擴充套件訪問者,所以此處肯定跟《新版俠客行》一樣便於擴充套件 image_5c2481f4_4404   看看如果擴充套件一個新的掌門人
package visitor.最新版本俠客行;
public class 不好不壞的掌門 implements 掌門人 {
@Override
public void 接受拜訪(訪問使者 賞善罰惡使者) {
賞善罰惡使者.拜訪(this);
}
}
但是,”訪問使者“裡面沒有能夠拜訪”不好不壞的掌門“方法啊?怎麼辦? 只能新增唄,如下圖所示,完蛋了........ image_5c2481f4_3f58

程式碼演化小結

看得出來,《最新版俠客行》 解決了複雜判斷的問題,也解決了訪問者擴充套件的問題 但是 對於被訪問者的型別的擴充套件,顯然是沒有擴充套件性的,不符合開閉原則 這一點體現出來了這種解決方法的 傾向性,傾向於 擴充套件行為,可以自如的增加新的行為 但是 不能輕鬆的增加元素型別   測試程式碼Test類不需要修改 看一下列印結果 image_5c2481f4_3462

最新版俠客行結構

image_5c2481f4_7425

回首意圖

再回頭看下訪問者模式的意圖 表示一個作用於某物件結構中的各元素的操作。它使你可以在不改變各元素類的前提下定義作用於這些元素的新操作。 就是上面示例中,對於來訪者的擴充套件嘛   最初的動機就是處理《俠客行》中類似的問題 集合容器中儲存了不同型別的物件,他們又可能有多種不同場景的操作 比如一份名單,班長可能拿過去收作業,班主任拿過去可能點名 名單裡面都有你也有他,你就是那個你,他還是那個他,但是你的作業是你的作業,他的作業是他的作業。 所以對於班長和班主任兩個訪問者,同學們的行為是不一樣的,對同一來訪者,不同的同學的行為又是不一樣的

結構

image_5c2481f4_1c63   抽象元素角色Element 抽象元素一般是抽象類或者介面 通常它定義一個 accept(抽象訪問者) 方法,用來將自身傳遞給訪問者 具體的元素角色ConcreateElement 具體元素實現了accept方法,在accept方法中呼叫訪問者的訪問方法以便完成對一個元素的操作 抽象訪問者Visitor 定義一個或者多個訪問操作 抽象訪問者需要面向具體的被訪問者元素型別,所以有幾個具體的元素型別需要被訪問,就有幾個過載方法 具體的訪問者ConcreateVisitor 具體的訪問者封裝了不同訪問者,不同型別物件的具體行為,也就是最終的分情況的處理邏輯 物件結構ObjectStructure 物件結構是元素的集合,用於存放元素的物件,並且一般提供遍歷內部元素的方法 客戶端角色Client 組織被訪問者,然後通過訪問者訪問   訪問者模式有兩個主要層次,訪問者以及被訪問元素 訪問者有不同的型別,被訪問元素有不同的型別 每一種訪問者對於每一種被訪問元素都有一種不同的行為,這不同的行為是封裝在訪問者的方法中 所以訪問者需要進行訪問方法visit的過載, 被訪問元素有幾種型別,就有幾種過載版本 面向細節的邏輯既然被封裝在訪問者中,被訪問元素就不需要面向細節了,只需要把自己的型別傳遞給訪問者即可 所以, 所有的被訪問元素都只有一個版本的accept方法

概念示例程式碼

我們可以抽象化的看下下面的例子 下面的程式碼很簡單,A有三種子型別,B有三種子型別 不同的A和不同的B,將會擦出不一樣的火花,也就是會出現9種可能的場景 將A定義為訪問者,那麼A就要藉助方法的過載實現不同型別被訪問者B的不同行為 而將方法的呼叫轉變為被訪問者的反向呼叫----this傳遞給訪問者

package visitor;

 

public class example {

public static void main(String[] args) {

 

A1 a1 = new A1();

A2 a2 = new A2();

A3 a3 = new A3();

 

B1 b1 = new B1();

B2 b2 = new B2();

B3 b3 = new B3();

 

b1.accept(a1);

b1.accept(a2);

b1.accept(a3);

b2.accept(a1);

b2.accept(a2);

b2.accept(a3);

b3.accept(a1);

b3.accept(a2);

b3.accept(a3);

}

}

 

 

abstract class A {

 

abstract void visit(B1 b1);

abstract void visit(B2 b2);

abstract void visit(B3 b3);

}

 

class A1 extends A {

@Override

void visit(B1 b1) {

System.out.println("A1 play with B1");

}

 

@Override

void visit(B2 b2) {

System.out.println("A1 play with B2");

}

 

@Override

void visit(B3 b3) {

System.out.println("A1 play with B3");

}

}

 

class A2 extends A {

@Override

void visit(B1 b1) {

System.out.println("A2 play with B1");

}

 

@Override

void visit(B2 b2) {

System.out.println("A2 play with B2");

}

 

@Override

void visit(B3 b3) {

System.out.println("A2 play with B3");

}

}

 

class A3 extends A {

@Override

void visit(B1 b1) {

System.out.println("A3 play with B1");

}

 

@Override

void visit(B2 b2) {

System.out.println("A3 play with B2");

}

 

@Override

void visit(B3 b3) {

System.out.println("A3 play with B3");

}

}

 

 

abstract class B {

abstract void accept(A a);

}

 

class B1 extends B {

@Override

void accept(A a) {

a.visit(this);

}

}

 

class B2 extends B {

@Override

void accept(A a) {

a.visit(this);

}

}

 

class B3 extends B {

@Override

void accept(A a) {

a.visit(this);

}

}

 

image_5c2481f4_6086 這種過載和回傳自身的形式,完全可以當作一個套路來使用,對於這種組合形式的場景,非常受用。 訪問者的自身藉助多型特性,又依賴方法過載,然後再借助於this回傳達到反向確定型別呼叫,真心精巧。

總結

訪問者模式靈活的處理了不同型別的元素,面對不同的訪問者,有不同的行為的場景。 這種組合場景,判斷邏輯複雜繁瑣,訪問者模式可以做到靈活的擴充套件增加更多的行為,而不需要改變原來的類。 訪問者模式傾向於擴充套件元素的行為,當擴充套件元素行為時,滿足開閉原則 但是對於擴充套件新的元素型別時,將會產生巨大的改動,每一個訪問者都需要變動,所以在使用訪問者模式是要考慮清楚元素型別的變化可能。 因為訪問者依賴的是具體的元素,而不是抽象元素,所以才難以擴充套件   訪問者依賴的是具體元素,而不是抽象元素,這破壞了依賴倒置原則,特別是在面向對 象的程式設計中,拋棄了對介面的依賴,而直接依賴實現類,擴充套件比較難。   當業務規則需要遍歷多個不同的物件時,而且不同的物件在不同的場景下又有不同的行為 你就應該考慮使用訪問者模式 如果物件結構中的物件不常變化,但是他們的行為卻經常變化時,也可以考慮使用,訪問者模式可以很靈活的擴充套件新的訪客。 原文地址: 訪問者模式 Visitor 行為型 設計模式(二十七)