设计模式-访问者模式及应用

1,008 阅读8分钟

在软件开发的过程中,经常会对一个数据结构(对象数据)进行不同的业务操作,被访问的方法也不同。例如我们对一个权限管理系统某一个用户对象做操作,用户的用户名查询、更改,用户的积分查询,用户的权限查询等。为满足不同的业务,且单一职责的原则。都是对一个对象,有不同的操作。

现实生活中,比如我们使用某应用软件观看一部电影,对本部电影发表自己的见解评论及评分。每个用户的评价肯定都是不同的,但是评价的对象都是这部电影。 这种被处理的数据相对稳定且唯一,而访问的方式有多种,使用今天所要介绍的设计模式(访问者模式),处理此类问题就很方便。

访问者模式,能把处理方法从数据结构中分离出来,并可以根据需要增加新的处理方法,且不用修改原来的程序代码与数据结构,这提高了程序的扩展性和灵活性。

定义及结构特点

访问者(Visitor)模式的定义:将作用于某种数据结构中的各元素的操作分离出来封装成独立的类,使其在不改变数据结构的前提下可以添加作用于这些元素的新的操作,为数据结构中的每个元素提供多种访问方式,它将对数据的操作与数据结构进行分离。

访问者模式的结构与实现

从前面的定义不难看出,访问者的结构就是将元素(操作资源)与操作分离,封装为独立的类。

  1. 模式的结构 访问者模式包含以下主要角色。
  • 抽象访问者(Visitor)角色:定义一个访问具体元素的接口,为每个具体元素类对应一个访问操作 visit() ,该操作中的参数类型标识了被访问的具体元素。
  • 具体访问者(ConcreteVisitor)角色:实现抽象访问者角色中声明的各个访问操作,确定访问者访问一个元素时该做什么。
  • 抽象元素(Element)角色:声明一个包含接受操作 accept() 的接口,被接受的访问者对象作为 accept() 方法的参数。
  • 具体元素(ConcreteElement)角色:实现抽象元素角色提供的 accept() 操作,其方法体通常都是 visitor.visit(this) ,另外具体元素中可能还包含本身业务逻辑的相关操作。
  • 对象结构(Object Structure)角色:是一个包含元素角色的容器,提供让访问者对象遍历容器中的所有元素的方法,通常由 List、Set、Map 等集合类实现。

2.访问者模式结构图 在这里插入图片描述

代码案例
1.案例1
  • 抽象访问者
public interface Visitor {
	// 提供不同访问元素接口
	void visit(ConcreteElementA element);
    void visit(ConcreteElementB element);
}
  • 具体访问者A
public class ConcreteVisitorA implements Visitor {

	//实现不同访问元素实现
	@Override
	public void visit(ConcreteElementA element) {
		System.out.println("访问者A,访问" + element.operation());
	}

	@Override
	public void visit(ConcreteElementB element) {
		System.out.println("访问者A,访问" + element.operation());
	}
}
  • 具体访问者B
public class ConcreteVisitorB implements Visitor{

	@Override
	public void visit(ConcreteElementA element) {
		System.out.println("访问者B,访问" + element.operation());
	}

	@Override
	public void visit(ConcreteElementB element) {
		System.out.println("访问者B,访问" + element.operation());
	}
}
  • 抽象元素
//抽象元素
public interface Element {
	
	//一般提供一个接受访问者接口
	void accept(Visitor visitor);
}
  • 具体元素A
public class ConcreteElementA implements Element {

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

	public String operation(){
		return "具体元素A";
	}
}
  • 具体元素B
public class ConcreteElementB implements Element{

	@Override
	public void accept(Visitor visitor) {
		visitor.visit(this);
	}
	public String operation(){
		return "具体元素B";
	}
}
  • 对象结构 这个角色比较特殊,这里特殊说明,对象结构这里是维护多个元素,执行时通过遍历元素的方式传递访问者。
public class ObjectStructure {
	
	private List<Element> list = new ArrayList<Element>();
    
	public void accept(Visitor visitor) {
        Iterator<Element> 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);
    }
}
  • client使用方
public class Client {
	
	public static void main(String[] args) {
		ObjectStructure objectStructure = new ObjectStructure();
		//添加两个访问元素 A\B
		objectStructure.add(new ConcreteElementA());
		objectStructure.add(new ConcreteElementB());
		
		ConcreteVisitorA concreteVisitorA = new ConcreteVisitorA();
		objectStructure.accept(concreteVisitorA);
	}
}

访问者A,访问具体元素A 访问者A,访问具体元素B

2.案例2

案例2,这里写一个项目实战场景中的运用。首先我们的(风控)项目中,规则引擎中的规则元素对象,包含很多规则的元素(属性),决策跑规则中的条件,判定是否满足规则,来做命中后的前后操作。比如说触发规则的条件,规则配置的条件信息,规则命中后的后置操作等等。这里简单通过一个类图展示一下规则元素的属性。 在这里插入图片描述

在开发的过程中,我们需要解析规则中配置了哪些字段,规则操作中(命中后)配置的详情,规则触发的配置等等,这些属性都属于规则元素的属性,业务直接又需要解析规则中的各属性,如果写不同的接口方法,来操作当前的规则元素,耦合性很差。且如果再对规则元素做相关的操作,还要增加接口方法,数据访问与具体的元素未隔离,不易拓展。

