訪問者模式 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種組合了,因此纔會邏輯複雜,擴展性差
因此,那麼 根本問題就是靈活的肯定這兩個維度,來訪者和當前類型 ,進而肯定具體的行爲,對吧?
 
再回頭審視一下《俠客行》的示例,對於訪問者,有張3、李4、龍木島主,還可能會有其餘人,
顯然,咱們應該 嘗試將訪問者進行抽象,張三,李四,龍木島主,他們都是具體的訪問者。
並且,並且,並且, 他們都會訪問不一樣類型的掌門人,既然是訪問  不一樣類型掌門人
也就是方法名同樣,類型不同?
這不就是 方法重載

新版代碼示例

掌門人相關角色不變
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回傳達到反向肯定類型調用,真心精巧。

總結

訪問者模式靈活的處理了不一樣類型的元素,面對不一樣的訪問者,有不一樣的行爲的場景。
這種組合場景,判斷邏輯複雜繁瑣,訪問者模式能夠作到靈活的擴展增長更多的行爲,而不須要改變原來的類。
訪問者模式傾向於擴展元素的行爲,當擴展元素行爲時,知足開閉原則
可是對於擴展新的元素類型時,將會產生巨大的改動,每個訪問者都須要變更,因此在使用訪問者模式是要考慮清楚元素類型的變化可能。
由於訪問者依賴的是具體的元素,而不是抽象元素,因此才難以擴展
 
訪問者依賴的是具體元素,而不是抽象元素,這破壞了依賴倒置原則,特別是在面向對
象的編程中,拋棄了對接口的依賴,而直接依賴實現類,擴展比較難。
 
當業務規則須要遍歷多個不一樣的對象時,並且不一樣的對象在不一樣的場景下又有不一樣的行爲
你就應該考慮使用訪問者模式
若是對象結構中的對象不常變化,可是他們的行爲卻常常變化時,也能夠考慮使用,訪問者模式能夠很靈活的擴展新的訪客。
相關文章
相關標籤/搜索