【设计模式手册018】访问者模式 - 分离数据结构与操作

【设计模式手册018】访问者模式 - 分离数据结构与操作

本文是「设计模式手册」系列第018篇,我将以深入浅出、追本溯源的风格,带你真正理解访问者模式的精髓。

1. 我们为何需要访问者模式?

在软件设计中,我们经常会遇到这样的场景:需要对一个复杂对象结构中的各个元素执行某些操作,但这些操作的具体实现各不相同。比如:

  • 编译器设计:抽象语法树的类型检查、代码生成、优化
  • 文档处理:HTML/XML文档的渲染、导出、分析
  • 文件系统:文件和目录的搜索、统计、权限检查
  • UI组件:界面元素的渲染、布局计算、事件处理

初级程序员的写法

java 复制代码
// 抽象语法树节点
public interface ASTNode {
    String getType();
}

public class VariableNode implements ASTNode {
    private String name;
    private String type;
    
    @Override
    public String getType() { return "Variable"; }
    
    // 为了支持不同操作,不断添加方法
    public void typeCheck() {
        System.out.println("类型检查变量: " + name);
    }
    
    public void generateCode() {
        System.out.println("生成变量代码: " + name);
    }
    
    public void optimize() {
        System.out.println("优化变量: " + name);
    }
}

public class FunctionNode implements ASTNode {
    private String name;
    private List<ASTNode> parameters;
    
    @Override
    public String getType() { return "Function"; }
    
    // 同样需要添加各种操作方法
    public void typeCheck() {
        System.out.println("类型检查函数: " + name);
        for (ASTNode param : parameters) {
            // 需要类型转换,违反开闭原则
            if (param instanceof VariableNode) {
                ((VariableNode) param).typeCheck();
            }
        }
    }
    
    public void generateCode() {
        System.out.println("生成函数代码: " + name);
        for (ASTNode param : parameters) {
            if (param instanceof VariableNode) {
                ((VariableNode) param).generateCode();
            }
        }
    }
    
    // 每增加一个新操作,就要修改所有节点类
}

这种写法的痛点

  • 违反开闭原则:每增加新操作都要修改所有节点类
  • 职责混乱:节点类既包含数据结构又包含操作逻辑
  • 类型转换:需要大量的instanceof检查和类型转换
  • 难以维护:操作逻辑分散在各个节点类中

2. 访问者模式:本质与定义

2.1 模式定义

访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。

2.2 模式结构

java 复制代码
// 访问者接口
public interface Visitor {
    void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
    void visit(ConcreteElementC element);
}

// 元素接口
public interface Element {
    void accept(Visitor visitor);
}

// 具体元素
public class ConcreteElementA implements Element {
    private String name;
    
    public ConcreteElementA(String name) {
        this.name = name;
    }
    
    public String getName() {
        return name;
    }
    
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);  // 关键:调用访问者的visit方法
    }
}

public class ConcreteElementB implements Element {
    private int value;
    
    public ConcreteElementB(int value) {
        this.value = value;
    }
    
    public int getValue() {
        return value;
    }
    
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

// 具体访问者
public class ConcreteVisitor implements Visitor {
    @Override
    public void visit(ConcreteElementA element) {
        System.out.println("访问元素A: " + element.getName());
        // 对元素A的特定操作
    }
    
    @Override
    public void visit(ConcreteElementB element) {
        System.out.println("访问元素B: " + element.getValue());
        // 对元素B的特定操作
    }
    
    @Override
    public void visit(ConcreteElementC element) {
        // 对元素C的特定操作
    }
}

// 对象结构
public class ObjectStructure {
    private List<Element> elements = new ArrayList<>();
    
    public void addElement(Element element) {
        elements.add(element);
    }
    
