访问者模式 Visitor 行为型 设计模式(二十七)

访问者模式 Visitor 
 
image_5c2481f3_6073
《侠客行》是当代做家金庸创做的长篇武侠小说,新版电视剧《侠客行》中,开篇有一段独白: 
茫茫海外,传说有座侠客岛,岛上赏善罚恶二使,每隔十年必到中原武林,向各大门派下发放赏善罚恶令,
强邀掌门人赴岛喝腊八粥,拒接令者,皆造屠戮,无一幸免,接令而去者,杳无音讯,生死未仆,侠客岛之行,已被视为死亡之旅。”
不过话说电视剧,我老是以为老版的好看。

意图

表示一个做用于某对象结构中的各元素的操做。
它使你能够在不改变各元素类的前提下定义做用于这些元素的新操做。

意图解析

咱们以代码描述一下《侠客行》中的这个场景
假定
赏善罚恶二使,一个叫作张三,一个叫作李四,面对一众掌门
张三负责赏善,对好人赏赐,坏人他不处理;
相反,李四负责罚恶,好人不处理,对坏人惩罚

侠客行代码示例

定义了 “掌门人”接口
package visitor.侠客行;
public interface 掌门人 {

}
“掌门人”有两种类型
没作过坏事的掌门作过坏事的掌门
package visitor.侠客行;

public class 没作过坏事的掌门 implements 掌门人 {

}
package visitor.侠客行;

public class 作过坏事的掌门 implements 掌门人 {

}
定义了 侠客岛,侠客岛管理维护“江湖的掌门人”,使用List
提供了掌门人的添加方法  “add掌门人(掌门人 某掌门)”
定义了“赏善罚恶(String 处理人)”方法,用于赏善罚恶,接受参数为处理人
 
若是是赏善大使张三,他会赏赐好人,无论坏人
若是是罚恶大使李四,他会惩罚坏人,无论好人
package visitor.侠客行;
 
import java.util.ArrayList;
import java.util.List;
 
public class 侠客岛 {
 
  private List<掌门人> 掌门人List = new ArrayList<>();
   
  public void add掌门人(掌门人 某掌门) {
    掌门人List.add(某掌门);
  }
   
  public void 赏善罚恶(String 处理人) {
   
    if (处理人.equals("张三")) {
     
      for (掌门人 某掌门X : 掌门人List) {
       
        if (某掌门X instanceof 没作过坏事的掌门) {
         
          System.out.println("好掌门, 张三: 赏赐没作过坏事的掌门");
         
        } else if (某掌门X instanceof 作过坏事的掌门) {
         
          System.out.println("坏掌门, 张三: 无论作过坏事的掌门");
        }
         
        System.out.println();
      }
    } else if (处理人.equals("李四")) {
     
      for (掌门人 某掌门X : 掌门人List) {
       
        if (某掌门X instanceof 没作过坏事的掌门) {
         
          System.out.println("好掌门, 李四: 无论没作过坏事的掌门");
         
        } else if (某掌门X instanceof 作过坏事的掌门) {
         
          System.out.println("坏掌门, 李四: 惩罚作过坏事的掌门");
        }
        System.out.println();
    }
  }
  }
}
 

 

测试代码
image_5c2481f3_37bc
 
上面的测试代码中,咱们创造了侠客岛的“赏善罚恶二使”
而且将几个“掌门人”交于他们处理
打印结果分别展现了对于这几个“掌门人”,张三和李四的不一样来访,产生的不一样结果
 
若是咱们想增长来访者怎么办?好比此次是龙木岛主亲自出岛处理,好人赏赐,坏人直接处理怎么办?
咱们能够直接新增赏善罚恶方法的处理逻辑,以下图所示,新增长了一个else if
能够经过测试代码看到结果
image_5c2481f3_7af6
 
若是有些掌门人既没有作什么好事,也没有作什么坏事怎么处理?也就是新增一种掌门人?
你会发现,全部的判断的地方,也仍是都须要新增长一个else if ...... ̄□ ̄||
由于 上面的示例,使用的是两层判断逻辑,每一层都跟具体的类型有关系!!!
不论是增长新的来访者,仍是增长新的种类的成员,都不符合开闭原则,并且判断逻辑复杂混乱
 
