设计模式 -- 访问者模式(Visitor Pattern)

封装一些作用于某些数据结构中的各元素的操作,它可以在不改变数据结构的前提下赋予这些元素新的操作。

应用场景

  • 对象结构比较稳定,但是需要在对象结构的基础上定义新的操作。

  • 需要对同一个类的不同对象执行不不同的操作,但是不希望增加操作的时候改变这些类。

总结

  • 准确识别出Visitor实用的场景,如果一个对象结构不稳定决不可使用,不然在增删元素时改动将非常巨大。

  • 对象结构中的元素要可以迭代访问

  • Visitor里一般存在与元素个数相同的visit方法。

  • 元素通过 this 方法通过 accept 将自己传递给了Visitor。

特点:

  • 访问者模式把数据结构和作用于结构上的操作解耦合,使得操作集合可相对自由地演化。

  • 访问者模式适用于数据结构相对稳定算法又易变化的系统。因为访问者模式使得算法操作增加变得容易。若系统数据结构对象易于变化,经常有新的数据对象增加进来,则不适合使用访问者模式。

  • 访问者模式的优点是增加操作很容易,因为增加操作意味着增加新的访问者。访问者模式将有关行为集中到一个访问者对象中,其改变不影响系统数据结构。其缺点就是增加新的数据结构很困难。

优点:

  • 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。

  • 添加新的操作或者说访问者会非常容易。

  • 将对各个元素的一组操作集中在一个访问者类当中。

  • 使得类层次结构不改变的情况下,可以针对各个层次做出不同的操作,而不影响类层次结构的完整性。

  • 可以跨越类层次结构,访问不同层次的元素类,做出相应的操作。

  • 使得给结构稳定的对象增加新算法变得容易,提搞了代码的可维护性,可扩展性。

缺点:

  • 增加新的元素会非常困难。

  •  实现起来比较复杂,会增加系统的复杂性。

  • 破坏封装,如果将访问行为放在各个元素中,则可以不暴露元素的内部结构和状态,但使用访问者模式的时候,为了让访问者能获取到所关心的信息,元素类不得不暴露出一些内部的状态和结构,就像收入和支出类必须提供访问金额和单子的项目的方法一样。

适用性:

  • 数据结构稳定,作用于数据结构的操作经常变化的时候。

  • 当一个数据结构中,一些元素类需要负责与其不相关的操作的时候,为了将这些操作分离出去,以减少这些元素类的职责时,可以使用访问者模式。

  • 有时在对数据结构上的元素进行操作的时候,需要区分具体的类型,这时使用访问者模式可以针对不同的类型,在访问者类中定义不同的操作,从而去除掉类型判断。

设计模式 -- 访问者模式(Visitor Pattern)_第1张图片

举例是这样,但是不合理:

设计模式 -- 访问者模式(Visitor Pattern)_第2张图片

应该是:

设计模式 -- 访问者模式(Visitor Pattern)_第3张图片

public class VisitorPattern {
    public static void main(String[] args) {
        ObjectStructure os = new ObjectStructure();
        os.add(new ConcreteElementA());
        os.add(new ConcreteElementB());
        Visitor visitor = new ConcreteVisitorA();
        os.accept(visitor);
        System.out.println("------------------------");
        visitor = new ConcreteVisitorB();
        os.accept(visitor);
    }
}


//抽象访问者
interface Visitor {
    void visit(ConcreteElementA element);

    void visit(ConcreteElementB element);
}


//具体访问者A类
class ConcreteVisitorA implements Visitor {
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者A访问-->" + element.operationA());
    }

    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者A访问-->" + element.operationB());
    }
}


//具体访问者B类
class ConcreteVisitorB implements Visitor {
    public void visit(ConcreteElementA element) {
        System.out.println("具体访问者B访问-->" + element.operationA());
    }

    public void visit(ConcreteElementB element) {
        System.out.println("具体访问者B访问-->" + element.operationB());
    }
}


