「补课」进行时:设计模式(18)——访问者模式

1. 前文汇总

「补课」进行时:设计模式系列

2. 引言

访问者模式也可以说是所有设计模式中最难的一种设计模式了,当然我们平常也很少会用到它。设计模式的作者是这么评价访问者模式的:大多情况下,你并不需要使用访问者模式,但是一旦需要使用它时,那就真的需要使用了。

3. 一个简单的示例

又快到年底, CEO 和 CTO 开始评定员工一年的工作绩效,员工分为工程师和经理, CTO 关注工程师的代码量、经理的新产品数量; CEO 关注的是工程师的KPI和经理的KPI以及新产品数量。

由于 CEO 和 CTO 对于不同员工的关注点是不一样的,这就需要对不同员工类型进行不同的处理。访问者模式此时可以派上用场了。

首先定义一个员工基类 Staff :

public abstract class Staff {
    public String name;
    // 员工KPI
    public int kpi;

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

Staff 类定义了员工基本信息及一个 accept() 方法, accept() 方法表示接受访问者的访问,由子类具体实现。

Visitor 是个接口,传入不同的实现类,可访问不同的数据。

下面是工程师和经理的具体实现类:

public class Engineer extends Staff {
    public Engineer(String name) {
        super(name);
    }
    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 工程师一年的代码数量
    public int getCodeLines() {
        return new Random().nextInt(10 * 10000);
    }
}

public class Manager extends Staff {
    public Manager(String name) {
        super(name);
    }
    @Override
    void accept(Visitor visitor) {
        visitor.visit(this);
    }
    // 一年做的产品数量
    public int getProducts() {
        return new Random().nextInt(10);
    }
}

工程师是代码数量,经理是产品数量,他们的职责不一样,也就是因为差异性,才使得访问模式能够发挥它的作用。

下面是 Visitor 接口的定义:

public interface Visitor {
    // 访问工程师类型
    void visit(Engineer engineer);
    // 访问经理类型
    void visit(Manager manager);
}

Visitor 声明了两个 visit 方法,分别是对工程师和经理对访问函数。

接下来定义两个具体的访问者: CEO 和 CTO 。

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());
    }
}

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());
    }
}

接着是一个报表类,公司的 CEO 和 CTO 通过这个报表查看所有员工的业绩:

public class BusinessReport {
    private List<Staff> mStaffs = new LinkedList<>();
    public BusinessReport() {
        mStaffs.add(new Manager("经理-A"));
        mStaffs.add(new Engineer("工程师-A"));
        mStaffs.add(new Engineer("工程师-B"));
        mStaffs.add(new Manager("经理-B"));
        mStaffs.add(new Engineer("工程师-C"));
    }
    /**
     * 为访问者展示报表
     * @param visitor 公司高层,如 CEO、CTO
     */
    public void showReport(Visitor visitor) {
        for (Staff staff : mStaffs) {
            staff.accept(visitor);
        }
    }
}

最后是一个场景类:

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

执行结果如下:

=========== CEO看报表 ===========
经理: 经理-A, KPI: 7, 新产品数量: 8
工程师: 工程师-A, KPI: 6
工程师: 工程师-B, KPI: 3
经理: 经理-B, KPI: 4, 新产品数量: 4
工程师: 工程师-C, KPI: 2
=========== CTO看报表 ===========
经理: 经理-A, 产品数量: 6
工程师: 工程师-A, 代码行数: 61280
工程师: 工程师-B, 代码行数: 10353
经理: 经理-B, 产品数量: 5
工程师: 工程师-C, 代码行数: 65827

4. 访问者模式

4.1 定义

访问者模式(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. (封装一些作用于某种数据结构中的各元素的操作, 它可以在不改变数据结构的前提下定义作用于这些元素的新的操作。 )

4.2 通用类图

  • Visitor 抽象访问者:抽象类或者接口,声明访问者可以访问哪些元素。
  • ConcreteVisitor 具体访问者:它影响访问者访问到一个类后该怎么干, 要做什么事情。
  • Element 抽象元素:接口或者抽象类,声明接受哪一类访问者访问。
  • ConcreteElement 具体元素:实现方法。
  • ObjectStruture 结构对象:元素产生者,一般容纳在多个不同类、不同接口的容器。

4.3 通用代码

抽象元素:

public abstract class Element {
    // 定义业务逻辑
    abstract void doSomething();
    // 定义允许访问角色
    abstract void accept(IVisitor visitor);
}

具体元素:

public class ConcreteElement1 extends Element{
    @Override
    void doSomething() {

    }

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

public class ConcreteElement2 extends Element{
    @Override
    void doSomething() {

    }

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

抽象访问者:

public interface IVisitor {
    void visit(ConcreteElement1 ele1);
    void visit(ConcreteElement2 ele2);
}

具体访问者:

public class Visitor implements IVisitor{
    @Override
    public void visit(ConcreteElement1 ele1) {
        ele1.doSomething();
    }

    @Override
    public void visit(ConcreteElement2 ele2) {
        ele2.doSomething();
    }
}

结构对象:

public class ObjectStruture {
    public static Element createElement() {
        Random random = new Random();
        if (random.nextInt(100) > 50) {
            return new ConcreteElement1();
        } else {
            return new ConcreteElement2();
        }
    }
}

场景类:

public class Client {
    public static void main(String[] args) {
        for (int i = 0; i < 10; i++) {
            Element e1 = ObjectStruture.createElement();
            e1.accept(new Visitor());
        }
    }
}

4.4 优点

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

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

  2. 具有优秀的扩展性。

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

  3. 使得数据结构和作用于结构上的操作解耦,使得操作集合可以独立变化。

    员工属性(数据结构)和CEO、CTO访问者(数据操作)的解耦。

  4. 灵活性。

4.5 缺点

  1. 具体元素对访问者公布细节,违反了迪米特原则。

    CEO、CTO需要调用具体员工的方法。

  2. 具体元素变更时导致修改成本大。

    变更员工属性时,多个访问者都要修改。

  3. 违反了依赖倒置原则,为了达到「区别对待」而依赖了具体类,没有用来抽象。

    访问者 visit 方法中,依赖了具体员工的具体方法。

posted @ 2020-12-14 08:54  极客挖掘机  阅读(753)  评论(0编辑  收藏  举报