上面的过程在程序世界中, 也会常常出现。
实际开发中,常常用到集合框架
集合框架中也常常会保存不一样的类型(此处指的是不一样的最终类型,若是抬杠,还不都是Object    ̄□ ̄||)
好比多个不一样的子类,像上面示例中的好掌门和坏掌门,都是掌门人类型,可是具体子类型不一样。
对于集合中的元素,可能会有不一样的处理操做
好比上面示例中的,张三和李四的到来,处理确定不同,没干过坏事的和干过坏事的处理也不同
好比去体检,不一样的项目的医生会有不一样的行为操做,你和跟你一块儿排队体检的人也不同,可是你仍是你,他仍是他
 
 
在上面的《侠客行》的示例中,咱们使用了 双重判断来肯定下面两层问题:
一层是来访者是谁? 另一层是当前的掌门人是什么类型?
若是有X种来访者,Y种类型掌门人,怕是要搞出来X*Y种组合了,因此才会逻辑复杂,扩展性差
因此,那么 根本问题就是灵活的肯定这两个维度,来访者和当前类型 ,进而肯定具体的行为,对吧?
 
再回头审视一下《侠客行》的示例,对于访问者,有张3、李4、龙木岛主,还可能会有其余人,
显然,咱们应该 尝试将访问者进行抽象,张三,李四,龙木岛主,他们都是具体的访问者。
并且,并且,并且, 他们都会访问不一样类型的掌门人,既然是访问  不一样类型掌门人
也就是方法名同样,类型不同?
这不就是 方法重载

新版代码示例

掌门人相关角色不变
package visitor.新版侠客行;
public interface 掌门人 {
}

package visitor.新版侠客行;
public class 没作过坏事的掌门 implements 掌门人 {
}


package visitor.新版侠客行;
public class 作过坏事的掌门 implements 掌门人 {
}
新增长访问者角色,访问者既可能访问好人,也可能访问坏人,使用方法的重载在解决 
方法都是拜访,有两种类型的重载版本
package visitor.新版侠客行;
public interface 访问使者 {
  void 拜访(作过坏事的掌门 坏人);
  void 拜访(没作过坏事的掌门 好人);
}
张三负责赏善,当他访问到好人时,赏赐,坏人不处理
package visitor.新版侠客行;
 
public class 张三 implements 访问使者 {
    @Override
    public void 拜访(没作过坏事的掌门 好人) {
        System.out.println("好掌门, 张三: 赏赐没作过坏事的掌门");
    }
     
    @Override
    public void 拜访(作过坏事的掌门 坏人) {
        System.out.println("坏掌门, 张三: 无论作过坏事的掌门");
    }
}
李四负责罚恶,访问到好人时不处理,遇到坏人时,就惩罚!
package visitor.新版侠客行;
 
public class 李四 implements 访问使者 {
 
    @Override
    public void 拜访(没作过坏事的掌门 好人) {
        System.out.println("好掌门, 李四: 无论没作过坏事的掌门");
    }

    @Override
    public void 拜访(作过坏事的掌门 坏人) {
        System.out.println("坏掌门, 李四: 惩罚作过坏事的掌门");
    }
}
引入了访问使者角色,咱们就不须要对使者进行判断了
借助了使者的多态性,不论是何种使者都有访问不一样类型掌门人的方法
因此能够去掉了一层逻辑判断,代码简化以下
package visitor.新版侠客行;
 
import java.util.ArrayList;
import java.util.List;
 
public class 侠客岛 {
  private List<掌门人> 掌门人List = new ArrayList<>();
   
  public void add掌门人(掌门人 某掌门) {
    掌门人List.add(某掌门);
  }
   
  public void 赏善罚恶(访问使者 使者) {
      for (掌门人 某掌门X : 掌门人List) {
         if (某掌门X instanceof 没作过坏事的掌门) {
             使者.拜访((没作过坏事的掌门)某掌门X);
         } else if (某掌门X instanceof 作过坏事的掌门) {
             使者.拜访((作过坏事的掌门)某掌门X);
         }
         System.out.println();
      }
  }
}
测试代码也稍做调整
定义了两个访问者,传递给“赏善罚恶”方法
package visitor.新版侠客行;
 
