Monkey Patch猴子补丁编程方式及其在Ruby中的运用
何谓猴子补丁(MonkeyPatch)?在动态语言中,不修改源代码而对功能进行追加和变更。
使用猴子补丁的目的:
1、追加功能
2、功能变更
3、修正程序错误
4、增加钩子,在执行某个方法的同时执行一些其他的处理,如打印日志,实现AOP等,
5、缓存,在计算量很大,结算之后的结果可以反复使用的情况下,在一次计算完成之后,对方法进行替换可以提高处理速度。
Ruby的类都是开放类,即在类定义之后还可以任意添加内容,这就使得在Ruby中使用猴子补丁变得特别容易了。另外,Ruby还提供了对方法、类和模块的进行操作的功能,让我们使用猴子补丁更加得心应手。Ruby提供的基本功能如下:
alias:给方法另起别名
include:引入其他模块的方法
remove_method:取消本类中的方法
undef:取消方法
在Ruby中使用MonkeyPatch
我当时遇到的场景是这样的:
我司使用第三方库fog进行EC2的操作。创建实例等很多命令都需要设置实例类型这个参数。在fog里,EC2的所有类型都定义在fog/aws/models/compute/flavors.rb的FLAVORS数组里。如果设置的类型不在FLAVORS数组里,fog都会视作是无效的参数而报错。
后来,亚马逊发布了新的实例类型D2。虽然Ruby的第三方社区非常活跃,但是fog的开发社区还是没有及时添加D2到flavors.rb里;而我司的工作又迫切需要使用D2类型的实例。
背景交待完毕,接下来看看有什么样的解决方法。
方法一:我们可以向fog提交一个PullRequest来添加新类型。
但是这个方法行不通。我们使用的knife-ec2对fog的版本依赖必须是1.25.*,但是fog已经更新到了1.31.0,而且fog从1.27.0开始结构上有很大的变化。显然,我们不可能再等knife-ec2升级支持新版本的fog,所以我们提交PullRequest更新fog不能解决问题。
方法二:手动更新旧版fog既然不能使用最新版的fog,我们可以手动编辑1.25版的fog,再打包成Gem使用。这个方法比前一个方法更容易操作,但是带来的问题时不易于维护。为了一个极小的改动,把自己的代码加入到第三方库中总是让人觉得不够「干净」。
最后,在同事的指点下,我采用了第三种方法,即MonkeyPatch。我在我司的Ruby项目里添加了一个文件lib/PROJECT_NAME/monkey_patches/flavors.rb,接着在文件中添加以下代码来修改fog/aws/models/compute/flavors:
require'fog/aws/models/compute/flavors' classObject defredef_without_warning(const,value) mod=self.is_a?(Module)?self:self.class mod.send(:remove_const,const)ifmod.const_defined?(const) mod.const_set(const,value) end end moduleFog moduleCompute classAWS NEW_FLAVORS=FLAVORS+[ { :id=>"d2.xlarge", ... }, { :id=>"d2.2xlarge", ... }, { :id=>"d2.4xlarge", ... }, { :id=>"d2.8xlarge", ... } ] redef_without_warning:FLAVORS,NEW_FLAVORS end end end
总结
通过在自己的代码中添加一个Monkeypatch,我们成功地实现了向fog中动态添加新实例类型。我司终于可以使用fog创建D2类型的机器了;而且这个方法改动的代码量最小,也更加容易维护。
MonkeyPatch并非是完美的解决方法,它会引入一些陷阱。所以这个技巧在软件工程领域还有一些争议。不过,我还是觉得MonkeyPatch是一个不错的零时性解决方法。