您现在的位置是:首页 >技术交流 >《设计模式》访问者模式网站首页技术交流

《设计模式》访问者模式

ReadThroughLife 2024-06-17 10:14:51
简介《设计模式》访问者模式

《设计模式》访问者模式

定义

  • 访问者模式用于封装一些作用于某种数据结构中的各元素的操作,将数据结构和数据操作分离,它可以在不改变这个数据结构的前提下定义作用于这些元素的新操作
  • 属于行为型模式

访问模式的角色组成

  • Visitor(抽象访问者):抽象访问者为对象结构中每个具体元素声明一个访问操作,从这个操作的名称或参数类型可以清楚知道需要访问的具体元素的类型。具体访问者需要实现这些操作方法,提供对这些元素的访问操作。
  • ConcreteVisitor(具体访问者):具体访问者实现了每个由抽象访问者声明的操作,每个操作用于访问对象结构中一种类型的元素。
  • Element(抽象元素):抽象元素定义一个 accept() 方法,参数为抽象访问者。
  • ConcreteElement(具体元素):具体元素实现了 accept() 方法,在 accept() 方法中调用访问者的访问方法以便完成对一个元素的操作。
  • ObjectStructure(对象结构):对象结构是一个元素的集合,用于存放元素对象,并且提供了遍历其内部元素的方法。

访问者模式的 UML 类图

在这里插入图片描述

?情景案例:目前汽车市场上有三种主流类型的能源汽车,分别为:1、电动汽车。2、油电混动汽车。3、传统油车。汽车购买者一般在购买之前,会对这三种能源类型的汽车进行多方面的比较,例如油耗、续航里程、价格等方面,从而综合考虑选择适合自己的能源汽车。我们以消费者购买能源汽车之前综合对比的情景,使用访问者模式对该情景进行编码。

UML 类图

在这里插入图片描述

抽象元素 Car 接口

public interface Car {
    void accept(Visitor visitor);
}

具体元素 ElectroCar 类

public class ElectroCar implements Car {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

具体元素 OilCar 类

public class OilCar implements Car {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

具体元素 HybridCar 类

public class HybridCar implements Car {
    @Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}

抽象访问者 Visitor 接口

public interface Visitor {
    void visit(ElectroCar electroCar);
    void visit(OilCar oilCar);
    void visit(HybridCar hybridCar);
}

具体访问者 FuelVisitor 类

public class FuelVisitor implements Visitor {

    @Override
    public void visit(ElectroCar electroCar) {
        System.out.println("电车不需要燃油,只要充电");
    }

    @Override
    public void visit(OilCar oilCar) {
        System.out.println("燃油车需要燃油");
    }

    @Override
    public void visit(HybridCar hybridCar) {
        System.out.println("混动车需要充电,也需要燃油,但是相较于油车减少了燃油");
    }
}

具体访问者 EnduranceVisitor 类

public class EnduranceVisitor implements Visitor {
    @Override
    public void visit(ElectroCar electroCar) {
        System.out.println("电车的续航里程一般较短,取决于电车容量大小");
    }

    @Override
    public void visit(OilCar oilCar) {
        System.out.println("燃油车续航里程相对较长,取决于油箱容量");
    }

    @Override
    public void visit(HybridCar hybridCar) {
        System.out.println("混动车续航里程相对较长,取决于油箱容量和电池容量的大小");
    }
}

具体访问者 PriceVisitor 类

public class PriceVisitor implements Visitor {
    @Override
    public void visit(ElectroCar electroCar) {
        System.out.println("电车作为新势力价格相对较贵");
    }

    @Override
    public void visit(OilCar oilCar) {
        System.out.println("燃油车价格相对较低");
    }

    @Override
    public void visit(HybridCar hybridCar) {
        System.out.println("混动车价格相对于油车略高,相对于电车价格略低");
    }
}

对象结构 ObjectStructure 类

public class ObjectStructure {
    private List<Car> cars = new ArrayList<>();

    public void attach(Car car) {
        cars.add(car);
    }

    public void detach(Car car) {
        cars.remove(car);
    }

    public void accept(Visitor visitor) {
        Iterator<Car> iterator = cars.iterator();
        while (iterator.hasNext()) {
            iterator.next().accept(visitor);
        }
    }
}

客户端 Client 类

public class Client {
    public static void main(String[] args) {
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.attach(new ElectroCar());
        objectStructure.attach(new OilCar());
        objectStructure.attach(new HybridCar());

        FuelVisitor fuelVisitor = new FuelVisitor();
        EnduranceVisitor enduranceVisitor = new EnduranceVisitor();
        PriceVisitor priceVisitor = new PriceVisitor();

        System.out.println("----------油耗对比-----------");
        objectStructure.accept(fuelVisitor);
        System.out.println("----------续航里程对比-----------");
        objectStructure.accept(enduranceVisitor);
        System.out.println("---------价格对比-----------");
        objectStructure.accept(priceVisitor);
    }
}

如果在上述情景案例中增加了一种新的具体访问类,例如对比车型的选择数量,那么无需修改源代码,只要增加一个新的具体访问者类即可。在该具体访问者中封装了新的操作元素对象的方法。从增加新的访问者的角度来看,访问者模式符合开闭原则。

但是,如果要在上述情景案例中增加一种新的具体元素,例如增加氢能汽车。由于原有情境中并未相应的访问接口(在抽象访问者中没有声明访问“氢能汽车”的方法),因此必须对原有情景案例代码进行修改,在原有的抽象访问者类和具体访问者类中增加相应的访问方法。从增加新的元素的角度来看,访问者模式违背了开闭原则。

因此,访问者模式的优点

