preface

I like to learn design patterns from some examples (or use scenarios) and deepen my understanding of design patterns through specific application scenarios.

I’ve been using this for a long time, but it turns out that this is a design pattern to give you an idea of some common design patterns. This article will continue to explore the visitor pattern in design patterns. This pattern is not very common, but we can understand the idea behind it and use it in specific scenarios.

Knowing the visitor pattern

intentions

Represents an operation that operates on elements in an object structure, allowing you to define new operations on those elements without changing their classes.

The simplest visitor

Reduce coupling between two objects by defining a visitor instead of accessing the object directly

var data = []

var handler = function() {}

handler.prototype.get = function() {}

var vistor = function(handler, data) {
  handler.get(data)
}
Copy the code

The compiler

The compiler converts the source program into an AST (abstract syntax tree) and performs type checking, code optimization, flow analysis, and other operations on the AST, most of which require different processing for different nodes. For example, assignment statement nodes are handled differently from expression nodes, so there are classes for assignment statements, classes for variable access, classes for arithmetic expressions, and so on.

The problem is that spreading all these operations across node classes makes the overall code difficult to understand, maintain, and change. The combination of TypeCheck, GenerateCode, and PrettyPrint code can be confusing. And when we add an operation, we recompile all of these classes.

Using the Visitor Pattern

The Visitor pattern helps us wrap the related operations in each class in a separate object (Visitor) that is passed to the currently visited element as the AST is traversed. When an element “receives” the visitor, the element sends the visitor a request containing information about its own class. The request also takes the element itself as a parameter, for which the visitor then performs the action.

For example, if a compiler that does not use visitors calls the TypeCheck operation for type checking, each node will implement its own TypeCheck on the TypeCheck of its calling member. If the compiler uses visitors for type checking, it creates a TypeCheckVisitor object and calls the Accept operation on the abstract syntax tree with this object as a parameter. Each node calls back the visitor when it implements Accept: an assignment node calls the visitor’s VisitAssignment operation, and a variable reference calls VisitVariableRef. The TypeCheck operation formerly of class AssignmentNode is now called the VisitAssignment operation of TypeCheckingVisitor.

To allow visitors to do more than just make type checks, we need all visitors to the abstract syntax tree to have an abstract parent class NodeVisitor. NodeVisitor defines an operation for each node class and eliminates the need to add application-specific code to the node class. The Visitor pattern encapsulates the operations of each compilation step in a Visitor associated with that step.

When is the visitor pattern considered?

  • An object structure contains many class objects with different interfaces, and you want to perform operations on these objects that depend on their concrete classes
  • There are many different and unrelated operations to be performed on objects in an object structure, and you want to avoid having those operations “contaminate” the classes of those objects. Visitor allows you to group related operations into a class.
  • The class that defines the structure of an object rarely changes, but it is often necessary to define new operations on that structure.

Features of the Visitor pattern:

  • With the internal structure of the class unchanged, the object is treated differently by different visitors
  • Adding new operations is relatively easy without changing the structure itself
  • The code for what the visitor does is centralized
  • When using the visitor pattern, you break the encapsulation of composite classes

Let’s meet the Babel Visitor

Babel’s workflow goes through three phases: Parse, Traverse, and generate, with visitor mode being used in the Traverse phase. At this stage Babel does two things:

  • Perform depth-first traversal of the AST tree
  • Add, update, and remove nodes

From the compiler example above, we can see that the visitor pattern actually separates the object structure from the operation logic so that the two can be extended independently. The implementation of the Babel Traverse stage is to separate the logic of traversing the AST from the operation node, and at the traverse AST the registered visitor is called to process it. This makes the AST structure and traversal algorithm fixed, and the visitor can be independently extended by plug-ins.

The AST tree structure

Assume the following code:

function print(str){
    console.log(str)
}
Copy the code

The key structure of the AST tree is as follows (AST Explorer) :

The type field display is the node type, more node type definition can check https://github.com/babel/babylon/blob/master/ast/spec.mdCopy the code

Babel traverses the AST tree to process a node in the form of a visitor. This is done through a visitor object, which defines functions for various nodes so that different processing can be done for different nodes. The Babel plug-in we wrote actually did the same by defining an instantiation visitor object to handle a series of AST nodes.

To better understand the role of the Babel visitor, let’s write a Babel plug-in.

Print the location information plug-in

When we used console.log to debug our code, a bunch of consoles. to solve this problem, we implemented a plug-in to print location information. The plugin will print the number of lines and columns of the current console code.

export default function ({types}) {
  return {
    visitor: {
      // Our visitor code will be placed here
      CallExpression(path, state) {
        const calleeName = generate(path.node.callee).code;
        if (targetCalleeName.includes(calleeName)) {
          const { line, column } = path.node.loc.start;
          path.node.arguments.unshift(
            types.stringLiteral(`loc:[${line}.${column}] `)); }}}}; }Copy the code

Before conversion:

const str = "babel";
console.log(str);

const a = 1,
  b = 2;
console.log(a + b);
Copy the code

After the transformation:

const str = "babel";
console.log("Loc: [2, 0]", str);
const a = 1,
      b = 2;
console.log("Loc: [6, 0]", a + b);
Copy the code

reference

  • Design Patterns: The Foundation of Reusable Object-oriented Software
  • The Babel Plugin