28.男人和女人 - 访问者模式(visitor)

28.1 男人和女人

  • 时间: 7月18日21点   地点: 小菜大鸟住所的客厅   人物: 小菜, 大鸟.

     “…..”

     那人这本书的内容要比封面更吸引人, 女人这本书的封面通常比内容更吸引人.
     “….”
     “行了, 行了, 没有事业的成功, 你找出再多的男女差异也找不到女朋友的. 还是好好学习吧.” 大鸟打断了小蔡说话, “今天我们需要了最后一个模式, 叫做访问者模式.”
     “哦, 那我们上课吧.” 小才不得不停止.
     “访问者讲的是表示作用于某对象结构中的各元素的操作. 他是你可以在不改变个元素的类的前提下定义左右与这些元素的新操作.” 大鸟开始如夫子般念叨起来.
     “….”
     “先不谈模式, 你能不能把你刚才的那些对比, 用控制台的程序打印出来, 打到屏幕上?” 大鸟避而不答.

28.2 最简单的编程实现

     十分钟后, 小菜的程序就出来了.

1
2
3
4
5
6
7
8
public static void main(String[] args) {
System.out.println("男人成功时, 背后多半有一个伟大的女人.");
System.out.println("女人成功时, 背后多半有一个不成功的男人.");
System.out.println("男人失败时, 闷头喝酒, 谁也不用劝.");
System.out.println("女人失败时, 两眼泪汪汪, 事业劝不了");
System.out.println("男人恋爱时, 凡是不懂也要装懂.");
System.out.println("女士恋爱时, 遇事懂也装作不懂.");
}

     “小菜呀, 这样的代码你也拿得出手.” 大鸟讥讽地说道.
     “你不是要把那些对比打印在屏幕上吗? 我做到了呀.” 小菜理直气壮.
     “但这跟打印 ‘hello world’ 有什么区别, 难道你是第一天学习编程?”
     “那你要我怎么写才可以?”
     “你至少要分析一下, 这里面有没有什么类可以提炼, 有没有什么方法可以共享什么的.”
     “哦, 你是这个意思, 早说呀. 这里面至少男人和女人应该是两个不同的类, 男人和女人应该继承人这样一个抽象类. 所谓成功, 失败, 恋爱是指一个人的属性, 一个状态. 成功是如何如何, 失败时如何如何不过是一种反应. 好了, 我写的出来了.” 小才开始得意道.
     “这还有点面向对象的意思. 快点写吧.”

