【設(shè)計模式筆記】(十四)- 訪問者模式

一、簡述

訪問者模式是一種將數(shù)據(jù)操作和數(shù)據(jù)結(jié)構(gòu)分離的設(shè)計模式,是23種設(shè)計模式中非常復(fù)雜的一種,而且使用頻率并不高。

定義:封裝一些作用于某種數(shù)據(jù)結(jié)構(gòu)中的各元素的操作(訪問),可以在不改變這個數(shù)據(jù)的前提下定義作用于這些元素的新操作。

顧名思義,某些不能改變的數(shù)據(jù),對于不同的訪問者有不同的訪問(或者操作),為不同的訪問者提供相對應(yīng)的操作。例如:公司CEO就能看到公司所有的真實財報數(shù)據(jù),而作為一個員工可能就只能知道同比去年的增長比例。

訪問者模式
  • Visitor:訪問者抽象類(或者接口),它定義了對每一個元素(Element)訪問的行為,它的參數(shù)就是可以訪問的元素;理論上,它的方法個數(shù)與元素個數(shù)是一樣的,因此,訪問者模式要求元素的類族要穩(wěn)定,不能頻繁的添加、移除元素。如果出現(xiàn)頻繁修改Visitor接口的情況,說明可能并不適合使用訪問者模式。
  • ConcreteVisitor:具體的訪問者,需要實現(xiàn)每一個元素類訪問時所產(chǎn)生的具體行為。
  • Element:元素接口(或抽象類),它定義了一個接收訪問者的方法(accept()方法),意義在于每一個元素都要刻意被訪問者訪問。
  • ElementA、ElementB:具體的元素類,提供接受訪問方法的具體實現(xiàn),而這個具體的實現(xiàn),通常情況下是使用訪問者提供的訪問該元素類的方法。
  • ObjectStructure:定義當(dāng)中所提到的對象結(jié)構(gòu),對象結(jié)構(gòu)是一個抽象表述,它內(nèi)部管理了元素集合,并且可以迭代這些元素共訪問者訪問。

訪問者模式的最大優(yōu)點就是增加訪問者非常容易,新創(chuàng)建一個實現(xiàn)了Visitor接口的類,然后實現(xiàn)兩個visit()方法對不同的元素進行不同的操作,從而達到數(shù)據(jù)與數(shù)據(jù)操作分離的目的。如果不實用訪問者模式,必定需要使用if-else和類型轉(zhuǎn)換,這便是代碼的維護難度升級了。由此可以看出訪問者模式的作用。

PS:訪問者模式違反了迪米特原則(對訪問者公布元素細節(jié))以及依賴倒置原則(依賴了具體類,沒有依賴抽象),由此可見,此模式需要應(yīng)用在特定的情況中。

二、案例實現(xiàn)

這里就以公司為例,公司員工暫且分為開發(fā)人員和運營人員,而公司的CEO和CTO對于不同員工的KPI關(guān)注點不同,因此我們需要做出不同的處理,接著看看代碼實現(xiàn)

員工基類

很簡單,名字初始化和一個抽象的accept()方法

public abstract class Staff {
    public String name;

    public int kpi;

    public Staff(String name){
        this.name = name;
        kpi = new Random().nextInt(10);
    }
    //接受Visitor訪問
    public abstract void accept(Visitor visitor);
}

具體員工類

具體的員工,根據(jù)各自不同的職責(zé)添加了不同的方法,開發(fā)人員的KPI和代碼產(chǎn)量相關(guān),于是添加了獲取代碼行數(shù)的方法,而運營人員的KPI和新增用戶量相關(guān),于是添加了獲取新增用戶數(shù)的方法。

/**
 * 開發(fā)人員
 */
