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

简介:访问者模式在设计模式中的知名度虽然不如单例模式,但也是少数几个大家都能叫得上名字的设计模式了。不过因为访问者模式的复杂性,人们很少在应用系统中使用,经过本文的探索,我们一定会产生新的认识,发现其更加灵活广泛的使用方式。

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

访问者模式在设计模式中的知名度虽然不如单例模式,但也是少数几个大家都能叫得上名字的设计模式了(另外几个可能就是“观察者模式”,“工厂模式” 了)。不过因为访问者模式的复杂性,人们很少在应用系统中使用,经过本文的探索,我们一定会产生新的认识,发现其更加灵活广泛的使用方式。

和一般介绍设计模式的文章不同,本文不会执着于死板的代码模板,而是直接从开源项目以及应用系统中的实践出发,同时对比其他类似的设计模式,最后阐述其在编程范式中的本质。

一 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<>();@Overridepublic 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;}@Overridepublic < 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;}@Overridepublic < 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;}@Overridepublic < 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;}@Overridepublic < 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;}@Overridepublic < R> R accept(SqlVisitor< R> sqlVisitor) {return sqlVisitor.visit(this);}
}class LiteralExpression extends Expression {private final String literal;LiteralExpression(String literal) {this.literal = literal;}@Overridepublic < 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;}@Overridepublic < 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>> {@OverrideList< String> visit(SelectNode selectNode) {List<String> res = new ArrayList<>();res.addAll(selectNode.getFields().accept(this));res.addAll(selectNode.getWhere().accept(this));return res;}@OverrideList< String> visit(FieldsNode fieldsNode) {List< String> res = new ArrayList<>();for (Expression field : fieldsNode.getFields()) {res.addAll(field.accept(this));}return res;}@OverrideList< String> visit(WhereNode whereNode) {List< String> res = new ArrayList<>();for (Expression condition : whereNode.getConditions()) {res.addAll(condition.accept(this));}return res;}@OverrideList< String> visit(IdExpression idExpression) {return Collections.emptyList();}@OverrideList< 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;}@OverrideList< String> visit(OperatorExpression operatorExpression) {List< String> res = new ArrayList<>();res.addAll(operatorExpression.getLeft().accept(this));res.addAll(operatorExpression.getRight().accept(this));return res;}@OverrideList< 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 testArrays.asList("test"),// where age > 20new WhereNode(Arrays.asList(new OperatorExpression(new IdExpression("age"),">",new LiteralExpression("20")))));// 使用 FunctionExtractorFunctionExtractor 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> {@OverrideR visit(SelectNode selectNode) {selectNode.getFields().accept(this);selectNode.getWhere().accept(this);return null;}@OverrideR visit(FieldsNode fieldsNode) {for (Expression field : fieldsNode.getFields()) {field.accept(this);}return null;}@OverrideR visit(WhereNode whereNode) {for (Expression condition : whereNode.getConditions()) {condition.accept(this);}return null;}@OverrideR visit(IdExpression idExpression) {return null;}@OverrideR visit(FunctionCallExpression functionCallExpression) {for (Expression argument : functionCallExpression.getArguments()) {argument.accept(this);}return null;}@OverrideR visit(OperatorExpression operatorExpression) {operatorExpression.getLeft().accept(this);operatorExpression.getRight().accept(this);return null;}@OverrideR visit(LiteralExpression literalExpression) {return null;}
}

SqlBasicVisitor 给每个结构都提供了一个默认的访问顺序,使用这个类我们来实现第二版的 FunctionExtractor:

class FunctionExtractor2 extends SqlBasicVisitor< Void> {private final List< String> functions = new ArrayList<>();@OverrideVoid 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 或许还不会那么受欢迎,更重要的是它优秀的抽象,它将常用的功能抽象为一个个小的访问者工具类,让复杂的字节码操作变得像搭积木一样简单。

假设需要按照如下方式修改类文件:

  1. 删除 name 属性
  2. 给所有属性添加 @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;}@Overridepublic 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);}@Overridepublic 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<>();@Overridepublic void visit(StartNode startNode) {if (startNode.next != null) {startNode.next.accept(this);}}@Overridepublic void visit(RouteNode routeNode) {// 计算出满足条件的分支for (CondtionNode conditionNode : routeNode.conditions) {if (evalCondition(conditionNode.condition)) {conditionNode.accept(this);}}if (routeNode.next != null) {routeNode.next.accept(this);}}@Overridepublic void visit(ConditionNode conditionNode) {if (conditionNode.next != null) {conditionNode.next.accept(this);}}@Overridepublic 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);// trueSystem.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<>();// 继续调用子结构的 extractFunctionsres.addAll(fields.extractFunctions());res.addAll(selectNode.extractFunctions());return res;}
}

访问者模式本质上就是将复杂的类层级结构中成员方法全部都抽象到一个类中去:

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

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