public class Test {
 
public static void main(String[] args){
 
    侠客岛 善善罚恶二使 = new 侠客岛();
     
    善善罚恶二使.add掌门人(new 作过坏事的掌门());
    善善罚恶二使.add掌门人(new 没作过坏事的掌门());
    善善罚恶二使.add掌门人(new 没作过坏事的掌门());
    善善罚恶二使.add掌门人(new 作过坏事的掌门());
     
    访问使者 张三 = new 张三();
    访问使者 李四 = new 李四();
     
    善善罚恶二使.赏善罚恶(李四);
    善善罚恶二使.赏善罚恶(张三);
    }
}

 

image_5c2481f3_6b9
 
能够看到,《新版侠客行》和老版本的功能的同样的,可是代码简化了
并且,最重要的是可以很方便的扩展使者,好比咱们仍旧增长“龙木岛主”这一访客。
package visitor.新版侠客行;
public class 龙木岛主 implements 访问使者 {
    @Override
    public void 拜访(作过坏事的掌门 坏人) {
        System.out.println("龙木岛主,惩罚坏人");
    }
    @Override
    public void 拜访(没作过坏事的掌门 好人) {
        System.out.println("龙木岛主,赏赐好人");
    }
}
新增长了"龙木岛主“访客后,客户端能够直接使用了,不须要修改”侠客岛“的代码了
测试代码增长以下两行,查看下面结果
image_5c2481f3_110a
 
可是若是增长新的掌门人类型呢?
由于咱们仍旧有具体类型的判断,以下图所示
image_5c2481f3_1f7e
因此,想要增长新的掌门人,又完蛋了   ̄□ ̄||
 
并且,如今的判断逻辑也仍是交织着,复杂的。
对于访问者的判断,咱们借助于多态以及方法的重载,去掉了一层访问者的判断
经过多态能够将请求路由到真实的来访者,经过方法重载,能够调用到正确的方法
 
若是能把这一层的if else if判断也去掉,是否是就能够灵活扩展掌门人了呢?
使者只知道某掌门X,可是他最终的具体类型,是不知道的
因此,没办法直接调用拜访方法的,由于咱们的确没有这种参数类型的方法 
image_5c2481f3_959
ps:有人以为“拜访”方法的类型使用 掌门人  不就行了么
可是对于不一样的具体类型有不一样的行为,那你在“拜访”方法中仍是少不了要进行判断,只是此处判断仍是“拜访”方法内判断的问题
 