public class Developer extends Staff {
    public Developer(String name) {
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
    //代碼量
    public int getCodeLines(){
        return new Random().nextInt(10 * 1000);
    }
}

/**
 * 運營人員
 */
public class Operator extends Staff {
    public Operator(String name) {
        super(name);
    }

    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    //新增用戶數(shù)
    public int getNewUserNum(){
        return new Random().nextInt(10 * 10000);
    }
}

訪問者

接下來看看訪問者類的定義

public interface Visitor {
    //訪問開發(fā)人員
    public void visit(Developer developer);
    //訪問運營人員
    public void visit(Operator operator);
}

這里可以看到,直接從方法上就區(qū)分DeveloperOperator,這里主要考慮到的是,如果使用基類Staff作為參數(shù)的話代碼就會是這個樣子

public void visit(Staff staff){
        if(staff instanceof Developer){
            Developer developer = (Developer)staff;
            System.out.print("開發(fā)" + developer.name
                    + ",KPI:" + developer.kpi + ",代碼" + developer.getCodeLines() + "行");
        }else if(staff instanceof Operator){
            Operator operator = (Operator) staff;
            System.out.print("運營" + operator.name + ",KPI:" + operator.kpi);
        }
    }

可以看到,在visit()方法中,我們就需要判斷參數(shù)的類型以及類型強制轉(zhuǎn)換,這樣的代碼難以擴展和維護。

這是訪問者模式的一個優(yōu)點,也是一個缺點,優(yōu)點在于代碼清晰,某種程度上代碼的維護和擴張更好;而缺點也是一樣,如果需要添加一類Staff,所有的Visitor都需要在實現(xiàn)一個新的visit()方法。

接下來是具體的訪問者代碼,這里設(shè)定CTO更加關(guān)注開發(fā)人員,CEO更加關(guān)注運營人員。

public class CTOVisitor implements Visitor {
    @Override
    public void visit(Developer developer) {
        System.out.print("開發(fā)" + developer.name
                + ",KPI:" + developer.kpi + ",代碼" + developer.getCodeLines() + "行");
    }

    @Override
    public void visit(Operator operator) {
        System.out.print("運營" + operator.name + ",KPI:" + operator.kpi);
    }
}

public class CEOVisitor implements Visitor {
    @Override
    public void visit(Developer developer) {
        System.out.print("開發(fā)" + developer.name + ",KPI:" + developer.kpi);
    }

    @Override
    public void visit(Operator operator) {
        System.out.print("運營" + operator.name
                + ",KPI:" + operator.kpi + "新增用戶:" + operator.getNewUserNum());
    }
}

對象結(jié)構(gòu)

這里的對象結(jié)構(gòu),直接就設(shè)定成了公司,集合就是員工們

public class Company {
    private List<Staff> staffList = new ArrayList<>();

    public void action(Visitor visitor){
        for(Staff staff:staffList){
            staff.accept(visitor);
        }
    }

    /**
     *
     * @param staff
     */
    public void addStaff(Staff staff){
        staffList.add(staff);
    }
}

客戶端代碼

public class Client {
    public static void main(String[] agrs){
        Company company = new Company();
        company.addStaff(new Developer("Bruce Wayne"));
        company.addStaff(new Developer("ClarkKent"));
        company.addStaff(new Developer("Barry Allen"));

        company.addStaff(new Operator("Diana Prince"));
        company.addStaff(new Operator("Oliver Queen"));
        company.addStaff(new Operator("Dinah Lance"));

        CEOVisitor ceo = new CEOVisitor();
        company.action(ceo);

        CTOVisitor cto = new CTOVisitor();
        company.action(cto);
    }
}

具體輸出如下:

CEO所看到的======
開發(fā)Bruce Wayne,KPI:6
開發(fā)ClarkKent,KPI:2
開發(fā)Barry Allen,KPI:8
運營Diana Prince,KPI:4,新增用戶:46642
運營Oliver Queen,KPI:1,新增用戶:7687
運營Dinah Lance,KPI:3,新增用戶:67382
 
CTO所看到的======
開發(fā)Bruce Wayne,KPI:6,代碼8285行
開發(fā)ClarkKent,KPI:2,代碼8351行
開發(fā)Barry Allen,KPI:8,代碼658行
運營Diana Prince,KPI:4
運營Oliver Queen,KPI:1
運營Dinah Lance,KPI:3

三、分派

變量被聲明時的類型叫做變量的靜態(tài)類型(Static Type),靜態(tài)變量類型又可以叫做明顯類型(Apparent Type);而變量所引用的對象的正式類型叫做變量的實際類型(Actual Type)。

List list = new ArrayList();

在Java代碼中很常見的一種寫法,聲明父類對象創(chuàng)建子類對象;聲明是List類型(也就是靜態(tài)類型即明顯類型),創(chuàng)建的是ArrayList的對象(實際類型)。

這里就需要提到一個詞,分派(Dispatch)。當(dāng)使用上述形式聲明并創(chuàng)建對象,根據(jù)對象的類型對方法進行選擇,這就是分派,而分派有可以分為靜態(tài)分派(Static Dispatch)和動態(tài)分派(Dynamic Dispatch)。

