关于java:重新认识访问者模式从实践到本质

6次阅读

共计 17567 个字符,预计需要花费 44 分钟才能阅读完成。

简介:访问者模式在设计模式中的知名度尽管不如单例模式,但也是少数几个大家都能叫得上名字的设计模式了。不过因为访问者模式的复杂性,人们很少在利用零碎中应用,通过本文的摸索,咱们肯定会产生新的意识,发现其更加灵便宽泛的应用形式。

作者 | 悬衡
起源 | 阿里技术公众号

访问者模式在设计模式中的知名度尽管不如单例模式,但也是少数几个大家都能叫得上名字的设计模式了(另外几个可能就是“观察者模式”,“工厂模式”了)。不过因为访问者模式的复杂性,人们很少在利用零碎中应用,通过本文的摸索,咱们肯定会产生新的意识,发现其更加灵便宽泛的应用形式。

和个别介绍设计模式的文章不同,本文不会执着于死板的代码模板,而是间接从开源我的项目以及利用零碎中的实际登程,同时对比其余相似的设计模式,最初论述其在编程范式中的实质。

一 Calcite 中的访问者模式

开源类库经常利用访问者格调的 API 屏蔽外部的复杂性,从这些 API 动手学习,可能让咱们先取得一个直观感触。

Calcite 是一个 Java 语言编写的数据库根底类库,诸如 Hive,Spark 等诸多出名开源我的项目都在应用。其中 SQL 解析模块提供了访问者模式的 API,咱们利用它的 API 能够疾速获取 SQL 中咱们须要的信息,以获取 SQL 中应用的所有函数为例:

import org.apache.calcite.sql.SqlCall;
import org.apache.calcite.sql.SqlFunction;
import org.apache.calcite.sql.SqlNode;
import org.apache.calcite.sql.parser.SqlParseException;
import org.apache.calcite.sql.parser.SqlParser;
import org.apache.calcite.sql.util.SqlBasicVisitor;

import java.util.ArrayList;
import java.util.List;

public class CalciteTest {public static void main(String[] args) throws SqlParseException {String sql = "select concat('test-', upper(name)) from test limit 3";
        SqlParser parser = SqlParser.create(sql);
        SqlNode stmt = parser.parseStmt();
        FunctionExtractor functionExtractor = new FunctionExtractor();
        stmt.accept(functionExtractor);
        // [CONCAT, UPPER]
        System.out.println(functionExtractor.getFunctions());
    }

    private static class FunctionExtractor extends SqlBasicVisitor< Void> {private final List< String> functions = new ArrayList<>();

        @Override
        public Void visit(SqlCall call) {if (call.getOperator() instanceof SqlFunction) {functions.add(call.getOperator().getName());
            }
            return super.visit(call);
        }

        public List< String> getFunctions() {return functions;}
    }
}

代码中 FunctionExtractor 是 SqlBasicVisitor 的子类,并且重写了它的 visit(SqlCall) 办法,获取函数的名称并收集在了 functions 中。

除了 visit(SqlCall) 外,还能够通过 visit(SqlLiteral)(常量),visit(SqlIdentifier)(表名 / 列名)等等,实现更加简单的剖析。

有人会想,为什么 SqlParser 不间接提供相似于 getFunctions 等办法间接获取 SQL 中的所有函数呢?在上文的示例中,getFunctions 可能的确更加不便,然而 SQL 作为一个很简单的构造,getFunctions 对于更加简单的剖析场景是不够灵便的,性能也是更差的。如果须要,齐全能够很简略地实现一个如上文的 FunctionExtractor 来满足需要。

二 入手实现访问者模式

咱们尝试实现一个简化版的 SqlVisitor。

先定义一个简化版的 SQL 构造。

将 select upper(name) from test where age > 20; 拆解到这个构造上层级关系如图:

咱们间接在 Java 代码中将上图的构造结构进去:

  SqlNode sql = new SelectNode(
             new FieldsNode(Arrays.asList(
                     new FunctionCallExpression("upper", Arrays.asList(new IdExpression("name")
                     ))
             )),
             Arrays.asList("test"),
             new WhereNode(Arrays.asList(new OperatorExpression(new IdExpression("age"),
                     ">",
                     new LiteralExpression("20")
             )))
     );

