设计模式之访问者模式

 模式动机
  对于系统中的某些对象,它们存储在同一个集合中,且具有不同的类型,而且对于该集合中的对象,可以接受一类称为访问者的对象来访问,而且不同的访问者其访问方式有所不同访问者模式为解决这类问题而诞生。
 
 
在现在世界也存在类似的情况,如医院里面的药单(处方单),可以将其看成是药品信息的集合,这些药品的类型并不相同,划价人员拿到药单之后根据药品名称和数量计算总价,药房工作人员根据药品名称和数量准备药品,不同类型的工作人员对同一个集合对象可以有不同的操作,而且可能还会增加新的类型工作人员操作药单。在这里,药单是集合对象,而里面的药品信息是一个个需要访问的元素,工作人员是访问者,他们需要访问存储在药单中的元素信息
 
 在java等oo中都提供了大量存储多个元素的集合对象,在一般情况下这些集合存储的都是同一类型的对象,在集合中采取的操作也都是针对相同类型对象的同类操作,但是有时后保存在一个集合对象中的元素对象类型并不相同,他们可能只是具有公共的父类型,如果需要针对一个包含不同类型元素的集合采取某种操作,而操作的细节根据元素的类型不同而有所不同时,就会出现大量对元素对象进行类型判断的条件转移语句,将导致代码复杂度增大
 
 
在实际使用时,对同一集合对象的操作并不是唯一的,对相同的元素对象可能存在多种不同的操作方式。如上面的药单。而且这些操作方式并不稳定可能还需要增加新的操作,以满足新的业务需求。此时,访问者模式就是一个值得考虑的解决方案。
访问者模式的目的是封装一些施加于某种数据结构元素之上的操作,一旦这些操作需要修改的话,接受这个操作的数据结构可以保持不变不同类型的元素提供多种访问操作方式,且可以在不修改原有系统的情况下增加新的操作方式,这就是访问者模式的模式动机。
 
模式定义:
ü访问者模式(Visitor Pattern):表示一个作用于某对象结构中的各元素的操作,它使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一种对象行为型模式。
 
üVisitor Pattern: Represent an operation to be performed on the elements of an object structure. Visitor lets you define a new operation without changing the classes of the elements on which it operates.
 
 ObjectStructure对象结构:
 对象结构是一个元素的集合,它用于存放元素对象,并且提供了遍历其内部元素的方法。他可以结合组合模式来实现,也可以是一个简单的集合对象,如一个list或一个set对象。
 
模式分析:
访问者模式包括2个层次结构,一个是访问者层次结构,提供了抽象访问者和具体访问者,一个是元素层次结构,提供了抽象元素和具体元素。相同的访问者可以以不同的方式访问不同的元素,相同的元素可以接受不同访问者以不同方式访问
 
 
  在访问者模式中,抽象访问者声明了访问元素对象的方法,通常为每一种类型的元素对象提供一个访问方法,而具体访问者可以实现这些访问方法。这些访问方法的设计有2中方法,一种是直接在方法名中标明待访问元素对象的类型,如visitElemlentA(ElementA
elementA),还有一种是统一取名为visit(),通过参数类型不同来定义重载的操作。当然如果所有的方法者对某一类型的的元素的访问操作都相同,则可以将操作代码移到抽象访问者类中,其典型代码如下:
public abstract class Visitor
{
public abstract void visit(ConcreteElementA elementA);
public abstract void visit(ConcreteElementB elementB);
public void visit(ConcreteElementC elementC)
{
//元素ConcreteElementC操作代码
}
}
 
典型的抽象元素类,在其中一般声明了一个accept()方法,用于接收访问者的访问,代码如下:
public interface Element
{
public void accept(Visitor visitor);
}
在具体元素类中还可以定义不同类型元素所特有的业务方法,代码如下:
public class ConcreteElementA implements Element
{
public void accept(Visitor visitor)
{
   visitor.visit(this);
}
 
public void operationA()
{
  //业务方法
}
}
在具体元素类的accept()方法中,通过调用Visitor类的visit方法实现对元素的访问,并以当前对象作为visit()方法的参数,其具体执行过程如下:
1.调用具体类的accept()方法,将已经实例化的Visitor子类对象作为参数。
2.在accept()方法内部调用Visitor对象的visit方法,将当前具体元素类对象作为参数。。
3.执行visitor对象的visit()方法,在其中也可以调用具体元素类对象的业务方法。
这种调用机制称为“双重分派”,正因为使用了双重分派,使得增加新的访问者无需修改现有类库代码,只需将新的访问者对象传入具体元素对象的accept()方法即可,程序运行时将回调在新增Visitor类中定义的visit()方法,从而实现不同形式的访问
 
在访问者模式中,对象结构是一个集合,它用于存储对象并接受访问者的访问,代码如下:
public class ObjectStructure
{
    private ArrayList list=new ArrayList();
    public void accept(Visitor visitor)
    {
        Iterator i=list.iterator();
        
        while(i.hasNext())
        {
            ((Element)i.next()).accept(visitor);    
        }
    }
    public void addElement(Element element)
    {
        list.add(element);
    }
    public void removeElement(Element element)
    {
        list.remove(element);
    }
} 

