Design Pattern | From Visitor Pattern to Pattern Matching

Preface


In the field of software development, the problems we encounter may be different each time. Some are related to e-commerce business, some are related to the underlying data structure, and some may focus on performance optimization. However, our approach to solving problems at the code level has certain commonalities. Has anyone summarized these commonalities?

Of course there is. In 1994, Erich Gamma, Richard Helm, Ralph Johnson and John Vlissides jointly published a book of great significance in the industry: Design Patterns: Elements of Reusable Object-Oriented Software. This book brings people to the development field. A series of abstractions have been made on the commonality of various issues, and 23 very classic design patterns have been formed. Many problems can be abstracted into one or more of these 23 design patterns. Because design patterns are very versatile, they have also become a universal language used by developers, and the code abstracted into design patterns is easier to understand and maintain.

On the whole, design patterns are divided into three categories:


1. Creational Patterns: Design patterns related to creating and reusing objects
2. Structural patterns: Design patterns related to combining and building objects
3. Behavioral patterns: Design patterns related to behavior between objects

The design pattern described in this article is the Visitor Pattern, which is a kind of Behavior Pattern, which is used to solve the problem of how to combine and expand objects with similar behaviors. More specifically, this article introduces the use scenarios, advantages, and disadvantages of Visitor Pattern, and Double Dispatch technology related to Visitor Pattern. And at the end of the article, it explains how to use Pattern Matching, which was just launched in Java 14, to solve the problems solved by the previous Visitor Pattern.

problem


Assuming that there is a map program, there are many nodes on the map, such as building (Building), factory (Factory), school (School), as shown below:

interface Node {
    String getName();
    String getDescription();
    // 其余的方法这里忽略......
}


class Building implements Node {
    ...
}


class Factory implements Node {
    ...
}


class School implements Node {
    ...
}

Here comes a new requirement: you need to add the function of drawing Node. If you think about it, it's very simple. Let's add a method draw() in Node, and then the rest of the implementation classes will implement this method separately. But there is a problem with doing this. We added the draw() method this time. What about adding an export method next time? In addition, the interface must be modified again. As a bridge connecting components, interfaces should be as stable as possible and should not be changed frequently. Therefore, you want to be able to make the interface scalability as high as possible, and to maximize the functional scope of the interface without frequently changing the interface. After some weighing, you came up with the following solution.

Initial solution


We define a new class DrawService and write all the draw logic in it. The code is as follows:

public class DrawService {
    public void draw(Building building) {
        System.out.println("draw building");
    }
    public void draw(Factory factory) {
        System.out.println("draw factory");
    }
    public void draw(School school) {
        System.out.println("draw school");
    }
    public void draw(Node node) {
        System.out.println("draw node");
    }
}

This is the class diagram:

You think the problem is solved now, so you are going to go home from get off work after a little test:

public class App {
    private void draw(Node node) {
        DrawService drawService = new DrawService();
        drawService.draw(node);
    }


    public static void main(String[] args) {
        App app = new App();
        app.draw(new Factory());
    }
}

Click to run, output:

draw node

How is this going? You take a closer look at your code again: "I did pass a Factory object, it should output draw factory". Seriously, you went to check some information, and then you found the reason.

explain the reason


In order to understand the reason, we first understand the two variable type binding modes of the editor.

★  Dynamic/Late Binding

Let's take a look at this code

class NodeService {
    public String getName(Node node) {
        return node.getName();
    }
}

When the program runs NodeService::getName, it must determine the type of the parameter Node, whether it is Factory, School, or Building, so that it can call the getName method of the corresponding implementation class. Can the program get this information during the compilation phase? Obviously not, because the type of Node may change according to the operating environment, and it may even be transmitted from another system. It is impossible for us to get this information during the compilation stage. What the program can do is to start it first, and when it runs to the getName method, look at the type of Node, and then call the getName() implementation of the corresponding type to get the result. Deciding which method to call at runtime (not at compile time) is called Dynamic/Late Binding.

★  Static/Early Binding

Let's look at another piece of code

public void drawNode(Node node) {
    DrawService drawService = new DrawService();
    drawService.draw(node);
}

When we run to drawService.draw(node), does the compiler know the type of node? It must be known at runtime, so why do we pass a Factory in, but output draw node instead of draw factory? We can think about this problem from the point of view of the program. There are only 4 draw methods in DrawService, and the parameter types are Factory, Building, School and Node. What if the caller passes in a City? After all, the caller can implement a City class to pass in. What method should the program call in this case? We do not have a draw(City) method. In order to prevent this from happening, the program directly chooses to use the DrawService::draw(Node) method during the compilation phase. No matter what implementation is passed by the caller, we will use the DrawService::draw(Node) method to ensure the safe operation of the program. Deciding which method to call at compile time (not at runtime) is called Static/Early Binding. This also explains why we output draw node.