    public void accept(Visitor visitor) {
        for (Element element : elements) {
            element.accept(visitor);  // 双分派的关键
        }
    }
}

3. 深入理解:访问者模式的三重境界

3.1 第一重:双分派(Double Dispatch)

核心思想:通过两次动态绑定,在运行时确定要调用的具体方法。

java 复制代码
// 第一次分派:element.accept(visitor) -> 具体元素的accept方法
// 第二次分派:visitor.visit(this) -> 具体访问者的visit方法

public class ElementA implements Element {
    @Override
    public void accept(Visitor visitor) {
        // 第一次分派:根据element的实际类型调用accept
        visitor.visit(this);  // 第二次分派:根据visitor的实际类型调用visit
    }
}

3.2 第二重:数据与操作的分离

设计原则的体现:元素类只负责维护数据结构,访问者类负责实现具体操作。

java 复制代码
// 好的设计:职责清晰
public class ASTNode { /* 只包含数据结构 */ }
public class TypeChecker implements Visitor { /* 只包含类型检查逻辑 */ }
public class CodeGenerator implements Visitor { /* 只包含代码生成逻辑 */ }

3.3 第三重:开闭原则的极致体现

元素结构稳定后,新增操作只需添加新的访问者,无需修改现有代码。

java 复制代码
// 新增操作,只需新增访问者
public class NewOperationVisitor implements Visitor {
    // 实现各个visit方法
}

// 使用方式不变
objectStructure.accept(new NewOperationVisitor());

4. 实战案例:完整的编译器抽象语法树处理

让我们来看一个完整的例子:

java 复制代码
// 抽象语法树节点
public interface ASTNode {
    void accept(ASTVisitor visitor);
    Position getPosition();
}

@Data
@AllArgsConstructor
class Position {
    private int line;
    private int column;
    private String fileName;
}

// 具体节点类型
@Data
public class VariableNode implements ASTNode {
    private String name;
    private String type;
    private Position position;
    
    public VariableNode(String name, String type, Position position) {
        this.name = name;
        this.type = type;
        this.position = position;
    }
    
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visit(this);
    }
}

@Data
public class FunctionNode implements ASTNode {
    private String name;
    private String returnType;
    private List<VariableNode> parameters;
    private BlockNode body;
    private Position position;
    
    public FunctionNode(String name, String returnType, List<VariableNode> parameters, 
                       BlockNode body, Position position) {
        this.name = name;
        this.returnType = returnType;
        this.parameters = parameters;
        this.body = body;
        this.position = position;
    }
    
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visit(this);
    }
}

@Data
public class BlockNode implements ASTNode {
    private List<ASTNode> statements;
    private Position position;
    
    public BlockNode(List<ASTNode> statements, Position position) {
        this.statements = statements;
        this.position = position;
    }
    
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visit(this);
    }
}

@Data
public class AssignmentNode implements ASTNode {
    private VariableNode left;
    private ASTNode right;
    private Position position;
    
    public AssignmentNode(VariableNode left, ASTNode right, Position position) {
        this.left = left;
        this.right = right;
        this.position = position;
    }
    
    @Override
    public void accept(ASTVisitor visitor) {
        visitor.visit(this);
    }
}

// 访问者接口
public interface ASTVisitor {
    void visit(VariableNode node);
    void visit(FunctionNode node);
    void visit(BlockNode node);
    void visit(AssignmentNode node);
}

// 类型检查访问者
@Slf4j
public class TypeCheckVisitor implements ASTVisitor {
    private final SymbolTable symbolTable = new SymbolTable();
    private final List<String> errors = new ArrayList<>();
    
    @Override
    public void visit(VariableNode node) {
        log.debug("类型检查变量: {}:{}", node.getName(), node.getType());
        
        // 检查变量是否已声明
        if (!symbolTable.contains(node.getName())) {
            errors.add(String.format("错误[%d:%d]: 变量 %s 未声明", 
                node.getPosition().getLine(), node.getPosition().getColumn(), node.getName()));
        }
    }
    
    @Override
    public void visit(FunctionNode node) {
        log.debug("类型检查函数: {}", node.getName());
        
        // 进入新的作用域
        symbolTable.enterScope();
        
        // 检查参数类型
        for (VariableNode param : node.getParameters()) {
            symbolTable.declare(param.getName(), param.getType());
        }
        
        // 检查函数体
        if (node.getBody() != null) {
            node.getBody().accept(this);
        }
        
        // 退出作用域
        symbolTable.exitScope();
    }
    