这个类中都有一个雷同的办法,就是 accept:

@Override
public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
}

这里会通过多态散发到 SqlVisitor 不同的 visit 办法上:

abstract class SqlVisitor< R> {abstract R visit(SelectNode selectNode);

    abstract R visit(FieldsNode fieldsNode);

    abstract R visit(WhereNode whereNode);

    abstract R visit(IdExpression idExpression);

    abstract R visit(FunctionCallExpression functionCallExpression);

    abstract R visit(OperatorExpression operatorExpression);

    abstract R visit(LiteralExpression literalExpression);
}

SQL 构造相干的类如下:

abstract class SqlNode {
    // 用来接管访问者的办法
    public abstract < R> R accept(SqlVisitor< R> sqlVisitor);
}

class SelectNode extends SqlNode {

    private final FieldsNode fields;

    private final List< String> from;

    private final WhereNode where;

    SelectNode(FieldsNode fields, List< String> from, WhereNode where) {
        this.fields = fields;
        this.from = from;
        this.where = where;
    }

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }

    //... get 办法省略
}

class FieldsNode extends SqlNode {

    private final List< Expression> fields;

    FieldsNode(List<Expression> fields) {this.fields = fields;}

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

class WhereNode extends SqlNode {
    private final List< Expression> conditions;

    WhereNode(List< Expression> conditions) {this.conditions = conditions;}

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

abstract class Expression extends SqlNode {

}

class IdExpression extends Expression {

    private final String id;

    protected IdExpression(String id) {this.id = id;}

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

class FunctionCallExpression extends Expression {

    private final String name;

    private final List< Expression> arguments;

    FunctionCallExpression(String name, List< Expression> arguments) {
        this.name = name;
        this.arguments = arguments;
    }

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

class LiteralExpression extends Expression {

    private final String literal;

    LiteralExpression(String literal) {this.literal = literal;}

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

class OperatorExpression extends Expression {

    private final Expression left;

    private final String operator;

    private final Expression right;

    OperatorExpression(Expression left, String operator, Expression right) {
        this.left = left;
        this.operator = operator;
        this.right = right;
    }

    @Override
    public < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);
    }
}

有的读者可能会留神到,每个类的 accept 办法的代码都是一样的,那为什么不间接写在父类 SqlNode 中呢?如果尝试一下就会发现根本无法通过编译,因为咱们的 SqlVisitor 中基本就没有提供 visit(SqlNode),即便增加了 visit(SqlNode),通过了编译,程序的运行后果也是不合乎预期的,因为此时所有的 visit 调用都会指向 visit(SqlNode),其余重载办法就形同虚设了。

导致这种景象的起因是,不同的 visit 办法相互之间只有参数不同,称为“重载”,而 Java 的“重载”又被称为“编译期多态”,只会依据 visit(this) 中 this 在编译时的类型决定调用哪个办法,而它在编译时的类型就是 SqlNode,只管它在运行时可能是不同的子类。

所以,咱们可能常常会据说用动静语言写访问者模式会更加简略,特地是反对模式匹配的函数式程序设计语言(这在 Java 18 中曾经有较好反对),前面咱们再回过头来用模式匹配从新实现下本大节的内容,看看是不是简略了很多。

接下来咱们像之前一样,是应用 SqlVisitor 尝试解析出 SQL 中所有的函数调用。

先实现一个 SqlVisitor,这个 SqlVisitor 所作的就是依据以后节点的构造以此调用 accept,最初将后果组装起来,遇到 FunctionCallExpression 时将函数名称增加到汇合中:

class FunctionExtractor extends SqlVisitor< List< String>> {

    @Override
    List< String> visit(SelectNode selectNode) {List<String> res = new ArrayList<>();
        res.addAll(selectNode.getFields().accept(this));
        res.addAll(selectNode.getWhere().accept(this));
        return res;
    }