//抽象元素类
interface Element {
    void accept(Visitor visitor);
}


//具体元素A类
class ConcreteElementA implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationA() {
        return "具体元素A的操作。";
    }
}


//具体元素B类
class ConcreteElementB implements Element {
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

    public String operationB() {
        return "具体元素B的操作。";
    }
}


//对象结构角色
class ObjectStructure {
    private List list = new ArrayList();

    public void accept(Visitor visitor) {
        Iterator i = list.iterator();
        while (i.hasNext()) {
            ((Element) i.next()).accept(visitor);
        }
    }

    public void add(Element element) {
        list.add(element);
    }

    public void remove(Element element) {
        list.remove(element);
    }
}


设计模式 -- 访问者模式(Visitor Pattern)_第4张图片

设计模式 -- 访问者模式(Visitor Pattern)_第5张图片

伪动态双分派

分派(dispatch):根据对象的类型而对方法进行的选择。

  • 多分派:根据多个判断依据(即参数类型和个数)判断出了方法的版本。

  • 单分派:动态分派判断的方法是在运行时获取到man和woman的实际引用类型,再确定方法的版本,而由于此时判断的依据只是实际引用类型,只有一个判断依据,所以这就是单分派的概念,

  • 静态分派:发生在编译时的分派,例如重载(overload)。静态多分派

  • 动态分派:发生在运行时的分派,例如重写(overwrite)。动态单分派

设计模式 -- 访问者模式(Visitor Pattern)_第6张图片

伪动态双分派

动态双分派就是在运行时依据两个实际类型去判断一个方法的运行行为,而访问者模式实现的手段是进行了两次动态单分派来达到这个效果。

设计模式 -- 访问者模式(Visitor Pattern)_第7张图片

//单个单子的接口(相当于Element)
public interface Bill {
    void accept(Viewer viewer);
}


//抽象单子类,一个高层次的单子抽象
public abstract class AbstractBill implements Bill{
    protected double amount;
    protected String item;
    
    public AbstractBill(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }
    
    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }
}


//收入单子
public class IncomeBill extends AbstractBill{
    
    public IncomeBill(double amount, String item) {
        super(amount, item);
    }

    public void accept(Viewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer)viewer).viewIncomeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }
}


//消费的单子
public class ConsumeBill extends AbstractBill{

    public ConsumeBill(double amount, String item) {
        super(amount, item);
    }

    public void accept(Viewer viewer) {
        if (viewer instanceof AbstractViewer) {
            ((AbstractViewer)viewer).viewConsumeBill(this);
            return;
        }
        viewer.viewAbstractBill(this);
    }
}


//超级访问者接口(它支持定义高层操作)
public interface Viewer{
    void viewAbstractBill(AbstractBill bill);
}


//比Viewer接口低一个层次的访问者接口
public abstract class AbstractViewer implements Viewer{

    //查看消费的单子
    abstract void viewConsumeBill(ConsumeBill bill);
    
    //查看收入的单子
    abstract void viewIncomeBill(IncomeBill bill);
    
    public final void viewAbstractBill(AbstractBill bill){}
}


//老板类,查看账本的类之一,作用于最低层次结构
public class Boss extends AbstractViewer{
    
    private double totalIncome;
    
    private double totalConsume;
    
    //老板只关注一共花了多少钱以及一共收入多少钱,其余并不关心
    public void viewConsumeBill(ConsumeBill bill) {
        totalConsume += bill.getAmount();
    }

    public void viewIncomeBill(IncomeBill bill) {
        totalIncome += bill.getAmount();
    }

    public double getTotalIncome() {
        System.out.println("老板查看一共收入多少,数目是:" + totalIncome);
        return totalIncome;
    }

    public double getTotalConsume() {
        System.out.println("老板查看一共花费多少,数目是:" + totalConsume);
        return totalConsume;
    }
}


//注册会计师类,查看账本的类之一,作用于最低层次结构
public class CPA extends AbstractViewer{

