代码复用之道:回调机制及c++实现…

      首先要严正声明:这里的 回调机制不等同于回调函数,而是代表了软件先进生产力的发展要求,代表了先进设计模式的前进方向,代表了广大软件开发人员的最根本利益。
      再来说个笑话:古来时常听说回调,我也还记得,可是不甚清楚。我翻开书籍一查,这书籍没有年代,歪歪斜斜的每页上都写着“设计模式”几个字。我横竖睡不着,仔细看了半夜,才从字缝里看出字来,满本都写着两个字是“回调”!
      好。言归正传,编程最重要的普适原则之一(或者可以去掉之一?)是dry,即don't repeat yourself。实现这条原则的最基本方法,对于非面向对象语言如C语言来说是函数,对于面向对象语言来说一般是使用类。如果语言支持宏和模板的话,在一些时候合理使用它们也将很有帮助。 对于简单的代码复用,这些就已经齐活了。但是如果函数/类包装的功能复杂的话,又会如何呢?
      让我们举个例子来帮助理解:假设现在有一个任务,购买饮料。有以下几个人,他们的喜好是不同的,A首选可乐,没有可乐就看看有没有雪碧,再没有就购买矿泉水;B只要矿泉水;C首选芬达,次选可乐,都没有的话则选择纯净水。这个任务对于human being来说是小菜一碟,拿个笔记本记下就可以go go go了。但如果为这个任务设计函数/类时不引入回调机制的话,参数该是多么复杂啊!而且如果引入新需求:假设新来的D首选白酒+矿泉水,如果两者缺一而且手头有花生米的话选择啤酒,否则选择可乐。这时重构的工作量可能接近于重写。
      说到回调,可能大家首先想到的是回调函数,回调机制包括但并不局限于回调函数(不过在C语言里两者基本相同),其核心思想就是由调用者实现部分甚至全部操作细节。这个思想的用途十分广泛,属于杀人灭口,居家旅行必备良药。在设计良好的c++的程序中,实现回调的最主要方法应该是多态。下面我将一一介绍c++中实现回调机制的常用体位并比较其优缺点。
      1)方法一:回调函数,c++是c的超集,所以兵器库中当然包括回调函数。
      a) 原始回调函数, 伪代码如下:
      typedef void (*ChooseGoodsFp)(const Items& items, Items& goods);
      void buy(ChooseGoodsFp fp)
      {
              Shop& shop(getShop());
              Items goods;
              fp(shop.getItems(), goods);
              buyGoods(goods);
      }
      上文中把选择商品的回调函数作为buy方法的参数传入。如果buy属于一个类如Buyer,回调函数也可以作为Buyer构造函数的一个参数传入。然后我们分别为ABCD四个人实现不同的选择商品函数。至此mission complete。
      b) boost::function和boost::bind实现类回调函数。上述做法的最大缺点是回调函数只能是全局函数或者类静态函数,而不能是类成员函数。有时候这样就够用了,比如对类A使用排序算法时传入比较函数作为参数。但很多时候这个限制太大了:回到购买饮料任务,有一类人属于类Person,他们的选择并不是固定的,而是取决于一个变量mode心情,如果mode是悲伤or高兴,选择酒;如果mode是正常,选择水。如果用上诉做法,实现起来就很麻烦:要实现一个全局函数,对此成员函数加壳。采用boost::function和boost::bind来实现回调机制则没有这个限制。伪代码如下:
      typedef boost::function<void (const Items&, Items&)> ChooseGoodsFp;
      void buy(ChooseGoodsFp fp)
      {
              Shop& shop(getShop());
              Items goods;
              fp(shop.getItems(), goods);
              buyGoods(goods);
      }
      调用的时候假设类实例为person,选择函数为choose。则调用代码如下:
      buy(boost::bind(&Person::choose, &person));
      ok,可以看出相比a,b使用类成员函数可简单多了。
      但b方法也是有缺点的:
      缺点一、由于要构建和析构函数对象,当函数执行时间较短且调用次数较多的时候,方法b显得较慢,根据测试比下文要提到的多态方法都要慢很多!
      缺点二、呃,上文只需要一个回调函数,但如果getShop和buyGoods也需要引入回调呢?这时候相比多态方法,代码会显得臃肿。我曾看过网上一篇文章,主张用boost::function和boost::bind干掉多态,粗看似乎可行,但具体实现时发现,枢纽类可能要设置超过10个回调函数,这时候代码十分的累赘和难受,重构时更是十分耗眼神哈。
      b-2) 如果不能使用boost::function和boost::bind?嘿嘿。那就自己实现类似的做法呗:使用模板方法来构建函数对象。要做到跟boost::bind一样太费体,不过简单支持几个参数还是挺容易实现的。
      2)方法二:多态方法/虚函数方法/设计模式方法。当当当当,这应该是面向对象的主流设计方法。
      c) 继承多态(template模式),template模式调用者必须继承特定基类(非接口),从而带来种种问题,所以一般不采用。伪代码如下:
      class BuyerBase
      {
      public:
              virtual void choose(const Items& items, Items& goods) = 0;
              virtual ~BuyerBase(){}
              void buy()
            {
                    Shop& shop(getShop());
                    Items goods;
                    choose(shop.getItems(), goods);
                    buyGoods(goods);
            }
      };
      然后为A,B,C,D, Person定义BuyerBase的子类,实现choose方法,如果getShop和buyGoods需要引入回调,也可以将其声明为虚函数。
      这么做的优点?呃,建议是不要这么做。所有template模式能做到的,都可以用方法d更漂亮地做到。
      d) 实现多态(太多设计模式采用这个体位了)。如果本文你只想记下一种方法,请选择这个。伪代码如下:
      class BuyHandler
      {
      public:
      virtual void choose(const Items& items, Items& goods) = 0;             
      virtual ~BuyHandler(){}
      };
      class Buyer
      {
      public:
              Buyer(BuyHandler* handler_);
          void buy()
              {
                Shop& shop(getShop());
                      Items goods;
                    handler->choose(shop.getItems(), goods);
                buyGoods(goods);
              }
      private:
              BuyHandler* handler;
      };
      优点:代码可读性可扩展性都不错,运行效率不错,如果有编码错误一般编译时即可发现。
      缺点一、与方法b正相反,如果每个handler都只有少量方法如上文中只有一个,代码反而比b臃肿。
      缺点二、对象要实现接口BuyHandler,实现接口一般比继承类副作用来得小,不过还是对函数名称之类提出了限制,不像b那么灵活。
      缺点三、BuyHandler的赋值不管是放在构造函数还是弄出一个方法,Buyer类的使用总是麻烦了一些。

      3)方法三:使用模板,
      e) 模板strategy模式,先看看伪代码吧:
      template<class BuyStrategy>
      class Buyer
      {
      public:
              Buyer(BuyStrategy& bs_);
              void buy()
              {
                    Shop& shop(getShop());
                    Items goods;
                      if ((typename BuyStrategy::ChooseResult result = st.choose(shop.getItems(), goods)) != BuyStrategy::SUCC)
                    {
                          st.log(result);
                          BuyStrategy::choose(shop.getItems(), goods);
                    }
                    buyGoods(goods);
              }

      private:
              BuyStrategy& bs;
      };
      优点:模板一如既往地强大而变态,Buyer类使用方便,代码运行效率比d还高,因为没有虚函数调用的额外代价。编码错误一般编译时也能检测出来(比如把一个没有正确choose方法的类抓去做模板参数)。不需要继承体系,还可以使用类静态函数和静态变量,甚至还可以使用类中的类型定义(上文的typename BuyStrategy::ChooseResult)。
      缺点一、可读性不如d,如果Buyer类代码较多,具体要实现哪些方法、变量、类型定义,不是很容易看出来。注释当然是种解决方法,但需要注释的帮助也意味着可读性存在不足,而且维持注释跟代码的同步又是额外的工作量。即使有注释,如果模板参数类型的接口太庞大(方法+类型+变量),扩展任务还是让人望而生畏。
      缺点二、模板的通病:编译较慢,生成的东东较大,出错定位麻烦些。查看代码时也比较不方便,vs等IDE经常没法正确解析模板类中的函数并辅助查找调整操作。

      总结:复杂代码复用最常用的方法是引入回调机制,其核心是由调用者决定部分甚至全部实现细节。本文总结了C++中实现回调的多种体位,并分析了它们各自的优缺点。
      方法a : 效率高,可读性可以,使用类成员函数麻烦。
      方法b : 效率最低,自由度最大,对于函数名称和函数参数限制最小,不需要继承体系,适合于回调接口不多的情况,回调接口多的枢纽类使用这个将很折腾(不重构还好,重构太费眼神,吃过苦头就知道我的意思了)。
      方法c : 不建议使用,考虑用d或者e代替吧。
      方法d : 正统主流方法,大部分情况下考虑使用。
      方法e : 比较变态,有点像使用宏,编写和调用都比较简便,不需要继承体系。如果策略数目较少(模板不会实例化太多),或者可以预先定义完全(所以不用顾虑可读性和扩展),可以考虑使用它。当传统主流方法不能满足需求时,也可以考虑下它。不过使用这个模式要适度,过度的话会严重损害程序的可读性和扩展性,具体说就是模板参数类型的接口一定要简单。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值