    @Override
    public void visit(BlockNode node) {
        log.debug("类型检查代码块");
        
        symbolTable.enterScope();
        
        for (ASTNode statement : node.getStatements()) {
            statement.accept(this);
        }
        
        symbolTable.exitScope();
    }
    
    @Override
    public void visit(AssignmentNode node) {
        log.debug("类型检查赋值语句");
        
        // 检查左值是否已声明
        if (!symbolTable.contains(node.getLeft().getName())) {
            errors.add(String.format("错误[%d:%d]: 赋值前变量 %s 未声明", 
                node.getLeft().getPosition().getLine(), 
                node.getLeft().getPosition().getColumn(),
                node.getLeft().getName()));
        }
        
        // 检查右值
        node.getRight().accept(this);
    }
    
    public boolean hasErrors() {
        return !errors.isEmpty();
    }
    
    public List<String> getErrors() {
        return new ArrayList<>(errors);
    }
}

// 代码生成访问者
@Slf4j
public class CodeGenerationVisitor implements ASTVisitor {
    private final StringBuilder code = new StringBuilder();
    private int indentLevel = 0;
    
    @Override
    public void visit(VariableNode node) {
        code.append(node.getName());
    }
    
    @Override
    public void visit(FunctionNode node) {
        // 生成函数头
        appendIndent();
        code.append("function ").append(node.getName()).append("(");
        
        // 生成参数
        for (int i = 0; i < node.getParameters().size(); i++) {
            if (i > 0) code.append(", ");
            node.getParameters().get(i).accept(this);
        }
        
        code.append(") {\n");
        
        // 生成函数体
        indentLevel++;
        if (node.getBody() != null) {
            node.getBody().accept(this);
        }
        indentLevel--;
        
        appendIndent();
        code.append("}\n\n");
    }
    
    @Override
    public void visit(BlockNode node) {
        for (ASTNode statement : node.getStatements()) {
            statement.accept(this);
        }
    }
    
    @Override
    public void visit(AssignmentNode node) {
        appendIndent();
        node.getLeft().accept(this);
        code.append(" = ");
        node.getRight().accept(this);
        code.append(";\n");
    }
    
    private void appendIndent() {
        for (int i = 0; i < indentLevel; i++) {
            code.append("    ");
        }
    }
    
    public String getGeneratedCode() {
        return code.toString();
    }
}

// 符号表
@Slf4j
public class SymbolTable {
    private final Deque<Map<String, String>> scopes = new ArrayDeque<>();
    
    public SymbolTable() {
        enterScope(); // 全局作用域
    }
    
    public void enterScope() {
        scopes.push(new HashMap<>());
        log.debug("进入新作用域,当前深度: {}", scopes.size());
    }
    
    public void exitScope() {
        if (scopes.size() > 1) {
            scopes.pop();
            log.debug("退出作用域,当前深度: {}", scopes.size());
        }
    }
    
    public void declare(String name, String type) {
        if (!scopes.isEmpty()) {
            scopes.peek().put(name, type);
            log.debug("声明变量: {}:{}", name, type);
        }
    }
    
    public boolean contains(String name) {
        return scopes.stream().anyMatch(scope -> scope.containsKey(name));
    }
    
    public String getType(String name) {
        return scopes.stream()
                    .filter(scope -> scope.containsKey(name))
                    .map(scope -> scope.get(name))
                    .findFirst()
                    .orElse(null);
    }
}

// 使用示例
@Slf4j
public class CompilerDemo {
    public static void main(String[] args) {
        // 构建一个简单的AST:function add(a, b) { result = a + b; }
        Position pos = new Position(1, 1, "test.js");
        
        // 参数
        List<VariableNode> parameters = Arrays.asList(
            new VariableNode("a", "number", pos),
            new VariableNode("b", "number", pos)
        );
        
        // 函数体:result = a + b
        // 注意:这里简化了表达式节点
        VariableNode resultVar = new VariableNode("result", "number", pos);
        VariableNode aVar = new VariableNode("a", "number", pos);
        VariableNode bVar = new VariableNode("b", "number", pos);
        
        // 赋值语句
        AssignmentNode assignment = new AssignmentNode(resultVar, aVar, pos); // 简化
        
        BlockNode body = new BlockNode(Arrays.asList(assignment), pos);
        
        // 函数定义
        FunctionNode function = new FunctionNode("add", "number", parameters, body, pos);
        
        // 类型检查
        TypeCheckVisitor typeChecker = new TypeCheckVisitor();
        function.accept(typeChecker);
        
        if (typeChecker.hasErrors()) {
            log.error("类型检查失败:");
            typeChecker.getErrors().forEach(log::error);
        } else {
            log.info("类型检查通过");
        }
        
        // 代码生成
        CodeGenerationVisitor codeGenerator = new CodeGenerationVisitor();
        function.accept(codeGenerator);
        
        log.info("生成的代码:\n{}", codeGenerator.getGeneratedCode());
    }
}

