访问者模式:设计与实践

24 阅读14分钟

访问者模式:设计与实践

一、什么是访问者模式

1. 基本定义

访问者模式(Visitor Pattern)是一种行为型设计模式,由《设计模式:可复用面向对象软件的基础》(GOF著作)定义为:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作

该模式通过引入一个访问者对象,将对集合中各类元素的操作逻辑封装在访问者中,而非元素类内部。核心是实现“数据结构”与“数据操作”的分离,当需要对元素执行新操作时,只需添加新的访问者,无需修改元素类。

2. 核心思想

访问者模式的核心在于分离数据与操作。当系统中存在一个稳定的数据结构(如由多个不同类型元素组成的集合),但需要对这些元素执行多变的操作时,通过访问者模式可将操作逻辑从元素类中抽离,使元素类专注于数据存储,访问者专注于操作实现。这种设计既保证了数据结构的稳定性,又为操作逻辑提供了灵活的扩展能力。

二、访问者模式的特点

1. 分离数据结构与操作

元素类只负责存储数据,操作逻辑由访问者实现,两者相互独立,符合单一职责原则。

2. 新增操作便捷

添加新操作只需创建新的访问者类,无需修改现有元素类和其他访问者,符合开闭原则。

3. 集中化操作逻辑

同一类操作的逻辑集中在一个访问者中,便于维护和管理(如统一修改统计口径)。

4. 支持多态操作

访问者能根据元素类型执行不同操作(通过方法重载),实现对不同元素的差异化处理。

5. 遍历复杂结构

访问者模式常与组合模式结合,能高效遍历复杂的对象结构(如树形结构)。

特点说明
分离数据结构与操作元素存储数据,访问者实现操作,职责分离
新增操作便捷新增操作只需添加访问者,无需修改元素
集中化操作逻辑同类操作集中在一个访问者中,便于维护
支持多态操作通过方法重载对不同元素执行差异化处理
遍历复杂结构适合遍历由多种元素组成的复杂集合

三、访问者模式的标准代码实现

1. 模式结构

访问者模式包含五个核心角色:

  • 抽象元素(Element):定义接收访问者的接口,声明accept(Visitor visitor)方法。
  • 具体元素(ConcreteElement):实现抽象元素接口,accept方法中调用访问者的对应方法。
  • 抽象访问者(Visitor):声明对所有具体元素的访问操作(方法重载)。
  • 具体访问者(ConcreteVisitor):实现抽象访问者接口,定义对每种元素的具体操作。
  • 对象结构(ObjectStructure):管理元素集合,提供遍历元素的方法,通常会迭代调用元素的accept方法。

2. 代码实现示例

2.1 抽象元素与具体元素
/**
 * 抽象元素
 */
public interface Element {
    /**
     * 接收访问者
     */
    void accept(Visitor visitor);
}

/**
 * 具体元素A
 */
public class ConcreteElementA implements Element {
    private String propertyA; // 元素A特有属性

    public ConcreteElementA(String propertyA) {
        this.propertyA = propertyA;
    }

    // 元素A特有方法
    public String getPropertyA() {
        return propertyA;
    }

    @Override
    public void accept(Visitor visitor) {
        // 调用访问者对元素A的操作
        visitor.visit(this);
    }
}

/**
 * 具体元素B
 */
public class ConcreteElementB implements Element {
    private int propertyB; // 元素B特有属性

    public ConcreteElementB(int propertyB) {
        this.propertyB = propertyB;
    }

    // 元素B特有方法
    public int getPropertyB() {
        return propertyB;
    }

    @Override
    public void accept(Visitor visitor) {
        // 调用访问者对元素B的操作
        visitor.visit(this);
    }
}
2.2 抽象访问者与具体访问者
/**
 * 抽象访问者
 * 声明对所有具体元素的访问方法
 */
public interface Visitor {
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}

/**
 * 具体访问者1:执行操作X
 */
public class ConcreteVisitorX implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("访问者X操作元素A,属性值:" + element.getPropertyA());
        // 元素A的操作X逻辑
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("访问者X操作元素B,属性值:" + element.getPropertyB());
        // 元素B的操作X逻辑
    }
}

/**
 * 具体访问者2:执行操作Y
 */
public class ConcreteVisitorY implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("访问者Y操作元素A,属性长度:" + element.getPropertyA().length());
        // 元素A的操作Y逻辑
    }

    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("访问者Y操作元素B,属性平方:" + element.getPropertyB() * element.getPropertyB());
        // 元素B的操作Y逻辑
    }
}
2.3 对象结构
import java.util.ArrayList;
import java.util.List;