    @Override
    List< String> visit(FieldsNode fieldsNode) {List< String> res = new ArrayList<>();
        for (Expression field : fieldsNode.getFields()) {res.addAll(field.accept(this));
        }
        return res;
    }

    @Override
    List< String> visit(WhereNode whereNode) {List< String> res = new ArrayList<>();
        for (Expression condition : whereNode.getConditions()) {res.addAll(condition.accept(this));
        }
        return res;
    }

    @Override
    List< String> visit(IdExpression idExpression) {return Collections.emptyList();
    }

    @Override
    List< String> visit(FunctionCallExpression functionCallExpression) {
        // 取得函数名称
        List< String> res = new ArrayList<>();
        res.add(functionCallExpression.getName());
        for (Expression argument : functionCallExpression.getArguments()) {res.addAll(argument.accept(this));
        }
        return res;
    }

    @Override
    List< String> visit(OperatorExpression operatorExpression) {List< String> res = new ArrayList<>();
        res.addAll(operatorExpression.getLeft().accept(this));
        res.addAll(operatorExpression.getRight().accept(this));
        return res;
    }

    @Override
    List< String> visit(LiteralExpression literalExpression) {return Collections.emptyList();
    }
}

main 中的代码如下:

public static void main(String[] args) {
      // sql 定义
      SqlNode sql = new SelectNode( //select
              // concat("test-", upper(name))
              new FieldsNode(Arrays.asList(
                      new FunctionCallExpression("concat", Arrays.asList(new LiteralExpression("test-"),
                              new FunctionCallExpression(
                                      "upper",
                                      Arrays.asList(new IdExpression("name"))
                              )
                      ))
              )),
              // from test
              Arrays.asList("test"),
              // where age > 20
              new WhereNode(Arrays.asList(new OperatorExpression(new IdExpression("age"),
                      ">",
                      new LiteralExpression("20")
              )))
      );
      // 应用 FunctionExtractor
      FunctionExtractor functionExtractor = new FunctionExtractor();
      List< String> functions = sql.accept(functionExtractor);
      // [concat, upper]
      System.out.println(functions);
  }

以上就是规范的访问者模式的实现,直观感触上比之前 Calcite 的 SqlBasicVisitor 用起来麻烦多了,咱们接下来就去实现 SqlBasicVisitor。

三 访问者模式与观察者模式

在应用 Calcite 实现的 FunctionExtractor 中,每次 Calcite 解析到函数就会调用咱们实现的 visit(SqlCall),称它为 listen(SqlCall) 仿佛比 visit 更加适合。这也显示了访问者模式与观察者模式的紧密联系。

在咱们本人实现的 FunctionExtractor 中,绝大多数代码都是在依照肯定的程序遍历各种构造,这是因为访问者模式给予了使用者足够的灵活性,能够让实现者自行决定遍历的程序,或者对不须要遍历的局部进行剪枝。

然而咱们的需要“解析出 SQL 中所有的函数”,并不关怀遍历的程序,只有在通过“函数”时告诉一下咱们即可,对于这种简略需要,访问者模式有点适度设计,观察者模式会更加适合。

大多数应用访问者模式的开源我的项目会给“规范访问者”提供一个默认实现,比方 Calcite 的 SqlBasicVisitor,默认实现会依照默认的程序对 SQL 构造进行遍历,而实现者只须要重写它关怀的局部就行了,这样就相当于在访问者模式的根底上又实现了观察者模式,即不失落访问者模式的灵活性,也取得观察者模式应用上的便利性。

咱们给本人的实现也来增加一个 SqlBasicVisitor 吧:

class SqlBasicVisitor< R> extends SqlVisitor< R> {

    @Override
    R visit(SelectNode selectNode) {selectNode.getFields().accept(this);
        selectNode.getWhere().accept(this);
        return null;
    }

    @Override
    R visit(FieldsNode fieldsNode) {for (Expression field : fieldsNode.getFields()) {field.accept(this);
        }
        return null;
    }

    @Override
    R visit(WhereNode whereNode) {for (Expression condition : whereNode.getConditions()) {condition.accept(this);
        }
        return null;
    }

