设计模式(16)访问者模式

访问者模式是一种行为型模式,它是23种设计模式中最复杂的一个,虽然使用频率不高,但是并不代表可以忽略,在合适的地方,它会带来意想不到的灵活性。访问者模式,顾名思义使用了这个模式后就可以在不修改已有程序结构的前提下,通过添加额外的“访问者”来完成对已有代码功能的提升。

定义

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

使用场景

(1)对象结构比较稳定,但经常需要在此对象结构上定义新的操作。
(2)需要对一个对象结构中的对象进行很多不同的且不相关的操作,而需要避免这些操作“污染”这些对象的类,也不希望在增加新操作时修改这些类。

UML类图

(1)Visitor:接口或者抽象类,它定义了对每一个元素(Element)访问的行为,它的参数就是可以访问的元素,它的方法数理论上来讲与元素个数是一样的,因此,访问者模式要求元素的类族要稳定,如果经常添加、移除元素类,必然会导致频繁地修改Visitor接口,如果这样则不适合使用访问者模式。
(2)ConcreteVisitor1ConcreteVisitor2:具体的访问类,它需要给出对每一个元素类访问时所产生的具体行为。
(3)Element:元素接口或者抽象类,它定义了一个接受访问者的方法(Accept),其意义是指每一个元素都要可以被访问者访问。
(4)ConcreteElementAConcreteElementB:具体的元素类,它提供接受访问方法的具体实现,而这个具体的实现,通常情况下是使用访问者提供的访问该元素类的方法。
(5)ObjectStructure:定义当中所说的对象结构,对象结构是一个抽象表述,它内部管理了元素集合,并且可以迭代这些元素供访问者访问。

简单实现

情景:年终了,公司会给员工进行业绩考核。但是,不同领域的管理人员对于员工的评定标准不一样。现在员工有攻城狮和经理,评定者有CEO和CTO,我们假定CTO只关注攻城狮的代码量、经理的新产品数量,而CEO关注的是攻城狮的KPI和经理的KPI以及新产品数量。

员工基类:

/**
 * 员工基类(Element) 
 */
public abstract class Staff {
    //员工姓名
    public String name;
    //员工KPI
    public int kpi;

    public Staff(String name) {
        super();
        this.name = name;
        this.kpi = new Random().nextInt(10);
    }
    //接受Visitor的访问
    public abstract void accept(Visitor visitor);
}

攻城狮:

/**
 * 攻城狮 
 */
public class Engineer extends Staff {

    private int codeLines; //代码数量

    public Engineer(String name) {
        super(name);
        codeLines = new Random().nextInt(10 * 10000);
    }

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

    //攻城狮这一年写的代码数量
    public int getCodeLines(){
        return codeLines;
    }
}

经理:

/**
 * 经理
 */
public class Manager extends Staff {

    private int products;//产品数量

    public Manager(String name) {
        super(name);
        products = new Random().nextInt(10);
    }

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

    //一年内做的产品数量
    public int getProducts(){
        return products;
    }
}

Visitor类:

public interface Visitor {
    /**
     * 访问攻城狮类型
     */
    public void visit(Engineer engineer);

    /**
     * 访问经理类型
     */
    public void visit(Manager manager);
}

CEO访问者:

public class CEOVisitor implements Visitor {

    @Override
    public void visit(Engineer engineer) {
        System.out.println("攻城狮:" + engineer.name + ", KPI:" + engineer.kpi);
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理:" + manager.name + ", KPI:" + manager.kpi + ", 新产品数量 :" + manager.getProducts());
    }
}

CTO访问类:

public class CTOVisitor implements Visitor {

    @Override
    public void visit(Engineer engineer) {
        System.out.println("攻城狮:" + engineer.name + ", 代码数量:" + engineer.getCodeLines());
    }

    @Override
    public void visit(Manager manager) {
        System.out.println("经理:" + manager.name +", 产品数量 :" + manager.getProducts());
    }
}

员工报表类:

//员工业务报表类(ObjectStructure)
public class BusinessReport {

    List<Staff> mStaffs = new LinkedList<Staff>();

    public BusinessReport() {
        mStaffs.add(new Manager("王经理"));
        mStaffs.add(new Engineer("攻城狮-A"));
        mStaffs.add(new Engineer("攻城狮-B"));
        mStaffs.add(new Manager("李经理"));
        mStaffs.add(new Engineer("攻城狮-C"));
    }

    /**
     * 为访问者展示报表 
     * @param visitor 如CEO、CTO
     */
    public void showReport(Visitor visitor){
        for(Staff staff : mStaffs){
            staff.accept(visitor);
        }
    }
}

