The Beauty of Design Patterns 68-Visitor Mode (Part 1): Take you hand in hand to restore the thinking process of the birth of the Visitor Mode

68 | Visitor Mode (Part 1): Take you hand in hand to restore the thinking process of the birth of the Visitor Mode

As we mentioned earlier, the principle and implementation of most design patterns are very simple, but there are exceptions, such as the visitor pattern to be talked about today. It can be regarded as one of the most difficult to understand among the 23 classic design patterns. Because it is difficult to understand and implement, applying it will lead to poor readability and maintainability of the code. Therefore, the visitor pattern is rarely used in actual software development. It is recommended that You don't use the visitor pattern.

Nevertheless, in order for you to be able to see the design intent of the code at a glance when you read the code that applies the visitor pattern in the future, and for the integrity of the entire column, I think it is necessary to tell you about this model. In addition, in order to maximize the learning effect, today I will not only explain the principle and implementation, but more importantly, I will take you to restore the thinking process of the birth of the visitor mode, so that you can feel the creation of a new It is not difficult to come up with the design pattern.

Without further ado, let's officially start today's study!

Take you to "invent" the visitor mode

Suppose we crawl a lot of resource files from the website, and there are three formats of them: PDF, PPT, Word. We are now going to develop a tool to process this batch of resource files. One of the functions of this tool is to extract the text content in these resource files and put them into txt files. If you could do it, how would you do it?

It is not difficult to realize this function. Different people have different ways of writing it. I will post one of the code implementation methods here. Among them, ResourceFile is an abstract class that contains an abstract function extract2txt(). PdfFile, PPTFile, and WordFile all inherit the ResourceFile class and rewrite the extract2txt() function. In ToolApplication, we can use the polymorphic feature to decide which method to execute according to the actual type of the object.

public abstract class ResourceFile {
  protected String filePath;

  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }

  public abstract void extract2txt();
}

public class PPTFile extends ResourceFile {
  public PPTFile(String filePath) {
    super(filePath);
  }

  @Override
  public void extract2txt() {
    //...省略一大坨从PPT中抽取文本的代码...
    //...将抽取出来的文本保存在跟filePath同名的.txt文件中...
    System.out.println("Extract PPT.");
  }
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public void extract2txt() {
    //...
    System.out.println("Extract PDF.");
  }
}

public class WordFile extends ResourceFile {
  public WordFile(String filePath) {
    super(filePath);
  }

  @Override
  public void extract2txt() {
    //...
    System.out.println("Extract WORD.");
  }
}

// 运行结果是:
// Extract PDF.
// Extract WORD.
// Extract PPT.
public class ToolApplication {
  public static void main(String[] args) {
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.extract2txt();
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

If the functions of the tool continue to expand, not only to be able to extract text content, but also to support a series of functions such as compression, extraction of file meta information (file name, size, update time, etc.) to build an index, then if we continue to follow the above The realization idea, there will be the following problems:

  • Violating the principle of opening and closing, add a new function, and the code of all classes must be modified;
  • Although the functions increase, the code of each class continues to expand, and the readability and maintainability have deteriorated;
  • Coupling all the higher-level business logic to the PdfFile, PPTFile, and WordFile classes makes the responsibilities of these classes not single enough and becomes a hodgepodge.

In response to the above problems, our common solution is to disassemble and decouple, decouple business operations from specific data structures, and design them into independent classes. Here we refactor the above code according to the evolution of the visitor pattern. The code after refactoring is as follows.

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }
  //...
}
//...PPTFile、WordFile代码省略...
public class Extractor {
  public void extract2txt(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }

  public void extract2txt(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }

  public void extract2txt(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}

public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      extractor.extract2txt(resourceFile);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

The most critical point of design is that we design the operation of extracting text content into three overloaded functions. Function overloading is a common syntax mechanism in object-oriented programming languages ​​such as Java and C++. The so-called overloaded function refers to a group of functions with the same function name and different parameters in the same class.

However, if you are careful enough, you will find that the above code cannot be compiled, and an error will be reported on line 37. Why is this?

We know that polymorphism is a kind of dynamic binding, which can obtain the actual type of the object at runtime to run the method corresponding to the actual type. Function overloading is a kind of static binding, and the actual type of the object cannot be obtained at compile time, but the method corresponding to the declared type is executed according to the declared type.

In the 35th to 38th lines of the above code, the declaration type of the object contained in resourceFiles is ResourceFile, and we have not defined the extract2txt() overload function whose parameter type is ResourceFile in the Extractor class, so it will not pass during the compilation phase. , not to mention executing different overloaded functions at runtime depending on the actual type of the object. So how to solve this problem?

The solution is a bit difficult to understand, let's look at the code first, and then I will explain it to you slowly.

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  abstract public void accept(Extractor extractor);
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public void accept(Extractor extractor) {
    extractor.extract2txt(this);
  }

  //...
}

//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
//...Extractor代码不变...

public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(extractor);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

When executing line 30, according to the polymorphic feature, the program will call the accept function of the actual type, such as the accept function of PdfFile, which is the code on line 16. The this type in the 16 lines of code is PdfFile, which is determined during compilation, so the overloaded function extract2txt(PdfFile pdfFile) of the extractor will be called. Is this implementation idea very skillful? This is the key to understanding the visitor mode, and it is also the reason why the visitor mode I said before is not easy to understand.