5. Spring Boot中的优雅实现

在Spring Boot中,我们可以利用依赖注入让访问者模式更加优雅:

java 复制代码
// 报表元素接口
public interface ReportElement {
    void accept(ReportVisitor visitor);
}

// 具体报表元素
@Data
public class HeaderElement implements ReportElement {
    private String title;
    private LocalDate generationDate;
    
    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

@Data
public class TableElement implements ReportElement {
    private List<String> headers;
    private List<List<String>> rows;
    
    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

@Data
public class ChartElement implements ReportElement {
    private String chartType;
    private Map<String, Number> data;
    private String title;
    
    @Override
    public void accept(ReportVisitor visitor) {
        visitor.visit(this);
    }
}

// 报表访问者接口
public interface ReportVisitor {
    void visit(HeaderElement element);
    void visit(TableElement element);
    void visit(ChartElement element);
    
    String getResult();
}

// HTML导出访问者
@Component
@Slf4j
public class HtmlExportVisitor implements ReportVisitor {
    private final StringBuilder html = new StringBuilder();
    
    @Override
    public void visit(HeaderElement element) {
        log.debug("导出HTML页眉");
        html.append("<header>")
            .append("<h1>").append(element.getTitle()).append("</h1>")
            .append("<p>生成时间: ").append(element.getGenerationDate()).append("</p>")
            .append("</header>\n");
    }
    
    @Override
    public void visit(TableElement element) {
        log.debug("导出HTML表格");
        html.append("<table class='report-table'>\n")
            .append("<thead><tr>");
        
        for (String header : element.getHeaders()) {
            html.append("<th>").append(header).append("</th>");
        }
        html.append("</tr></thead>\n<tbody>");
        
        for (List<String> row : element.getRows()) {
            html.append("<tr>");
            for (String cell : row) {
                html.append("<td>").append(cell).append("</td>");
            }
            html.append("</tr>");
        }
        
        html.append("</tbody>\n</table>\n");
    }
    
    @Override
    public void visit(ChartElement element) {
        log.debug("导出HTML图表");
        html.append("<div class='chart'>")
            .append("<h3>").append(element.getTitle()).append("</h3>")
            .append("<div id='chart-" + element.hashCode() + "' data-type='")
            .append(element.getChartType()).append("'>");
        
        for (Map.Entry<String, Number> entry : element.getData().entrySet()) {
            html.append("<div class='data-point' data-label='")
                .append(entry.getKey()).append("' data-value='")
                .append(entry.getValue()).append("'></div>");
        }
        
        html.append("</div></div>\n");
    }
    
    @Override
    public String getResult() {
        return "<!DOCTYPE html>\n<html>\n<body>\n" + html.toString() + "\n</body>\n</html>";
    }
}

// PDF导出访问者
@Component
@Slf4j
public class PdfExportVisitor implements ReportVisitor {
    private final StringBuilder pdfContent = new StringBuilder();
    
    @Override
    public void visit(HeaderElement element) {
        log.debug("导出PDF页眉");
        pdfContent.append("*** ").append(element.getTitle()).append(" ***\n")
                 .append("生成时间: ").append(element.getGenerationDate()).append("\n\n");
    }
    
    @Override
    public void visit(TableElement element) {
        log.debug("导出PDF表格");
        
        // 表头
        for (String header : element.getHeaders()) {
            pdfContent.append(String.format("%-15s", header));
        }
        pdfContent.append("\n");
        
        // 分隔线
        for (int i = 0; i < element.getHeaders().size(); i++) {
            pdfContent.append("---------------");
        }
        pdfContent.append("\n");
        
        // 数据行
        for (List<String> row : element.getRows()) {
            for (String cell : row) {
                pdfContent.append(String.format("%-15s", cell));
            }
            pdfContent.append("\n");
        }
        pdfContent.append("\n");
    }
    
