天天看點

設計模式筆記--通路者模式

常用設計模式有23中,分為:

建立型模式(主要用于建立對象)

1、單例模式    2、工廠方法模式    3、抽象工廠模式    4、建造者模式     5、原型模式 

行為型模式 (主要用于描述對象或類是怎樣互動和怎樣配置設定職責)

1、模闆方法模式  2、中介者模式  3、指令模式    4、責任鍊模式   5、政策模式   6、疊代器模式  

7、觀察者模式      8、備忘錄模式   9、通路者模式   10、狀态模式   11、解釋器模式

結構型模式(主要用于處理類或對象的組合)

1、代理模式  2、裝飾模式   3、擴充卡模式   4、組合模式   5、外觀模式(門面模式)   6、享元模式    7、橋梁模式

通路者模式  

通路者模式(Visitor Pattern)是一個相對簡單的模式,其定義如下:

封裝一些作用于某種資料結構中的各元素的操作,它可以在不改變資料結構的前提下定義作用于這些元素的新的操作。

設計模式筆記--通路者模式

● Visitor——抽象通路者 抽象類或者接口,聲明通路者可以通路哪些元素,具體到程式中就是visit方法的參數定義哪些對象是可以被通路的。

● ConcreteVisitor——具體通路者 它影響通路者通路到一個類後該怎麼幹,要做什麼事情。

● Element——抽象元素 接口或者抽象類,聲明接受哪一類通路者通路,程式上是通過accept方法中的參數來定義的。

● ConcreteElement——具體元素 實作accept方法,通常是visitor.visit(this),基本上都形成了一種模式了。

● ObjectStruture——結構對象 元素産生者,一般容納在多個不同類、不同接口的容器,如List、Set、Map等,在項目中,一般很少抽象出這個角色。  

通用源碼

先看抽象元素,如代碼清單25-11所示。

<span style="font-size:18px;">代碼清單25-11 抽象元素
public abstract class Element {
//定義業務邏輯
public abstract void doSomething();
//允許誰來通路
public abstract void accept(IVisitor visitor);
}</span>
           

抽象元素有兩類方法:一是本身的業務邏輯,也就是元素作為一個業務處理單元必須完成的職責;另外一個是允許哪一個通路者來通路。

具體元素

<span style="font-size:18px;">代碼清單25-12 具體元素
public class ConcreteElement1 extends Element{
//完善業務邏輯
public void doSomething(){
//業務處理
}
//允許那個通路者通路
public void accept(IVisitor visitor){
visitor.visit(this);
}
}</span>
           
<span style="font-size:18px;">public class ConcreteElement2 extends Element{
//完善業務邏輯
public void doSomething(){
//業務處理
}
//允許那個通路者通路
public void accept(IVisitor visitor){
visitor.visit(this);
}
}</span>
           

我們再來看抽象通路者,一般是有幾個具體元素就有幾個通路 方法,如代碼清單 25-13所示。

<span style="font-size:18px;">代碼清單25-13 抽象通路者
public interface IVisitor {
//可以通路哪些對象
public void visit(ConcreteElement1 el1);
public void visit(ConcreteElement2 el2);
}</span>
           

具體通路者如代碼清單25-14所示。

<span style="font-size:18px;">代碼清單25-14 具體通路者
public class Visitor implements IVisitor {
//通路el1元素
public void visit(ConcreteElement1 el1) {
el1.doSomething();
}
//通路el2元素
public void visit(ConcreteElement2 el2) {
el2.doSomething();
}
}</span>
           

結構對象是産生出不同的元素對象,我們使用工廠方法模式來模拟,如代碼清單25-15 所示。

<span style="font-size:18px;">代碼清單25-15 結構對象
public class ObjectStruture {
//對象生成器,這裡通過一個工廠方法模式模拟
public static Element createElement(){
Random rand = new Random();
if(rand.nextInt(100) > 50){
return new ConcreteElement1();
}else{
return new ConcreteElement2();
}
}
}</span>
           

進入了通路者角色後,我們對所有的具體元素的通路就非常簡單了,我們通過一個場景類模拟這種情況,如代碼清單25-16所示。

<span style="font-size:18px;">代碼清單25-16 場景類
public class Client {
public static void main(String[] args) {
for(int i=0;i<10;i++){
//獲得元素對象
Element el = ObjectStruture.createElement();
//接受通路者通路
el.accept(new Visitor());
}
}
}</span>
           

通過增加通路者,隻要是具體元素就非常容易通路,對元素的周遊就更加容易了,甭管它是什麼對象,隻要它在一個容器中,都可以通過通路者來通路,任務集中化。 這就是通路者模式。  

優點 

●  符合單一職責原則 具體元素角色也就是Employee抽象類的兩個子類負責資料的加載,而Visitor類則負責報表的展現,兩個不同的職責非常明确地分離開來,各自演繹變化。

