行为型-访问者模式

统一规划访问者

  按照书中所述(具体忘了是那里看的),这个模式可能是行为型模式中最难的一个模式了。个人觉得的确是这样,第一次看这个模式的时候,完全是一脸懵逼,完全想不出可以利用的场景。觉得可能用的比较少吧,就这样没有完全的理解它的意图。个人认为理解一个设计模式的第一步,就是能自己找到使用的场景,并且比较合理不会生搬硬套的。第二部就是能对这个模式进行一些自己的改进和适配。最后异步就是真的融会贯通,能根据不同的场景,结合不同设计模式不同的变种,可能写完才发现自己原来用了设计模式。

初识访问者

  先了解这个模式解决的问题:将数据的结构和数据的操作分离;为什么要这样做,如果数据的结构可能是易变的,数据的操作也是易变的,如果操作和数据不分离,那么对原来的改动可能会比较大;好吧,有的东西就是不容易说,先看一个我觉得比较好的例子,访问这模式到底在干嘛:

//被访问者
public class Element {

    public void doVisit() {
        System.out.println("element do visit!");
    }

    public void accept(Visitor visitor) {
        visitor.visit(this);
    }

}
//访问者
public class Visitor {
    public void visit(Element e) {
        e.doVisit();
    }
}
//测试
public static void main(String[] args) {
    new Element().accept(new Visitor());
}

一个访问者的雏形就呈现出来了,可以看见,被访问者接受访问者的对象来访问自己(accept(Visitor)),而访问者可以利用访问获取到的被访问对象,从而调用到被访问者对象中的方法;因为这是一个雏形,也许体会不到这种模式的好处,但是基本能知道他干了什么。

可以看到雏形中的代码的耦合性是非常强的,毕竟这不是观察者模式的全部,接下来对他进行改进,但是一般的模板式的代码个人觉得不是非常的有感觉,那么先来设计一个场景。现在有一个需求,假设我有一个管控系统,里面有支付,退款这2种报表要输出,而且对报表也有需求,就是需要一个表头在上的报表,和一个表头在左的报表(先不考虑需求合不合理吧)。
第一步将被访问者接口化

public interface IElement {
    void accept(IVisitor visitor);
}

public class Charge implements IElement {
    
    private String chargeId;

    private String productName;

    private Long timeStamp;
    ...

    //省略get/set
    
    public Map<String, Object> getData() {
        Map<String, Object> map = new HashMap();
        //这里可以用反射获取键值对或者用JSONObject,但是为了体现charge 和 refund的不同,因为相同的话直接可以写在抽象类里
        map.put("chargeId", this.chargeId);
        ...
        return map;
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }

}

public class Refund implements IElement {

    private String refundId;

    private String chargeId;

    ....

    public Map<String, Object> getData() {
        Map<String, Object> map = new HashMap<>();
        map.put("refundId", this.refundId);
        ...
        return map;
    }

    @Override
    public void accept(IVisitor visitor) {
        visitor.visit(this);
    }
}

第二步将访问者接口化

public interface IVisitor {
    void visit(IElement element);
}

public class VisitorTop implements IVisitor {

    Map<String, List<Object>> datas;

    public VisitorTop() {
        this.datas = new HashMap<>();
    }

    @Override
    public void visit(IElement element) {
        Map<String, Object> map = element.getData();
        //填充数据
        map.forEach((k,v) -> {
            List<Object> row = datas.get(k);
            if(row == null) {
                row = new ArrayList<>();
                row.add(v);
                datas.put(k, row);
            } else {
                row.add(v);
            }
        });
        
    }

    public void printTopTable() {
        //表头在上的报表打印
    }

}

public class VisitorLeft implements IVisitor {

    Map<String, List<Object>> datas;

    public VisitorLeft() {
        this.datas = new HashMap<>();
    }

    @Override
    public void visit(IElement element) {
        //这里其实和VisitorTop相同,这里应该有不同的处理逻辑,本例中相同,不同体现在报表的打印上;
        Map<String, Object> map = element.getData();
        //填充数据
        map.forEach((k,v) -> {
            List<Object> row = datas.get(k);
            if(row == null) {
                row = new ArrayList<>();
                row.add(v);
                datas.put(k, row);
            } else {
                row.add(v);
            }
        });
    }

    public void printLeftTable() {
        //打印表头在左的报表;
    }
}

这样我们就形成了一个报表的打印组合,看上去已经比较灵活了:

支付订单 -- 顶部表头报表
退款订单 -- 顶部表头报表
支付订单 -- 左部表头报表
退款订单 -- 左部表头报表

已经可以实现上面的功能了,但是这里还缺多少一个重要的步骤,报表有很多条数据的呀,这个数据谁来提供。这里就需要访问者中另一个重要的角色,它负责获取批量的被观察对象,这里一个简单的伪代码;

public class TableManager {

    public List<Refund> listRefund() {
        //从数据库中查出退款数据
    }

    public List<Charge> listCharge() {
        //从数据库中查出订单数据
    }
}
//然后开始测试
public static void main(String[] args) {
    //获取数据
    TableManager manager = new TableManager();
    List<Refund> refunds = manager.listRefund();
    List<Charge> charges = manager.listCharge();
    //创建观察者
    IVisitor topVisitorForRefund = new VisitorTop();
    Ivisitor leftVisitorForRefund = new VIsitorLeft();
    IVisitor topVisitorForCharge = new VisitorTop();
    Ivisitor leftVisitorForCharge = new VIsitorLeft();
    //开始组装报表
    refunds.forEach(r -> r.accept(topVisitorForRefund));
    refunds.forEach(r -> r.accept(leftVisitorForRefund));

    charge.forEach(c -> c.accept(topVisitorForCharge));
    charge.forEach(c -> c.accept(leftVisitorForCharge));
    //分别打印4张报表 这里统一成接口也可以但是为了区别开来;
    topVisitorForRefund.printTopTable();
    leftVisitorForRefund.printLeftTable();

    topVisitorForCharge.printTopTable();
    leftVisitorForCharge.printLeftTable();
}

上面就完成了各种报表的需求,试想以下按常规方案完成这个需求需要怎么做呢。
首先我们一样查出数据的list;
然后将专门有个类来组装数据,有多少种数据就需要创建多少种组装数据的方法;
然后在需要一个类输出数据(可能实际需求是输出成excel格式然后命名成不同的名称导出之类的),这也需要一种数据一个方法;
最后依次调用这些方法完成;
等等...好像听起来复杂度和原来差不了多少啊...
是的,设计模式在代码并不复杂的情况下收效甚微,这也就是为什么说不要滥用设计模式的原因,这其中分寸的把握需要长时间的历练和参透,目前对自己的希望就是写的代码能让自己舒服一点,没有什么大道理。
再自己体会下访问者模式的特点,将数据结构和数据的操作分离,数据的结构是被访问者, 数据的操作是访问者,数据结构的修改不会影响到数据的操作,数据操作的修改也不会影响到数据的结构,这样的结构最后用一个访问的动作连接起来,它的扩展性就非常的好。自由的添加访问者类,也可以自由的添加被访问的对象。毕竟在一个报表系统中,需要的可能是各种各样的报表,也需要各种各样的数据来生成报表。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,236评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,867评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,715评论 0 340
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,899评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,895评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,733评论 1 283
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,085评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,722评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,025评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,696评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,816评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,447评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,057评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,009评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,254评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,204评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,561评论 2 343