/**
 * 对象结构
 * 管理元素集合,提供遍历接口
 */
public class ObjectStructure {
    private List<Element> elements = new ArrayList<>();

    // 添加元素
    public void addElement(Element element) {
        elements.add(element);
    }

    // 移除元素
    public void removeElement(Element element) {
        elements.remove(element);
    }

    // 接受访问者,遍历所有元素
    public void accept(Visitor visitor) {
        for (Element element : elements) {
            element.accept(visitor);
        }
    }
}
2.4 客户端使用示例
/**
 * 客户端
 */
public class Client {
    public static void main(String[] args) {
        // 创建对象结构并添加元素
        ObjectStructure structure = new ObjectStructure();
        structure.addElement(new ConcreteElementA("元素A1"));
        structure.addElement(new ConcreteElementB(20));
        structure.addElement(new ConcreteElementA("元素A2"));
        structure.addElement(new ConcreteElementB(30));

        // 访问者X执行操作
        System.out.println("=== 访问者X执行操作 ===");
        Visitor visitorX = new ConcreteVisitorX();
        structure.accept(visitorX);

        // 访问者Y执行操作
        System.out.println("\n=== 访问者Y执行操作 ===");
        Visitor visitorY = new ConcreteVisitorY();
        structure.accept(visitorY);
    }
}

3. 代码实现特点总结

角色核心职责代码特点
抽象元素(Element)定义接收访问者的接口声明accept(Visitor visitor)方法,是所有元素的统一接口
具体元素(ConcreteElement)实现元素接口,对接访问者实现accept方法,调用visitor.visit(this)将自身传递给访问者
抽象访问者(Visitor)声明对所有具体元素的访问方法包含与具体元素对应的重载visit方法,如visit(ConcreteElementA)
具体访问者(ConcreteVisitor)实现对元素的具体操作实现Visitor接口,在visit方法中定义对特定元素的操作逻辑
对象结构(ObjectStructure)管理元素集合,提供遍历能力包含元素集合和accept方法,迭代调用所有元素的accept方法

四、支付框架设计中访问者模式的运用

支付交易数据的多维度统计分析为例,说明访问者模式在支付系统中的具体应用:

1. 场景分析

支付系统需要对交易数据进行多维度统计,典型需求包括:

  • 按支付渠道统计交易笔数和金额(如支付宝、微信支付)
  • 按交易金额区间统计分布(如0-100元、100-1000元)
  • 按交易状态统计成功率(成功、失败、处理中)
  • 按时间段统计交易趋势(如每小时交易量)

交易数据的结构(PaymentTransaction)相对稳定,但统计维度可能频繁新增(如按商户类型、地区统计)。若将统计逻辑直接写入交易类,会导致交易类臃肿且难以维护,访问者模式可完美解决这一问题。

2. 设计实现

2.1 交易元素与访问者接口
import java.math.BigDecimal;
import java.time.LocalDateTime;

/**
 * 交易元素接口
 */
public interface TransactionElement {
    void accept(TransactionVisitor visitor);
}

/**
 * 支付交易(具体元素)
 */
public class PaymentTransaction implements TransactionElement {
    private String transactionId; // 交易ID
    private String channel; // 支付渠道(alipay/wechat/unionpay)
    private BigDecimal amount; // 交易金额
    private String status; // 状态(SUCCESS/FAIL/PROCESSING)
    private LocalDateTime createTime; // 交易时间
    private String merchantId; // 商户ID

    public PaymentTransaction(String transactionId, String channel, BigDecimal amount,
                             String status, LocalDateTime createTime, String merchantId) {
        this.transactionId = transactionId;
        this.channel = channel;
        this.amount = amount;
        this.status = status;
        this.createTime = createTime;
        this.merchantId = merchantId;
    }

    // getter方法
    public String getTransactionId() { return transactionId; }
    public String getChannel() { return channel; }
    public BigDecimal getAmount() { return amount; }
    public String getStatus() { return status; }
    public LocalDateTime getCreateTime() { return createTime; }
    public String getMerchantId() { return merchantId; }

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

/**
 * 退款交易(具体元素,扩展场景)
 */
public class RefundTransaction implements TransactionElement {
    private String refundId;
    private String originalTransactionId; // 原交易ID
    private BigDecimal refundAmount;
    private String status;