  • 靜態(tài)分派,對應(yīng)的就是編譯時,根據(jù)靜態(tài)類型信息發(fā)生的分派。方法重載就屬于靜態(tài)分派
  • 動態(tài)分派,對應(yīng)的就是運行時,動態(tài)地置換掉某個方法。方法重寫就屬于動態(tài)分派

靜態(tài)分派

簡化三個類之間的關(guān)系

public class Staff {
     
}

public class Developer extends Staff {

}

public class Operator extends Staff {

}

執(zhí)行類,execute()方法有三個重載方法,方法的參數(shù)分別上面對應(yīng)的三個類型StaffDeveloper、Operator的對象。

public class Execute {

    public void execute(Staff staff){
        System.out.println("員工");
    }

    public void execute(Developer developer){
        System.out.println("開發(fā)人員");
    }

    public void execute(Operator operator){
        System.out.println("運營人員");
    }
}

測試代碼以及測試結(jié)果

public class Client {
    public static void main(String[] agrs){
        System.out.println("運行結(jié)果:");
       
        Staff staff = new Staff();
        Staff staff1 = new Developer();
        Staff staff2 = new Operator();
        
        Execute execute = new Execute();
        execute.execute(staff);
        execute.execute(staff1);
        execute.execute(staff2);
    }
}

運行結(jié)果:
員工
員工
員工

可以推斷出,傳入三個對象,最后執(zhí)行的方法都是參數(shù)類型是Staff的方法,即使三個對象有不同的真實類型

方法重載中實際起作用的是它們靜態(tài)類型,也就是在編譯時期就完成了分派,即靜態(tài)分派。

動態(tài)分派

三個類自帶execute()方法,DeveloperOperator繼承Staff,并重寫了execute()方法

public class Staff {
   public void execute(){
       System.out.println("員工");
   }
}

public class Developer extends Staff {
    @Override
    public void execute() {
        System.out.println("開發(fā)人員");
    }
}

public class Operator extends Staff {
    @Override
    public void execute() {
        System.out.println("運營人員");
    }
}

測試代碼以及結(jié)果

public class Client {
    public static void main(String[] agrs) {
        System.out.println("運行結(jié)果:");
        Staff staff = new Staff();
        staff.execute();
        
        Staff staff1 = new Developer();
        staff1.execute();
        
        Staff staff2 = new Operator();
        staff2.execute();
    }
}

運行結(jié)果:
員工
開發(fā)人員
運營人員

測試時的情況相同,三個對象,其靜態(tài)類型都是Staff,而實際類型分別是Staff、DeveloperOperator。可以看到重寫execute()方法都生效了,各自輸出了對應(yīng)的內(nèi)容。

Java編譯器在編譯時期并不總是知道哪些代碼會被執(zhí)行,因為編譯器僅僅知道對象的靜態(tài)類型,而不知道對象的真實類型;而方法的調(diào)用則是根據(jù)對象的真實類型,而不是靜態(tài)類型。

單分派與多分派

首先需要了解一個叫宗量的概念。一個方法所屬的對象叫做方法的接收者,方法的接收者與方法的參量統(tǒng)稱做方法的宗量。而根據(jù)分派可以基于多少種宗量,可以將面向?qū)ο蟮恼Z言劃分為單分派語言多分派語言。

單分派語言根據(jù)一個宗量的類型(真實類型)進行對方法的選擇
多分派語言根據(jù)多個的宗量的類型對方法進行選擇

Java屬于什么類型呢?

我們可以分析一下,Java中靜態(tài)分派時決定方法的選擇的宗量包括方法的接收者和方法參數(shù)的靜態(tài)類型,所以是多分派;而在動態(tài)分派時,方法的選擇只會考慮方法的接收者的實際類型,所以是單分派。其實Java語言是支持靜態(tài)多分派和動態(tài)單分派的語言。

雙(重)分派

那雙重分派又是什么呢?分派和訪問者模式又有什么關(guān)系呢?接下來就會解釋這些問題

Java支持靜態(tài)多分派和動態(tài)單分派,并不支持動態(tài)多分派;于是就有了兩次單分派組成的雙重分派來替代動態(tài)多分派。而訪問者模式正好就用到了雙重分派的技術(shù)。

雙重分派技術(shù)就是在選擇一個方法的時候,不僅僅要根據(jù)方法的接收者的運行時區(qū)別,還要根據(jù)參數(shù)的運行時區(qū)別(這樣達到兩次分派的效果)。