通过代码的方式,看下项目中的最佳实践。首先规则,作为具体元素,继承抽象接口。

  • 抽象元素
public interface Visitable {
    void accept(Visitor var1);
}
  • 具体元素 (规则)
public class Rule Visitable {
    // ...
    /**
     * 规则条件配置
     *
     * @see RuleCondition
     */
    private RuleCondition condition;


    /**
     * 规则操作配置
     *
     * @see RuleAction
     */
    private List<RuleAction> actions;
    
    /**
     * 规则触发操作
     */
    private String triggers;
    
    // ....略
    
// accept 接受访问者接口    
@Override
    public void accept(Visitor visitor) {
        visitor.visit(this);
    }
}
  • 抽象访问者
public interface Visitor<T> {
    void visit(T var1);
}
  • 具体访问者

代码这里不详贴,看下几个实现类。 在这里插入图片描述

  • 使用方

直接通过具体元素(规则),调用accept()方接受访问者,具体对元素的操作在访问者中。 在这里插入图片描述

然后获取访问者对元素的操作结果 在这里插入图片描述

以上是项目实战中的场景应用,如果后续我们增加了新的需求,直接拓展访问者即可,无需更改影响已有的代码逻辑。

访问者模式的优缺点及应用场景

1.优点:

  • 扩展性好。能够在不修改对象结构中的元素的情况下,为对象结构中的元素添加新的功能。
  • 复用性好。可以通过访问者来定义整个对象结构通用的功能,从而提高系统的复用程度。
  • 灵活性好。访问者模式将数据结构与作用于结构上的操作解耦,使得操作集合可相对自由地演化而不影响系统的数据结构。
  • 符合单一职责原则。访问者模式把相关的行为封装在一起,构成一个访问者,使每一个访问者的功能都比较单一。

2.缺点:

  • 增加新的元素类很困难。在访问者模式中,每增加一个新的元素类,都要在每一个具体访问者类中增加相应的具体操作,这违背了“开闭原则”。
  • 破坏封装。访问者模式中具体元素对访问者公布细节,这破坏了对象的封装性。
  • 违反了依赖倒置原则。访问者模式依赖了具体类,而没有依赖抽象类。
访问者模式的应用场景

当对一个元素有不同的多种操作时,使用访问者模式。

通常在以下情况可以考虑使用访问者(Visitor)模式。

  • 对象结构相对稳定,但其操作算法经常变化的程序。
  • 对象结构中的对象需要提供多种不同且不相关的操作,而且要避免让这些操作的变化影响对象的结构。
  • 对象结构包含很多类型的对象,希望对这些对象实施一些依赖于其具体类型的操作。

访问者模式在源码中的使用

1.spring源码中的使用 spring容器中,BeanDefinition接口用于存储,spring bean的基础信息。包括bean的name,bean的class、scope,是否懒加载 isLazyInit等等。实现类常用的主要有 RootBeanDefinition、ChildBeanDefinition、GenericBeanDefinition等。

在spring beans中有个BeanDefinitionVisitor类中,有对BeanDefinition对象的一系列信息补充,如源码:

public class BeanDefinitionVisitor {
    @Nullable
    private StringValueResolver valueResolver;

    public BeanDefinitionVisitor(StringValueResolver valueResolver) {
        Assert.notNull(valueResolver, "StringValueResolver must not be null");
        this.valueResolver = valueResolver;
    }

    protected BeanDefinitionVisitor() {
    }

    public void visitBeanDefinition(BeanDefinition beanDefinition) {
        // 一些列的访问者方法
        this.visitParentName(beanDefinition);
        this.visitBeanClassName(beanDefinition);
        this.visitFactoryBeanName(beanDefinition);
        this.visitFactoryMethodName(beanDefinition);
        this.visitScope(beanDefinition);
        if (beanDefinition.hasPropertyValues()) {
            this.visitPropertyValues(beanDefinition.getPropertyValues());
        }

        if (beanDefinition.hasConstructorArgumentValues()) {
            ConstructorArgumentValues cas = beanDefinition.getConstructorArgumentValues();
            this.visitIndexedArgumentValues(cas.getIndexedArgumentValues());
            this.visitGenericArgumentValues(cas.getGenericArgumentValues());
        }

    }

    protected void visitBeanClassName(BeanDefinition beanDefinition) {
        String beanClassName = beanDefinition.getBeanClassName();
        if (beanClassName != null) {
            String resolvedName = this.resolveStringValue(beanClassName);
            if (!beanClassName.equals(resolvedName)) {
                beanDefinition.setBeanClassName(resolvedName);
            }
        }
    }
    protected void visitScope(BeanDefinition beanDefinition) {
        String scope = beanDefinition.getScope();
        if (scope != null) {
            String resolvedScope = this.resolveStringValue(scope);
            if (!scope.equals(resolvedScope)) {
                beanDefinition.setScope(resolvedScope);
            }
        }
    }
    // 略...

这里的访问者模式使用的比较简单,BeanDefinition对象作为元素,访问者中不同的方法,就是对BeanDefinition对象的补充。也无做元素抽象,访问者抽象等设计。