    // 构造方法和getter省略

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

/**
 * 交易访问者接口
 */
public interface TransactionVisitor {
    // 访问支付交易
    void visit(PaymentTransaction transaction);
    // 访问退款交易(扩展支持)
    void visit(RefundTransaction transaction);
}
2.2 具体统计访问者
2.2.1 渠道统计访问者
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 按渠道统计访问者
 */
public class ChannelStatVisitor implements TransactionVisitor {
    // 渠道→交易笔数
    private Map<String, Integer> channelCountMap = new HashMap<>();
    // 渠道→交易总金额
    private Map<String, BigDecimal> channelAmountMap = new HashMap<>();

    @Override
    public void visit(PaymentTransaction transaction) {
        String channel = transaction.getChannel();
        // 累计笔数
        channelCountMap.put(channel, channelCountMap.getOrDefault(channel, 0) + 1);
        // 累计金额
        BigDecimal total = channelAmountMap.getOrDefault(channel, BigDecimal.ZERO);
        channelAmountMap.put(channel, total.add(transaction.getAmount()));
    }

    @Override
    public void visit(RefundTransaction transaction) {
        // 退款交易暂不统计渠道数据(或按原交易渠道统计)
    }

    // 获取统计结果
    public Map<String, StatResult> getChannelStats() {
        Map<String, StatResult> result = new HashMap<>();
        channelCountMap.forEach((channel, count) -> {
            result.put(channel, new StatResult(count, channelAmountMap.get(channel)));
        });
        return result;
    }

    // 统计结果模型
    public static class StatResult {
        private int count;
        private BigDecimal totalAmount;

        public StatResult(int count, BigDecimal totalAmount) {
            this.count = count;
            this.totalAmount = totalAmount;
        }

        // getter省略
    }
}
2.2.2 金额区间统计访问者
import java.math.BigDecimal;
import java.util.HashMap;
import java.util.Map;

/**
 * 按金额区间统计访问者
 */
public class AmountRangeVisitor implements TransactionVisitor {
    private Map<String, Integer> rangeCountMap = new HashMap<>();

    @Override
    public void visit(PaymentTransaction transaction) {
        BigDecimal amount = transaction.getAmount();
        // 定义区间
        String range = getAmountRange(amount);
        // 累计笔数
        rangeCountMap.put(range, rangeCountMap.getOrDefault(range, 0) + 1);
    }

    @Override
    public void visit(RefundTransaction transaction) {
        // 退款交易按退款金额统计
        BigDecimal amount = transaction.getRefundAmount();
        String range = getAmountRange(amount);
        rangeCountMap.put(range, rangeCountMap.getOrDefault(range, 0) + 1);
    }

    // 确定金额区间
    private String getAmountRange(BigDecimal amount) {
        if (amount.compareTo(new BigDecimal("100")) < 0) {
            return "0-100元";
        } else if (amount.compareTo(new BigDecimal("1000")) < 0) {
            return "100-1000元";
        } else if (amount.compareTo(new BigDecimal("10000")) < 0) {
            return "1000-10000元";
        } else {
            return "10000元以上";
        }
    }

    // 获取统计结果
    public Map<String, Integer> getRangeStats() {
        return rangeCountMap;
    }
}
2.2.3 交易状态统计访问者
import java.util.HashMap;
import java.util.Map;

/**
 * 按状态统计访问者
 */
public class StatusStatVisitor implements TransactionVisitor {
    private Map<String, Integer> statusCountMap = new HashMap<>();

    @Override
    public void visit(PaymentTransaction transaction) {
        String status = transaction.getStatus();
        statusCountMap.put(status, statusCountMap.getOrDefault(status, 0) + 1);
    }

    @Override
    public void visit(RefundTransaction transaction) {
        String status = transaction.getStatus();
        // 退款状态前缀标识
        statusCountMap.put("REFUND_" + status, statusCountMap.getOrDefault("REFUND_" + status, 0) + 1);
    }

    // 获取统计结果
    public Map<String, Integer> getStatusStats() {
        return statusCountMap;
    }
}
2.3 对象结构与客户端使用
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

/**
 * 交易集合(对象结构)
 */
public class TransactionRepository {
    private List<TransactionElement> transactions = new ArrayList<>();

    // 添加交易
    public void addTransaction(TransactionElement transaction) {
        transactions.add(transaction);
    }

