Ruby 101:动态编程

  当method_missing魔法失效时……

  在上篇文章里我们通过重写Hash类method_missing思路方法把Hash对象模拟成匿名对象但是这种做法有时会产生些莫名其妙问题举个例子吧假如我把process思路方法(完整实现参见上篇文章代码31)options参数从这样:



  代码 1

  改成这样:



  代码 2

  我们将会发现不论options参数count取什么值我们总是得到两本书为什么?想想看method_missing思路方法触发条件是什么?仅当我们思路方法不存在时method_missing思路方法才有机会出场但是Hash类本身就有count思路方法:



  图 1

  这打破了method_missing思路方法触发条件换句话说method_missing思路方法被count思路方法"截胡"(麻将术语)了Hash类count思路方法返回键/值对个数而options参数默认就有两个键/值对如果我们没有添加额外键/值对count思路方法返回值将会总是2这就是为什么修改options参数的后我们总是得到两本书那么如何解决这个问题?

  显然method_missing思路方法和count思路方法无法同时存在否则转发消息逻辑总是被忽略但我们不能移除count思路方法这样会导致依赖它代码不能正常工作在这种情况下我们只好把转发消息逻辑移至别处了那么放哪呢?还记得Ruby 101:对象和思路方法最后那个BookStore类吗?我们可以仿效它做法为Hash类创建个代理类然后把转发消息逻辑放到代理类method_missing思路方法里:



  代码 3

  这样就不怕count思路方法干扰了:



  图 2

  当然如果你想要只是这些那就没有必要另起炉灶了使用Ruby自带OpenStruct类也可以达到相同效果:



  图 3

  但是OpenStruct类不支持嵌套Hash对象你只能通过person1.address[:city]来访问下面这个对象城市信息:



  代码 4

  如果你希望通过person1.address.city来访问这个对象城市信息要么显式地把内嵌Hash对象创建为OpenStruct对象:



  代码 5

  要么扩展AnonymousObject类使它支持嵌套Hash对象如果你有兴趣不妨把握这个机会练习下吧!

  消息代理

  既然我们可以通过代理类截获并转发消息何不在此基础上加点想象力?假设我有这样个类:



  代码 6

  我想在method1思路方法的前做些事情method2思路方法的后做另些事情忽略method3思路方法把method4思路方法转到method3思路方法上你有什么建议?我可以直接在代码里表达这些需求吗?如果可以我希望像下面这样表达:



  代码 7

  当我通过代理类依次Class1类4个思路方法时我期望这样输出结果:



  代码 8

  现在问题是我真可以这样吗?不知道呢下吧看看能够走到什么程度

  首先我们需要个Proxy类它提供相关思路方法收集并保存我们"需求":



  代码 9

  接着我们需要重写method_missing思路方法:



  代码 10

  处理消息逻辑并不复杂除非@ignore包含这个思路方法名字否则将会依次进行前期处理、消息转发和后期处理如果我们就此撒手不干那么Proxy类就只能这样用了:



  代码 11

  这显然不是我想要仔细观察代码7不难发现proxy是个思路方法而object1对象则是它参数proxy思路方法接受个代码块用来配置proxy思路方法创建代理对象要创建这样思路方法并不难:



  代码 12

  下面我们来看看如何使用这个思路方法:



  代码 13

  嗯我们离目标非常近了但是before等思路方法前面那个"p."可以去掉吗?想想看对象思路方法实质上就是向对象发送消息如果去掉before等思路方法前面那个"p."那么这些消息将会发往默认对象而此时默认对象是来没有before等思路方法 2来亦非接收这些消息正确对象我们期望接收这些消息对象是由proxy思路方法创建代理对象如果有办法把代码块执行时默认对象改为代理对象代码7就可以实现了那么能否改变代码块执行时默认对象?当然可以现在正是instance_eval思路方法大展拳脚时候:



  代码 14

  然而改变代码块执行时默认对象有时会产生些莫名其妙问题比如下面这个代理工厂:



  代码 15

  猜猜看method1思路方法和method2思路方法的前将会分别输出什么?由于代码块执行时默认对象是代理对象既没有@msg_before_meth1例子变量又没有msg_before_meth2思路方法于是method1思路方法的前会输出个空行这是puts nil行为而在method2思路方法的前会抛出异常告知找不到msg_before_meth2思路方法如何办?解决思路方法其实很简单由于代码块可以访问create_proxy思路方法本地变量我们可以把@msg_before_meth1例子变量和msg_before_meth2思路方法值绑定到本地变量然后在代码块里使用这些本地变量就行了:



  代码 16

  现在我们来看看输出结果:



  图 4

  非常好!接下来我们看看还有什么需要完善

  首先是before思路方法目前它只允许个目标思路方法对应个代码块试想如果我想在某个思路方法的前分别进行安全检查和资源调配我就必须把它们混到个代码块里这显然不是个好主意我希望before思路方法支持个目标思路方法对应多个代码块这将有助于合理分割代码逻辑此外我还希望before思路方法允许这些代码块访问目标对象当然代码块有权选择是否访问为此我需要把before思路方法以及method_missing思路方法相关部分修改如下:



  代码 17



  代码 18

  需要介绍说明@before[m] ||= 相当于@before[m] || @before[m] = 在这里效果相当于@before[m] = not @before[m]或者@before[m] = unless @before[m]下面我们来看看运行结果:



  图 5

  非常好!至于after思路方法依样画葫芦就可以了

  接着轮到ignore思路方法目前它只接受个参数这意味着我次只能忽略条消息如果我要忽略多条消息就不得不重复ignore思路方法了如果我可以像下面这样忽略多条消息该多好啊:



  代码 19

  可以吗?当然可以我们可以使ignore思路方法接受可变参数:



  代码 20

  为了避免出现重项我使用uniq!思路方法把@ignore处理了值得注意如果你使用是uniq思路方法而不是uniq!思路方法@ignore将不会受到任何影响uniq思路方法在不影响原来集合情况下返回个新集合而uniq!则就地修改原来集合

  最后是forward思路方法情况和ignore思路方法类似目前它只接受两个参数这意味着我次只能转发条消息如果我要转发多条消息就不得不重复forward思路方法了如果我可以像下面这样转发多条消息该多好啊:



  代码 21

  能行吗?想想看:method4 => :method3是什么?有些同学已经反应过来了是Hash对象当它是最后个参数时Ruby允许我们把{}去掉于是看起来就像组消息映射关系由于这些映射关系本来就是保存在Hash对象里我们只需修改forward思路方法把参数指定映射关系添加到@forward就行了:



  代码 22

  有意思我们还可以利用这些思路方法的间微妙关系描绘些实用处理逻辑比如下面这个:



  代码 23

  首先我屏蔽外界直接method4思路方法接着我把method3思路方法和method6思路方法转到method4思路方法上并设置在它们的前分别进行区别资源配置其中object1是代码7里创建那个目标对象这个处理逻辑可能来源于这样业务需求系统原本通过method3思路方法来执行某些任务由于业务发展method3思路方法实现出现了僵化于是催生了method4思路方法我希望保留method3思路方法请求途径但背后通过method4思路方法来执行相应任务同时增加个"虚拟"请求途径(的所以说"虚拟"是目标对象并不包含method6思路方法定义)这两个请求途径将会针对区别资源配置展开

  虽然代码21proxy2是个代理对象但我希望它用起来就像object1由于Proxy类和Class1类都是Object类子类根据上结论如果我在proxy2上从Object类继承过来思路方法那么我将会得到proxy2对象信息而不是object1对象这显然不够透明为了获得object1对象信息我将不得不在Proxy类里重写这些思路方法但是……Object类例子思路方法有52个啊!如何办?如果你使用版本是1.9或以上那么你只需让Proxy类继承自BasicObject类就行了那么BasicObject类是何方神圣?我想下图应该能够介绍说明问题了:



  图 6

  正如你所看到BasicObject类是Object类基类同时也是整个继承体系根类如果你直关注这个系列你应该不止次碰到"这个说法其实不够准确但就目前而言你大可放心这样理解"这句话了事实上BasicObject类就是那些时候例外BasicObject类例子思路方法比Object类少很多只保留了最基本7个:



  查看原图(大图)

  图 7

  没有多余思路方法烦扰使得BasicObject类非常适合成为代理类基类但是如果没有指明基类默认将会是Object类如果你使用版本是1.8或以下那么你需要是Jim WeirichBlankSlate类使用方法和BasicObject类似但额外提供了些有趣功能比如隐藏/重现某些思路方法如果你有兴趣不妨看看它是如何做到

  事件回调

  正如你所看到method_missing思路方法力量非常强大以至于我多篇文章都对它有所涉及事实上每当我们需要实现动态接口(dynamic erface)时method_missing思路方法和send思路方法都会无可避免地牵涉进来回顾method_missing思路方法整个作用过程首先我们定义这样个思路方法然后当特定条件满足时这个思路方法将被想想看这像什么?有些同学可能已经看出来了这就像为特定事件创建回调思路方法事实上我们通常把method_missing思路方法称作钩子思路方法(hook method)类似还有method_added思路方法、d思路方法、extended思路方法和inherited思路方法等等如果我们实现了这些思路方法当它们对应事件发生时Ruby将会它们这种回调机制是内置并且由解析器负责执行那么Ruby有否提供现成机制帮助我们创建自定义事件呢?很抱歉Ruby没有正式事件概念但它为我们提供了基本材料——Proc对象和Method对象下面我们来看看如何实现自定义事件

  假如我有个Button类我想为它实现个click事件我希望它用起来就像……嗯……IronRuby那样:



  代码 24

  以上是IronRuby支持 3种常见做法从Ruby角度来看click显然是个思路方法它接受个代码块那么当我们提供代码块时返回值是什么如果我们没有提供代码块它会否抛出异常?为了回答这些问题我们需要借助IronRubyirb(这里使用是IronRuby 1.0 RC1):



  图 8

  从上图可以看到IronRuby事件是个RubyEvent对象当我们没有提供代码块时click思路方法将会返回RubyEvent对象而当我们提供代码块时这个代码块会被隐式转换成Proc对象click思路方法则返回这个Proc对象那么如何实现这个RubyEvent类?当我们感到无从下手时不妨想像下完成的时使用情景根据图8试验结果我们可能会这样使用它:



  代码 25

  从上面代码可以看到RubyEvent类至少包含两个例子思路方法——add思路方法和call思路方法它们分别负责添加单个Proc对象和所有Proc对象实现这样个类点都不难:



  代码 26

  值得注意add思路方法在添加的前会先进行重复检查现在请研究个问题如何移除现有Proc对象?还是让我们先看看IronRuby是如何反应吧:



  图 9

  我们可以通过click思路方法获取RubyEvent对象然后通过RubyEvent对象remove思路方法移除现有Proc对象这个remove思路方法实现起来也不难:



Tags: 

延伸阅读

最新评论

发表评论