IT博客汇
  • 首页
  • 精华
  • 技术
  • 设计
  • 资讯
  • 扯淡
  • 权利声明
  • 登录 注册

    设计模式之美-课程笔记42-访问者模式

    10k发表于 2023-10-30 00:00:00
    love 0

    访问者模式

    访问者模式的诞生的思维过程

    从一个例子发明访问者模式

    1. 假设我们从网站上爬取了很多资源文件,它们的格式有三种:PDF、PPT、Word。我们现在要开发一个工具来处理这批资源文件。这个工具的其中一个功能是,把这些资源文件中的文本内容抽取出来放到 txt 文件中。
    2. 我的思路是:ResourceFile一个类,用来定义各种文件资源,这个类包含一个方法叫做getText用来获取文件的文本内容。然后另一个定义的类是FileTool,定义一个静态方法,传入资源文件并调用其getText方法,将得到的文本内容再写入到一个新的txt文件。
    3. 注意这个地方可以用interface定义ResourceFile然后让各种类型的方法实现这个类。
    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;
      }
    }
    
    1. 这里的问题是如果工具类的方法不停的扩展功能:
      1. 我们需要不断地修改每一个ResourceFile 的实现类;
      2. 每个类都在不断的变大,可读性和可维护性变差;
      3. 比较上层的业务逻辑(功能)耦合到了
      4. PdfFile、PPTFile、WordFile 类中,导致这些类的职责不够单一,变成了大杂烩。
    2. 解决思路是将操作和数据结果解耦
    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;
      }
    }
    
    1. 这个代码是编译不过的。在ToolApplication中for循环调用extract2txt,调用几个重载函数,编译器并不能区分。
    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;
      }
    }
    
    1. 以上代码会根据多台特性在main函数中的for循环中,会调用各种类型文件的accept方法。accept方法再调用extract方法,并将各个类本身当做参数传入。这个时候调用的就是各自的extract2txt方法。
    2. 后续如果要增加一个压缩功能。我们可以类似实现Extractor一样实现一个Compressor类,然后在每个实现类中定义新的accept。
    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;
      }
    }
    
    1. 这个还是没有满足OCP,还是在增加功能的时候修改到了原始的文件类。
    2. 新的实现如下。增加一个visitor类有对不同文件类型的visit方法,然后每个accept方法不再直接调用原来的操作类,而是visitor类的visit方法。因为每个操作类实现了visitor接口,所以增加新功能时候,只需在main中给accept方法传入对应的操作类即可调用其visit方法(visit方法在每个操作类实现,根据文件类型不同调用不同的操作)。
    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;
      }
    }
    

    感觉像是在耦合的地方解耦就是通过新加一个借助接口特性的类,动态根据参数决定其行为。这个参数是在main中传入的,所以就满足了不修改原来工具和资源类的OCP。

    回看访问者模式

    1. Allows for one or more operation to be applied to a set of objects at runtime, decoupling the operations from the object structure. 允许一个或者多个操作应用到一组对象上,解耦操作和对象本身。

    2. 一般来说,访问者模式针对的是一组类型不同的对象(PdfFile、PPTFile、WordFile)。不过,尽管这组对象的类型是不同的,但是,它们继承相同的父类(ResourceFile)或者实现相同的接口。在不同的应用场景下,我们需要对这组对象进行一系列不相关的业务操作(抽取文本、压缩等),但为了避免不断添加功能导致类(PdfFile、PPTFile、WordFile)不断膨胀,职责越来越不单一,以及避免频繁地添加功能导致的频繁代码修改,我们使用访问者模式,将对象与操作解耦,将这些业务操作抽离出来,定义在独立细分的访问者类(Extractor、Compressor)中.

    为什么支持双分配的语言不需要访问者模式

    1. 单分配: 单分配指的是执行哪个对象的方法,根据对象的运行时类型来决定。执行对象的哪个方法,根据方法参数的编译时类型来确定。
    2. 双分配:双分配指的是执行哪个对象的方法,根据对象的运行时类型来决定。执行对象的哪个方法,根据方法参数的运行时类型来确定。
    3. Dispatch分配:一个对象调用另一个对象的方法,相当于发送消息,传递对象名称、方法名称、方法参数。
    4. single、double单双:单双的区别是就在单双分配的定义中。即执行对象的哪个方法,单分配只跟对象的运行时类型有关;双分配是跟对象和方法参数两者的运行时类型有关。
    5. 以Java为例。
      1. Java的多态可以在运行时获得对象的实际类型,然后根据对象类型调用哪个方法。
      2. Java的重载不是在运行时而是在编译时确定传入的参数类型。
      3. 所以Java执行哪个对象的哪个方法只跟对象的运行时类型有关,跟参数的运行时类型无关。所以Java只支持single Dispatch。
    6. 假设Java支持双分配,那么上文中的整个工具类的实现就不需要访问者模式了。ToolApplication的循环就可以知道谁是谁(啥时候该用哪个对象和方法)。
    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;
      }
    }
    

    其他实现

    1. 可以用工厂模式
    public abstract class ResourceFile {
      protected String filePath;
      public ResourceFile(String filePath) {
        this.filePath = filePath;
      }
      public abstract ResourceFileType getType();
    }
    
    public class PdfFile extends ResourceFile {
      public PdfFile(String filePath) {
        super(filePath);
      }
    
      @Override
      public ResourceFileType getType() {
        return ResourceFileType.PDF;
      }
    
      //...
    }
    
    //...PPTFile/WordFile跟PdfFile代码结构类似,此处省略...
    
    public interface Extractor {
      void extract2txt(ResourceFile resourceFile);
    }
    
    public class PdfExtractor implements Extractor {
      @Override
      public void extract2txt(ResourceFile resourceFile) {
        //...
      }
    }
    
    //...PPTExtractor/WordExtractor跟PdfExtractor代码结构类似,此处省略...
    
    public class ExtractorFactory {
      private static final Map<ResourceFileType, Extractor> extractors = new HashMap<>();
      static {
        extractors.put(ResourceFileType.PDF, new PdfExtractor());
        extractors.put(ResourceFileType.PPT, new PPTExtractor());
        extractors.put(ResourceFileType.WORD, new WordExtractor());
      }
    
      public static Extractor getExtractor(ResourceFileType type) {
        return extractors.get(type);
      }
    }
    
    public class ToolApplication {
      public static void main(String[] args) {
        List<ResourceFile> resourceFiles = listAllResourceFiles(args[0]);
        for (ResourceFile resourceFile : resourceFiles) {
          Extractor extractor = ExtractorFactory.getExtractor(resourceFile.getType());
          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;
      }
    }
    
    1. 多一个压缩功能就新增一个压缩类,然后分别实现不同类型分别实现这个压缩接口。然后再定义压缩工厂。
    2. 在功能比较少的情况下工厂模式实现明了,简单易懂。
    3. 当工具类的方法多了之后,比如十几个,访问模式就会好很多,实现代码相对少,一定程度上可维护性好一些。


沪ICP备19023445号-2号
友情链接