    @Override
    public void visit(ChartElement element) {
        log.debug("导出PDF图表");
        pdfContent.append("图表: ").append(element.getTitle()).append("\n");
        pdfContent.append("类型: ").append(element.getChartType()).append("\n");
        
        for (Map.Entry<String, Number> entry : element.getData().entrySet()) {
            pdfContent.append("  ").append(entry.getKey()).append(": ").append(entry.getValue()).append("\n");
        }
        pdfContent.append("\n");
    }
    
    @Override
    public String getResult() {
        return pdfContent.toString();
    }
}

// 报表生成服务
@Service
@Slf4j
public class ReportService {
    private final HtmlExportVisitor htmlVisitor;
    private final PdfExportVisitor pdfVisitor;
    
    public ReportService(HtmlExportVisitor htmlVisitor, PdfExportVisitor pdfVisitor) {
        this.htmlVisitor = htmlVisitor;
        this.pdfVisitor = pdfVisitor;
    }
    
    public String generateHtmlReport(List<ReportElement> elements) {
        log.info("生成HTML报表");
        elements.forEach(element -> element.accept(htmlVisitor));
        return htmlVisitor.getResult();
    }
    
    public String generatePdfReport(List<ReportElement> elements) {
        log.info("生成PDF报表");
        elements.forEach(element -> element.accept(pdfVisitor));
        return pdfVisitor.getResult();
    }
    
    public byte[] generateExcelReport(List<ReportElement> elements) {
        // 可以轻松添加新的导出格式,无需修改现有代码
        log.info("生成Excel报表");
        // 实现Excel导出逻辑...
        return new byte[0];
    }
}

// REST控制器
@RestController
@RequestMapping("/api/reports")
@Slf4j
public class ReportController {
    private final ReportService reportService;
    
    public ReportController(ReportService reportService) {
        this.reportService = reportService;
    }
    
    @PostMapping("/export/html")
    public ResponseEntity<String> exportHtml(@RequestBody List<ReportElement> elements) {
        try {
            String html = reportService.generateHtmlReport(elements);
            return ResponseEntity.ok(html);
        } catch (Exception e) {
            log.error("HTML导出失败", e);
            return ResponseEntity.status(500).body("导出失败");
        }
    }
    
    @PostMapping("/export/pdf")
    public ResponseEntity<String> exportPdf(@RequestBody List<ReportElement> elements) {
        try {
            String pdf = reportService.generatePdfReport(elements);
            return ResponseEntity.ok(pdf);
        } catch (Exception e) {
            log.error("PDF导出失败", e);
            return ResponseEntity.status(500).body("导出失败");
        }
    }
}

6. 访问者模式的变体与进阶用法

6.1 带上下文的访问者

java 复制代码
// 带上下文的访问者
public class ContextAwareVisitor implements ASTVisitor {
    private final VisitContext context;
    
    public ContextAwareVisitor(VisitContext context) {
        this.context = context;
    }
    
    @Override
    public void visit(VariableNode node) {
        // 使用上下文信息
        if (context.isInLoop()) {
            // 循环内的特殊处理
        }
        // 正常处理...
    }
    
    // 其他visit方法...
}

// 访问上下文
@Data
public class VisitContext {
    private boolean inLoop = false;
    private boolean inFunction = false;
    private int loopDepth = 0;
    private String currentFunction = null;
    
    public void enterLoop() {
        inLoop = true;
        loopDepth++;
    }
    
    public void exitLoop() {
        loopDepth--;
        if (loopDepth == 0) {
            inLoop = false;
        }
    }
    
    public void enterFunction(String functionName) {
        inFunction = true;
        currentFunction = functionName;
    }
    
    public void exitFunction() {
        inFunction = false;
        currentFunction = null;
    }
}

6.2 访问者模式 + 组合模式

结合组合模式处理树形结构:

java 复制代码
// 组合节点
public class CompositeNode implements ASTNode {
    private List<ASTNode> children = new ArrayList<>();
    