● 優秀的擴充性

● 靈活性非常高

缺點 ● 具體元素對通路者公布細節 通路者要通路一個類就必然要求這個類公布一些方法和資料,也就是說通路者關注了其他類的内部細節,這是迪米特法則所不建議的。  

● 具體元素變更比較困難 具體元素角色的增加、删除、修改都是比較困難的  

● 違背了依賴倒置轉原則 

通路者依賴的是具體元素,而不是抽象元素,這破壞了依賴倒置原則,特别是在面向對象的程式設計中,抛棄了對接口的依賴,而直接依賴實作類,擴充比較難。  

使用場景  

● 一個對象結構包含很多類對象,它們有不同的接口,而你想對這些對象實施一些依賴于其具體類的操作,也就說是用疊代器模式已經不能勝任的情景。

● 需要對一個對象結構中的對象進行很多不同并且不相關的操作,而你想避免讓這些操作“污染”這些對象的類。  

業務規則要求周遊多個不同的對象時,一定要考慮使用通路者模式

示例  檢視公司員工

設計模式筆記--通路者模式

通路者接口IVisitor程式,如代碼清單25-5所示。

<span style="font-size:18px;">代碼清單25-5 通路者接口
public interface IVisitor {
//首先,定義我可以通路普通員工
public void visit(CommonEmployee commonEmployee);
//其次,定義我還可以通路部門經理
public void visit(Manager manager);
} </span>
           

該接口的意義是:該接口可以通路兩個對象,一個是普通員工,一個是高層員工   

通路者實作

<span style="font-size:18px;">public class Visitor implements IVisitor {
//通路普通員工,列印出報表
public void visit(CommonEmployee commonEmployee) {
System.out.println(this.getCommonEmployee(commonEmployee));
}
//通路部門經理,列印出報表
public void visit(Manager manager) {
System.out.println(this.getManagerInfo(manager));
}
//組裝出基本資訊
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":"男info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
//組裝出部門經理的資訊
private String getManagerInfo(Manager manager){
String basicInfo = this.getBasicInfo(manager);
String otherInfo = "業績:"+manager.getPerformance() + "\t";
return basicInfo + otherInfo;
}
//組裝出普通員工資訊
private String getCommonEmployee(CommonEmployee commonEmployee){
String basicInfo = this.getBasicInfo(commonEmployee);
String otherInfo = "工作:"+commonEmployee.getJob()+"\t";
return basicInfo + otherInfo;
}
}</span>
           

在具體的實作類中,定義了兩個私有方法,作用就是産生需要列印的資料和格式,然後 在通路者通路相關的對象時産生這個報表。

抽象員工Employee稍有修改,如代碼清單25-7所 示。

<span style="font-size:18px;">代碼清單25-7 抽象員工類
public abstract class Employee {
public final static int MALE = 0; //0代表是男性
public final static int FEMALE = 1; //1代表是女性
//甭管是誰,都有工資
private String name;
//隻要是員工那就有薪水
private int salary;
//性别很重要
private int sex;
//以下是簡單的getter/setter
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public int getSalary() {
return salary;
}
public void setSalary(int salary) {
this.salary = salary;
}
public int getSex() {
return sex;
}
public void setSex(int sex) {
this.sex = sex;
}
//我允許一個通路者通路
public abstract void accept(IVisitor visitor);
}</span>
           

增加了accept方法,接受通路者的通路。  

我們繼續來看員工實作類,普通員工代碼清單25-8所示。

<span style="font-size:18px;">代碼清單25-8 普通員工
public class CommonEmployee extends Employee {
//工作内容,這非常重要,以後的職業規劃就是靠它了
private String job;
public String getJob() {
return job;
}
public void setJob(String job) {
this.job = job;
}
//我允許通路者通路
@Override
public void accept(IVisitor visitor){
visitor.visit(this);
}
}</span>
           

上面是普通員工的實作類,該類的accept方法很簡單,這個類就把自身傳遞過去,也就 是讓通路者通路本身這個對象。

再看Manager類,如代碼清單25-9所示。

<span style="font-size:18px;">代碼清單25-9 管理層員工
public class Manager extends Employee {
//這類人物的職責非常明确:業績
private String performance;
public String getPerformance() {
return performance;
}
public void setPerformance(String performance) {
this.performance = performance;
}
//部門經理允許通路者通路
@Override
public void accept(IVisitor visitor){
visitor.visit(this);
}
}</span>
           

所有的業務定義都已經完成,我們來看看怎麼模拟這個邏輯,如代碼清單25-10所示。