    // 批量添加交易
    public void addTransactions(List<TransactionElement> transactions) {
        this.transactions.addAll(transactions);
    }

    // 接受访问者统计
    public void accept(TransactionVisitor visitor) {
        for (TransactionElement transaction : transactions) {
            transaction.accept(visitor);
        }
    }
}

/**
 * 统计服务(客户端)
 */
public class TransactionStatService {
    public void analyzeTransactions() {
        // 1. 准备交易数据
        TransactionRepository repository = new TransactionRepository();
        repository.addTransactions(buildSampleTransactions());

        // 2. 按渠道统计
        ChannelStatVisitor channelVisitor = new ChannelStatVisitor();
        repository.accept(channelVisitor);
        System.out.println("=== 渠道统计结果 ===");
        channelVisitor.getChannelStats().forEach((channel, result) -> 
            System.out.printf("%s:%d笔,总金额%s%n", 
                channel, result.getCount(), result.getTotalAmount()));

        // 3. 按金额区间统计
        AmountRangeVisitor amountVisitor = new AmountRangeVisitor();
        repository.accept(amountVisitor);
        System.out.println("\n=== 金额区间统计结果 ===");
        amountVisitor.getRangeStats().forEach((range, count) -> 
            System.out.printf("%s:%d笔%n", range, count));

        // 4. 按状态统计
        StatusStatVisitor statusVisitor = new StatusStatVisitor();
        repository.accept(statusVisitor);
        System.out.println("\n=== 状态统计结果 ===");
        statusVisitor.getStatusStats().forEach((status, count) -> 
            System.out.printf("%s:%d笔%n", status, count));
    }

    // 构建示例交易数据
    private List<TransactionElement> buildSampleTransactions() {
        List<TransactionElement> transactions = new ArrayList<>();
        // 添加支付交易
        transactions.add(new PaymentTransaction("T1", "alipay", new BigDecimal("200"), 
            "SUCCESS", LocalDateTime.now(), "MCH001"));
        transactions.add(new PaymentTransaction("T2", "wechat", new BigDecimal("50"), 
            "SUCCESS", LocalDateTime.now(), "MCH002"));
        transactions.add(new PaymentTransaction("T3", "alipay", new BigDecimal("1500"), 
            "FAIL", LocalDateTime.now(), "MCH001"));
        // 添加退款交易
        transactions.add(new RefundTransaction("R1", "T1", new BigDecimal("100"), "SUCCESS"));
        return transactions;
    }

    public static void main(String[] args) {
        new TransactionStatService().analyzeTransactions();
    }
}

3. 模式价值体现

  • 统计逻辑与交易数据分离PaymentTransaction只存储交易数据,统计逻辑在访问者中实现,交易类无需包含各种统计方法,符合单一职责原则
  • 新增统计维度便捷:如需添加“按商户类型统计”,只需新增MerchantTypeVisitor,无需修改交易类和现有访问者,符合开闭原则
  • 多维度统计可并行:同一批交易数据可同时被多个访问者处理(如一次遍历完成渠道、金额、状态统计),提高统计效率
  • 差异化处理不同交易类型:通过访问者的方法重载,可对PaymentTransactionRefundTransaction执行不同统计逻辑(如退款金额单独统计)
  • 统计逻辑集中管理:同类统计逻辑(如所有金额相关统计)可集中在对应的访问者中,便于统一修改统计口径(如调整金额区间划分)

五、开源框架中访问者模式的运用

MyBatis的SQL节点解析器为例,说明访问者模式在开源框架中的典型应用:

1. 核心实现分析

MyBatis在解析Mapper.xml中的动态SQL节点(如<if><where><foreach>)时,使用了访问者模式。SQL节点构成了稳定的数据结构,而对节点的操作(如解析为SQL片段、参数处理)则封装在访问者中。

1.1 元素接口与具体元素

MyBatis中SqlNode接口作为抽象元素,定义了apply方法(类似accept):

/**
 * SQL节点接口(抽象元素)
 */
public interface SqlNode {
    boolean apply(DynamicContext context);
}

/**
 * 文本节点(具体元素)
 */
public class TextSqlNode implements SqlNode {
    private String text;

    @Override
    public boolean apply(DynamicContext context) {
        // 处理文本节点
        return true;
    }
}

/**
 * If节点(具体元素)
 */
public class IfSqlNode implements SqlNode {
    private ExpressionEvaluator evaluator;
    private String test;
    private SqlNode contents;

