【设计模式27】访问者模式+源码分析:Eclipse JDT AST中浏览者模式

664 阅读6分钟

1. 简介

1.1 定义

访问者模式(Visitor Pattern)定义:表示一个作用于某对象结构中的各元素的操作,它 使我们可以在不改变各元素的类的前提下定义作用于这些元素的新操作。访问者模式是一 种对象行为型模式。

1.2 结构模式

image.png 图1 浏览者模式结构图

1.3 模式分析

想象一个使用场景,有一张药单,里面包含药品、数量、价格等不同类型的集合。对于护士而言看到药品和数量信息就是去取并进行相关医疗工作,而对于缴费工作人员看到药品、数量、价格就是去核对。 这是一个

  • 背景:多种类型的物品继承同一个公共祖先,并且在一个集合中。
  • 需求:有多种类型的visitor会根据不同的实际类型,需要作出不同决策。

2. 访问者模式实例

顾客在超市中将选择的商品,如苹果、图书等放在购物车中,然后到收银员处付款。在购物过程中,顾客需要对这些商品进行访问,以便确认这些商品的质量,之后收银员计算价格时也需要访问购物车内顾客所选择的商品。此时,购物车作为一个 ObjeetStructure(对象结构)用于存储各种类型的商品,而顾客和收银员作为访问这些商品的访问者,他们需要对商品进行检查和计价。不同类型的商品其访问形式也可能不同,如苹果需要过秤之后再计价,而图书不需要。使用访问者模式来设计该购物过程。 image.png 图2 购物车类图 ​

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 + "选苹果。");    
    }
                             
	public void visit(Book book) {
    	System. out. println("顾客"+ name + "买书。”);
    }
}
public class Saler extends Visitor {
	public void visit(Apple apple) {
		System. out. println("收银员"+ name+"给苹果过秤,然后计算其价格。");    	
    }
    
    public void visit(Apple apple) {
		System. out. print 1n("收银员"+ name +"直接计算书的价格。”);    
    }

}
public interface Product {
	void accept(Visitor visitor);
}
public class Apple implements Product {
	public void accept (Visitor visitor) {
    	visitor. visit(this);
    }
}
public class Book implements Product {
	public void accept (Visitor visitor) {
    	visitor. visit(this);
    }
}
import java. util.*

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) {
    	1ist.add(product);
    }
    
    public void removeProduct (Product product) {
    	list.remove(product);
    }

}
public class Client {
	public static void main(String a[]) {
    	Product b1 = new Book();
        Product b2 = new Book();
        
        Producr a1 = new Apple();
        
        BuyBasket basket = new BuyBasket();
		basket.addProduct(b1);
		basket.addProduct(b2);
		basket.addProduct(a1);
        
        Visitor customer = new Customer();
        basket.accept(visitor);
        
    }
}

3. 源码分析:Eclipse JDT AST中的访问者模式

在遍历语法树的时候,如果我们需要添加新的访问者,会重写一个新的类去继承 abstract class ASTVisitor,然后重写我们需要的方法,所有的visit方法默认 return true;。 现在我需要通过语法树去统计每个类被引用的次数,只需要写一个新的类去继承 ASTVisitor 抽象类,然后重写 public boolean visit(MethodDeclaration node) public boolean visit(FieldDeclaration node)两个函数,统计语法树的子节点出现的被引用的类的情况。