前面的那段if else if判断逻辑,访问的方法都是  使者.拜访,只不过具体类型不一样
image_5c2481f4_73d5
可是如何肯定类型?问题也就转换为”到底怎么判断某掌门X的类型“或者”到底谁知道某掌门X的类型“
那谁知道他的类型呢?
若是不借助外力,好比 instanceof 判断的话,还有谁知道? 某掌门X 他本身知道!!!他本身知道!!!
因此,若是是在  某掌门X本身内部的方法,就能够获取到this了,这就是当前对象的真实类型
把这个类型在回传给来访使者不就能够了么
因此
给掌门人定义一个“ 接受拜访”方法,无论何种类型的掌门人,都可以接受各类访客的拜访
接受拜访(访问使者 赏善罚恶使者){
赏善罚恶使者.拜访(this);
 

最新版侠客行代码示例

提及来有点迷惑,我看看代码
《最新版侠客行》
掌门人都增长了”接受拜访“的方法
package visitor.最新版本侠客行;
public interface 掌门人 {
void 接受拜访(访问使者 赏善使者);
}

 

package visitor.最新版本侠客行;
public class 没作过坏事的掌门 implements 掌门人 {
  @Override
  public void 接受拜访(访问使者 赏善罚恶使者) {
    赏善罚恶使者.拜访(this);
  }
}

 

package visitor.最新版本侠客行;
public class 作过坏事的掌门 implements 掌门人 {
  @Override
  public void 接受拜访(访问使者 赏善罚恶使者) {
    赏善罚恶使者.拜访(this);
  }

}
访问使者相关角色与《新版侠客行》中同样
package visitor.最新版本侠客行;
public interface 访问使者 {
    void 拜访(作过坏事的掌门 坏人);
    void 拜访(没作过坏事的掌门 好人);
}
 
 
package visitor.最新版本侠客行;
public class 张三 implements 访问使者 {
    @Override
    public void 拜访(没作过坏事的掌门 好人) {
        System.out.println("好掌门, 张三: 赏赐没作过坏事的掌门");
    }
    @Override
    public void 拜访(作过坏事的掌门 坏人) {
        System.out.println("坏掌门, 张三: 无论作过坏事的掌门");
    }
}
 
package visitor.最新版本侠客行; public class 李四 implements 访问使者 { @Override public void 拜访(没作过坏事的掌门 好人) { System.out.println("好掌门, 李四: 无论没作过坏事的掌门"); } @Override public void 拜访(作过坏事的掌门 坏人) { System.out.println("坏掌门, 李四: 惩罚作过坏事的掌门"); } }
此时的侠客岛轻松了,再也不须要来回的判断类型了
package visitor.最新版本侠客行;
 
import java.util.ArrayList;
import java.util.List;
public class 侠客岛 {
    private List<掌门人> 掌门人List = new ArrayList<>();
    public void add掌门人(掌门人 某掌门) {
        掌门人List.add(某掌门);
    }
    public void 赏善罚恶(访问使者 使者) {
        for (掌门人 某掌门X : 掌门人List) {
            某掌门X.接受拜访(使者);
            System.out.println();
        }
    }
}
image_5c2481f4_4529
从结果看跟上一个版本同样
可是很显然,咱们的侠客岛轻松了
 
接下来咱们看一下新增长访客和新增长掌门人的场景
扩展龙木岛主
package visitor.最新版本侠客行;

public class 龙木岛主 implements 访问使者 {
@Override
public void 拜访(作过坏事的掌门 坏人) {
System.out.println("龙木岛主,惩罚坏人");
}

@Override
public void 拜访(没作过坏事的掌门 好人) {
System.out.println("龙木岛主,赏赐好人");
}
}
测试代码以下,显然由于拜访使者的抽象,才得以可以更好的扩展访问者,因此此处确定跟《新版侠客行》同样便于扩展
image_5c2481f4_4404
 
看看若是扩展一个新的掌门人
package visitor.最新版本侠客行;
public class 很差不坏的掌门 implements 掌门人 {
@Override
public void 接受拜访(访问使者 赏善罚恶使者) {
赏善罚恶使者.拜访(this);
}
}
可是,”访问使者“里面没有可以拜访”很差不坏的掌门“方法啊?怎么办?
只能添加呗,以下图所示,完蛋了........
image_5c2481f4_3f58

代码演化小结

看得出来,《最新版侠客行》 解决了复杂判断的问题,也解决了访问者扩展的问题
可是 对于被访问者的类型的扩展,显然是没有扩展性的,不符合开闭原则
这一点体现出来了这种解决方法的 倾向性,倾向于 扩展行为,能够自如的增长新的行为
可是 不能轻松的增长元素类型
 
测试代码Test类不须要修改
看一下打印结果
image_5c2481f4_3462

最新版侠客行结构

image_5c2481f4_7425

回首意图

再回头看下访问者模式的意图
表示一个做用于某对象结构中的各元素的操做。它使你能够在不改变各元素类的前提下定义做用于这些元素的新操做。
就是上面示例中,对于来访者的扩展嘛
 
最初的动机就是处理《侠客行》中相似的问题
集合容器中保存了不一样类型的对象,他们又可能有多种不一样场景的操做
好比一份名单,班长可能拿过去收做业,班主任拿过去可能点名
名单里面都有你也有他,你就是那个你,他仍是那个他,可是你的做业是你的做业,他的做业是他的做业。
因此对于班长和班主任两个访问者,同窗们的行为是不同的,对同一来访者,不一样的同窗的行为又是不同的

结构

image_5c2481f4_1c63
 
抽象元素角色Element
抽象元素通常是抽象类或者接口
一般它定义一个 accept(抽象访问者) 方法,用来将自身传递给访问者
具体的元素角色ConcreateElement
具体元素实现了accept方法,在accept方法中调用访问者的访问方法以便完成对一个元素的操做
抽象访问者Visitor
定义一个或者多个访问操做
抽象访问者须要面向具体的被访问者元素类型,因此有几个具体的元素类型须要被访问,就有几个重载方法
具体的访问者ConcreateVisitor
具体的访问者封装了不一样访问者,不一样类型对象的具体行为,也就是最终的分状况的处理逻辑
对象结构ObjectStructure
对象结构是元素的集合,用于存放元素的对象,而且通常提供遍历内部元素的方法
客户端角色Client
组织被访问者,而后经过访问者访问
 
访问者模式有两个主要层次,访问者以及被访问元素
访问者有不一样的类型,被访问元素有不一样的类型
每一种访问者对于每一种被访问元素都有一种不一样的行为,这不一样的行为是封装在访问者的方法中
因此访问者须要进行访问方法visit的重载, 被访问元素有几种类型,就有几种重载版本
面向细节的逻辑既然被封装在访问者中,被访问元素就不须要面向细节了,只须要把本身的类型传递给访问者便可
因此, 全部的被访问元素都只有一个版本的accept方法

概念示例代码

咱们能够抽象化的看下下面的例子
下面的代码很简单,A有三种子类型,B有三种子类型
不一样的A和不一样的B,将会擦出不同的火花,也就是会出现9种可能的场景
将A定义为访问者,那么A就要借助方法的重载实现不一样类型被访问者B的不一样行为
而将方法的调用转变为被访问者的反向调用----this传递给访问者
package visitor;
 
public class example {
public static void main(String[] args) {
 
A1 a1 = new A1();
A2 a2 = new A2();
A3 a3 = new A3();
 
B1 b1 = new B1();
B2 b2 = new B2();
B3 b3 = new B3();
 
b1.accept(a1);
b1.accept(a2);
b1.accept(a3);
b2.accept(a1);
b2.accept(a2);
b2.accept(a3);
b3.accept(a1);
b3.accept(a2);
b3.accept(a3);
}
}
 
 
abstract class A {
 
abstract void visit(B1 b1);
abstract void visit(B2 b2);
abstract void visit(B3 b3);
}
 
class A1 extends A {
@Override
void visit(B1 b1) {
System.out.println("A1 play with B1");
}
 
@Override
void visit(B2 b2) {
System.out.println("A1 play with B2");
}
 
@Override
void visit(B3 b3) {
System.out.println("A1 play with B3");
}
}
 
class A2 extends A {
@Override
void visit(B1 b1) {
System.out.println("A2 play with B1");
}
 
@Override
void visit(B2 b2) {
System.out.println("A2 play with B2");
}
 
@Override
void visit(B3 b3) {
System.out.println("A2 play with B3");
}
}
 
class A3 extends A {
@Override
void visit(B1 b1) {
System.out.println("A3 play with B1");
}
 
@Override
void visit(B2 b2) {
System.out.println("A3 play with B2");
}
 
@Override
void visit(B3 b3) {
System.out.println("A3 play with B3");
}
}
 
 
abstract class B {
abstract void accept(A a);
}
 
class B1 extends B {
@Override
void accept(A a) {
a.visit(this);
}
}
 
class B2 extends B {
@Override
void accept(A a) {
a.visit(this);
}
}
 
class B3 extends B {
@Override
void accept(A a) {
a.visit(this);
}
}

 

 

image_5c2481f4_6086
这种重载和回传自身的形式,彻底能够看成一个套路来使用,对于这种组合形式的场景,很是受用。
访问者的自身借助多态特性,又依赖方法重载,而后再借助于this回传达到反向肯定类型调用,真心精巧。

总结

访问者模式灵活的处理了不一样类型的元素,面对不一样的访问者,有不一样的行为的场景。
这种组合场景,判断逻辑复杂繁琐,访问者模式能够作到灵活的扩展增长更多的行为,而不须要改变原来的类。
访问者模式倾向于扩展元素的行为,当扩展元素行为时,知足开闭原则
可是对于扩展新的元素类型时,将会产生巨大的改动,每个访问者都须要变更,因此在使用访问者模式是要考虑清楚元素类型的变化可能。
由于访问者依赖的是具体的元素,而不是抽象元素,因此才难以扩展
 
访问者依赖的是具体元素,而不是抽象元素,这破坏了依赖倒置原则,特别是在面向对
象的编程中,抛弃了对接口的依赖,而直接依赖实现类,扩展比较难。
 
当业务规则须要遍历多个不一样的对象时,并且不一样的对象在不一样的场景下又有不一样的行为
你就应该考虑使用访问者模式
若是对象结构中的对象不常变化,可是他们的行为却常常变化时,也能够考虑使用,访问者模式能够很灵活的扩展新的访客。
相关文章
相关标签/搜索