設計模式中的訪問者模式
訪問者模式是GOF的23中設計模式中比較複雜的一種模式,最近在項目的開發中使用到了訪問者模式的,依據項目的代碼,來對該模式進行總結
訪問者模式的定義:
訪問者模式表示一個作用於某對象結構中國的各元素的操作,它使你可以在不改變各元素的前提下,定義作用於這些元素的新操作
定義比較抽象,我舉一個我們的使用場景的例子,在我們的業務場景中,項目分爲兩類,院內項目和國撥項目.這個分類是穩定的,我們對外提供的接口,需要用戶以各種自定義的方式來查詢自己需要的項目,這些個查詢操作是不固定的,有可能會動態地增加,根據開放--封閉原則,我們將經常變化的部分(就是對項目的查詢操作)和不變的部分(項目的結構)進行分離,者就是訪問者模式的很典型的應用場景
使用訪問者模式,必須定義兩個類層次,一個對應於接受操作的元素(在我們的項目中,就是"項目"這一層次),另一個對應於定義對元素的操作的訪問者(訪問者模式中的visitor,在我們的項目中,也就是各種各樣的查詢和篩選操作).給訪問者類層次增加一個新的子類,就可以創建一個新的查詢操作,爲項目增加新的功能
訪問者模式的適用性
在以下的情況下使用訪問者模式
一個對象結構包含很多類對象,它們有不同的接口,而你想對這些對象實施一些依賴於其具體類的操作
需要對一個對象結構中的對象進行很多的不同且不相關的操作,而你想避免讓這些操作污染這些對象的類,Visitor可以讓你將相關的操作集中起來定義到一個類中,當該對象結構被很多應用共享的時候,用visitor模式讓每個應用僅包含需要用到的操作
定義對象結構的類很少改變,但是經常需要在此結構上定義新的操作,改變對象結構類需要重新定義所有的訪問者的接口,這可能需要很大的代價,如果對象結構經常改變,那麼,可能還是在這些類中定義這些操作比較好
訪問者模式的類圖
訪問者模式的示例代碼
首先是Element
public interface Element {
void accept(Visitor visitor);
}
ElementA:
public class ElementA implements Element {
@Override
public void accept(Visitor visitor) {
visitor.visit(this);
}
public void doOtherThing(){
System.out.println("I am ElementA");
}
}
ElementB:
public class ElementB implements Element {
@Override
public void accept(Visitor visitor) {
}
public void doSomething(){
System.out.println("我是ElementB");
}
}
接下來是Visitor的層次結構:
Visitor:
public interface Visitor {
void visit(ElementA element);
void visit(ElementB element);
}
ConcreteVisitorA:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConcreteVisitorA implements Visitor {
private static Logger logger = LoggerFactory.getLogger(ConcreteVisitorA.class);
@Override
public void visit(ElementA element) {
logger.info("使用ConcreteVisitorA來訪問ElementA");
element.doOtherThing();
}
@Override
public void visit(ElementB element) {
logger.info("使用ConcreteVisitorA來訪問ElementB");
element.doSomething();
}
}
ConcreteVisitorB:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class ConcreteVisitorB implements Visitor {
private static Logger logger = LoggerFactory.getLogger(ConcreteVisitorB.class);
@Override
public void visit(ElementA element) {
logger.info("使用ConcreteVisitorB來訪問ElementA");
element.doOtherThing();
}
@Override
public void visit(ElementB element) {
logger.info("使用ConcreteVisitorB來訪問ElementB");
element.doSomething();
}
}
ObjectStruct:
public class ObjectStruct {
private List<Element> elementList;
public void addElement(Element element) {
elementList.add(element);
}
public List<Element> getElementList() {
return elementList;
}
private void init() {
//防止其他的線程修改數據,使用CopyonWriteArrayList,修改的時候,會在副本上修改
elementList = new CopyOnWriteArrayList<>();
elementList.add(new ElementA());
elementList.add(new ElementB());
elementList.add(new ElementB());
}
public ObjectStruct() {
init();
}
}
客戶端:
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class VisitorClient {
private static Logger logger = LoggerFactory.getLogger(VisitorClient.class);
public static void main(String[] args) {
ObjectStruct objectStruct = new ObjectStruct();
Visitor visitor = new ConcreteVisitorA();
objectStruct.getElementList().forEach(item -> item.accept(visitor));
Visitor visitor1 = new ConcreteVisitorA();
objectStruct.getElementList().forEach(item -> item.accept(visitor1));
Visitor visitor2 = new Visitor() {
@Override
public void visit(ElementA element) {
logger.info("使用新的Visitor來訪問元素A");
}
@Override
public void visit(ElementB element) {
logger.info("使用新的Visitor來訪問元素B");
}
};
objectStruct.getElementList().forEach(item -> item.accept(visitor2));
}
}
訪問者模式的缺點
打破了封裝,在Visitor裏邊依賴了Element層次結構中的實現類,這就是根據實現類編程,而不是根據接口編程
要求Element的層次結構固定,如果想要在Element的層次結構上添加新的子類,需要改動所有的Visitor,改動非常大
分派
在接觸訪問者模式的時候,遇到了分派的概念,訪問者模式是"僞動態雙分派",怎麼理解這麼一句話呢?
首先,我們來了解一下分派的概念:
分派的概念:
變量被聲明時的類型叫做變量的靜態類型(Static Type) 又叫明顯類型(Apparent Type)。變量所引用的對象的真實類型又叫做變量的實際類型(Actual Type)。
根據對象的類型而對方法進行的選擇,就是分派(Dispatch)。
根據分派發生的時期,可以將分派分爲兩種,即靜態分派和動態分派。
靜態分派和動態分派
靜態分派(Static Dispatch) 發生在編譯時期,分派根據靜態類型信息發生。方法重載(Overload)就是靜態分派的最典型的應用。(所謂的:編譯時多態)
動態分派(Dynamic Dispatch) 發生在運行時期,動態分派動態地置換掉某個方法。面向對象的語言利用動態分派來實現方法置換產生的多態性。(所謂的:運行時多態)
也就是,運行的時候,根據參數的類型,選擇合適的重載方法,就是動態分派
單分派和多分派:
方法的宗量:
方法的接收者與方法的參數統稱爲方法的宗量,根據分派基於多少宗量,可以把分派劃分爲單分派和多分派(雙分派是多分派的一種形式)單分派是根據一個宗量來對方法進行選擇,多分派則是根據多個宗量來對方法進行選擇
以下是一個來自上的例子:
public class Dispatch {
static class QQ {
}
static class _360 {
}
public static class Father {
public void hardChoice(QQ arg) {
System.out.println("father choice qq");
}
public void hardChoice(_360 arg) {
System.out.println("father choice 360");
}
}
public static class Son extends Father {
@Override
public void hardChoice(QQ arg) {
System.out.println("son choice qq");
}
@Override
public void hardChoice(_360 arg) {
System.out.println("son choice 360");
}
}
public static void main(String[] args) {
Father father = new Father();
Father son = new Son();
father.hardChoice(new _360());
son.hardChoice(new QQ());
}
}
在編譯器階段,編譯器對方法的選擇過程,也就是靜態分派的過程,這時候,選擇目標方法的依據有兩點,一是靜態類型是Father 還是Son,二是方法參數是QQ還是_360,這次選擇的結果,產生了兩條invokevirtual虛擬機指令,這兩條指令的參數分別爲常量池中指向Father.hardChoice(QQ);和Father.hardChoice(360)方法的符號引用,因爲是根據靜態類型和方法參數兩個宗量來進行方法的選擇,所以,Java語言的靜態分派屬於多分派
運行期間,虛擬機對方法的選擇,也就是動態分派的過程,執行到 son.hardChoice(new QQ());這句代碼對應的虛擬機指令的時候,由於編譯期已經確定,目標方法的簽名必須爲 hardChoice(QQ),虛擬機不會關心傳遞過來的參數的類型是哪一個QQ的實現,參數的靜態類型和實際類型都不會對方法的選擇產生影響,唯一可以影響虛擬機對方法選擇的因素只有此方法的接收者的實際類型是Father還是Son,因爲只有一個選擇的依據(就是接收者的實際的類型) 所以,Java語言的動態分派屬於單分派
上邊的訪問者模式的代碼
objectStruct.getElementList().forEach(item -> item.accept(visitor1));
item.accept(visitor); 所有的item接收的方法的類型都是visitor類型,方法的參數類型不會影響虛擬機對方法的選擇,虛擬機具體是調用ElementA的accept()方法還是調用ElementB的accept()方法,是由item的實際的類型來決定的,在此完成了一次動態單分派
而 accept(visitor)方法的實現
visitor.visit(this)
在運行時根據this的具體類型來選擇是調用visitor的 visit(ElementA element)方法還是調用visit(ElementB element)方法,在此完成了一次動態的單分派
兩次動態單分派結合起來,就完成了一次僞動態雙分派,先確定了調用哪個Element的accept()方法,然後再確定了調用Visitor中的針對哪個Element的visit()方法,這就是僞動態雙分派
在accept的方法實現中,傳遞this進去 具體的visitor根據this的類型,又完成了一次分派,找到了需要調用的方法