天天看點

通路者模式

一、定義

      通路者模式(Visitor Pattern) 是一種将資料結構與資料操作分離的設計模式。是指封裝一些作用于某種資料結構中的各元素的操作,它可以在不改變資料結構的前提下定義作用于這些元索的新的操作。屬于行為型模式。

  通路者模式被稱為最複雜的設計模式,并且使用頻率不高,設計模式的作者也評價為:大多情況下,你不需要使用通路者模式,但是一旦需要使用它時,那就真的需要使用了。通路者模式的基本思想是, 針對系統中擁有固定類型數的對象結構(元素) , 在其内提供一個accept()方法用來接受通路者對象的通路。不同的通路者對同一進制素的通路内容不同,使得相同的元素集合可以産生不同的資料結果。accept() 方法可以接收不同的通路者對象然後在内部将自己(元素) 轉發到接收到的通路者對象的visit() 方法内。通路者内部對應類型的visit() 方法就會得到回調執行, 對元素進行操作。也就是通過兩次動态分發(第一次是對通路者的分發accept() 方法,第二次是對元索的分發visit() 方法) , 才最終将一個具體的元素傳遞到一個具體的通路者。如此一來,就解耦了資料結構與操作,且資料操作不會改變元素狀态。通路者模式的核心是,解耦資料結構與資料操作,使得對元索的操作具備優秀的擴充性。可以通過擴充不同的資料操作類型(通路者)實作對相同元索集的不同的操作。通路者模式主要包含五種角色:

  • 抽象通路者(Visitor) :接口或抽象類, 該類提供了對每一個具體元素(Element) 的通路行為visit() 方法, 其參數就是具體的元素(Element) 對象。理論上來說, Visitor的方法個數與元索(Element) 個數是相等的。如果元素(Element) 個數經常變動, 會導緻Visitor的方法也要進行變動,此時,該情形并不适用通路者模式;
  • 具體通路者(Concrete Visitor) :實作對具體元素的操作;
  • 抽象元素(Element) :接口或抽象類, 定義了一個接受通路者通路的方法accept() , 表示所有元索類型都支援被通路者通路;
  • 具體元素(Concrete Element) :具體元素類型, 提供接受通路者的具體實作。通常的實作都為:visitor.visit(this) ;
  • 結構對象(Object Struture) :該類内部維護了元素集合, 并提供方法接受通路者對該集合所有元素進行操作。
通路者模式

二、通路者模式的案例

       通路者模式在生活場景中也是非常當多的, 例如每年年底的KPI考核, KPI考核标準是相對穩定的, 但是參與KPI考核的員工可能每年都會發生變化, 那麼員工就是通路者。我們平時去食堂或者餐廳吃飯,餐廳的菜單和就餐方式是相對穩定的,但是去餐廳就餐的人員是每天都在發生變化的,是以就餐人員就是通路者。

  當系統中存在類型數目穩定(固定)的一類資料結構時,可以通過通路者模式友善地實作對該類型所有資料結構的不同操作,而又不會資料産生任何副作用(髒資料)。簡言之,就是對集合中的不同類型資料(類型數量穩定)進行多種操作,則使用通路者模式。下面總結一下通路者模式的适用場景:

  • 資料結構穩定,作用于資料結構的操作經常變化的場景;
  • 需要資料結構與資料操作分離的場景;
  • 需要對不同資料類型(元素)進行操作,而不使用分支判斷具體類型的場景。

1.标準寫法

// 抽象元素
public interface IElement {
    void accept(IVisitor visitor);
}      
// 具體元素
public class ConcreteElementA implements IElement {

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    public String operationA() {
        return this.getClass().getSimpleName();
    }

}      
// 具體元素
public class ConcreteElementB implements IElement {

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    public int operationB() {
        return new Random().nextInt(100);
    }
}      
// 抽象通路者
public interface IVisitor {

    void visit(ConcreteElementA element);

    void visit(ConcreteElementB element);
}      
// 具體通路者
public class ConcreteVisitorA implements IVisitor {

    public void visit(ConcreteElementA element) {
        String result = element.operationA();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }

    public void visit(ConcreteElementB element) {
        int result = element.operationB();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }
}      
// 具體通路者
public class ConcreteVisitorB implements IVisitor {