Client访问:

public class Client {
    public static void main(String[] args) {
        //构建报表
        BusinessReport report = new BusinessReport();
        System.out.println("===== 给CEO看报表 =====");
        //设置访问者CEO
        report.showReport(new CEOVisitor());
        System.out.println("===== 给CTO看报表 =====");
        //设置访问者CTO
        report.showReport(new CTOVisitor());
    }
}

结果:

===== 给CEO看报表 =====
经理:王经理, KPI:2, 新产品数量 :5
攻城狮:攻城狮-A, KPI:5
攻城狮:攻城狮-B, KPI:7
经理:李经理, KPI:9, 新产品数量 :8
攻城狮:攻城狮-C, KPI:1
===== 给CTO看报表 =====
经理:王经理, 产品数量 :5
攻城狮:攻城狮-A, 代码数量:26238
攻城狮:攻城狮-B, 代码数量:8282
经理:李经理, 产品数量 :8
攻城狮:攻城狮-C, 代码数量:47927

从上面代码中可以看出,如果要增加一个访问者,你新创建一个实现了Visitor接口的类,然后实现两个visit方法来对不同的元素进行不同的操作,从而达到数据对象与数据操作相分离的效果。如果不使用访问者模式,而又想对不同元素进行不同的操作,那么必定会使用if-else和类型转换,这使得代码难以升级维护。

Android中的访问者模式

安卓中的著名开源库ButterKnifeDaggerRetrofit都是基于APT(Annotation Processing Tools)实现。而编译注解核心依赖APT。当我们通过APT处理注解时,最终会将获取到的元素转换为相应的Element元素,以便获取到它们对应信息。那么元素基类的源码如下:(路径:javax.lang.model.element.Element

public interface Element extends javax.lang.model.AnnotatedConstruct {

    /**
     * Returns the {@code kind} of this element.
     *
     * @return the kind of this element
     */
    ElementKind getKind();//获取元素类型

    //代码省略

    /**
     * Applies a visitor to this element.
     *
     * @param <R> the return type of the visitor's methods
     * @param <P> the type of the additional parameter to the visitor's methods
     * @param v   the visitor operating on this element
     * @param p   additional parameter to the visitor
     * @return a visitor-specified result
     */
    <R, P> R accept(ElementVisitor<R, P> v, P p);//接受访问者的访问
}

ElementVisitor就是访问者类型,ElementVisitor源码如下:

public interface ElementVisitor<R, P> {
    /**
     * Visits an element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visit(Element e, P p);

    /**
     * A convenience method equivalent to {@code v.visit(e, null)}.
     * @param e  the element to visit
     * @return a visitor-specified result
     */
    R visit(Element e);

    /**
     * Visits a package element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visitPackage(PackageElement e, P p);

    /**
     * Visits a type element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visitType(TypeElement e, P p);

    /**
     * Visits a variable element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visitVariable(VariableElement e, P p);

    /**
     * Visits an executable element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visitExecutable(ExecutableElement e, P p);

    /**
     * Visits a type parameter element.
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     */
    R visitTypeParameter(TypeParameterElement e, P p);

    /**
     * Visits an unknown kind of element.
     * This can occur if the language evolves and new kinds
     * of elements are added to the {@code Element} hierarchy.
     *
     * @param e  the element to visit
     * @param p  a visitor-specified parameter
     * @return a visitor-specified result
     * @throws UnknownElementException
     *  a visitor implementation may optionally throw this exception
     */
    R visitUnknown(Element e, P p);
}

ElementVisitor中定义了多种visit接口,每个接口处理一种元素类型,那么这就是典型的访问者模式。

总结

  • 优点

(1)各角色职责分离,符合单一职责原则
(2)具有优秀的扩展性
(3)使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化
(4)灵活性

  • 缺点

(1)具体元素对访问者公布细节,违反了迪米特原则
(2)具体元素变更时导致修改成本大
(3)违反了依赖倒置原则,为了达到“区别对待”而依赖了具体类,没有依赖抽象

版权声明:
作者:Joe.Ye
链接:https://www.appblog.cn/index.php/2023/03/16/design-pattern-visitor-pattern/
来源:APP全栈技术分享
文章版权归作者所有,未经允许请勿转载。

THE END
分享
二维码
打赏
海报
设计模式(16)访问者模式
访问者模式是一种行为型模式,它是23种设计模式中最复杂的一个,虽然使用频率不高,但是并不代表可以忽略,在合适的地方,它会带来意想不到的灵活性。访问者模……
<<上一篇
下一篇>>
文章目录
关闭
目 录