最近回去听beyond的歌,发现有几首被下架了,找遍了所有播放器,都没找到,然后网上查了一下原因,感觉,,怎么说呢,,我们来研究今天的设计模式吧。。。。。。
都说驴唇不对马嘴,我们来谈谈这个话题,我对马这个动物进行了封装,它有体重、年龄、性别、体力几个属性,有吃饭、负重奔跑、睡觉几个接口,这样封装足够目前的需求使用了,在后面的开发维护过程中我的需求里添加了卖马的功能,这个功能会用到马匹的年龄、性别、体力、体重等所有的属性,但是将此功能(sell)直接封装成类的成员函数貌似也不太合适,因为这个函数算法变化的机率很大,并且这个功能虽然用到了马的很多属性,但是此功能并不属于马的基础功能,好吧,咱们先把这个功能封装到马的类里,那么后面如果有需求要用马肉来做成驴肉火烧,需要用到马的体重来估算可以做成的火烧的个数,你说这个功能要不要封装到马类里面。
所以随着需求的不断变化递进,总有一些依赖类但不适合封装到类中的操作出现,在哪使用就在哪创建这个操作函数吗?也不好,可能这个功能操作需要在不同的应用场景里使用,随着这些操作的增多,我们就需要考虑如何去维护他们了。
进一步考虑,对象结构(存储维护着很多类的很多实例的结构类)在实际应用中也要考虑这个问题。我们有个班级的类,里面维护着N个普通学生、N个老师、1个班主任、2个班长。我们有很多操作需要访问这个对象结构,比如整个班级去献血,男生献血400ml,女生献血200ml;比如整个班级做学期评分,普通学生按照旷课和考试成绩评分,班长在此基础上有加分项,老师按照此学科及格、优秀率评分,班主任在此基础上有加分项;在比如班级植树活动,老师植树3棵,班长植树2棵,普通学生1棵树;这些功能明显会调用对象结构中每种对象的不同属性,但是这些功能并不适合封装在对象结构维护类中,但是随着这些类似的功能的增加,我们要考虑如何维护这些功能。
我们来总结一下,在上述应用场景里,有些功能需要访问目标(具体类、对象结构)的信息,但封装到具体目标中不仅会污染这些目标,还会随着功能的增到导致目标的臃肿不宜维护。我们可以对这些功能进行抽象封装,让他们去访问目标并完成自己的功能,同时也可以方便的扩展,针对这种场景,衍生出的设计模式即访问者模式。
访问者模式:表示一个作用于某对象结构中的各元素的操作。它使你可以在不改变各元素的类的前提下定义作用于这些元素的新操作。
一、名词解释
1、双分派:参考连接 https://blog.csdn.net/wang498233076/article/details/99545737,分派就是根据参数确定具体调用的函数,访问者模式使用的就是双分派的技术,咱们可以沿着上面的逻辑继续思考一下基本的实现流程,我想对A B C三个类做操作扩展,那么我要定义一个Visit基类,封装visitA(A a),visitB(B b),visitC(C c)三个接口,基于Visit类派生出ConcreteVisit1并实现三个接口,在应用场景中,我们调用Visit引用实现对ConcreteVisit1三个接口的访问,这个过程中,应用场景需要知道 A B C三个类(new出来后作为参数传给ConcreteVisit1实例),后面增加新的操作,只需要定义新的基于Visit派生出来的类即可。这样应用场景即可不用知道具体Visit到底是哪个派生类了。但是问题亦然存在,应用场景需要知道A B C 三个类。在某些场景中,这对扩展很不利。
我们继续思考,有没有方法让应用场景对A B C 也不需要了解呢,那么只能对A B C 进行抽象 ,让应用场景使用他们的基类引用(c++ 指针)。这时候,我们需要将这个引用传给Visit的三个接口,你会发现Visit无法定位到底使用哪个接口,这就是因为java/c++没有实现双分派导致的,如何处理呢?看链接吧孩子。
二、特征
1、操作:访问者封装的是操作,扩展的也是操作。
2、对象结构:也就是说此模式大多用于访问一个结构,这个结构维护指定(一般不变,一个也没问题)数量的类实例化出来的一个对象集合。要保证类的个数不能变,它们的变化会导致访问者接口封装发生变化。至于这个对象集合是如何维护的,是否需要对这些类做抽象,我觉得要视情况而定,双分派只是在这个模式的极致情况下才用到。
三、作用
有时候会发现我们需要对某个对象集合进行很多边缘操作,这些边缘操作不适合封装到对应的类里,但是这些操作会慢慢增多不容易维护,这时我们要考虑将这些操作封装管理,这种情况经常发生在当在代码重构的过程中。
四、实现
Visitor:规范对对象结构中所有对象的访问接口,这要求对象结构中的类的种类不可变,如果无法保证此项,最好不要用访问者模式,那么为啥不把所有接口的形参都声明成虚基类Element的引用呢?你猜……那么为啥不把接口都统一成名称都叫做VisitElement的一批接口呢,只要保证参数使用具体派生元素类引用就可以了吧?我认为这样也行,但是没太大用,有几个具体Element你照样还得写几个接口,如果对象结构中的类的种类发生了变化,你照样玩不转。
ConcreteVisitorA:具体访问者,它内部实现了一种对对象结构的访问操作,如果添加新的操作,再派生一个ConcreteVisitorC就可以了。
ObjectStruct:对象结构,用来维护一批需要被访问的对象,其内部有一种可以索引到所有成员对象的机制。当然,如果场景限制了对象结构使其无法透明的使用所有对象,我觉得抽象也就无所谓了,也就没必要设计这种双分派机制了,直接遍历每个对象并直接调用相应的VisitElement*函数就好了。
Element:对于可以透明使用所有对象的对象结构,可以对所有对象对应的类进行抽象,实现双分派机制,这样有利于对象结构的维护。
ConcreteElementA:具体对象,维护了某些属性提供给visitor访问,再Accept中调用Visit的具体访问函数并把自己的this指针作为指针传给函数。
我们来实现一下访问者模式的代码,大家看过药神吧,我们假设电影里的主要人物有两种,一种是病人,一种是药贩子,我们来实现一下医生治病和警察抓人的场景:
//ELement接口类
public abstract class Element {
public String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
abstract public void accept(Visitor visit);
}
//药神
public class ConcreteElementA extends Element {
@Override
public void accept(Visitor visit) {
// TODO Auto-generated method stub
visit.visitElementA(this);
}
public boolean hasMedicine()
{
return true;
}
}
//病人
public class ConcreteElementB extends Element{
@Override
public void accept(Visitor visit) {
// TODO Auto-generated method stub
visit.visitElementB(this);
}
public boolean hasIllness()
{
return true;
}
}
//访问者
abstract class Visitor {
abstract public void visitElementA(ConcreteElementA a);
abstract public void visitElementB(ConcreteElementB b);
abstract public void execute();
}
//医生
public class ConcreteVisitorA extends Visitor{
private boolean cured;
public ConcreteVisitorA()
{
cured = true;
}
@Override
public void visitElementA(ConcreteElementA a) {
// TODO Auto-generated method stub
if(! a.hasMedicine())
cured = false;
}
@Override
public void visitElementB(ConcreteElementB b) {
// TODO Auto-generated method stub
if(! b.hasIllness())
cured = false;
}
@Override
public void execute() {
// TODO Auto-generated method stub
if(cured)
System.out.println("the doctors can cure diseases");
else
System.out.println("the doctors can't cure diseases");
}
}
//警察
public class ConcreteVisitorB extends Visitor{
private String blackList;
public ConcreteVisitorB()
{
blackList = new String();
}
@Override
public void visitElementA(ConcreteElementA a) {
// TODO Auto-generated method stub
blackList += a.getName();
blackList += ";";
}
@Override
public void visitElementB(ConcreteElementB b) {
// TODO Auto-generated method stub
blackList += b.getName();
blackList += ";";
}
@Override
public void execute() {
// TODO Auto-generated method stub
System.out.printf("the black list is:%s\n", blackList);
}
}
//应用场景
public class Client {
public static void main(String[] args) {
ConcreteElementA eleA = new ConcreteElementA();
eleA.setName("yao shen");
ConcreteElementB eleB = new ConcreteElementB();
eleB.setName("zhang san");
ObjectStruct stc = new ObjectStruct();
stc.addElement(eleA);
stc.addElement(eleB);
Visitor visit = new ConcreteVisitorA();
stc.accept(visit);
visit.execute();
visit = new ConcreteVisitorB();
stc.accept(visit);
visit.execute();
}
}
这里,贩卖药物的团伙就是一个对象结构,不同的访问者对这个团伙有不同的访问操作,我们实现了基于这个对象结构的 “治病” 操作,还是先了基于这个对象结构的 “抓捕” 操作,后面如果出现新的操作,派生即可。
五、缺点
1、因为访问者需要访问对象结构中的具体元素的信息,这在某些方面会破坏这些元素类的封装。
2、访问者虚基类依赖了具体的元素类,不符合依赖倒置原则。