    //注会在看账本时,如果是支出,则如果支出是工资,则需要看应该交的税交了没
    public void viewConsumeBill(ConsumeBill bill) {
        if (bill.getItem().equals("工资")) {
            System.out.println("注会查看账本时,如果单子的消费目的是发工资,则注会会查看有没有交个人所得税。");
        }
    }

    //如果是收入,则所有的收入都要交税
    public void viewIncomeBill(IncomeBill bill) {
        System.out.println("注会查看账本时,只要是收入,注会都要查看公司交税了没。");
    }
}


//财务主管类,查看账本的类之一,作用于高层的层次结构
public class CFO implements Viewer {

    //财务主管对每一个单子都要核对项目和金额
    public void viewAbstractBill(AbstractBill bill) {
        System.out.println("财务主管查看账本时,每一个都核对项目和金额,金额是" + bill.getAmount() + ",项目是" + bill.getItem());
    }

}


//账本类(相当于ObjectStruture)
public class AccountBook {
    //单子列表
    private List billList = new ArrayList();

    //添加单子
    public void addBill(Bill bill){
        billList.add(bill);
    }

    //供账本的查看者查看账本
    public void show(Viewer viewer){
        for (Bill bill : billList) {
            bill.accept(viewer);
        }
    }
}


public class Client {

    public static void main(String[] args) {
        AccountBook accountBook = new AccountBook();
        //添加两条收入
        accountBook.addBill(new IncomeBill(10000, "卖商品"));
        accountBook.addBill(new IncomeBill(12000, "卖广告位"));
        //添加两条支出
        accountBook.addBill(new ConsumeBill(1000, "工资"));
        accountBook.addBill(new ConsumeBill(2000, "材料费"));
        
        Viewer boss = new Boss();
        Viewer cpa = new CPA();
        Viewer cfo = new CFO();
        
        //两个访问者分别访问账本
        accountBook.show(cpa);
        accountBook.show(boss);
        accountBook.show(cfo);
        
        ((Boss) boss).getTotalConsume();
        ((Boss) boss).getTotalIncome();
    }
}

设计模式 -- 访问者模式(Visitor Pattern)_第8张图片


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

首先来看JDK的NIO模块下的FileVisitor接口,它提供了递归遍历文件树的支持。这个接口上的方法表示了遍历过程中的关键过程,允许在文件被访问、目录将被访问、目录已被访问、发生错误等过程中进行控制。换句话说,这个接口在文件被访问前、访问中和访问后,以及产生错误的时候都有相应的钩子程序进行处理。

调用FileVisitor中的方法,会返回访问结果的FileVisitResult对象值,用于决定当前操作完成后接下来该如何处理。FileVisitResult的标准返回值存放在FileVisitResult枚举类型中,代码如下。

设计模式 -- 访问者模式(Visitor Pattern)_第9张图片

设计模式 -- 访问者模式(Visitor Pattern)_第10张图片

设计模式 -- 访问者模式(Visitor Pattern)_第11张图片

(1)FileVisitResult.CONTINUE:这个访问结果表示当前的遍历过程将会继续。

(2)FileVisitResult.SKIP_SIBLINGS:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前文件/目录的兄弟节点。

(3)FileVisitResult.SKIP_SUBTREE:这个访问结果表示当前的遍历过程将会继续,但是要忽略当前目录下的所有节点。

(4)FileVisitResult.TERMINATE:这个访问结果表示当前的遍历过程将会停止。

通过访问者去遍历文件树会比较方便,比如查找文件夹内符合某个条件的文件或者某一天内所创建的文件,这个类中都提供了相对应的方法。它的实现其实也非常简单,代码如下。

public final class Files {
    ...
    public static Path walkFileTree(Path start, FileVisitor visitor)
            throws IOException
    {
        return walkFileTree(start,
                            EnumSet.noneOf(FileVisitOption.class),
                            Integer.MAX_VALUE,
                            visitor);
    }
    ...
}