28.3 简单的面向对象实现.

     半小时后, 小菜写出了第二版的程序.
     “人” 类, 是 “男人” 和 “女人” 类的抽象类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public abstract class Person {
protected String action;
public String getAction() {
return action;
}
public void setAction(String action) {
this.action = action;
}
// 得到结论或反映
public abstract void getConclusion();
}

     “男人” 类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Man extends Person {
// 得到结论或反应
@Override
public void getConclusion() {
if (action == "成功") {
System.out.println("男人成功时, 背后多半有一个伟大的女人.");
} else if (action == "失败") {
System.out.println("男人失败时, 闷头喝酒, 谁也不用劝.");
} else if (action == "恋爱") {
System.out.println("男人恋爱时, 凡是不懂也要装懂.");
}
}
}

     “女人” 类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Woman extends Person {
// 得到结论或反应
@Override
public void getConclusion() {
if (action == "成功") {
System.out.println("女人成功时, 背后多半有一个不成功的男人.");
} else if (action == "失败") {
System.out.println("女人失败时, 两眼泪汪汪, 事业劝不了");
} else if (action == "恋爱") {
System.out.println("女士恋爱时, 遇事懂也装作不懂.");
}
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public static void main(String[] args) {
List<Person> persons = new ArrayList<>();
Person man1 = new Man();
man1.setAction("成功");
persons.add(man1);
Person woman1 = new Woman();
woman1.setAction("成功");
persons.add(woman1);
Person man2 = new Man();
man2.setAction("失败");
persons.add(man2);
Person woman2 = new Woman();
woman2.setAction("失败");
persons.add(woman2);
Person man3 = new Man();
man3.setAction("恋爱");
persons.add(man3);
Person woman3 = new Woman();
woman3.setAction("恋爱");
persons.add(woman3);
for (Person person : persons) {
person.getConclusion();
}
}

     结果显示.

1
2
3
4
5
6
男人成功时, 背后多半有一个伟大的女人.
女人成功时, 背后多半有一个不成功的男人.
男人失败时, 闷头喝酒, 谁也不用劝.
女人失败时, 两眼泪汪汪, 事业劝不了
男人恋爱时, 凡是不懂也要装懂.
女士恋爱时, 遇事懂也装作不懂.

     “大鸟, 现在算是面向对象的编程了吧.”
     “粗略看, 应该算是, 你不觉得你在 ‘男人’ 类与 ‘女人’ 类中的那些 if…else… 语句很碍眼吗?”
     “不这样不行啊, 反正也不多.”
     “如果我现在要增加一个 ‘结婚’ 的状态, 你需要改什么?”
     “那这两个类都需要增加判断分支了.” 小菜无奈的说, “你说的意思我知道, 可是我真没有办法去处理这些分支, 我也想过, 把这些状态写成类, 可是哪有如何处理呢? 没办法.”
     “哈, 办法总是有, 只不过麻烦些.”

28.4 用了模式的实现

     大鸟帮助小菜活出了结构图并写出了代码.



     状态的抽象类和人的抽象类

1
2
3
4
5
6
7
8
9
10
11
12
13
public abstract class Action {
// 得到男人的结论或反应
public abstract void getManConclusion(Man ConcreteElementA);
// 得到女人的结论或反应
public abstract void getWomanConclusion(Woman concreteElementB);
}
public abstract class Person {
// 接受
// 他是用来获得 '状态' 对象的.
public abstract void accept(Action visitor);
}

     “这里的关键就在于只分为男人和女人, 这个性别分类是很稳定的, 所以可以在状态类中, 增加 ‘男人反应’ 和 ‘女人反应’ 这两个方法, 方法的个数也是很稳定的, 不会容易放生变化. 而 ‘人’ 抽象类中有一个方法 ‘接受’, 它是用来获得 ‘状态’ 对象的. 每一种状态都继承 ‘状态’ 抽象类, 事项具体两个反应方法.”

     具体 ‘状态’ 类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
// 成功
public class Success extends Action {
@Override
public void getManConclusion(Man concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 背后多半有一个伟大的女人.");
}
@Override
public void getWomanConclusion(Woman concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 背后多半有一个不成功的男人.");
}
}
public class Failing extends Action {
@Override
public void getManConclusion(Man concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 闷头喝酒, 谁也不用劝.");
}
@Override
public void getWomanConclusion(Woman concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 两眼泪汪汪, 事业劝不了");
}
}
// 恋爱
public class Amativeness extends Action {
@Override
public void getManConclusion(Man concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 凡是不懂也要装懂.");
}
@Override
public void getWomanConclusion(Woman concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 遇事懂也装作不懂.");
}
}

     男人和女人类

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// 男人
public class Man extends Person {
@Override
public void accept(Action visitor) {
// 首先在客户程序中将具体状态作为参数传递给 '男人' 类
// 完成一次分派, 然后'男人'类, 调用作为参数的'具体状态'
// 中的方法'男人反应', 同时将自己(this)作为参数传递进去.
// 这边完成了第二次分派.
visitor.getManConclusion(this);
}
}
// 女人
public class Woman extends Person {
@Override
public void accept(Action visitor) {
visitor.getWomanConclusion(this);
}
}

     “这里需要提一下当中用到一种双分派的技术, 首先在客户程序中将具体状态作为参数传递给 ‘男人’ 类完成一次分派, 然后 ‘男人’ 类调用作为参数的 ‘具体状态’ 中的方法 ‘男人反应’, 同时将自己(this)作为参数传递进去. 这边完成了第二次分派. 双分派以为着得到执行的操作决定于请求的种类和两个接受者的类型. ‘接受’ 方法就是一个双分派的操作, 他得到执行的操作不仅决定于 ‘状态’ 类的具体状态, 还决定于他访问的 ‘人’ 的类别.”
     对象结构类, 由于总是需要 ‘男人’ 与 ‘女人’ 在不同状态的对比, 所以我们需要一个 ‘对象结构’ 类类针对不同的 ‘状态’ 遍历 ‘男人’ 与 ‘女人’, 得到不同的反应.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public class ObjectStructure {
private List<Person> elements = new ArrayList<>();
// 增加
public void attach(Person element) {
elements.add(element);
}
// 移除
public void detach(Person element) {
elements.remove(element);
}
// 查看显示
public void display(Action visitor) {
for (Person e : elements) {
e.accept(visitor);
}
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public static void main(String[] args) {
ObjectStructure o = new ObjectStructure();
o.attach(new Man());
o.attach(new Woman());
// 成功时的反应
Success v1 = new Success();
o.display(v1);
// 失败时的反应
Failing v2 = new Failing();
o.display(v2);
// 恋爱时的反应
Amativeness v3 = new Amativeness();
o.display(v3);
}

     “这样做到底有什么好处呢?” 小菜问道
     “你仔细看看, 现在这样做, 就意味着, 如果我们要增加 ‘结婚’ 的状态来考察 ‘男人’ 和 ‘女人’ 的反应. 只需要怎么就可以了?”
     “哦, 我明白你的意思, 由于用了双分派, 使得我们只需要增加一个 ‘状态’ 子类, 就可以在客户端调用查看, 不需要改动任何其他类的代码.”
     “来, 写出来试试看.”
     结婚状态类

1
2
3
4
5
6
7
8
9
10
11
12
13
public class Marriage extends Action {
@Override
public void getManConclusion(Man concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 感慨道: 恋爱游戏终结时, '有妻徒刑' 遥无期");
}
@Override
public void getWomanConclusion(Woman concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + getClass().getSimpleName() +
"时, 欣慰曰: 爱情长跑路漫漫, 婚姻保险保平安");
}
}

     客户端代码, 增加下面一段代码就可以完成.

1
2
3
4
......
Marriage v4 = new Marriage();
o.display(v4);
.....

     “哈, 完美的体现的开放封闭原则, 实在是高呀. 这叫什么模式来着?”
     “他应该算是 GOF 最复杂的一个模式了, 叫做访问者模式.”

28.5 访问者模式

访问者模式(Visitor), 表示一个作用于某对象结构中的个元素的操作. 他可以是你在不改变各元素类的前提下定义作用于这些元素的新操作.



     “在这里, Element 就是我们的 ‘人’ 类, 而 ConcreteElementA 和 ConcreteElementB 就是 ‘男人’ 和 ‘女人’, Visitor 就是我们写的 ‘状态’ 类, 具体的 ConcreteVisitor 就是那些 ‘成功’, ‘失败’, ‘恋爱’ 等状态. 至于 ObjectStructure 就是 ‘对象结构’ 类了.”
     “怪不得这幅类图我感觉跟刚才写的代码类图完全对应.”
     “本来我是想直接说访问者模式的, 但是我为什么突然愿意与你聊男人和女人的对比呢, 原因就因为你说了一句话: ‘男女对比这么多的原因就是因为人类在性别上就只有男人和女人两类.’ 而这也正是访问者模式可以实施的前提.”
     “这个前提是什么呢?”
     “你想呀, 如果人类的性别不止是男是女, 而是可能由多种性别, 那就意味着 ‘状态’ 类中的抽象方法就不稳定了, 每加一种类别, 就需要在状态类和它所有的下属类中增加一个方法, 这就不符合开放封闭原则.”
     “哦, 也就是说, 访问者模式是用于数据结构相对稳定的系统?
     “对的, 它把数据结构和作用于数据结构上的操作之间的耦合解脱开, 时的操作集合可以相对自由的演化.
     “访问者模式的目的是什么?” 小菜问道.
     “访问者模式的目的是要把处理从数据结构中分离出来.很多系统可以按照算法和数据结构分开, 如果这的系统有比较稳定的数据结构, 又有已与变化的算法的话, 使用访问者模式就是比较合适的, 因为访问者模式使得算法操作的增加变得容易. 反之, 如果这样的系统的数据结构对象易于变化, 经常要有新的数据对象增加近来, 就不适合使用访问者模式.”
     “那其实访问者模式的优点就是增加新的操作很容易, 因为增加一个新的操作就意味着增加一个新的访问者. 访问者模式将有关的行为集中到一个访问者对象中.”
     “是的, 总结的很好.” 大鸟接着说, “通常 ConcreteVisitor 可以单独开发, 不必跟 ConcreteElementA 或 ConcreteElementB 写在一起. 正因为这样, ConcreteVisitor 能提高 ConcreteElement 之间的独立性, 如果把一个处理动作设计成 ConcreteElementA 和 ConcreteElementB 类的方法, 每次想新增 ‘处理’ 以扩充功能时就得去修改 ConcreteElementA 和 ConcreteElementB了. 这也就是你之前写的代码, 在 ‘男人’ 和 ‘女人’ 类中增加了对 ‘成功’, ‘失败’ 等状态的判断, 造成了处理方法和数据结构的紧耦合.”
     “
那访问者没模式的缺点就是使新增的数据结构变得困难了.**”
     “所以GoF 四人中的一个作者就说过: ‘大多数时候你并不需要访问者模式, 但当一旦你需要访问者模式时, 你就真的需要它了.’ 实际上, 我们很难找到数据结构不变化的情况, 所以用访问者模式的机会就不太多了. 这也就是为什么你谈到男人和女人的对比时, 我很高兴和你讨论的原因, 因为人类的性别这样的数据结构是不会变化的.”
     “哈, 看来是我为你找到了一个教学的好案例.” 小菜得意道.
     “和往常一样, 我们需要些一些基本的代码来巩固我们的学习. 有了 UML 图, 相信你应该没什么问题了.”

28.6 访问者模式的基本代码.

     Visitor 类, 为该对象结构中 ConcreteElement 的每一个类声明一个 Visit 操作.

1
2
3
4
5
public abstract class Visitor {
public abstract void visitConcreteElementA(ConcreteElementA concreteElementA);
public abstract void visitConcreteElementB(ConcreteElementB concreteElementB);
}

     “ConcreteVisitor1 和 ConcreteVisitor2 类, 具体访问者, 实现每个由 Visitor 声明的操作. 每个操作实现算法的一部分, 该算法片段乃是对应于结构中对象的类.”

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
public class ConcreteVisitor1 extends Visitor {
@Override
public void visitConcreteElementA(ConcreteElementA concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + "被"
+ getClass().getSimpleName() + "访问");
}
@Override
public void visitConcreteElementB(ConcreteElementB concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + "被"
+ getClass().getSimpleName() + "访问");
}
}
public class ConcreteVisitor2 extends Visitor {
@Override
public void visitConcreteElementA(ConcreteElementA concreteElementA) {
System.out.println(concreteElementA.getClass().getSimpleName() + "被"
+ getClass().getSimpleName() + "访问");
}
@Override
public void visitConcreteElementB(ConcreteElementB concreteElementB) {
System.out.println(concreteElementB.getClass().getSimpleName() + "被"
+ getClass().getSimpleName() + "访问");
}
}

     Element 类, 定义一个 Accept 操作, 他以一个访问者为参数.

1
2
3
public abstract class Element {
public abstract void accept(Visitor visitor);
}

     ConcreteElementA 和 ConcreteElementB 类, 具体元素, 实现 Accept 操作.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
public class ConcreteElementA extends Element {
@Override
public void accept(Visitor visitor) {
visitor.visitConcreteElementA(this);
}
// 其他相关方法.
public void operationA() {
}
}
public class ConcreteElementB extends Element {
@Override
public void accept(Visitor visitor) {
visitor.visitConcreteElementB(this);
}
// 其他相关方法.
public void operationA() {
}
}

     ObjectStructure 类, 能枚举它的元素, 可以提供一个高层的接口以允许访问者访问它的元素.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public class ObjectStructure {
private List<Element> elements = new ArrayList<>();
public void attach(Element element) {
elements.add(element);
}
public void detach(Element element) {
elements.remove(element);
}
public void accept(Visitor visitor) {
for (Element e : elements) {
e.accept(visitor);
}
}
}

     客户端代码

1
2
3
4
5
6
7
8
9
10
11
public static void main(String[] args) {
ObjectStructure o = new ObjectStructure();
o.attach(new ConcreteElementA());
o.attach(new ConcreteElementB());
ConcreteVisitor1 v1 = new ConcreteVisitor1();
o.accept(v1);
ConcreteVisitor2 v2 = new ConcreteVisitor2();
o.accept(v2);
}

28.7 比上不足, 比下有余

     “啊, 访问者模式比较麻烦哦…”
     “…….”

~感谢捧场,您的支持将鼓励我继续创作~