The final solution


It turns out that this is because the compiler does not know the type of the variable. In this case, we can directly tell the compiler what type it is. Can this be done? This can of course be done, we check the variable type in advance.

if (node instanceof Building) {
    Building building = (Building) node;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) node;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) node;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

This code is feasible, but it is very cumbersome to write. We need to let the caller determine the node type and choose the method to be called. Is there a better solution? Yes, that is the Visitor Pattern. The Visitor Pattern uses a method called Double Dispatch, which can transfer the routing work from the caller to the respective implementation class, so that the client does not need to write these tedious judgment logic. Let's first look at what the implemented code looks like.

interface Visitor {
    void visit(Node node);
    void visit(Factory factory);
    void visit(Building building);
    void visit(School school);
}


class DrawVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("draw node");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("draw factory");
    }


    @Override
    public void visit(Building building) {
        System.out.println("draw building");
    }


    @Override
    public void visit(School school) {
        System.out.println("draw school");
    }
}


interface Node {
    ...
    void accpet(Visitor v);
}


class Factory implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Factory类型的,并且知道Visitor::visit(Factory)方法确实存在,
         * 因此会直接调用Visitor::visit(Factory)方法
         */
        v.visit(this);
    }
}


class Building implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是Building类型的,并且知道Visitor::visit(Building)方法确实存在,
         * 因此会直接调用Visitor::visit(Building)方法
         */
        v.visit(this);
    }
}


class School implements Node {
    ...


    @Override
    public void accept(Visitor v) {
        /**
         * 调用方知道visit的参数就是School类型的,并且知道Visitor::visit(School)方法确实存在,
         * 因此会直接调用Visitor::visit(School)方法
         */
        v.visit(this);
    }
}

The caller can use it like this

Visitor drawVisitor = new DrawVisitor();
Factory factory = new Factory();
factory.accept(drawVisitor);

It can be seen that the Visitor Pattern actually elegantly implements our if instanceof above, so that the caller's code is much cleaner, the overall class diagram is as follows

Why is it called Double Dispatch?


After understanding how the Visitor Pattern solves this problem, some students may become curious. Why is the technology used by the Visitor Pattern called Double Dispatch? What exactly is Double Dispatch? Before understanding Double Dispatch, let us first understand what is called Single Dispatch

★  Single Dispatch

Choose different calling methods according to different runtime class implementations, which is called Single Dispatch, such as

String name = node.getName();

Are we calling Factory::getName, School::getName or Building::getName? This mainly depends on the implementation of node, which is Single Dispatch: a layer of routing

★  Double Dispatch

Review the Visitor Pattern code we just saw

node.accept(drawVisitor);

There are two layers of routing:

  • Choose the specific implementation method of accept (Factory::accept, School::accept or Building::accept)

  • Select the specific method of visit (in this example, there is only one DrawVisit::visit)

After routing twice, the corresponding logic is executed. This is called Double Dispatch



Advantages of Visitor Pattern


1. The Visitor Pattern can increase the scalability of the interface as much as possible without changing the interface frequently (only need to change once: adding an accept method)    

Still the above draw example, suppose we now have a new requirement and we need to add the function of displaying node information. Of course, the traditional method is to add a new method showDetails() in Node, but now we don't need to change the interface, we only need to add a new Visitor.

class ShowDetailsVisitor implements Visitor {


    @Override
    public void visit(Node node) {
        System.out.println("node details");
    }


    @Override
    public void visit(Factory factory) {
        System.out.println("factory details");
    }


    @Override
    public void visit(Building building) {
        System.out.println("building details");
    }


    @Override
    public void visit(School school) {
        System.out.println("school details");
    }
}


// 调用方这么使用
Visitor showDetailsVisitor = new ShowDetailsVisitor();
Factory factory = new Factory();
factory.accept(showDetailsVisitor); // factory details

From this example, we can see a typical usage scenario of Visitor Pattern: it is very suitable for use in scenarios where interface methods need to be added frequently. For example, we now have 4 classes A, B, C, D, three methods x, y, z, horizontal drawing method, vertical drawing class, we can get the following picture:

               x      y      z
    A       A::x   A::y   A::z
    B       B::x   B::y   B::z
    C       C::x   C::y   C::z