Now, if we want to continue to add new functions, such as the compression function mentioned above, according to different file types, use different compression algorithms to compress resource files, how can we achieve it? We need to implement a new class Compressor class similar to the Extractor class, and define three overloaded functions in it to realize the compression of different types of resource files. In addition, we also need to define a new accept overload function in each resource file class. The specific code is as follows:

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  abstract public void accept(Extractor extractor);
  abstract public void accept(Compressor compressor);
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public void accept(Extractor extractor) {
    extractor.extract2txt(this);
  }

  @Override
  public void accept(Compressor compressor) {
    compressor.compress(this);
  }

  //...
}
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...
//...Extractor代码不变

public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(extractor);
    }

    Compressor compressor = new Compressor();
    for(ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(compressor);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

There are still some problems in the above code. To add a new business, it is still necessary to modify each resource file class, which violates the principle of opening and closing. To solve this problem, we abstract a Visitor interface, which contains three overloaded visit() functions with very common names, which handle three different types of resource files respectively. The specific business processing is determined by the specific class that implements the Visitor interface. For example, Extractor is responsible for extracting text content, and Compressor is responsible for compression. When we add a new business function, the resource file class does not need to be modified, only the code of ToolApplication needs to be modified.

According to this idea, we can refactor the code. The code after refactoring is as follows:

public abstract class ResourceFile {
  protected String filePath;
  public ResourceFile(String filePath) {
    this.filePath = filePath;
  }
  abstract public void accept(Visitor vistor);
}

public class PdfFile extends ResourceFile {
  public PdfFile(String filePath) {
    super(filePath);
  }

  @Override
  public void accept(Visitor visitor) {
    visitor.visit(this);
  }

  //...
}
//...PPTFile、WordFile跟PdfFile类似,这里就省略了...

public interface Visitor {
  void visit(PdfFile pdfFile);
  void visit(PPTFile pdfFile);
  void visit(WordFile pdfFile);
}

public class Extractor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Extract PPT.");
  }

  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Extract PDF.");
  }

  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Extract WORD.");
  }
}

public class Compressor implements Visitor {
  @Override
  public void visit(PPTFile pptFile) {
    //...
    System.out.println("Compress PPT.");
  }

  @Override
  public void visit(PdfFile pdfFile) {
    //...
    System.out.println("Compress PDF.");
  }

  @Override
  public void visit(WordFile wordFile) {
    //...
    System.out.println("Compress WORD.");
  }

}

public class ToolApplication {
  public static void main(String[] args) {
    Extractor extractor = new Extractor();
    List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
    for (ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(extractor);
    }

    Compressor compressor = new Compressor();
    for(ResourceFile resourceFile : resourceFiles) {
      resourceFile.accept(compressor);
    }
  }

  private static List<ResourceFile> listAllResourceFiles(String resourceDirectory) {
    List<ResourceFile> resourceFiles = new ArrayList<>();
    //...根据后缀(pdf/ppt/word)由工厂方法创建不同的类对象(PdfFile/PPTFile/WordFile)
    resourceFiles.add(new PdfFile("a.pdf"));
    resourceFiles.add(new WordFile("b.word"));
    resourceFiles.add(new PPTFile("c.ppt"));
    return resourceFiles;
  }
}

Revisiting the visitor pattern

Just now I took you step by step to restore the thinking process of the birth of the visitor mode. Now, let's go back and summarize the principle and code implementation of this mode.

The English translation of the visitor pattern is Visitor Design Pattern. In the book "Design Patterns" by GoF, it is defined as follows:

Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure.

Translated into Chinese is: Allow one or more operations to be applied to a set of objects, decoupling operations and objects themselves.

The definition is relatively simple, and it is not difficult to understand in combination with the previous examples, so I will not explain too much. For the code implementation of the visitor mode, in fact, in the above example, the final code after layer-by-layer refactoring is the implementation code of the standard visitor mode. Here, I have summarized another class diagram and pasted it below. You can take a look at it together with the previous example code.

insert image description here

Finally, let's take a look at the application scenarios of the visitor pattern.

In general, the Visitor pattern targets a set of objects of different types (PdfFile, PPTFile, WordFile). However, although the types of this group of objects are different, they inherit from the same parent class (ResourceFile) or implement the same interface. In different application scenarios, we need to perform a series of unrelated business operations on this group of objects (extracting text, compressing, etc.), but in order to avoid continuous expansion of classes (PdfFile, PPTFile, WordFile) due to continuous addition of functions, responsibilities are increasingly The less monotonous, and to avoid frequent code modification caused by frequent addition of functions, we use the visitor pattern to decouple objects from operations, extract these business operations, and define them in separate subdivided visitor classes (Extractor, Compressor )middle.

key review

Well, that's all for today's content. Let's summarize and review together, what you need to focus on.

The visitor pattern allows one or more operations to be applied to a set of objects. The design intention is to decouple the operations and the objects themselves, keep the class with a single responsibility, satisfy the principle of opening and closing, and deal with the complexity of the code.

For the visitor mode, the main difficulty of learning lies in the code implementation. The main reason why the code implementation is more complicated is that function overloading is statically bound in most object-oriented programming languages. That is to say, which overloaded function of the class to call is determined by the declared type of the parameter during compilation, not by the actual type of the parameter at runtime.

It is precisely because the code implementation is difficult to understand, so applying this pattern in the project will lead to poor readability of the code. If your colleagues don't understand this design pattern, they may not be able to read and maintain the code you wrote. So, don't use this mode unless you have to.

class disscussion

In fact, the example given today can be done without the visitor pattern. Can you think of other implementation ideas?

Welcome to leave a message and share your thoughts with me. If you gain something, you are welcome to share this article with your friends.

Guess you like

Origin blog.csdn.net/fegus/article/details/130519342