在訪問者模式中,客戶端將具體的對象傳遞給訪問者,也就是staff.accept(visitor);方法的調(diào)用,完成第一次分派;然后具體的訪問者作為參數(shù)傳入到具體的對象的方法中,也就是這句代碼visitor.visit(this);,將this作為參數(shù)傳遞進去完成第二次分派。雙分派意味著得到的執(zhí)行操作決定于請求的種類和接受者的類型。雙重分派的核心就是this對象。

從訪問者模式可以看出,雙重分派就是在沖在方法委派的前面加上了繼承的重寫,使得從某種角度來說重載變成了動態(tài)。

Android源碼中的訪問者模式

相信注解應(yīng)該不會陌生,現(xiàn)在很多出名框架的使用方式都是使用注解,例如:ButterKnife、Dagger、Retrofit等等,都是以注解的方式使用,已達到簡化代碼或者降低耦合度的目的。而注解又可以分為運行時注解編譯時注解,運行時注解由于性能問題也一直被人詬病,編譯時注解的核心原理依賴APT(Annotation Processing Tools)實現(xiàn),之前提到的框架也是基于APT實現(xiàn)的。

而對于注解的解析過程就是遵從訪問者模式的,其元素就是包、類、方法、方法參數(shù)等(其實就是可以被添加注解那些元素),對于元素的訪問者支持所有的元素訪問,通過繼承一個抽象的元素訪問者實現(xiàn)針對不同類型進行不同的處理。

注解相關(guān)具體的內(nèi)容我不是很了解,只是簡單的說明一下

四、總結(jié)

訪問者模式把數(shù)據(jù)結(jié)構(gòu)和作用于結(jié)構(gòu)上的操作解耦合,使得操作集合可相對自由地演化。訪問者模式適用于數(shù)據(jù)結(jié)構(gòu)相對穩(wěn)定算法又易變化的系統(tǒng)。因為訪問者模式使得算法操作增加變得容易。若系統(tǒng)數(shù)據(jù)結(jié)構(gòu)對象易于變化,經(jīng)常有新的數(shù)據(jù)對象增加進來,則不適合使用訪問者模式。

優(yōu)點

  • 擴展性好: 在不修改對象結(jié)構(gòu)中的元素的情況下,為對象結(jié)構(gòu)中的元素添加新的功能。
  • 復(fù)用性好: 通過訪問者來定義整個對象結(jié)構(gòu)通用的功能,從而提高復(fù)用程度。
  • 分離無關(guān)行為: 通過訪問者來分離無關(guān)的行為,把相關(guān)的行為封裝在一起,構(gòu)成一個訪問者,這樣每一個訪問者的功能都比較單一。

缺點

  • 對象結(jié)構(gòu)變化很困難: 不適用于對象結(jié)構(gòu)中的類經(jīng)常變化的情況,因為對象結(jié)構(gòu)發(fā)生了改變,訪問者的接口和訪問者的實現(xiàn)都要發(fā)生相應(yīng)的改變,代價太高。
  • 破壞封裝: 訪問者模式通常需要對象結(jié)構(gòu)開放內(nèi)部數(shù)據(jù)給訪問者和ObjectStructrue,這破壞了對象的封裝性。
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • 目錄 本文的結(jié)構(gòu)如下: 引言 什么是訪問者模式 模式的結(jié)構(gòu) 典型代碼 訪問者模式中的偽動態(tài)雙分派 代碼示例 訪問者...
    w1992wishes閱讀 947評論 0 6
  • 在閻宏博士的《JAVA與模式》一書中開頭是這樣描述訪問者(Visitor)模式的:訪問者模式是對象的行為模式。訪問...
    Ant_way閱讀 757評論 0 0
  • 定義 訪問者模式是對象的行為模式。訪問者模式的目的是封裝一些施加于某種數(shù)據(jù)結(jié)構(gòu)元素之上的操作。一旦這些操作需要修改...
    步積閱讀 1,627評論 0 3
  • 注:都是在百度搜索整理的答案,如有侵權(quán)和錯誤,希告知更改。 一、哪些情況下的對象會被垃圾回收機制處理掉 ?當(dāng)對象對...
    Jenchar閱讀 3,312評論 3 2
  • 變量沒有類型,對象才有類型 一、基本數(shù)據(jù)類型 整型 int和long浮點型 float布爾型 bool 兩個內(nèi)建(...
    ustcmio閱讀 515評論 0 1

友情鏈接更多精彩內(nèi)容