大多數情況下你不需要存取者模式,但當一旦需要存取者模式時,那就是真的需要它了,這是設計模式創始人的原話。可以看出應用場景比較少,但需要它的時候是不可或缺的,這篇文章就開始學習最後一個設計模式——存取者模式。
存取者模式概念:封裝作用於某物件結構中的各元素的操作,它使你可以在不改變各元素的類的前提下定義作用於這些元素的新操作。
通俗的解釋就是,系統中有一些固定結構的物件(元素),在其內部提供一個accept()方法用來接受存取者物件的存取,不同的存取者對同一元素的存取內容不同,所以使得相同的元素可以產生不同的元素結果。
比如在一個人事管理系統中,有多個工種的員工和多個老闆,不同的老闆對同一個員工的關注點是不同的,CTO可能關注的就是技術,CEO可能更注重績效。
員工就是一個穩定的元素,老闆就是變化的,對應概念就是:封裝員工的一些操作,可以在不改變員工類的前提下,增加新的老闆存取同一個員工。
在存取者模式中包含五個角色,抽象元素、具體元素、抽象存取者、具體存取者、結構元素。
抽象元素:定義一個接受存取的方法accept,引數為存取者物件。
具體元素:提供接受存取者存取的具體實現呼叫存取者的存取visit,並定義額外的資料操作方法。
抽象存取者:這個角色主要是定義對具體元素的存取方法visit,理論上來說方法數等於元素(固定型別的物件,也就是被存取者)個數。
具體存取者:實現對具體元素的存取visit方法,引數就是具體元素。
結構物件:建立一個陣列用來維護元素,並提供一個方法存取所有的元素。
在一個公司有幹活的工程師和管理者,也有抓技術的CTO和管績效的CEO,CTO和CEO都會存取管理員和工程師,當公司來了新的老闆,只需要增加存取者即可。
工程師和管理者就是元素、公司就是結構體、CEO、CTO就是存取者。
抽象元素:
/**
* 員工 抽象元素 被存取者
* @author tcy
* @Date 29-09-2022
*/
public interface ElementAbstract {
void accept(VisitorAbstract visitor);
}
具體元素-工程師:
/**
* 工程師 具體元素 被存取者
* @author tcy
* @Date 29-09-2022
*/
public class ElementEngineer implements ElementAbstract {
private String name;
private int kpi;
ElementEngineer(String name){
this.name = name;
this.kpi = new Random().nextInt(10);
}
public String getName() {
return name;
}
public int getKpi() {
return kpi;
}
@Override
public void accept(VisitorAbstract visitor) {
visitor.visit(this);
}
public int getCodeLineTotal(){
return this.kpi * 1000000;
}
}
具體元素-管理者:
/**
* 管理者 具體元素 被存取者
* @author tcy
* @Date 29-09-2022
*/
public class ElementManager implements ElementAbstract {
private String name;
private int kpi;
ElementManager(String name){
this.name = name;
this.kpi = new Random().nextInt(10);
}
public String getName() {
return name;
}
public int getKpi() {
return kpi;
}
@Override
public void accept(VisitorAbstract visitor) {
visitor.visit(this);
}
public int getProductNum(){
return this.kpi * 10;
}
}
抽象存取者:
/**
* 抽象存取者
* @author tcy
* @Date 29-09-2022
*/
public interface VisitorAbstract {
void visit(ElementEngineer engineer);
void visit(ElementManager manager);
}
具體存取者-CEO
/**
* 具體存取者CEO
* @author tcy
* @Date 29-09-2022
*/
public class VisitorCEO implements VisitorAbstract {
@Override
public void visit(ElementEngineer engineer) {
System.out.println("工程師:" + engineer.getName() + "KPI:" + engineer.getKpi());
}
@Override
public void visit(ElementManager manager) {
System.out.println("經理:" + manager.getName() + "KPI:" + manager.getKpi() + " 今年共完成專案:" + manager.getProductNum() + "個");
}
}
具體存取者-CTO
/**
* 具體存取者CTO
* @author tcy
* @Date 29-09-2022
*/
public class VisitorCTO implements VisitorAbstract {
@Override
public void visit(ElementEngineer engineer) {
System.out.println("工程師:" + engineer.getName() + " 今年程式碼量" + engineer.getCodeLineTotal() + "行");
}
@Override
public void visit(ElementManager manager) {
System.out.println("經理:" + manager.getName() + " 今年共完成專案:" + manager.getProductNum() + "個");
}
}
結構體:
/**
* 結構物件
* @author tcy
* @Date 29-09-2022
*/
public class Structure {
List<ElementAbstract> list = new ArrayList<>();
public Structure addEmployee(ElementAbstract employee){
list.add(employee);
return this;
}
public void report(VisitorAbstract visitor){
list.forEach(employee -> {
employee.accept(visitor);
});
}
}
使用者端:
/**
* @author tcy
* @Date 29-09-2022
*/
public class Client {
public static void main(String[] args) {
//元素物件
ElementEngineer engineerZ = new ElementEngineer("小張");
ElementEngineer engineerW = new ElementEngineer("小王");
ElementEngineer engineerL = new ElementEngineer("小李");
ElementManager managerZ = new ElementManager("張總");
ElementManager managerW = new ElementManager("王總");
ElementManager managerL = new ElementManager("李總");
//結構體物件
Structure structure = new Structure();
structure.addEmployee(engineerZ).addEmployee(engineerW).addEmployee(engineerL).addEmployee(managerZ).addEmployee(managerW).addEmployee(managerL);
structure.report(new VisitorCTO());
System.out.println("---------------------------------------");
structure.report(new VisitorCEO());
}
}
存取者不愧是最難的設計模式,方法間的呼叫錯綜複雜,日常開發的使用頻率很低,很多程式設計師寧可程式碼寫的麻煩一點也不用這種設計模式,但是作為學習者就要學習各種設計模式了。
JDK的NIO中的 FileVisitor 介面採用的就是存取者模式。
在早期的 Java 版本中,如果要對指定目錄下的檔案進行遍歷,必須用遞迴的方式來實現,這種方法複雜且靈活性不高。
Java 7 版本後,Files 類提供了 walkFileTree() 方法,該方法可以很容易的對目錄下的所有檔案進行遍歷,需要 Path、FileVisitor 兩個引數。其中,Path 是要遍歷檔案的路徑,FileVisitor 則可以看成一個檔案存取器,原始碼如下。
FileVisitor 主要提供了 4 個方法,且返回結果的都是 FileVisitResult 物件值,用於決定當前操作完成後接下來該如何處理。FileVisitResult 是一個列舉類,代表返回之後的一些後續操作,原始碼如下。
FileVisitResult 主要包含 4 個常見的操作。
通過存取者去遍歷檔案樹會比較方便,比如查詢資料夾內符合某個條件的檔案或者某一天內所建立的檔案,這個類中都提供了相對應的方法。它的實現也非常簡單,程式碼如下。
在JDK的應用中我們提供的檔案就看做是一個穩定元素,對應存取者模式中的抽象元素;而Files.walkFileTree()方法中的FileVisitor 引數就可看做是角色中的存取者。
存取者模式中有一個重要的概念叫:偽動態雙分派。
我們一步一步解讀它的含義,什麼叫分派?根據物件的型別而對方法進行的選擇,就是分派(Dispatch)。
發生在編譯時的分派叫靜態分派,例如過載(overload),發生在執行時的分派叫動態分派,例如重寫(overwrite)。
其中分派又分為單分派和多分派。
單分派:依據單個變數進行方法的選擇就叫單分派,Java 動態分派(重寫)只根據方法的接收者一個變數進行分配,所以其是單分派。
多分派:依據多個變數進行方法的選擇就叫多分派,Java 靜態分派(過載)要根據方法的接收者與引數這兩個變數進行分配,所以其是多分派。
理解了概念我們接著看我們的案例:
@Override
public void accept(VisitorAbstract visitor) {
visitor.visit(this);
}
我們案例中的accept方法,是由元素的執行時型別決定的,應該是屬於動態單分派。
我們接著看 visitor.visit(this)又是一次動態單分派,兩次動態單分派實現了雙分派的效果,所以稱為偽動態雙分派。
這個概念理解就好,實際應用中知不知道這玩意都不影響。
當你有個類,裡面的包含各種型別的元素,這個類結構比較穩定,不會經常增刪不同型別的元素。而需要經常給這些元素新增新的操作的時候,考慮使用此設計模式。
適用物件結構比較穩定每增加一個元素存取者都要大變動,但加新的操作很簡單。集中相關的操作、分離無關的操作。
缺點只有兩個字-複雜,號稱是最複雜的設計模式。
設計模式的學習要成體系,推薦你看我往期釋出的設計模式文章。