    @Override
    R visit(IdExpression idExpression) {return null;}

    @Override
    R visit(FunctionCallExpression functionCallExpression) {for (Expression argument : functionCallExpression.getArguments()) {argument.accept(this);
        }
        return null;
    }

    @Override
    R visit(OperatorExpression operatorExpression) {operatorExpression.getLeft().accept(this);
        operatorExpression.getRight().accept(this);
        return null;
    }

    @Override
    R visit(LiteralExpression literalExpression) {return null;}
}

SqlBasicVisitor 给每个构造都提供了一个默认的拜访程序,应用这个类咱们来实现第二版的 FunctionExtractor:

class FunctionExtractor2 extends SqlBasicVisitor< Void> {private final List< String> functions = new ArrayList<>();

    @Override
    Void visit(FunctionCallExpression functionCallExpression) {functions.add(functionCallExpression.getName());
        return super.visit(functionCallExpression);
    }

    public List< String> getFunctions() {return functions;}
}

它的应用如下:

class Main {public static void main(String[] args) {
        SqlNode sql = new SelectNode(
                new FieldsNode(Arrays.asList(
                        new FunctionCallExpression("concat", Arrays.asList(new LiteralExpression("test-"),
                                new FunctionCallExpression(
                                        "upper",
                                        Arrays.asList(new IdExpression("name"))
                                )
                        ))
                )),
                Arrays.asList("test"),
                new WhereNode(Arrays.asList(new OperatorExpression(new IdExpression("age"),
                        ">",
                        new LiteralExpression("20")
                )))
        );
        FunctionExtractor2 functionExtractor = new FunctionExtractor2();
        sql.accept(functionExtractor);
        System.out.println(functionExtractor.getFunctions());
    }
}

四 访问者模式与责任链模式

ASM 也是一个提供访问者模式 API 的类库,用来解析与生成 Java 类文件,能想到的所有 Java 出名开源我的项目都有他的身影,Java8 的 Lambda 表达式个性甚至都是通过它来实现的。如果只是能解析与生成 Java 类文件,ASM 或者还不会那么受欢迎,更重要的是它优良的形象,它将罕用的性能形象为一个个小的访问者工具类,让简单的字节码操作变得像搭积木一样简略。

假如须要依照如下形式批改类文件:

  • 删除 name 属性
  • 给所有属性增加 @NonNull 注解

然而出于复用和模块化的角度思考,咱们想把两个步骤别离拆成独立的功能模块,而不是把代码写在一起。在 ASM 中,咱们能够别离实现两个小访问者,而后串在一起,就变成可能实现咱们需要的访问者了。

删除 name 属性的访问者:

class DeleteFieldVisitor extends ClassVisitor {
    // 删除的属性名称, 对于咱们的需要,它就是 "name"
    private final String deleteFieldName;

    public DeleteFieldVisitor(ClassVisitor classVisitor, String deleteFieldName) {super(Opcodes.ASM9, classVisitor);
        this.deleteFieldName = deleteFieldName;
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {if (name.equals(deleteFieldName)) {
            // 不再向上游传递该属性, 对于上游来说,就是被 "删除了"
            return null;
        }
        // super.visitField 会去持续调用上游 Visitor 的 visitField 办法
        return super.visitField(access, name, descriptor, signature, value);
    }
}

给所有属性增加 @NonNull 注解的访问者:

class AddAnnotationVisitor extends ClassVisitor {public AddAnnotationVisitor(ClassVisitor classVisitor) {super(Opcodes.ASM9, classVisitor);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) {FieldVisitor fieldVisitor = super.visitField(access, name, descriptor, signature, value);
        // 向上游 Visitor 额定传递一个 @NonNull 注解
        fieldVisitor.visitAnnotation("javax/annotation/Nonnull", false);
        return fieldVisitor;
    }
}

在 main 中咱们将它们串起来应用:

public class AsmTest {public static void main(String[] args) throws URISyntaxException, IOException {Path clsPath = Paths.get(AsmTest.class.getResource("/visitordp/User.class").toURI());
        byte[] clsBytes = Files.readAllBytes(clsPath);

