访问者和缺省适配器模式

CY 2020年01月20日 353次浏览

偶然间了解到了JSQLParser,Github上的源码介绍里面就说,这个工具是用Visitor Pattern

JSqlParser parses an SQL statement and translate it into a hierarchy of Java classes. The generated hierarchy can be navigated using the Visitor Pattern

访问者设计模式的用意就是将操作和数据结构分离开。

男人女人

看了一下《大话设计模式》上的男人女人的例子,用Java描述了一遍,下面是代码:

/**
 * 访问者的总接口
 */
public interface Action {

    void visit(Man man);

    void visit(Woman woman);
}

这个Action接口就是访问者接口,可以产生不确定个数的实现,用这个Action类来进行行为的扩展。

/**
 * 数据结构的总接口
 */
public interface Person {

    void accept(Action action);
}

Person接口是数据结构接口,这个例子中只有男人和女人,当然这个数据结构和Action接口中的visit方法数量息息相关。

// 男人的实现
public class Man implements Person {

    @Override
    public void accept(Action action) {
        action.visit(this);
    }
}

// 女人的实现
public class Woman implements Person {

    @Override
    public void accept(Action action) {
        action.visit(this);
    }
}
public class ObjectStructure {

    private List<Person> elements = new ArrayList<>();

    public void attach(Person person) {
        elements.add(person);
    }

    public void detach(Person person) {
        elements.remove(person);
    }

    public void display(Action visitor) {
        for (Person element : elements) {
            element.accept(visitor);
        }
    }
}

ObjectStructure是用来进行具体操作的一个类,里面的attach方法目的是添加数据结构对象,detach方法是为了删除数据结构对象。

public class Main {

    public static void main(String[] args) {
        ObjectStructure objectStructure = new ObjectStructure();
        objectStructure.attach(new Man());
        objectStructure.attach(new Woman());

        // Action的Success行为
        objectStructure.display(new Action() {
            @Override
            public void visit(Man man) {
                System.out.println(man.getClass().getSimpleName() + "成功时,背后多半有一个伟大的女人!");
            }

            @Override
            public void visit(Woman woman) {
                System.out.println(woman.getClass().getSimpleName() + "成功时,背后大多有一个不成功的男人!");
            }
        });

        // Action的Fail行为
        objectStructure.display(new Action() {
            @Override
            public void visit(Man man) {
                System.out.println(man.getClass().getSimpleName() + "失败时,闷头喝酒谁也不用劝!");
            }

            @Override
            public void visit(Woman woman) {
                System.out.println(woman.getClass().getSimpleName() + "失败时,眼泪汪汪谁也劝不了!");
            }
        });

        // Action的Loving行为
        objectStructure.display(new Action() {
            @Override
            public void visit(Man man) {
                System.out.println(man.getClass().getSimpleName() + "恋爱时,凡事不懂也要装懂!");
            }

            @Override
            public void visit(Woman woman) {
                System.out.println(woman.getClass().getSimpleName() + "恋爱时,遇事懂也装作不懂!");
            }
        });
    }
}

可以看到Main类中有三个Action接口的实现,每一种实现就相当于是一种行为,可以继续添加实现,来添加行为,这样就做到了数据结构和行为分离了。

运行结果:

Man成功时,背后多半有一个伟大的女人!
Woman成功时,背后大多有一个不成功的男人!
Man失败时,闷头喝酒谁也不用劝!
Woman失败时,眼泪汪汪谁也劝不了!
Man恋爱时,凡是不懂也要装懂!
Woman恋爱时,遇事懂也装作不懂!

下面是一个例子,添加一个Action的实现。

// Action的Marriage行为
objectStructure.display(new Action() {
    @Override
    public void visit(Man man) {
        System.out.println(man.getClass().getSimpleName() + "结婚时,感慨道:恋爱游戏终结时,‘有妻徒刑’遥无期!");
    }

    @Override
    public void visit(Woman woman) {
        System.out.println(woman.getClass().getSimpleName() + "结婚时,欣慰曰:爱情长跑路漫漫,婚姻保险保平安!");
    }
});

运行结果多出部分:

Man结婚时,感慨道:恋爱游戏终结时,‘有妻徒刑’遥无期!
Woman结婚时,欣慰曰:爱情长跑路漫漫,婚姻保险保平安!

如果需要什么新操作,可以继续添加Action的实现类。


字符串解析