<span style="font-size:18px;">public class Client {
public static void main(String[] args) {
for(Employee emp:mockEmployee()){
emp.accept(new Visitor());
}
}
//模拟出公司的人員情況,我們可以想象這個資料是通過持久層傳遞過來的
public static List mockEmployee(){
List empList = new ArrayList();
//産生張三這個員工
CommonEmployee zhangSan = new CommonEmployee();
zhangSan.setJob("編寫Java程式,絕對的藍領、苦工加搬運工");
zhangSan.setName("張三");
zhangSan.setSalary(1800);
zhangSan.setSex(Employee.MALE);
empList.add(zhangSan);
//産生李四這個員工
CommonEmployee liSi = new CommonEmployee();
liSi.setJob("頁面美工,審美素質太不流行了!");
liSi.setName("李四");
liSi.setSalary(1900);
liSi.setSex(Employee.FEMALE);
empList.add(liSi);
//再産生一個經理
Manager wangWu = new Manager();
wangWu.setName("王五");
wangWu.setPerformance("基本上是負值,但是我會拍馬屁呀");
wangWu.setSalary(18750);
wangWu.setSex(Employee.MALE);
empList.add(wangWu);
return empList;
}
} </span>
           

隻要再産生一個IVisitor的實作類就可以産生一個新的報表格式,而其他的類都不用修改, 這就是通路者模式的優勢所在  

通路者模式的擴充   

1)統計功能   

設計模式筆記--通路者模式

仔細看IVisitor接口,增加了一個getTotalSalary方法  

IVisitor實作類  

<span style="font-size:18px;">代碼清單25-18 具體通路者
public class Visitor implements IVisitor {
//部門經理的工資系數是5
private final static int MANAGER_COEFFICIENT = 5;
//員工的工資系數是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通員工的工資總和
private int commonTotalSalary = 0;
//部門經理的工資總和
private int managerTotalSalary =0;
//計算部門經理的工資總和
private void calManagerSalary(int salary){
this.managerTotalSalary = this.managerTotalSalary + salary
*MANAGER_COEFFICIENT ;
}
//計算普通員工的工資總和
private void calCommonSlary(int salary){
this.commonTotalSalary = this.commonTotalSalary +
salary*COMMONEMPLOYEE_COEFFICIENT;
}
//獲得所有員工的工資總和
public int getTotalSalary(){
return this.commonTotalSalary + this.managerTotalSalary;
}
}  </span>
           

注意,我們在實作時已經考慮員工工資和經理工資的系數不同。  

Client類的模拟 

<span style="font-size:18px;">代碼清單25-19 場景類
public class Client {
public static void main(String[] args) {
IVisitor visitor = new Visitor();
for(Employee emp:mockEmployee()){
emp.accept(visitor);
}
System.out.println("本公司的月工資總額是:"+visitor.getTotalSalary());
}
}</span>
           

其中mockEmployee靜态方法沒有任何改動,  

2)多個通路者  

設計模式筆記--通路者模式

多了兩個接口和兩個實作類,分别負責展示表和彙總表的業務處理,IVisitor接口沒有改變  

展示表的實作  

<span style="font-size:18px;">代碼清單25-21 具體展示表
public class ShowVisitor implements IShowVisitor {
private String info = "";
//列印出報表
public void report() {
System.out.println(this.info);
}
//通路普通員工,組裝資訊
public void visit(CommonEmployee commonEmployee) {
this.info = this.info + this.getBasicInfo(commonEmployee)
+ "工作:"+commonEmployee.getJob()+"\t\n";
}
//通路經理,然後組裝資訊
public void visit(Manager manager) {
this.info = this.info + this.getBasicInfo(manager) + "業績:
"+manager.getPerformance() + "\t\n";
}
//組裝出基本資訊
private String getBasicInfo(Employee employee){
String info = "姓名:" + employee.getName() + "\t";
info = info + "性别:" + (employee.getSex() == Employee.FEMALE?"女":
"男") + "\t";
info = info + "薪水:" + employee.getSalary() + "\t";
return info;
}
}</span>
           

彙總表實作資料彙總功能, 

<span style="font-size:18px;">代碼清單25-23 具體彙總表
public class TotalVisitor implements ITotalVisitor {
//部門經理的工資系數是5
private final static int MANAGER_COEFFICIENT = 5;
//員工的工資系數是2
private final static int COMMONEMPLOYEE_COEFFICIENT = 2;
//普通員工的工資總和
private int commonTotalSalary = 0;
//部門經理的工資總和
private int managerTotalSalary =0;
public void totalSalary() {
System.out.println("本公司的月工資總額是" + (this.commonTotalSalary +
this.managerTotalSalary));
}
//通路普通員工,計算工資總額
public void visit(CommonEmployee commonEmployee) {
this.commonTotalSalary = this.commonTotalSalary + commonEmployee.getSalary() }
//通路部門經理,計算工資總額
public void visit(Manager manager) {
this.managerTotalSalary = this.managerTotalSalary + manager.getSalary() }
}</span>
           

