设计模式——访问者模式

一 简介

设计模式有三大类分别是创建型模式、结构型模式和行为型模式, 而访问者模式是属于行为型模式中的一种。

二 定义

表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素类的前提下定义作用于这些元素的新操作。

先来看第一句话,说是一个作用于某对象结构中的各元素的操作,这里提到了三个事物,一个是对象结构,一个是各元素,一个是操作。那么我们可以这么理解,有这么一个操作,它是作用于一些元素之上的,而这些元素属于某一个对象结构。
最关键的第二句来了,它说使用了访问者模式之后,可以让我们在不改变各元素类的前提下定义作用于这些元素的新操作。这里面的关键点在于前半句,即不改变各元素类的前提下,在这个前提下定义新操作是访问者模式精髓中的精髓。

三 UML类图

在这里插入图片描述
访问者模式主要由这五个角色组成,抽象访问者(Visitor)、具体访问者(ConcreteVisitor)、抽象元素(Element)、具体元素(ConcreteElement:ElementA、ElementB)和结构对象(ObjectStructure)。

  • Visitor:接口或者抽象类,定义了对每个 Element
    访问的行为,它的参数就是被访问的元素,它的方法个数理论上与元素的个数是一样的,因此,访问者模式要求元素的类型要稳定,如果经常添加、移除元素类,必然会导致频繁地修改
    Visitor 接口,如果出现这种情况,则说明不适合使用访问者模式。

  • ConcreteVisitor:具体的访问者,它需要给出对每一个元素类访问时所产生的具体行为。

  • Element:元素接口或者抽象类,它定义了一个接受访问者(accept)的方法,其意义是指每一个元素都要可以被访问者访问。

  • ElementA、ElementB:具体的元素类,它提供接受访问的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。

  • ObjectStructure:定义当中所提到的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素提供访问者访问。

四 代码示例

我们来看一个财务方面的简单例子,我们都知道财务都是有账本的,这个账本就可以作为一个对象结构,而它其中的元素有两种,收入和支出,这满足我们访问者模式的要求,即元素的个数是稳定的,因为账本报表中的元素只能是收入和支出。

账本的访问者都会有哪些呢?假设有老板和会计这两位访问者

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

Report类作为抽象元素(相当于Element),它定义了配件的基本信息及一个accept方法,accept方法表示接受访问者的访问,由子类具体实现。Visitor是个接口,传入不同的实现类,可访问不同的数据。下面看看收入单子和消费单子的代码:

//支出
public class ConsumeReport implements Report{

    private double amount;
    
    private String item;
    
    public ConsumeReport(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }

    public void accept(Visitor visiter) {
        visiter.visit(this);
    }

    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }

}

//收入

public class IncomeReport implements Report{

    private double amount;
    
    private String item;
    
    public IncomeReport(double amount, String item) {
        super();
        this.amount = amount;
        this.item = item;
    }

    public void accept(Visitor visiter) {
        visiter.visit(this);
    }

    public double getAmount() {
        return amount;
    }

    public String getItem() {
        return item;
    }

}

上面两个具体元素类,实现了accept方法,它直接让访问者访问自己,这相当于一次静态分派。

接下来给出访问者接口,创建Visitor类,声明了两个visit方法,分别是收入和支出的访问函数,具体代码如下:

public interface Visitor {

    void visit(ConsumeReport report);

    void visit(IncomeReport report);

}

有了访问基类,现在可以分别给老板和会计提供他们不同的行为来查看。老板只关注总的收入支出,会计关注税收。

public class Boss implements Visitor {

    private double totalIncome;

    private double totalConsume;

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

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

    
    @Override
    public void visit(ConsumeReport report) {
         老板只关注一共花了多少钱以及一共收入多少钱,其余并不关心
        totalConsume += report.getAmount();
    }

    @Override
    public void visit(IncomeReport report) {
        // 老板只关注一共花了多少钱以及一共收入多少钱,其余并不关心
        totalIncome += report.getAmount();
    }

}
public class CPA implements Visitor {

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

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

}

接下来创建一个报告平台(也就是ObjectStructure),用来提供给访问者查看。它管理这些收入和支出的集合,并且可以给访问者遍历访问

public class ReportPlatform {
    
    // 单子列表
    private List<Report> reports = new ArrayList<Report>();

    // 添加单子
    public void addReport(Report report) {
        reports.add(report);
    }

    // 供账本的查看者查看账本
    public void show(Visitor visitor) {
        for (Report report : reports) {
            report.accept(visitor);
        }
    }
}

最终客户端代码:

public class Client {

    
    public static void main(String[] args) {

        ReportPlatform reportPlatform = new ReportPlatform();
            //添加两条收入
        reportPlatform.addReport(new IncomeReport(10000, "卖商品"));
        reportPlatform.addReport(new IncomeReport(12000, "卖广告位"));
                //添加两条支出
        reportPlatform.addReport(new ConsumeReport(1000, "工资"));
        reportPlatform.addReport(new ConsumeReport(2000, "材料费"));
        
                Visitor boss = new Boss();
                 Visitor cpa = new CPA();
        
            //两个访问者分别访问账本
             reportPlatform.show(cpa);
             reportPlatform.show(boss);
        
            ((Boss) boss).getTotalConsume();
            ((Boss) boss).getTotalIncome();
        
    }

}

输出结果

注会查看账本时,只要是收入,注会都要查看公司交税了没。
注会查看账本时,只要是收入,注会都要查看公司交税了没。
注会查看账本时,如果单子的消费目的是发工资,则注会会查看有没有交个人所得税。
老板查看一共花费多少,数目是:3000.0
老板查看一共收入多少,数目是:22000.0

其实访问者模式有点绕,我这边比喻吧:假如老板现在手里拿到两份报表,但是老板想知道哪份是收入哪份是支出,是不是要走个if else 判断。可以看看下面的例子。

如果Visitor方法不采取多个方法重载,改为void visit(Report report),那么到时候老板的visit的方法里面就需要通过ifelse来区分哪个是收入,哪个是支出。

public void visit(Report report) {
        if (report instanceof ConsumeReport) {
            ConsumeReport temp = (ConsumeReport) report;
            System.out.println("支出 " + temp.getItem() + temp.getAmount());
        } else if (report instanceof IncomeReport) {
            IncomeReport temp = (IncomeReport) report;
            System.out.println("收入 " + temp.getItem() + temp.getAmount());
        }
    }

那如果采用访问者模式,那现在是由报表接收访问者,然后进入方法里面,由访问者调用accept方法,而由于accept方法经过重载,那么系统会帮我们选择由哪个报表来执行。这样老板这访问者就拿到想要的报表,就可以对报表进行其他奇怪的操作了

public void show(Visitor visitor) {
    for (Report report : reports) {
        report.accept(visitor);
    }
}

五 访问者模式的特点

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

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

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

第一点,数据结构和作用于结构上的操作解耦合,使操作集合可以相对自由的演化,这在上面的例子当中指的是,我们把账本以及账本的元素与查看账本的人解耦,使得这些访问者的行为可以相对独立的变化,这点其实并不难理解。这一点其实说的是访问者模式的优点。

至于剩下的两点,开始提到访问者模式适用于数据结构相对稳定,而算法行为又易变化的系统,这点不难理解,试想一下,如果账本结构不稳定,经常有元素加进来,那么假设有了第三种非支出也非收入的单子,那我们需要做以下两件事。

1)添加一个类CReport,实现Report接口。

2)在Visitor接口中添加一个方法visit(CReport report),并且在所有Visitor接口的实现类中都增加visit(CReport report)方法的具体实现。

这其中第一件事并不难,而且也符合开闭原则,但是第二件事就值得商榷了。它修改了抽象,导致所有细节都跟着变化,这完全破坏了开闭原则。所以第二点说使用访问者模式的前提是数据结构相对稳定也就不奇怪了。

然而对于算法操作,在访问者模式的使用下,我们可以自由的添加,这个在上面已经提及到,也就是说我们如果要增加查看账本的类,是非常简单的,我们只需要写一个类去实现Visitor接口,这是开闭原则的完美诠释。

访问者模式中,元素的添加会破坏开闭原则,访问者的添加又符合开闭原则,所以有文献称该模式是倾斜的开闭原则,即一边是符合开闭原则的,一边又是破坏了开闭原则的,有点倾斜的感觉。

六 总结

访问者模式的优点。

  1. 各角色职责分离,符合单一职责原则

通过UML类图和上面的示例可以看出来,Visitor、ConcreteVisitor、Element、ObjectStructure,职责单一,各司其责。

  1. 具有优秀的扩展性

如果需要增加新的访问者,增加实现类 ConcreteVisitor 就可以快速扩展。

  1. 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化收入(数据结构)和支出、访问者(数据操作)的解耦。
  2. 灵活性

访问者模式的缺点

  1. 具体元素对访问者公布细节,违反了迪米特原则 老板、会计需要调用具体报表的方法。
  2. 具体元素变更时导致修改成本大
  3. 违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有以来抽象 访问者visit方法中,依赖了具体报表项目的具体方法。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值