        // 串联 Visitor
        // finalVisitor =  DeleteFieldVisitor -> AddAnnotationVisitor -> ClassWriter
        // ClassWriter 自身也是 ClassVisitor 的子类 
        ClassWriter cw = new ClassWriter(ClassWriter.COMPUTE_FRAMES);
        ClassVisitor finalVisitor = new DeleteFieldVisitor(new AddAnnotationVisitor(cw), 
                                                           "name");

        // ClassReader 就是被拜访的对象
        ClassReader cr = new ClassReader(clsBytes);
        cr.accept(finalVisitor, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);

        byte[] bytes = cw.toByteArray();
        Files.write(clsPath, bytes);
    }

}

通过访问者模式与责任链模式的联合,咱们不再须要将所有的逻辑都写在一个访问者中,咱们能够拆分出多个通用的访问者,通过组合他们实现更加多种多样的需要。

五 访问者模式与回调模式

“回调”能够算是“设计模式的设计模式”了,大量设计模式中都有它的思维,诸如观察者模式中的“观察者”,“命令模式”中的“命令”,“状态模式”中的“状态”实质上都能够看成一个回调函数。

访问者模式中的“访问者”显然也是一个回调,和其余回调模式最大的不同是,“访问者模式”是一种带有“导航”的回调,咱们通过传入的对象构造给实现者下一步回调的“导航”,实现者依据“导航”决定下一步回调的秩序。

如果我想先拜访 fieldA,再拜访 fieldB,最初 fieldC,对应到访问者的实现就是:

visit(SomeObject someObject) {someObject.fieldA.accept(this);
    someObject.fieldB.accept(this);
    someObject.fieldC.accept(this);
}

这在理论中的利用就是 HATEOAS(Hypermedia as the Engine of Application State),HATEOAS 格调的 HTTP 接口除了会返回用户申请的数据外,还会蕴含用户下一步应该拜访的 URL,如果将整个利用的 API 比作一个家的话,如果用户申请客厅的数据,那么接口除了返回客厅的数据外,还会返回与客厅相连的 “ 厨房 ”,“卧室”与“卫生间”的 URL:

这样做的益处是,能够无缝地降级与更换资源的 URL(因为这些 URL 都是服务端返回的),而且开发者在不须要文档的状况下顺着导航,能够摸索学会 API 的应用,解决了 API 组织凌乱的问题。对于 HATEOAS 更理论的例子能够见 How to GET a Cup of Coffee[1]。

六 理论利用

之前举的例子可能都更偏差于开源根底类库的利用,那么在更加宽泛的利用零碎中,它要如何利用呢?

1 简单的嵌套构造拜访

当初的 toB 利用为了满足不同企业的稀奇古怪的定制需要,提供的配置性能越來越简单,配置项之间不再是简略的正交独立的关系,而是互相嵌套递归,正是访问者模式施展的场合。

钉钉审批的流程配置就是一个十分复杂的构造:

做过简化的审批流模型如下:

模型和流程配置的对应关系如下图:

RouteNode 除了像一般节点一样通过 next 连贯下一个节点,其中蕴含的每个 condition 又是一个残缺的流程配置(递归定义),由此可见审批节点模型是简单的嵌套构造。

除了整体结构复杂外,每个节点的配置也相当简单:

面对如此简单的配置,最好能通过配置解析二方包(下文中都简称为 SDK)对应用层屏蔽配置的复杂性。如果 SDK 只是返回一个图构造给应用层的话,应用层就不得不感知节点之间的关联并且每次都须要编写容易出错的遍历算法,此时访问者模式就变成了咱们的不二之选。

访问者模式的实现套路和之前一样的,就不多说了,咱们举个应用层例子:

仿真:让用户在不理论运行流程的状况下就能看到流程的执行分支,不便调试

class ProcessSimulator implements ProcessConfigVisitor {private List< String> traces = new ArrayList<>();

    @Override
    public void visit(StartNode startNode) {if (startNode.next != null) {startNode.next.accept(this);
        }
    }

