趣讲设计模式 (13) Visitor

每天讲解一个设计模式,突然高产 _(:з」∠)_ 。

本系列所有的设计模式代码示例均用Java实现,学习前你需要有如下前置知识:

(1)熟悉一门面向对象语言,理解面向对象思想

(2)熟悉面向对象特性:抽象、接口、继承、委托、多态

(3)理解向上转型,向下转型

从第十一章开始,你的世界观会被不断地放大,请做好准备!

一、故事

老狗最近有很认真的在和鱼皮学习设计模式,因此今天鱼皮决定请老狗去农家乐摘桃子。

老狗和鱼皮一边吃桃子,一边欣赏风景,其乐融融。

老狗:“这桃园真不错,十几块钱一张门票,又能玩又能吃~”

鱼皮:“嘿嘿,但是你要是没买票的话可享受不到这种服务了。”

老狗:“你这是话里有话啊,我先说好,今天带我出来散心,不准提你的什么设计模式。”

鱼皮:“嘿嘿,晚了,今天带你来这里就是为了教你一个设计模式的,好好听着!”

二、Visitor(访问者)模式

还记得我们最开始学习的Iterator模式么,在Iterator模式中,我们将数据结构与对其的遍历功能分离,使用迭代器来遍历数据结构,从而减少了数据结构的代码量。而Visitor模式,是Iterator模式的升级版,我们不仅可以将数据结构和对其的遍历分离,更能将数据结构和对其的处理分离!

每当我们需要提供一种对数据结构处理的方式,我们可以定义一个Visitor(访问者),在Visitor对象中定义处理方法,并在数据结构对象中接受某个Visitor,以调用其处理方法来操作数据结构。

更重要的是,Visitor模式遵循了面向对象的开闭原则——对扩展开放,对修改关闭。我们想增加对数据结构的处理功能时,新增Visitor即可。

这么说可能比较难理解,下面我们来看一个例子。

三、实现

场景:有一个桃园,购买门票的访客将被接受,允许游览桃园并摘桃子,享受服务

接口:访客

类:桃园、善良的访客、坏坏的访客

1. UML类图

注意:Park中定义了一个accept方法,用于接受访客的访问。而Visitor中定义了访问和吃桃方法,接受一个Park对象,用于操作桃园中的数据结构。

2. 代码

(1)桃园,作用:被访问的对象,内置链表数据结构,提供了操作链表的基本方法(种桃和摘桃),并通过accept方法,接受特定访客的访问

public class Park
{
    private List<String> list;
    String name;

    public Park(String name)
    {
        this.list = new LinkedList<>();
        this.name = name;
        System.out.println("欢迎来到" + name);
    }
    // 种桃
    public void plantPeach(int num)
    {
        for (int i = 0; i < num; i++)
        {
            if (i % 3 == 0)
            {
                list.add("大桃子");
            } else
            {
                list.add("小桃子");
            }
        }
        System.out.println("多了" + num + "个桃子");
    }
    // 摘桃
    public String getPeach()
    {
        return list.remove(list.size() - 1);
    }
    // 拥有的桃子数量
    public int size()
    {
        return list.size();
    }
    // 接受访客的访问
    public void accept(Visitor visitor)
    {
        System.out.println("桃园允许一名访客进入");
        visitor.visit(this);
        visitor.eatPeach(this);
        System.out.println("为该访客提供一条龙服务");
    }
}

(2)访客,作用:接口,定义了访问桃园并吃桃(处理数据结构的方法)

public interface Visitor
{
    // 访问公园
    void visit(Park park);
    // 吃桃
    void eatPeach(Park park);
}

(3)善良的访客,作用:实现了访问和吃桃方法,模拟合法访客

public class GoodVisitor implements Visitor
{
    @Override
    public void visit(Park park)
    {
        System.out.println("善良的访客进入了桃园" + park.name);
    }

    @Override
    public void eatPeach(Park park)
    {
        System.out.println("善良的访客给桃园贡献了一个桃子");
        park.plantPeach(1);
        System.out.println("善良的访客吃了一个" + park.getPeach());
    }
}

(4)坏坏的访客,作用:实现了访问和吃桃方法,模拟非法访客(未买票)

public class BadVisitor implements Visitor
{
    @Override
    public void visit(Park park)
    {
        System.out.println("坏坏的访客进入了桃园" + park.name);
    }

    @Override
    public void eatPeach(Park park)
    {
        System.out.println("坏坏的访客吃了一个"+park.getPeach());
        System.out.println("坏坏的访客吃了一个"+park.getPeach());
    }
}

(5)测试类:桃园接受合法访客,提供一条龙服务。坏访客强行闯入桃园,但无法享受服务。访客们将游玩并吃桃。

public class Main
{
    public static void main(String[] args){
        Park park = new Park("大安桃园");
        park.plantPeach(5);
        // 定义两种访客
        Visitor goodVisitor = new GoodVisitor();
        Visitor badVisitor = new BadVisitor();
        // 公园接受已购票访客
        park.accept(goodVisitor);
        // 未购票访客强行访问并吃桃
        badVisitor.visit(park);
        badVisitor.eatPeach(park);
    }
}

结果:

我们注意到,善良的访客(已购票)将被提供一条龙服务,且无需自己执行visit和eatPeach等方法,一个accept全部搞定。

四、点评

难度:★ ★ 

实用:★ ★ ★

适用场景:需要对数据结构有多种不同的处理方式时,可以将对其的处理分离成访客。而我们的访客同时可以访问多种不同的数据结构对象,例如为visit方法指定不同的参数类型(visit(Park …) 和 visit(Sea …) )。

角色:访客(接口,访问数据结构)、具体的访客、对象结构(存放数据结构的对象,接受访问者以被访问)

优点:处理与结构分离,使数据结构对象专注于结构,简化代码。同时符合面向对象思想中的开闭原则,扩展对数据结构的处理功能时只需增加Visitor即可。且我们可以在数据结构对象的accept方法中定义方法模板,提供通用化访问流程。

缺点:当数据结构对象增加时,我们要为Visitor接口增加一个visit方法,参数类型为新增的数据结构对象。因此接下来我们要给每个已定义的子类都实现该方法,非常麻烦。

发表评论

电子邮件地址不会被公开。