    public void visit(ConcreteElementA element) {
        String result = element.operationA();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }


    public void visit(ConcreteElementB element) {
        int result = element.operationB();
        System.out.println("result from " + element.getClass().getSimpleName() + ": " + result);
    }
}      
// 結構對象
public class ObjectStructure {
    private List<IElement> list = new ArrayList<IElement>();

    {
        this.list.add(new ConcreteElementA());
        this.list.add(new ConcreteElementB());
    }

    public void accept(IVisitor visitor) {
        for (IElement element : this.list) {
            element.accept(visitor);
        }
    }
}      
public class Test {

    public static void main(String[] args) {
        ObjectStructure collection = new ObjectStructure();
        System.out.println("ConcreteVisitorA handle elements:");
        IVisitor visitorA = new ConcreteVisitorA();
        collection.accept(visitorA);
        System.out.println("------------------------------------");
        System.out.println("ConcreteVisitorB handle elements:");
        IVisitor visitorB = new ConcreteVisitorB();
        collection.accept(visitorB);
    }

}      

2.利用通路者模式實作KPI考核的場景:

        每到年底,管理層就要開始評定員工一年的工作績效,員工分為工程師和經理;管理層有CEO和CTO。那麼CTO關注工程師的代碼量、經理的新産品數量; CEO關注的是工程師的KPI和經理的KPI以及新産品數量。由于CEO和CTO對于不同員工的關注點是不一樣的, 這就需要對不同員工類型進行不同的處理。通路者模式此時可以派上用場了。

  Employee類定義了員工基本資訊及一個accept() 方法, accept() 方法表示接受通路者的通路,由具體的子類來實作。通路者是個接口,傳入不同的實作類,可通路不同的資料。

//元素抽象
public abstract class Employee {
    public String name;
    public int kpi;  //員工KPI

    public Employee(String name) {
        this.name = name;
        kpi = new Random().nextInt(10);
    }

    //接收通路者的通路
    public abstract void accept(IVisitor visitor);
}      
//// 具體元素:工程師
public class Engineer extends Employee {
    public Engineer(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核名額是每年的代碼量
    public int getCodeLines(){
        return new Random().nextInt(10* 10000);
    }
}      
//// 具體元素:項目經理
public class Manager extends Employee {
    public Manager(String name) {
        super(name);
    }

    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

    //考核的是每年新産品研發數量
    public int getProducts(){
        return new Random().nextInt(10);
    }
}      
// 抽象通路者
public interface IVisitor {

    void visit(Engineer engineer);

    void visit(Manager manager);

}      
//具體通路者:CEO
public class CEOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程師" +  engineer.name + ",KIP:" + engineer.kpi);
    }

    public void visit(Manager manager) {
        System.out.println("經理:" +  manager.name + ",KPI:" + manager.kpi + ",産品數量:" + manager.getProducts());
    }
}      
//具體通路者:CTO
public class CTOVistitor implements IVisitor {
    public void visit(Engineer engineer) {
        System.out.println("工程師" +  engineer.name + ",代碼行數:" + engineer.getCodeLines());
    }

    public void visit(Manager manager) {
        System.out.println("經理:" +  manager.name + ",産品數量:" + manager.getProducts());
    }
}      
//資料結構
public class BusinessReport {
    private List<Employee> employees = new LinkedList<Employee>();

    public BusinessReport() {
        employees.add(new Manager("産品經理A"));
        employees.add(new Engineer("程式員A"));
        employees.add(new Engineer("程式員B"));
        employees.add(new Engineer("程式員C"));
        employees.add(new Manager("産品經理B"));
        employees.add(new Engineer("程式員D"));
    }

    public void showReport(IVisitor visitor){
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }
}      
public class Test {
    public static void main(String[] args) {
        BusinessReport report = new BusinessReport();
        System.out.println("==========CEO看報表===============");
        report.showReport(new CEOVistitor());
        System.out.println("==========CTO看報表===============");
        report.showReport(new CTOVistitor());
    }
}      

通路者模式最大的優點就是增加通路者非常容易,我們從代碼中可以通路者,隻要新實作一個通路者接口的類,進而達到資料對象與資料操作分離的效果,如果不使用通路者模式,而又不想對不同的元素進行不同的操作,那麼必定使用 if-else 和類型轉換和類型轉換,這使得代碼難以更新維護;說到通路者模式就有一個擴充的點可以說下,那就是分派;分派有靜态分派和動态分派以及雙分派;