    @Override
    public void visit(RouteNode routeNode) {
        // 计算出满足条件的分支
        for (CondtionNode conditionNode : routeNode.conditions) {if (evalCondition(conditionNode.condition)) {conditionNode.accept(this);
            }
        }
        if (routeNode.next != null) {routeNode.next.accept(this);
        }
    }

    @Override
    public void visit(ConditionNode conditionNode) {if (conditionNode.next != null) {conditionNode.next.accept(this);
        }
    }

    @Override
    public void visit(ApproveNode approveNode) {
        // 记录下在仿真中拜访到的审批节点
        traces.add(approveNode.id);
        if (approveNode.next != null) {approveNode.next.accept(this);
        }
    }
}

2 SDK 隔离内部调用

为了保障 SDK 的纯正性,个别 SDK 中都不会去调用内部接口,然而为了实现一些需要又不得不这么做,此时咱们能够将内部调用放在应用层访问者的实现中,而后传入 SDK 中执行相干逻辑。

在下面提到的流程仿真中过程,条件计算常会包含内部接口调用,比方通过连接器调用一个用户指定接口决定流程分支,为了保障流程配置解析 SDK 的纯正性,不可能在 SDK 包中进行调用的,因而就在访问者中调用。

七 应用 Java18 实现访问者模式

回到最后的命题,用访问者模式取得 SQL 中所有的函数调用。后面说过,用函数式编程语言中常见的模式匹配能够更加不便地实现,而最新的 Java18 中曾经对此有比拟好的反对。

从 Java 14 开始,Java 反对了一种新的 Record 数据类型,示例如下:

// sealed 示意胶囊类型, 即 Expression 只容许是以后文件中 Num 和 Add
sealed interface Expression {
    // record 关键字代替 class, 用于定义 Record 数据类型
    record Num(int value) implements Expression {}
    record Add(int left, int right) implements Expression {}}

TestRecord 一旦实例化,字段就是不可变的,并且它的 equals 和 hashCode 办法会被主动重写,只有外部的字段都相等,它们就是相等的:

public static void main(String[] args) {Num n1 = new Num(2);
    // n1.value = 10; 这行代码会导致编译不过
    Num n2 = new Num(2);
    // true
    System.out.println(n1.equals(n2));
}

更加不便的是,利用 Java 18 中最新的模式匹配性能,能够拆解出其中的属性:

public int eval(Expression e) {return switch (e) {case Num(int value) -> value;
        case Add(int left, int right) -> left + right;
    };
}

咱们首先应用 Record 类型从新定义咱们的 SQL 构造:

sealed interface SqlNode {record SelectNode(FieldsNode fields, List< String> from, WhereNode where) implements SqlNode {}
    record FieldsNode(List< Expression> fields) implements SqlNode {}
    record WhereNode(List< Expression> conditions) implements SqlNode {}
    sealed interface Expression extends SqlNode {record IdExpression(String id) implements Expression {}
        record FunctionCallExpression(String name, List< Expression> arguments) implements Expression {}
        record LiteralExpression(String literal) implements Expression {}
        record OperatorExpression(Expression left, String operator, Expression right) implements Expression {}}
}

而后利用模式匹配,一个办法即可实现之前的拜访,取得所有函数调用:

public List< String> extractFunctions(SqlNode sqlNode) {return switch (sqlNode) {case SelectNode(FieldsNode fields, List< String> from, WhereNode where) -> {List< String> res = new ArrayList<>();
            res.addAll(extractFunctions(fields));
            res.addAll(extractFunctions(where));
            return res;
        }
        case FieldsNode(List< Expression> fields) -> {List< String> res = new ArrayList<>();
            for (Expression field : fields) {res.addAll(extractFunctions(field));
            }
            return res;
        }
        case WhereNode(List< Expression> conditions) -> {List< String> res = new ArrayList<>();
            for (Expression condition : conditions) {res.addAll(extractFunctions(condition));
            }
            return res;
        }
        case IdExpression(String id) -> Collections.emptyList();
        case FunctionCallExpression(String name, List< Expression> arguments) -> {
            // 取得函数名称
            List< String> res = new ArrayList<>();
            res.add(name);
            for (Expression argument : arguments) {res.addAll(extractFunctions(argument));
            }
            return res;
        }
        case LiteralExpression(String literal) -> Collections.emptyList();
        case OperatorExpression(Expression left, String operator, Expression right) -> {List< String> res = new ArrayList<>();
            res.addAll(extractFunctions(left));
            res.addAll(extractFunctions(right));
            return res;
        }
    }
}

比照一下第二大节的代码,最大的区别就是 sqlNode.accept(visitor) 被换成了对 extractFunctions 的递归调用。另外就是本来通过类来封装的行为,变成了更加轻量的函数。咱们将在下一大节探讨其更加深刻的含意。

八 重新认识访问者模式

在 GoF 的设计模式原著中,对访问者模式的形容如下:

示意一个作用于某对象构造中的各元素的操作。它使你能够在不扭转各元素的类的前提下定义作用于这些元素的新操作。

从这句话能够看出,访问者模式实现的所有性能实质上都能够通过给每个对象减少新的成员办法实现,利用面向对象多态的个性,父构造调用并且聚合子结构相应办法的返回后果,以之前的抽取 SQL 所有函数为例,这一次不必访问者实现,而是在每个类中减少一个 extractFunctions 成员办法:

class SelectNode extends SqlNode {