实例:

ü购物车
•顾客在超市中将选择的商品,如苹果、图书等放在购物车中,然后到收银员处付款。在购物过程中,顾客需要对这些商品进行访问,以便确认这些商品的质量,之后收银员计算价格时也需要访问购物车内顾客所选择的商品。此时,购物车作为一个ObjectStructure(对象结构)用于存储各种类型的商品,而顾客和收银员作为访问这些商品的访问者,他们需要对商品进行检查和计价。不同类型的商品其访问形式也可能不同,如苹果需要过秤之后再计价,而图书不需要。使用访问者模式来设计该购物过程。
 
抽象访问者:
public abstract class Visitor {
    protected String name;
    public void setName(String name)
    {
        this.name=name;
    }
    public abstract void visit(Apple apple);
    public abstract void visit(Book book);
    

}

具体访问者:

public class Customer extends Visitor {

    
    public void visit(Apple apple) 
    {
        System.out.println("顾客"+name+"选苹果");

    }

    @Override
    public void visit(Book book) 
    {
        System.out.println("顾客"+name+"买书");
    }

}
public class Saler extends Visitor {

    
    public void visit(Apple apple) 
    {
        System.out.println("收银员"+name+"给苹果过秤,然后计算其价格");

    }

    @Override
    public void visit(Book book) 
    {
        System.out.println("收银员"+name+"直接计算书的价格");
    }

}

抽象元素类:

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

具体元素类:

public class Apple implements Product {

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

    }

}

public class Book  implements Product {

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

    }

}

ObjectStructure:

public class BuyBasket {
    private ArrayList list=new ArrayList();
    
    public void accept(Visitor visitor)
    {
        Iterator i=list.iterator();
        while(i.hasNext())
        {
            ((Product) i.next()).accept(visitor);
        }
    }
    
    public void addProduct(Product product)
    {
        list.add(product);
    }
    public void removeProduct(Product product)
    {
        list.remove(product);
    }

}

测试代码:

public class Client {
 
    public static void main(String[] args) {
         Product b1=new Book();
         Product b2=new Book();
         Product a1=new Apple();
         Visitor visitor;
         
         BuyBasket basket=new BuyBasket();
         basket.addProduct(b1);
         basket.addProduct(b2);
         basket.addProduct(a1);
         
         
         visitor=new Saler();
         
         visitor.setName("张三");
         basket.accept(visitor);
         
         visitor=new Customer();
         visitor.setName("李四");
         basket.accept(visitor);

    }

}

print:

收银员张三直接计算书的价格
收银员张三直接计算书的价格
收银员张三给苹果过秤,然后计算其价格
顾客李四买书
顾客李四买书
顾客李四选苹果

在该系统中如果需要增加一个新的类型的访问者,只需要增加一个新的类继承抽象访问者类Visitor,然后实现其中声明的抽象方法即可。符合“开闭原则‘。

但是在系统中如果需要增加新的类型的具体元素类,则需要修改访问者类的代码,包括抽象访问者代码,需要为新的具体元素类定义新的访问方法,因此,增加新的具体元素类必须修改现有类库代码,从这个角度来讲违背了”开闭原则“。

综上,访问者模式对”开闭原则”的支持存在倾斜性,增加新的访问者方便,但是增加新的元素很难

Ÿ访问者模式的优点
•使得增加新的访问操作变得很容易。
•将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。
•可以跨过类的等级结构访问属于不同的等级结构的元素类。
•让用户能够在不修改现有类层次结构的情况下,定义该类层次结构的操作。
 
ü访问者模式的缺点
•增加新的元素类很困难。在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
•破坏封装。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
 
w模式适用环境
ü在以下情况下可以使用访问者模式:
•一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
•需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
•对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。
 
模式应用
ü(1) 在一些编译器的设计中运用了访问者模式,程序代码是被访问的对,它包括变量定义、变量赋值、逻辑运算、算术运算等语句,编译器需要对代码进行分析,如检查变量是否定义、变量是否赋值、算术运算是否合法等,可以将不同的操作封装在不同的类中,如检查变量定义的类、检查变量赋值的类、检查算术运算是否合法的类,这些类就是具体访问者可以访问程序代码中不同类型的语句。在编译过程中除了代码分析外,还包含代码优化、空间分配和代码生成等部分,也可以将每一个不同编译阶段的操作封装到了跟该阶段有关的一个访问者类中
 
 
ü(2) 在常用的Java XML处理技术DOM4J中,可以通过访问者模式的方式来读取并解析XML文档,VisitorSupportDOM4J提供的Visitor口的默认适配器,具体访问者只需继承VisitorSupport类即可。
 
public class MyVisitor extends VisitorSupport
{
    public void visit(Element element)
    {
        System.out.println(element.getName());
    }
    public void visit(Attribute attr)
    {
        System.out.println(attr.getName());
    }
}
 
 
关于访问者模式的循环依赖问题和双重分派问题:
http://www.verydemo.com/demo_c293_i9539.html
 
http://perhaps.cnblogs.com/archive/2005/08/20/219048.html
 
 
 
 
 
原文地址:https://www.cnblogs.com/youxin/p/3097154.html