  • 增加新的访问操作很方便,使用访问者模式,增加新的访问操作就意味着增加一个新的具体访问者类,实现简单,无需修改源代码,符合开闭原则。
  • 让用户能够在不修改现有元素类层次结构的情况下,定义作用于该层次结构的操作。

访问者模式的缺点

  • 无法增加新的元素类型,如果系统数据结构发生变化,则访问者类必须增加对应元素类型的操作,违背了开闭原则。
  • 破坏封装,访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。

访问者模式的适用场景

  • 系统中数据结构固定不变化,经常需要在已有的数据结构上定义新的数据操作。
  • 需要对一个对象结构中的对象进行很多不同且不相关的操作,并且需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。

?访问者模式在JDK源码中的应用

java.nio.file 包下的接口 FileVisitor,它定义了文件树中所有节点的访问方法,提供递归遍历文件树的支持,该接口有四个关键方法:

public interface FileVisitor<T> {
	// 在访问目录之前调用
    FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException;
	// 访问文件时调用
    FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException;
	// 在访问文件失败时调用
    FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException;
    // 在访问目录之后调用
    FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException;
}

FileVisitor 接口的实现类可以用来递归地访问一个目录及其子目录中的所有文件和子目录。通过实现 FileVisitor 接口中的方法,可以对目录和文件进行不同的操作。

例如,JDK 给出了 FileVisitor 接口的实现类示例 SimpleFileVisitor,定义了对文件和文件夹目录的简单数据操作。

public class SimpleFileVisitor<T> implements FileVisitor<T> {

    protected SimpleFileVisitor() {
    }
    
    @Override
    public FileVisitResult preVisitDirectory(T dir, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult visitFile(T file, BasicFileAttributes attrs)
        throws IOException
    {
        Objects.requireNonNull(file);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }
    
    @Override
    public FileVisitResult visitFileFailed(T file, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(file);
        throw exc;
    }

    @Override
    public FileVisitResult postVisitDirectory(T dir, IOException exc)
        throws IOException
    {
        Objects.requireNonNull(dir);
        if (exc != null)
            throw exc;
        return FileVisitResult.CONTINUE;
    }
}

⭐在JDK中,FileVisitor 接口主要应用于 File 类和 Path 类中的 walkFileTree() 方法中,这个方法会遍历指定目录及其子目录中的所有文件和子目录,同时调用实现了 FileVisitor 接口的类的相应方法,从而实现对这些文件和子目录的访问和处理。

假设我们要在一个目录及其子目录中查找所有的 .java 文件,并输出它们的路径。我们可以实现一个 FileVisitor 接口来完成这个任务。

public class FindJavaFiles extends FileVisitor<T> {
    @Override
    public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) {
        if (file.toString().endsWith(".java")) {
            System.out.println(file);
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Path file, IOException exc) {
        System.err.println(exc);
        return FileVisitResult.CONTINUE;
    }
}

之后在客户端可以使用 Path 类中的 walkFileTree() 方法来递归地访问指定目录中的所有文件和子目录,并调用我们实现的 FindJavaFiles 类中的 visitFile() 和 visitFileFailed() 方法。

FindJavaFiles 类

public class FindJavaFiles implements FileVisitor {
    @Override
    public FileVisitResult preVisitDirectory(Object dir, BasicFileAttributes attrs) throws IOException {
        Objects.requireNonNull(dir);
        Objects.requireNonNull(attrs);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFile(Object file, BasicFileAttributes attrs) throws IOException {
        if (file.toString().endsWith(".java")) {
            System.out.println(file);
        }
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult visitFileFailed(Object file, IOException exc) throws IOException {
        System.err.println(exc);
        return FileVisitResult.CONTINUE;
    }

    @Override
    public FileVisitResult postVisitDirectory(Object dir, IOException exc) throws IOException {
        Objects.requireNonNull(dir);
        if (exc != null) {
            throw exc;
        }
        return FileVisitResult.CONTINUE;
    }
}

客户端类

public class Main {
    public static void main(String[] args) throws IOException {
        Path startDir = Paths.get("/path/to/directory");
        Files.walkFileTree(startDir, new FindJavaFiles());
    }
}

?最后,欢迎大家在评论区积极讨论关于访问者模式的各种问题,让我们更好地了解访问者模式!

风语者!平时喜欢研究各种技术,目前在从事后端开发工作,热爱生活、热爱工作。