public static Path walkFileTree(Path start,
                                Set options,
                                int maxDepth,
                                FileVisitor visitor)
    throws IOException
{
    /**
     * Create a FileTreeWalker to walk the file tree, invoking the visitor
     * for each event.
     */
    try (FileTreeWalker walker = new FileTreeWalker(options, maxDepth)) {
        FileTreeWalker.Event ev = walker.walk(start);
        do {
            FileVisitResult result;
            switch (ev.type()) {
                case ENTRY :
                    IOException ioe = ev.ioeException();
                    if (ioe == null) {
                        assert ev.attributes() != null;
                        result = visitor.visitFile(ev.file(), ev.attributes());
                    } else {
                        result = visitor.visitFileFailed(ev.file(), ioe);
                    }
                    break;

                case START_DIRECTORY :
                    result = visitor.preVisitDirectory(ev.file(), ev.attributes());

                    // if SKIP_SIBLINGS and SKIP_SUBTREE is returned then
                    // there shouldn't be any more events for the current
                    // directory.
                    if (result == FileVisitResult.SKIP_SUBTREE ||
                        result == FileVisitResult.SKIP_SIBLINGS)
                        walker.pop();
                    break;

                case END_DIRECTORY :
                    result = visitor.postVisitDirectory(ev.file(), ev.ioeException());

                    // SKIP_SIBLINGS is a no-op for postVisitDirectory
                    if (result == FileVisitResult.SKIP_SIBLINGS)
                        result = FileVisitResult.CONTINUE;
                    break;

                default :
                    throw new AssertionError("Should not get here");
            }

            if (Objects.requireNonNull(result) != FileVisitResult.CONTINUE) {
                if (result == FileVisitResult.TERMINATE) {
                    break;
                } else if (result == FileVisitResult.SKIP_SIBLINGS) {
                    walker.skipRemainingSiblings();
                }
            }
            ev = walker.next();
        } while (ev != null);
    }

    return start;
}

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

再来看访问者模式在Spring中的应用,Spring IoC中有个BeanDefinitionVisitor类,其中有一个visitBeanDefinition()方法,源码如下。

设计模式 -- 访问者模式(Visitor Pattern)_第12张图片

我们看到,在visitBeanDefinition()方法中,访问了其他数据,比如父类的名字、自己的类名、在IoC容器中的名称等各种信息。


根据以下文章总结:

  1. Java设计模式:23种设计模式全面解析(超级详细)HYPERLINK http://c.biancheng.net/design_pattern/ 

  2. 3种设计模式详解 https://www.iteye.com/blog/zz563143188-1847029 

  3. Android系统编程思想:设计模式https://github.com/sucese/android-open-source-project-analysis/blob/master/doc/Android%E7%B3%BB%E7%BB%9F%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E7%AF%87/02Android%E7%B3%BB%E7%BB%9F%E8%BD%AF%E4%BB%B6%E8%AE%BE%E8%AE%A1%E7%AF%87%EF%BC%9A%E8%AE%BE%E8%AE%A1%E6%A8%A1%E5%BC%8F.md#35-%E8%A7%82%E5%AF%9F%E8%80%85%E6%A8%A1%E5%BC%8F

  4. 设计模式 https://blog.csdn.net/shusheng0007/category_8638565.html

  5. java设计模式 https://blog.csdn.net/qq_37909508/category_8976362.html

  6. 设计模式 https://www.cnblogs.com/zuoxiaolong/category/509144.html 

  7. 设计模式 在源码中的应用 https://blog.csdn.net/qq_36970993/category_10620886.html

  8. Android系统设计中存在设计模式分析 https://www.2cto.com/kf/201208/150650.html

  9. Android设计模式系列 - 基于android的各种代码分析各种设计模式 https://www.cnblogs.com/qianxudetianxia/category/312863.html 

你可能感兴趣的:(设计模式,设计模式,访问者模式)