@Override
public class MyVisitor extends ASTVisitor {
    public boolean visit(MethodDeclaration node) {
    	for (Object o : node.parameters()) {//遍历函数参数
    	//获取参数属于哪个类,然后为该类出现的次数+1
    }
	
    public boolean visit(FieldDeclaration node) {
    	//获取成员变量属于哪个类,然后为该类出现的次数+1
    }
}

如何调用呢?使用 compilationUnit.accept(myVisitor);即可。 我们发现定义新的访问规则其实挺容易的,不用去修改 CompilationUnit 本身,这就是访问者模式的优点。 compilationUnit.accept(myVisitor);的函数执行时,调用的相关源码如下

//ASTNode:
public final void accept(ASTVisitor visitor) {
    if (visitor == null) {
        throw new IllegalArgumentException();
    } else {
        if (visitor.preVisit2(this)) {
            this.accept0(visitor);
        }

        visitor.postVisit(this);
    }
}
//ASTVistor:
public boolean preVisit2(ASTNode node) {
        this.preVisit(node);
        return true;
    }
}
//ASTVistor:
public void preVisit(ASTNode node) {
}
//ASTNode:
abstract void accept0(ASTVisitor var1);
//CompilationUnit:
void accept0(ASTVisitor visitor) {
    boolean visitChildren = visitor.visit(this);
    if (visitChildren) {
        this.acceptChild(visitor, this.getPackage());
        this.acceptChildren(visitor, this.imports);
        this.acceptChildren(visitor, this.types);
    }

    visitor.endVisit(this);
}
//ASTNode:
final void acceptChild(ASTVisitor visitor, ASTNode child) {
    if (child != null) {
        child.accept(visitor);
    }
}
//ASTNode:
final void acceptChildren(ASTVisitor visitor, ASTNode.NodeList children) {
    ASTNode.NodeList.Cursor cursor = children.newCursor();

    try {
        while(cursor.hasNext()) {
            ASTNode child = (ASTNode)cursor.next();
            child.accept(visitor);
        }
    } finally {
        children.releaseCursor(cursor);
    }

}

//ASTVistor:
public void endVisit(ASTNode node) {
}

我们发现当调用accep方法时候,实际上是先调用visitor.preVisit(node),然后以前序遍历访问顺序分别访问Package、imports、types(该文件中的其他类),通过函数的重载实现了对于不同类型进行不同visit操作。 类图如图3所示:

image.png 图3 ASTNode、ASTVistor源码

总体上写的非常精妙的,但是如果有新的类 MyASTNode 实现了 ASTNode 抽象类,那么需要对ASTVistor 增加 preVisit(MyASTNode node),endVisit(MyASTNode node),visit(MyASTNode node) 三个函数了。不过介于目前Java语言已经趋于稳定,增加新的ASTNode的可能性不大。这里表现了一种 倾斜的开闭原则。

4. 总结

  1. 访问者模式的优点
    1. 使得增加新的访问操作变得很容易。使用访问者模式,增加新的访问操作就意味着增加一个新的访问者类,无须修改现有类库代码,符合“开闭原则”的要求。
    2. 将有关元素对象的访问行为集中到一个访问者对象中,而不是分散到一个个的元素类中。类的职责更加清晰,有利于对象结构中元素对象的复用,相同的对象结构可以供多个不同的访问者访问。
  2. 访问者模式的缺点
    1. 在访问者模式中,每增加一个新的元素类都意味着要在抽象访问者角色中增加一个新的抽象操作,并在每一个具体访问者类中增加相应的具体操作,违背了“开闭原则”的要求。
    2. 破坏封裝。访问者模式要求访问者对象访问并调用每一个元素对象的操作,这意味着元素对象有时候必须暴露一些自己的内部操作和内部状态,否则无法供访问者访问。
  3. 适用场景
    1. 一个对象结构包含很多类型的对象,希望对这些对象实施一些依赖其具体类型的操作。在访问者中针对每一种具体的类型都提供了一个访问操作,不同类型的对象可以有不同的访问操作。
    2. 需要对一个对象结构中的对象进行很多不同的并且不相关的操作,而需要避免让这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。访问者模式使得我们可以将相关的访问操作集中起来定义在访问者类中,对象结构可以被多个不同的访问者类所使用,将对象本身与对象的访问操作分离。
    3. 对象结构中对象对应的类很少改变,但经常需要在此对象结构上定义新的操作。