    public void addChild(ASTNode node) {
        children.add(node);
    }
    
    @Override
    public void accept(ASTVisitor visitor) {
        // 先访问自身(如果需要)
        // 然后访问所有子节点
        for (ASTNode child : children) {
            child.accept(visitor);
        }
    }
}

// 专用的组合访问者
public abstract class CompositeVisitor implements ASTVisitor {
    @Override
    public void visit(CompositeNode node) {
        // 默认实现:遍历所有子节点
        for (ASTNode child : node.getChildren()) {
            child.accept(this);
        }
    }
    
    // 其他visit方法...
}

7. 访问者模式 vs 其他模式

7.1 访问者模式 vs 迭代器模式

  • 迭代器模式:遍历集合元素,但不暴露内部结构
  • 访问者模式:对集合元素执行操作,需要知道元素的具体类型

7.2 访问者模式 vs 策略模式

  • 策略模式:封装算法,使算法可以相互替换
  • 访问者模式:封装对对象结构的操作,使操作可以独立变化

7.3 访问者模式 vs 装饰器模式

  • 装饰器模式:动态添加功能,透明扩展
  • 访问者模式:集中管理相关操作,非透明扩展

8. 总结与思考

8.1 访问者模式的优点

  1. 开闭原则:新增操作容易,无需修改元素类
  2. 单一职责:将相关操作集中在一个访问者中
  3. 灵活性:可以在不修改类层次结构的情况下定义新操作
  4. 数据分离:数据结构和操作逻辑完全分离

8.2 访问者模式的缺点

  1. 破坏封装:访问者需要了解元素的具体类,破坏封装性
  2. 元素变更困难:增加新的元素类需要修改所有访问者
  3. 复杂性:双分派机制理解成本较高
  4. 依赖具体类:访问者依赖具体元素类,而非抽象

8.3 深入思考

访问者模式的本质是**"操作的外部化"**。它将本该属于元素类的操作提取到独立的访问者类中,通过双分派机制在运行时绑定具体操作。

设计之美的思考

"访问者模式在稳定数据结构和变化操作之间找到了完美的平衡点。当你的对象结构相对稳定,但需要频繁添加新操作时,访问者模式就像是为你的系统安装了一个可插拔的'功能卡槽'。"

从源码的角度看,访问者模式在现实世界中有广泛应用:

  • Java的FileVisitor接口(NIO.2文件遍历)
  • ASM字节码操作框架
  • Java编译器注解处理器
  • 各种报表生成和数据导出系统

何时使用访问者模式

  • 对象结构相对稳定,但经常需要定义新操作
  • 需要对对象结构中的元素进行很多不同且不相关的操作
  • 操作需要跨越多个类,且这些类没有共同的接口
  • 希望避免污染元素类的接口

使用场景

  • 编译器抽象语法树处理
  • 复杂文档导出(HTML/PDF/Excel)
  • UI组件树遍历和操作
  • 复杂数据结构的分析和统计

下一篇预告:设计模式手册019 - 状态模式:如何优雅地管理对象状态转换?


版权声明:本文为CSDN博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接及本声明。

相关推荐
LYFlied8 小时前
【每日算法】LeetCode 560. 和为 K 的子数组
前端·数据结构·算法·leetcode·职场和发展
fei_sun8 小时前
【数据结构】败者树、B树、排序、查找
数据结构·b树
LYFlied8 小时前
【每日算法】 LeetCode 394. 字符串解码
前端·数据结构·算法·leetcode·面试·职场和发展
Dylan的码园8 小时前
栈与stack
java·数据结构·链表
有一个好名字8 小时前
设计模式-适配器模式
设计模式·适配器模式
lxh01138 小时前
复原IP地址
前端·数据结构·算法
好学且牛逼的马8 小时前
Apache Commons DbUtils
java·设计模式·apache
LYFlied9 小时前
【每日算法】LeetCode 20. 有效的括号
数据结构·算法·leetcode·面试
明洞日记9 小时前
【设计模式手册019】状态模式 - 管理对象状态转换
java·设计模式·状态模式