这两个方式,在编写的时候看起来区别不大,只有当需要添加修改功能的时候才能显现出他们的天壤之别,假设现在我们要给每个类增加一个新操作:

这种场景看起来是增加访问者更加方便。那么再看下一个场景,假设现在要在类层级结构中增加一个新类:

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

虽然在用户看来 盒马,饿了么和聚划算是三个不同的应用,但是底层系统可以有两种划分方式:

任何一种划分方式都要承受该种方式带来的缺点。所有现实中的应用,不论是架构还是编码,都没有上面的例子那么极端,而是两种混用。比如 盒马,饿了么,聚划算 都可以在拥有自己系统的同时,复用优惠券这样的按功能划分的系统。对应到编码也是一样,软件工程没有银弹,我们也要根据特性和场景决定是采用面向对象的抽象,还是访问者的抽象。更多的时候需要两者混用,将部分核心方法作为对象成员,利用访问者模式实现应用层的那些琐碎杂乱的需求。

原文链接

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

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/511297.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

3个案例,详解如何选择合适的研发模式

简介&#xff1a;3个案例&#xff0c;详解如何选择合适的研发模式&#xff0c;研发模式的选择与产品形态、发布方式、团队规模、协作成熟度密切相关。本文我们将根据不同的团队场景&#xff0c;分析如何选择适合团队的研发模式。 策划&编辑&#xff5c;雅纯 上一讲&#x…

如何打造一款极速数据湖分析引擎

简介&#xff1a;本文向读者详细揭秘了数据湖分析引擎的关键技术&#xff0c;并通过 StarRocks 来帮助用户进一步理解系统的架构。 作者&#xff1a; 阿里云 EMR 开源大数据 OLAP 团队 StarRocks 社区数据湖分析团队 前言 随着数字产业化和产业数字化成为经济驱动的重要动…

如何在 Linux 命令行中按大小对文件进行排序

作者 | 刘光录来源 | TIAPls 命令用于显示目录的内容。使用 -l 选项&#xff0c;可以列出文件和目录及其属性。今天我们来分享一下如何根据文件大小对列表进行排序。ls -l 命令可以显示文件大小&#xff0c;但也仅仅是能让我们看到文件的大小&#xff0c;它默认是按照字母顺序显…

福建品品香茶业有限公司业务迁移上云

福建品品香茶业有限公司数据量较大&#xff0c;进行业务迁移上云时阿里云根据其公司需求综合考虑&#xff0c;推荐将原有IOE架构改为分布式架构&#xff0c;使用ECSRDS承载业务&#xff0c;为客户带来极大价值。 企业介绍 福建品品香茶业有限公司是一家集茶叶种植、加工、销售…

璀璨智行:V2X车路协同智慧交通

V2X车用无线通信技术是指车对外界的信息交换&#xff0c;作为未来智能交通运输系统的关键技术&#xff0c;璀璨智行潜心研究V2X技术&#xff0c;致力于V2X车路协同的落地&#xff0c;在智慧交通领域做出了卓越的贡献。 创业机会点 魏军博表示&#xff1a;“面对交通系统效率低…

Databricks 企业版 SparkDelta Lake 引擎助力 Lakehouse 高效访问

简介&#xff1a;本文介绍了Databricks企业版Delta Lake的性能优势&#xff0c;借助这些特性能够大幅提升Spark SQL的查询性能&#xff0c;加快Delta表的查询速度。 作者&#xff1a; 李锦桂&#xff08;锦犀&#xff09; 阿里云开源大数据平台开发工程师 王晓龙&#xff08…

深度解析数据湖存储方案Lakehouse架构

简介&#xff1a;从数据仓库、数据湖的优劣势&#xff0c;湖仓一体架构的应用和优势等多方面深度解析Lakehouse架构。 作者&#xff1a;张泊 Databricks 软件工程师 Lakehouse由lake和house两个词组合而成&#xff0c;其中lake代表Delta Lake&#xff08;数据湖&#xff09;&…

1688 复杂业务场景下的 Serverless 提效实践

简介&#xff1a;我们主要负责 PC 端 1688.com 以及手机端阿里巴巴 APP&#xff0c;是目前国内最大的 B 类电商交易平台&#xff0c;主要面向 B2B 电商业务的场景&#xff0c;为中小企业提供零售、批发、分销以及加工定制等电商交易渠道。 前言 首先为大家简单介绍一下我们的…

阿里 蚂蚁自研 IDE 研发框架 OpenSumi 正式开源

简介&#xff1a;经历近 3 年时间&#xff0c;在阿里集团及蚂蚁集团共建小组的努力下&#xff0c;OpenSumi 作为国内首个强定制性、高性能&#xff0c;兼容 VS Code 插件体系的 IDE 研发框架&#xff0c;今天正式对外开源。 作者 | OpenSumi 来源 | 阿里技术公众号 经历近 3 …