    private final FieldsNode fields;

    private final List< String> from;

    private final WhereNode where;

    SelectNode(FieldsNode fields, List< String> from, WhereNode where) {
        this.fields = fields;
        this.from = from;
        this.where = where;
    }

    public FieldsNode getFields() {return fields;}

    public List< String> getFrom() {return from;}

    public WhereNode getWhere() {return where;}

    public List< String> extractFunctions() {List<String> res = new ArrayList<>();
        // 持续调用子结构的 extractFunctions
        res.addAll(fields.extractFunctions());
        res.addAll(selectNode.extractFunctions());
        return res;
    }
}

访问者模式实质上就是将简单的类层级构造中成员办法全副都形象到一个类中去:

这两种编写形式有什么区别呢?Visitor 这个名字尽管看起来像名词,然而从后面的例子和阐述来看,它的实现类全副是对于操作的形象,从模式匹配的实现形式中就更能看出这一点,ASM 中甚至将 Visitor 作为一个个小操作的形象进行排列组合,因而两种编写形式也对应两种世界观:

  • 面向对象:认为操作必须和数据绑定到一起,即作为每个类的成员办法存在,而不是独自抽取进去成为一个访问者
  • 函数式编程:将数据和操作拆散,将基本操作进行排列组合成为更加简单的操作,而一个访问者的实现就对应一个操作

这两个形式,在编写的时候看起来区别不大,只有当须要增加批改性能的时候能力显现出他们的天壤之别,假如当初咱们要给每个类减少一个新操作:

这种场景看起来是减少访问者更加不便。那么再看下一个场景,假如当初要在类层级构造中减少一个新类:

这两个场景对应了软件的两种拆分形式,一种是依照数据拆分,一种是依照性能点拆分,以阿里双十一的各个分会场与性能为例:盒马,饿了么和聚划算别离作为一个分会场参加了双十一的促销,他们都须要提供优惠券,订单和领取等性能。

尽管在用户看来 盒马,饿了么和聚划算是三个不同的利用,然而底层零碎能够有两种划分形式:

任何一种划分形式都要接受该种形式带来的毛病。所有事实中的利用,不论是架构还是编码,都没有下面的例子那么极其,而是两种混用。比方 盒马,饿了么,聚划算 都能够在领有本人零碎的同时,复用优惠券这样的按性能划分的零碎。对应到编码也是一样,软件工程没有银弹,咱们也要依据个性和场景决定是采纳面向对象的形象,还是访问者的形象。更多的时候须要两者混用,将局部外围办法作为对象成员,利用访问者模式实现应用层的那些琐碎芜杂的需要。

原文链接
本文为阿里云原创内容,未经容许不得转载。

正文完
 0