    @Override
    public boolean apply(DynamicContext context) {
        // 处理if节点
        return true;
    }
}
1.2 访问者实现

MyBatis通过NodeVisitor接口定义对节点的访问操作,XMLScriptBuilder作为具体访问者解析节点:

/**
 * 节点访问者接口
 */
public interface NodeVisitor {
    void visit(TextSqlNode node);
    void visit(IfSqlNode node);
    void visit(WhereSqlNode node);
    // 其他节点的visit方法
}

/**
 * SQL脚本构建器(具体访问者)
 */
public class XMLScriptBuilder implements NodeVisitor {
    private final Configuration configuration;
    private final XNode context;

    @Override
    public void visit(TextSqlNode node) {
        // 解析文本节点为SQL片段
    }

    @Override
    public void visit(IfSqlNode node) {
        // 解析if节点,处理test条件
    }

    // 解析所有节点
    public SqlNode parseScriptNode() {
        // 遍历所有SQL节点并应用访问者
        return processNodes(context.getChildren());
    }
}
1.3 对象结构

MyBatis中MixedSqlNode作为对象结构,管理多个SqlNode元素:

/**
 * 混合SQL节点(对象结构)
 */
public class MixedSqlNode implements SqlNode {
    private final List<SqlNode> contents;

    public MixedSqlNode(List<SqlNode> contents) {
        this.contents = contents;
    }

    @Override
    public boolean apply(DynamicContext context) {
        // 遍历所有子节点并应用
        for (SqlNode sqlNode : contents) {
            sqlNode.apply(context);
        }
        return true;
    }
}

2. 访问者模式在MyBatis中的价值

  • SQL节点与解析逻辑分离SqlNode专注于存储节点数据,解析逻辑由XMLScriptBuilder实现,便于维护
  • 支持多种解析策略:可通过不同访问者实现对同一批节点的不同解析逻辑(如动态SQL生成、节点校验)
  • 扩展新节点类型便捷:新增自定义SQL节点(如<authorize>权限节点)只需实现SqlNode并在访问者中添加visit方法

六、总结

1. 访问者模式的适用场景

  • 当需要对一个稳定的数据结构(如由多种元素组成的集合)执行多种不相关的操作时
  • 当新增操作比新增元素更频繁,且希望新增操作时无需修改元素类时
  • 当需要对元素执行复杂的差异化操作(根据元素类型执行不同逻辑)时
  • 当需要遍历复杂对象结构(如树形结构)并对每个节点执行操作时

2. 访问者模式与其他模式的区别

  • 与策略模式:两者都封装变化的逻辑,但策略模式封装的是算法本身,访问者模式封装的是对元素的操作,且能处理多种元素类型
  • 与迭代器模式:迭代器模式专注于遍历集合,访问者模式专注于对集合元素执行操作,常结合使用(迭代器遍历+访问者操作)
  • 与命令模式:命令模式封装的是“对象+操作”的组合,访问者模式则是“操作+多元素”的组合,更适合批量处理

3. 支付系统中的实践价值

  • 简化交易数据类:交易类只存储核心数据,统计、分析等操作由访问者实现,避免类膨胀
  • 灵活应对业务变化:新的统计需求(如监管要求的特定维度统计)可通过新增访问者快速实现
  • 提高代码复用性:同一访问者可在不同场景中复用(如渠道统计可用于报表、监控、对账等模块)
  • 便于团队协作:数据维护与业务分析可由不同团队负责(数据团队维护交易类,业务团队开发访问者)

4. 实践建议

  • 谨慎使用于频繁变更的元素结构:若元素类型频繁新增(如不断添加新的交易类型),会导致访问者接口频繁修改,违反开闭原则
  • 元素类提供足够的访问接口:访问者需要获取元素数据,元素类应提供必要的getter方法,避免访问者直接操作元素的私有字段
  • 结合组合模式处理复杂结构:当元素构成树形结构(如订单包含子订单),可结合组合模式,让访问者递归处理所有节点
  • 使用接口默认方法兼容扩展:在Java 8+中,访问者接口可通过默认方法(default)处理新增元素类型,减少对现有访问者的影响

访问者模式通过“分离数据结构与操作逻辑”,为支付系统中稳定数据与多变操作的场景提供了优雅解决方案,特别适合交易统计、多维度分析等场景,既能保证核心数据类的稳定,又能让业务操作灵活扩展,是支付架构设计中的重要模式。