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

共 5152字,需浏览 11分钟

 ·

2020-12-17 17:58

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 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 方法中,依赖了具体员工的具体方法。





感谢阅读



浏览 12
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报