剖析 kubernetes 集群内部 DNS 解析原理

作者 | 江小南来源 | 江小南和他的小伙伴们引言说到DNS域名解析&#xff0c;大家想到最多的可能就是/etc/hosts文件&#xff0c;并没有什么错&#xff0c;但是/etc/hosts只能做到本机域名解析&#xff0c;如果跨机器的解析就有点捉襟见肘了。在服务器中还有一个配置值得大家注意…

首次公开,阿里云开源PolarDB总体架构和企业级特性

简介&#xff1a;在3月2日的阿里云开源 PolarDB 企业级架构发布会上&#xff0c;阿里云 PolarDB 内核技术专家北侠带来了主题为《PolarDB 总体架构设计和企业级特性》的精彩演讲。 在3月2日的阿里云开源 PolarDB 企业级架构发布会上&#xff0c;阿里云 PolarDB 内核技术专家 北…

阿里云数据库开源发布:PolarDB HTAP的功能特性和关键技术

简介&#xff1a;在3月2日的阿里云开源 PolarDB 企业级架构发布会上&#xff0c;阿里云 PolarDB 内核技术专家严华带来了主题为《PolarDB HTAP详解》的精彩演讲。在PolarDB存储计算分离架构的基础上&#xff0c;我们研发了基于共享存储的MPP分布式执行引擎&#xff0c;解决了单…

倒计时 2 天!2022 中国算力大会:移动云邀您共见算力网络,创新发展

7 月 29 日 - 31 日由工业和信息化部、山东省人民政府主办的首届中国算力大会将在泉城济南盛大举行&#xff01;中国移动受邀承办“算力网络&#xff0c;创新发展” 论坛并设立展区分享行业前瞻洞察&#xff0c;构建开放共赢生态7 月 29 日下午&#xff0c;邀您共话算力精彩&am…

什么是好的错误消息? 讨论一下Java系统中的错误码设计

简介&#xff1a;一个好的Error Message主要包含三个部分&#xff1a;Context: 什么导致了错误&#xff1f;发生错误的时候代码想做什么&#xff1f;The error itself: 到底是什么导致了失败&#xff1f;具体的原因和当时的数据是什么&#xff1f;Mitigation: 有什么解决方案来…

阿里巴巴在开源压测工具 JMeter 上的实践和优化

简介&#xff1a;Apache JMeter 是 Apach 旗下的开源压测工具&#xff0c;创建于 1999 年初&#xff0c;迄今已有超过 20 年历史。JMeter 功能丰富&#xff0c;社区&#xff08;用户群体&#xff09;庞大&#xff0c;是主流开源压测工具之一。 作者&#xff1a;灵苒、涧泉 Ap…

普洛斯荣获两项“数据中心绿色等级评估”5A级认证

7月29日&#xff0c;由工业和信息化部及山东省人民政府主办的首届中国算力大会在济南成功举办&#xff0c;会上同时公布了本年度“数据中心绿色等级评估”评审结果。普洛斯常熟东南数据中心B栋及普洛斯怀来数据中心3号楼均荣获“数据中心绿色等级评估”&#xff08;规划类/基础…

深度解读企业云上办公利器「无影云电脑」

简介&#xff1a;信息化进程高速发展的今天&#xff0c;用户桌面办公的需求正不断发生变化&#xff1a;远程办公&#xff0c;BYOD的需求不断增长&#xff1b;快速交付&#xff0c;高效运维的需求接连上升&#xff1b;数据及网络安全的关注度持续提高&#xff1b;整体办公成本在…

云风:不加班、不炫技,把复杂的问题简单化

小学时跟随母亲去成人大学学习编程&#xff0c;初中开始参加信息学奥赛&#xff0c;高中写出人生中第一个成熟软件——Cview&#xff0c;大学发布开源软件风魂系列&#xff0c;后用于网易开发的《大话西游》《梦幻西游》等热门游戏&#xff0c;离开网易创立简悦科技……随着云风…

Timing:在线自习室快速搭建

通过超低延迟的音视频通信技术、视频连麦、弱网传输算法&#xff0c;快速搭建自习场景&#xff0c;提升自习效率。 客户简介 氪细胞主打产品Timing&#xff0c;是国内最早推出&#xff0c;也是规模最大的在线自习室&#xff0c;是新一代的教育与社交融合平台&#xff0c;主打高…

Nacos2.0的K8s服务发现生态应用及规划

简介&#xff1a;Nacos 是阿里巴巴于 2018 年开源的注册中心及配置中心产品&#xff0c;帮助用户的分布式微服务应用进行服务发现和配置管理功能。随着 Nacos2.0 版本的发布&#xff0c;在性能和扩展性上取得较大突破后&#xff0c;社区开始考虑如何提供更加云原生方向的功能和…