摘要
訪問者設計模式是一種行為型設計模式,它將數據結構與作用于結構上的操作解耦,允許在不修改數據結構的前提下增加新的操作行為。該模式包含關鍵角色如元素接口、具體元素類、訪問者接口和具體訪問者類。通過訪問者模式,可以在不改變對象結構的情況下,定義新的操作行為。文章通過示例場景和類圖、時序圖等詳細介紹了訪問者設計模式的結構和實現方式,并探討了其適用場景和實戰示例。
1. 訪問者設計模式定義
將數據結構與作用于結構上的操作解耦,使得在不修改數據結構的前提下,可以增加新的操作行為。訪問者模式允許你在不改變對象結構(如樹、圖、元素集合)的前提下,定義新的操作行為,通過將這些操作封裝到獨立的 "訪問者" 對象中。
1.1. 關鍵角色說明
角色 | 說明 |
| 定義 |
| 實現 |
| 抽象訪問者,定義訪問每個元素的接口 |
| 實現 |
1.2. 示例場景(通俗類比)
假設你有一個對象結構為:公司組織結構,每個節點可以是 員工、部門。你希望在不修改員工、部門類的前提下,分別實現:
- 統計薪資總額
- 導出組織結構為 HTML
- 打印匯報關系圖
通過訪問者模式,你可以創建多個 ConcreteVisitor
來實現上述功能,而無需改動 Element 本身代碼。
2. 訪問者設計模式結構
- 訪問者 (Visitor) 接口聲明了一系列以對象結構的具體元素為參數的訪問者方法。 如果編程語言支持重載, 這些方法的名稱可以是相同的, 但是其參數一定是不同的。
- 具體訪問者 (Concrete Visitor) 會為不同的具體元素類實現相同行為的幾個不同版本。
- 元素 (Element) 接口聲明了一個方法來 “接收” 訪問者。 該方法必須有一個參數被聲明為訪問者接口類型。
- 具體元素 (Concrete Element) 必須實現接收方法。 該方法的目的是根據當前元素類將其調用重定向到相應訪問者的方法。 請注意, 即使元素基類實現了該方法, 所有子類都必須對其進行重寫并調用訪問者對象中的合適方法。
- 客戶端 (Client) 通常會作為集合或其他復雜對象 (例如一個組合(opens new window)樹) 的代表。 客戶端通常不知曉所有的具體元素類, 因為它們會通過抽象接口與集合中的對象進行交互。
2.1. 訪問者設計模式類圖
2.2. 訪問者設計模式時序圖
3. 訪問者設計模式實現方式
訪問者設計模式的實現方式,核心在于:將作用于對象結構的操作行為封裝到獨立的訪問者類中,并通過 accept(Visitor)
方法把訪問者“注入”到元素中,從而實現對結構中不同元素的不同處理。
3.1. 步驟 1:定義元素接口 Element
public interface Element {void accept(Visitor visitor);
}
3.2. 步驟 2:實現具體元素類(ConcreteElement)
public class Employee implements Element {private String name;private double salary;public Employee(String name, double salary) {this.name = name;this.salary = salary;}// 提供訪問者訪問自己@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}// getterpublic String getName() { return name; }public double getSalary() { return salary; }
}
public class Department implements Element {private String deptName;public Department(String deptName) {this.deptName = deptName;}@Overridepublic void accept(Visitor visitor) {visitor.visit(this);}public String getDeptName() { return deptName; }
}
3.3. 步驟 3:定義訪問者接口 Visitor
public interface Visitor {void visit(Employee employee);void visit(Department department);
}
3.4. 步驟 4:實現具體訪問者(ConcreteVisitor)
public class ReportVisitor implements Visitor {@Overridepublic void visit(Employee employee) {System.out.println("員工:" + employee.getName() + ",薪資:" + employee.getSalary());}@Overridepublic void visit(Department department) {System.out.println("部門:" + department.getDeptName());}
}
3.5. 步驟 5:使用訪問者
public class Client {public static void main(String[] args) {List<Element> elements = Arrays.asList(new Employee("張三", 12000),new Employee("李四", 15000),new Department("風控部"));Visitor visitor = new ReportVisitor();for (Element element : elements) {element.accept(visitor); // 每個元素調用 visitor.visit(this)}}
}
3.6. 說明總結
點位 | 描述 |
解耦操作和結構 | 不改動 Element 的結構代碼,就能添加任意多種訪問邏輯。 |
遵循開閉原則 | 新增操作時,只需新建訪問者類即可。 |
單一職責更清晰 | 每個訪問者只關注自己的行為。 |
缺點:擴展結構麻煩 | 每次新增 Element 子類,所有 |
4. 訪問者設計模式適合場景
以下是訪問者(Visitor)設計模式適合與不適合使用的場景清單,便于你快速判斷在實戰開發中是否應當使用此模式。
4.1. ? 適合使用訪問者設計模式的場景
場景 | 說明 |
? 需要對對象結構中不同元素進行不同操作 | 如處理復雜對象樹時,不同節點類型需要不同處理(如 AST 抽象語法樹、HTML DOM、組織結構等)。 |
? 需要在不修改類的前提下增加新操作 | 元素類穩定,但經常添加新功能,適合將操作邏輯外移成訪問者類。符合開閉原則。 |
? 多個操作跨多個類共享處理邏輯 | 如統計報表、導出功能、數據校驗,每種功能可封裝為訪問者,避免元素類職責膨脹。 |
? 數據結構較復雜,邏輯需要分離 | 尤其在組合模式(Composite)中遍歷樹狀結構時,訪問者是理想搭檔。 |
? 需要記錄訪問軌跡/數據收集/行為鏈式執行 | 訪問者可收集上下文數據,實現功能鏈、審計等操作。 |
4.2. ? 不適合使用訪問者設計模式的場景
場景 | 原因 |
? 對象結構不穩定,經常增刪元素類型 | 每新增一個元素類,所有訪問者都必須修改,違反開閉原則,維護成本高。 |
? 操作種類少,變化不頻繁 | 如果只有一兩種操作,直接放到元素類中即可,訪問者反而引入了額外復雜性。 |
? 操作需要頻繁訪問元素內部狀態/私有字段 | 訪問者訪問元素的內部字段時會暴露實現細節,可能破壞封裝性。 |
? 數據驅動而非行為驅動系統 | 如果處理邏輯更多是對數據表進行規則驅動處理,不如使用策略模式、責任鏈、狀態機等。 |
? 系統對性能極度敏感,層層函數調用不可接受 | 訪問者調用鏈過長,對性能要求極高的系統中不推薦使用。 |
4.3. ? 實戰使用建議
建議點 | 內容 |
👍 推薦與組合模式搭配使用 | 樹形結構遍歷 + 多種處理邏輯,非常適合訪問者模式。 |
👍 可與責任鏈、模板方法組合 | 在訪問者中執行鏈式操作或分步驟邏輯。 |
?? 避免與頻繁變更的領域模型搭配 | 如果業務模型變化頻繁,訪問者維護成本非常高。 |
5. 訪問者設計模式實戰示例
以下是一個基于訪問者設計模式的 Spring 項目實戰示例,應用于金融風控場景,使用注解方式注入對象,涵蓋了完整的結構。
金融風控中,需要對不同類型的用戶(例如:個人、企業)進行多維度風險評估,比如信用評分、交易行為分析、黑名單檢查等。不同用戶類型暴露的數據結構不同,但我們希望將“風險評估邏輯”從數據結構中解耦出來。
使用訪問者模式可實現:
- 新增風險評估邏輯(訪問者)無需修改用戶結構;
- 用戶數據結構與評估操作解耦,符合開閉原則。
5.1. 📦 項目結構
risk-visitor
├── model
│ ├── User.java
│ ├── PersonalUser.java
│ └── CompanyUser.java
├── visitor
│ ├── RiskVisitor.java
│ ├── CreditScoreVisitor.java
│ └── FraudCheckVisitor.java
├── config
│ └── VisitorConfig.java
└── RiskEvaluationService.java
5.2. 用戶對象層(Element)
public interface User {void accept(RiskVisitor visitor);
}
@Data
public class PersonalUser implements User {private String name;private String idCard;private int creditScore;@Overridepublic void accept(RiskVisitor visitor) {visitor.visit(this);}
}
@Data
public class CompanyUser implements User {private String companyName;private String licenseNumber;private double registeredCapital;@Overridepublic void accept(RiskVisitor visitor) {visitor.visit(this);}
}
5.3. 風控訪問者接口與實現
public interface RiskVisitor {void visit(PersonalUser personalUser);void visit(CompanyUser companyUser);
}
5.3.1. 信用評分訪問者
@Component
public class CreditScoreVisitor implements RiskVisitor {@Overridepublic void visit(PersonalUser personalUser) {System.out.println("[信用評分] 用戶 " + personalUser.getName() + " 分數: " + personalUser.getCreditScore());}@Overridepublic void visit(CompanyUser companyUser) {System.out.println("[信用評分] 公司 " + companyUser.getCompanyName() + " 注冊資本: " + companyUser.getRegisteredCapital());}
}
5.3.2. 欺詐檢測訪問者
@Component
public class FraudCheckVisitor implements RiskVisitor {@Overridepublic void visit(PersonalUser personalUser) {System.out.println("[欺詐檢查] 檢查身份證是否黑名單:" + personalUser.getIdCard());}@Overridepublic void visit(CompanyUser companyUser) {System.out.println("[欺詐檢查] 檢查營業執照是否異常:" + companyUser.getLicenseNumber());}
}
5.4. 風控服務類(注解注入訪問者)
@Service
public class RiskEvaluationService {private final List<RiskVisitor> visitors;@Autowiredpublic RiskEvaluationService(List<RiskVisitor> visitors) {this.visitors = visitors;}public void evaluate(User user) {for (RiskVisitor visitor : visitors) {user.accept(visitor);}}
}
5.5. 啟動與使用
@SpringBootApplication
public class RiskApp implements CommandLineRunner {@Autowiredprivate RiskEvaluationService evaluationService;@Overridepublic void run(String... args) {PersonalUser personalUser = new PersonalUser();personalUser.setName("張三");personalUser.setIdCard("123456789");personalUser.setCreditScore(750);CompanyUser companyUser = new CompanyUser();companyUser.setCompanyName("風控科技");companyUser.setLicenseNumber("XYZ-2025");companyUser.setRegisteredCapital(5000_000);evaluationService.evaluate(personalUser);evaluationService.evaluate(companyUser);}public static void main(String[] args) {SpringApplication.run(RiskApp.class, args);}
}
5.6. ? 總結優點
- 易于擴展新評估策略,無需改動用戶結構;
- Spring 自動注入訪問者集合,靈活組合;
- 清晰分離了數據結構與操作行為。
6. 訪問者設計模式思考
訪問者設計模式(Visitor Pattern)在實際開發中常常與其他設計模式組合使用,以增強系統的可擴展性、解耦能力和靈活性。下面列出訪問者模式常與哪些設計模式組合使用,以及各組合的典型應用場景和優勢。
6.1. ? 訪問者模式常用組合設計模式
組合模式 | 組合目的/優勢 | 應用場景示例 |
組合模式(Composite) | 用于遍歷和訪問復雜對象結構,訪問者可遞歸處理整個樹形結構 | 文檔結構、組織架構、產品分類樹等 |
迭代器模式(Iterator) | 統一遍歷容器結構,配合訪問者實現對集合中元素的操作(如批量處理) | 批量風控評估、設備監控列表操作 |
責任鏈模式(Chain of Responsibility) | 多個訪問者對象串聯處理,解耦多個處理邏輯,每個訪問者判斷是否處理 | 多規則風控審批流程,每個處理節點負責一類校驗 |
模板方法模式(Template Method) | 訪問者中封裝處理通用流程,將子類特定行為抽象為鉤子方法 | 通用風險評估框架,子類定義評分規則 |
策略模式(Strategy) | 將訪問者作為策略進行注入或切換,使不同訪問行為可配置化 | 不同國家/行業的風險評估策略 |
狀態模式(State) | 被訪問對象的狀態決定了訪問者邏輯流程,用于基于狀態執行不同操作 | 用戶行為軌跡、風控狀態遷移等 |
工廠模式(Factory) | 訪問者工廠根據上下文動態創建訪問者對象,適配不同對象結構或執行策略 | 動態風控策略調度系統,按對象類型或場景創建訪問者 |
觀察者模式(Observer) | 訪問者中執行完后通知監聽者,適用于監控、日志、審計等異步行為 | 風控決策日志記錄、報警事件觸發 |
博文參考
- 訪問者設計模式
- 設計模式之訪問者模式 | DESIGN