場景類  

<span style="font-size:18px;">public class Client {
public static void main(String[] args) {
//展示報表通路者
IShowVisitor showVisitor = new ShowVisitor();
//彙總報表的通路者
ITotalVisitor totalVisitor = new TotalVisitor();
for(Employee emp:mockEmployee()){
emp.accept(showVisitor); //接受展示報表通路者
emp.accept(totalVisitor);//接受彙總表通路者
}
//展示報表
showVisitor.report();
//彙總報表
totalVisitor.totalSalary();
}
}</span>
           

3)雙分派   

說到通路者模式就不得不提一下雙分派(double dispatch)問題,什麼是雙分派呢?

我們先來解釋一下什麼是單分派(single dispatch)和多分派(multiple dispatch),

單分派語言處理一個操作是根據請求者的名稱和接收到的參數決定的,在Java中有靜态綁定和動态綁定之說,它的實作是依據重載(overload)和覆寫(override)實作的,我們來說一個簡單的例子。

例如,演員演電影角色,一個演員可以扮演多個角色,我們先定義一個影視中的兩個角色:功夫主角和白癡配角,如代碼清單25-25所示。

<span style="font-size:18px;">public interface Role {
//演員要扮演的角色
}
public class KungFuRole implements Role {
//武功天下第一的角色
}
public class IdiotRole implements Role {
//一個弱智角色
}</span>
           

角色有了,我們再定義一個演員抽象類,如代碼清單25-26所示。

<span style="font-size:18px;">代碼清單25-26 抽象演員
public abstract class AbsActor {
//演員都能夠演一個角色
public void act(Role role){
System.out.println("演員可以扮演任何角色");
}
//可以演功夫戲
public void act(KungFuRole role){
System.out.println("演員都可以演功夫角色");
}
}</span>
           

這裡使用了Java的重載,我們再來看青年演員和老年演員,采用覆寫的方式來 細化抽象類的功能,如代碼清單 25-27所示。

<span style="font-size:18px;">代碼清單25-27 青年演員和老年演員
public class YoungActor extends AbsActor {
//年輕演員最喜歡演功夫戲
public void act(KungFuRole role){
System.out.println("最喜歡演功夫角色");
}
}
public class OldActor extends AbsActor {
//不演功夫角色
public void act(KungFuRole role){
System.out.println("年齡大了,不能演功夫角色");
}
}</span>
           

覆寫和重載都已經實作,我們編寫一個場景,如代碼清單25-28所示。

<span style="font-size:18px;">代碼清單25-28 場景類
public class Client {
public static void main(String[] args) {
//定義一個演員
AbsActor actor = new OldActor();
//定義一個角色
Role role = new KungFuRole();
//開始演戲
actor.act(role);
actor.act(new KungFuRole());
}
}</span>
           

運作結果如下所示。

演員可以扮演任何角色 年齡大了,不能演功夫角色

重載在編譯器期就決定了要調用哪個方法,它是根據role的表面類型而決定調用act(Rolerole)方法,這是靜态綁定; 而Actor的執行方法act則是由其實際類型決定的,這是動态綁定。

一個演員可以扮演很多角色,我們的系統要适應這種變化,也就是根據演員、角色兩個對象類型,完成不同的操作任務,該如何實作呢?

我們讓通路者模式上場就可以解決該問題,隻要把角色類稍稍修改即可,如代碼清單25-29所示。

<span style="font-size:18px;">代碼清單25-29 引入通路者模式
public interface Role {
//演員要扮演的角色
public void accept(AbsActor actor);
}p
ublic class KungFuRole implements Role {
//武功天下第一的角色
public void accept(AbsActor actor){
actor.act(this);
}
}p
ublic class IdiotRole implements Role {
//一個弱智角色,由誰來扮演
public void accept(AbsActor actor){
actor.act(this);
}
}</span>
           

場景類稍有改動,如代碼清單25-30所示。

<span style="font-size:18px;">代碼清單25-30 場景類
public class Client {
public static void main(String[] args) {
//定義一個演員
AbsActor actor = new OldActor();
//定義一個角色
Role role = new KungFuRole();
//開始演戲
role.accept(actor);
}
}</span>
           

運作結果如下所示。 年齡大了,不能演功夫角色  

看到沒?不管演員類和角色類怎麼變化,我們都能夠找到期望的方法運作,這就是雙反派。雙分派意味着得到執行的操作決定于請求的種類和兩個接收者的類型,它是多分派的一個特例。從這裡也可以看到Java是一個支援雙分派的單分派語言。  

通路者模式是一種集中規整模式,特别适用于大規模重構的項目,在這一個階段需求已經非常清晰,原系統的功能點也已經明确,通過通路者模式可以很容易把一些功能進行梳理,達到最終目的——功能集中化    

繼續閱讀