这就是访问者模式的基本使用,接下来模拟一下JSQLParser使用访问者设计模式的方式,为了例子简单,我们解析一个字符串,用访问者模式区分普通字符和数字。

访问者接口:

// 一个String的访问者,它可以有无数个实现类
public interface StringVisitor {

    void visitor(CharacterElement characterString);

    void visitor(NumberElement numberString);
}

数据结构接口:

// 一个String解析的数据结构,它的实现类只有两个,实现类的数量和访问者接口中的方法数量必须要对应
public interface StringElement {

    void accept(StringVisitor stringVisitor);
}

数据结构接口的两个实现类:

// 字符实现类
@Data
@Accessors(chain = true)
public class CharacterElement implements StringElement {

    private char element;

    @Override
    public void accept(StringVisitor stringVisitor) {
        stringVisitor.visit(this);
    }
}

// 数字实现类
@Data
@Accessors(chain = true)
public class NumberElement implements StringElement {

    private char element;

    @Override
    public void accept(StringVisitor stringVisitor) {
        stringVisitor.visit(this);
    }
}

接下来的ObjectStructure类似:

public class ObjectStructure {

    private List<StringElement> elements = new ArrayList<>();

    public void attach(StringElement element) {
        elements.add(element);
    }

    public void detach(StringElement element) {
        elements.remove(element);
    }

    public void display(StringVisitor visitor) {
        for (StringElement element : elements) {
            element.accept(visitor);
        }
    }
}

然后写一个StringParser工具

public class StringParser {

    public static ObjectStructure parse(String str) {
        ObjectStructure objectStructure = new ObjectStructure();
        for (int i = 0; i < str.length(); i++) {
            char c = str.charAt(i);
            if (48 <= c && c <= 57) {
                objectStructure.attach(new NumberElement().setElement(c));
            } else {
                objectStructure.attach(new CharacterElement().setElement(c));
            }
        }
        return objectStructure;
    }
}

写一个客户端使用一下这个解析类:

public class Main {

    public static void main(String[] args) {
        String str = "afdjl1414241lskhlnbalg1";
        ObjectStructure parse = StringParser.parse(str);

        // 第一种操作
        parse.display(new StringVisitor() {
            @Override
            public void visit(CharacterElement characterString) {
                System.out.println("第一种操作:字符:" + characterString.getElement());
            }
            @Override
            public void visit(NumberElement numberString) {
                System.out.println("第一种操作:数字:" + numberString.getElement());
            }
        });

        // 第二种操作
        parse.display(new StringVisitor() {
            @Override
            public void visit(CharacterElement characterString) {
                System.out.println("第二种操作:字符:" + characterString.getElement());
            }
            @Override
            public void visit(NumberElement numberString) {
                System.out.println("第二种操作:数字:" + numberString.getElement());
            }
        });
    }
}

可以发现,现在扩展起来非常的方便,想写几种操作就写几种操作。与上面的那个男人女人例子唯一的区别就是多了个if...else

缺省适配器

那么现在遇到了一个问题,每次我写访问者的时候都要去实现两个方法,这里是两个方法,但有时候有一堆的方法怎么办,难道我只需要解析后的数字,但是也要实现字符相关的方法吗?显然这里可以用到接口适配器模式,又称为"缺省适配器",例如下面这样:

public abstract class StringVisitorAdapter implements StringVisitor {

    // 空实现
    @Override
    public void visit(CharacterElement characterString) {
    }

    // 空实现
    @Override
    public void visit(NumberElement numberString) {
    }
}

然后使用的时候就不需要重写不必要的方法了:

// 第三种操作
parse.display(new StringVisitorAdapter() {
    @Override
    public void visit(NumberElement numberString) {
        System.out.println("第三种操作:数字:" + numberString.getElement());
    }
});

除了缺省适配器,还有两种适配器,一种是类适配器,一种是对象适配器,不过这两种适配器和接口适配器完全不是一个意思。如果有必要,会专门抽出一篇文章做记录。

应用场景和产生的问题

在男人女人例子中,已经确定了数据只有两种,男人和女人,所以才可以在Action接口中写出两个方法,如果数据不确定的话,那么就需要继续在Action接口中添加方法,这样就违反了ocp,访问者模式的优点和缺点就明显了。

优点是可以很容易的去扩展操作,但是缺点是数据结构的变化变的困难了。

这样一来应用场景就更加清楚了,用在数据结构不变化,但是操作要经常变化的情况中。比如说,解析XML。