Under normal circumstances, our table is vertically expanded, that is to say, we are accustomed to adding implementation classes rather than implementation methods. The Visitor Pattern happens to be suitable for another scenario: horizontal expansion. We need to frequently add interface methods, rather than adding implementation classes. Visitor Pattern allows us to achieve this goal without frequently modifying the interface.

2. Visitor Pattern can easily make multiple implementation classes share one logic

Since all implementation methods are written in one class (such as DrawVisitor), we can easily make each type (such as Factory/Building/School) use the same logic instead of writing this logic repeatedly in each interface implementation Class.

Disadvantages of Visitor Pattern


  • Visitor Pattern breaks the encapsulation of the domain model

Under normal circumstances, we will write the logic of the Factory in the Factory class, but the Visitor Pattern requires us to move part of the Factory logic (such as draw) to another class (DrawVisitor). The logic of a domain model is scattered in two In this place, this brings inconvenience to the understanding and maintenance of the domain model.

  • The Visitor Pattern to some extent caused the realization of class logic coupling

All the methods (draw) of the implementation class (Factory/School/Building) are all written in one class (DrawVisitor), which is a logical coupling to some extent and is not conducive to code maintenance.

  • Visitor Pattern makes the relationship between classes complicated and not easy to understand

As the name Double Dispatch shows, we need two dispatches to successfully call the corresponding logic: the first step is to call the accpet method, the second is to call the visit method, the call relationship becomes more complicated, the code behind The maintainer can easily mess up the code.

Pattern Matching


Here is another episode. Java 14 introduced the Pattern Matching feature. Although this feature has existed in the Scala/Haskel field for many years, many students still don't know what it is because Java has just been introduced. Therefore, before explaining the relationship between Pattern Matching and Visitor Pattern, let's briefly introduce what Pattern Matching is. Remember we wrote this code?

if (node instanceof Building) {
    Building building = (Building) building;
    drawService.draw(building);
} else if (node instanceof Factory) {
    Factory factory = (Factory) factory;
    drawService.draw(factory);
} else if (node instanceof School) {
    School school = (School) school;
    drawService.draw(school);
} else {
    drawService.draw(node);
}

With Pattern Matching, we can simplify this code:

if (node instanceof Building building) {
    drawService.draw(building);
} else if (node instanceof Factory factory) {
    drawService.draw(factory);
} else if (node instanceof School school) {
    drawService.draw(school);
} else {
    drawService.draw(node);
}

However, Java's Pattern Matching is still a bit cumbersome, while Scala's can be better:

node match {
  case node: Factory => drawService.draw(node)
  case node: Building => drawService.draw(node)
  case node: School => drawService.draw(node)
  case _ => drawService.draw(node)
}

Because it is more concise, many people advocate Pattern Matching as a substitute for Visitor Pattern. I personally think that Pattern Matching looks a lot simpler. Many people think that Pattern Matching is the advanced version of the switch case. In fact, it is not. For details, please refer to TOUR OF SCALA-PATTERN MATCHING (https://docs.scala-lang.org/tour/pattern-matching.html), about Visitor Pattern The relationship with Pattern Matching can be seen in Scala's Pattern Matching = Visitor Pattern on Steroids, this article will not repeat it.

Reference materials:

  • Scala's Pattern Matching = Visitor Pattern on Steroids

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • When should I use the Visitor Design Pattern?

    http://andymaleh.blogspot.com/2008/04/scalas-pattern-matching-visitor-pattern.html 

  • Design Pattern - Behavioral Patterns - Visitor

    https://refactoring.guru/design-patterns/visitor 

  • Pattern Matching for instanceof in Java 14

    https://refactoring.guru/design-patterns/visitor 

Tao Department Technology Department-Industry and Intelligent Operation-Recruiting talents

We are the data insight team of Alibaba's operation workbench. There are massive amounts of data, high-performance real-time computing engines, and challenging business scenarios. From 618 to Double 11, from Taobao to Tmall, from data analysis to business precipitation, we will spread the will and atmosphere of pursuing perfection to every corner of the technology circle. Looking forward to your joining with technical pursuit and technical depth!

Recruitment position: Java technology expert, data engineer,
if you are interested, please send your resume to [email protected], welcome to pick up~

✿ Further   reading

Author | Yu Haining (Jing Fan)

Edit| Orange

Produced| Alibaba's new retail technology

Guess you like

Origin blog.csdn.net/Taobaojishu/article/details/111503210