1.靜态分派

靜态分派就是按照變量的靜态類型進行分派,進而靜态分派在編譯時期就可以确定方法的版本。而靜态分派最典型的應用就這段代碼。

public class Main {
    public void test(String string){
        System.out.println("string" + string);
    }
    public void test(Integer integer){
        System.out.println("integer" + integer);
    }

    public static void main(String[] args) {
        String string = "1";
        Integer integer = 1;
        Main main = new Main();
        main.test(integer);
        main.test(string);
    }
}      

在靜态分派判斷的時候我們根據多個判斷依據(即參數類型和個數)判斷出了方法的版本,那麼這就是多分派的概念。因為我們有一個以上的考量标準。是以Java是靜态多分派的語言。

2.動态分派

對于動态分派,與靜态相反,它不是在編譯期确定的方法版本,而是在運作時才能确定。動态分派最典型的應用就是多态的特性。舉個例子,來看下面的這段代碼。

public class Main {
    public static void main(String[] args) {
        Person man = new Man();
        Person woman = new WoMan();

        man.test();
        woman.test();
    }
}      
public interface Person {
    void test();
}      
public class Man implements Person {

    public void test() {
        System.out.println("男人");
    }
}      
public class WoMan implements Person {

    public void test() {
        System.out.println("女人");
    }
}      

       這段程式輸出結果為依次列印男人和女人,然而這裡的 test 方法版本,就無法根據Man和Woman的靜态類型去判斷了。他們的靜态類型都是 Person接口,根本無從判斷。

  顯然,産生的輸出結果,就是因為 test 方法的版本是在運作時判斷的,這就是動态分派。動态分派判斷的方法是在運作時擷取到Man和 Woman的實際引用類型,再确定方法的版本,而由于此時判斷的依據隻是實際引用類型,隻有一個判斷依據,是以這就是單分派的概念。這時我們的考量标準隻有一個,即變量的實際引用類型。相應的,這說明Java是動态單分派的語言。

 3.通路者模式中的僞動态雙分派

       通過前面分析, 我們知道Java是靜态多分派、動态單分派的語言。Java底層不支援動态的雙分派。但是通過使用設計模式, 也可以在Java語言裡實作僞動态雙分派。在通路者模式中使用的就是僞動态雙分派。所謂動态雙分派就是在運作時依據兩個實際類型去判斷一個方法的運作行為,而通路者模式實作的手段是進行了兩次動态單分派來達到這個效果。回到前面的KPI考核業務場景當中;

public void showReport(IVisitor visitor){
        for (Employee employee : employees) {
            employee.accept(visitor);
        }
    }      

這裡就是依據Employee和I Visitor兩個實際類型決定了showReport() 方法的執行結果,進而決定了accept() 方法的動作。分析accept() 方法的調用過程:當調用accept() 方法時, 根據Employee的實際類型決定是調用Engineer還是Manager 的accept() 方法。這時accept() 方法的版本已經确定, 假如是Engineer, 它的accept() 方法是調用這行代碼。visitor.visit(this) ;此時的this是Engineer類型, 是以對應的是I Visitor接口的visit(Engineer engineer) 方法, 此時需要再根據通路者的實際類型确定visit() 方法的版本, 如此一來, 就完成了動态雙分派的過程。以上的過程就是通過兩次動态雙分派, 第一次對accept() 方法進行動态分派, 第二次對通路者的visit 方法進行動态分派, 進而達到了根據兩個實際類型确定一個方法的行為的效果。

三、總結

優點:

  • 解耦了資料結構與資料操作,使得操作集合可以獨立變化;
  • 擴充性好:可以通過擴充通路者角色,實作對資料集的不同操作;
  • 元素具體類型并非單一,通路者均可操作;
  • 各角色職責分離,符合單一職責原則。
  • 無法增加元素類型:若系統資料結構對象易于變化,經常有新的資料對象增加進來,則通路者類必須增加對應元素類型的操作,連背了開閉原則;

這短短的一生我們最終都會失去,不妨大膽一點,愛一個人,攀一座山,追一個夢

上一篇: 通路者模式
下一篇: 通路者模式

繼續閱讀