Ruby clean code之block and instance eval
引子
自从来到ruby世界,我就被ruby那自由的语法、优雅的对象模型、漂亮的dsl深深地迷住了,了解更多的ruby特性,能够帮你实现更漂亮,流畅的api。在这篇文章中,我将以一个例子来演示如何使用ruby的block和instance_eval实现更具表现力的api。
需求
这个例子是一个来源于真实项目需求,为了演示方便,我对其做了一些简化。程序的输入是一个格式固定的json字符串,输出是从这个json中获取到一些属性值创建出来的一个给定类型的对象。然而,不同于以往的json和对象之间的序列化,反序列化,这里的从json字符串中的值与对象属性之间的对应关系有一定的逻辑。
json中的值和对象值对应关系有如下几种:
- json属性和对象属性直接对应。
- json属性和对象属性直接对应,当json中没有该属性时,使用给定默认值。
- 对象的属性的类型不是普通类型,当json中有对应属性的值时,需要根据json中的值创建一个对应的类型对象。
- 等等
我们先来看下最初的实现版本:
注:这里的json
不是一个字符串对象,而是经过JSON.parse
处理后得到一个嵌套的hash,下同。
Bad smell
看到这样的代码,你发现什么bad smell了吗?重复代码?不像,但是那么多的init_xxx
方法看起来就是有那么点不自然。
在我看来,这份代码有两个问题:
第一,从json到Post对象的转换职责,不应该是Post类的职责,这份代码违反了单一职责原则。
第二,由于无法很好地将json中的值和对象值对应关系规则建模,导致我们不得不创建多个init_xxx
方法,然后在在initialize
方法中逐一调用这些方法。然而在这些init_xxx
方法之间,存在着结构化重复。
如何改进?
首先,要分离职责,把json到Post对象的转换职责放到一个新类PostBuilder
中。
其次,要对对应关系进行抽象。
改进
我们在来分析一下json中的值和对象值对应关系规则,还是有规律可循的,对应关系都由三部分组成:json属性,对象属性名,转换规则(默认没有转换规则)。其中,通过jsonpath
来标识json属性,通过block来表示转换规则, 我们可以建立一个MapingRule
类来对此关系进行建模。
由此我们得到如下代码:
回顾
与最初版本相比,我们引入了jsonpath和block来对转换规则进行建模(创建了MappingRule类),在PostBuilder#build中循环应用各个rule完成对象的创建,消除了多个init_xxx
的重复。至此,代码已经达到一个令人满意的状态。然而,能否让我们的PostBuilder的接口更加漂亮些?
再改进,更具表达力的api
我们再来看下PostBuilder的使用场景:
- 创建一个PostBuilder对象。
- 给这个对象增加一些转换规则。
- 使用这个对象从json创建对象。
因此,可以说,在一个PostBuilder对象被添加规则之前,它是不完整的,是不可用的,即第一二步应该是一个原子操作,我们可以把initialize
变为private方法,增加一个config
类方法,这个方法可以接受一个block,在此block中对builder增加规则,在这个方法中创建一个builder实例,同时把这个实例传递给block完成buidler的创建。代码如下:
再改进,更简洁的api
至此,这个PostBuilder提供的api已经非常干净了,然而,这个api还是有改进空间的。在block中builder
这个单词出现在每个增加规则的地方。有没有办法把这个重复也给消除掉呢?答案是可以的,instance_eval
隆重登场了。对PostBuilder.config
方法做如下修改:
那么,创建builder的代码就简化为:
在PostBuilder.config
中使用instance_eval
对block进行evaluate,相当于在新创建的builder上执行block中的代码,同样能达到对builder增加规则的效果。
使用instance_eval能够使代码变得更加简洁,然而随之而来的风险是,你也给了你的api调用者一个在这个新建对象上执行任意代码的机会。因此,在简洁性和风险之间,你需要做一个权衡。
再抽象
再回头看看PostBuilder,只需些许改动,我们就能从json创建任意类型的对象,于是我们得到一个InstanceBuilder
类,如下:
你可以试着实现一个这个InstaneBuilder#instane_class
方法。
结语
通观上面的例子,我们通过使用ruby的block和instance_eval,把一个复杂丑陋的代码变得干净,层次清晰,同时,更加容易扩展。
在这里,我抛出自己对编写代码的一点想法,供各位参考:
- 在开始编写实现代码前,先考虑一下如何提供一套干净的,更具表达力的api,让api调用者喜欢使用你的api(sinatra做了一个很好的榜样)。
- 恰当地使用block,instance_eval 能够很容易的构建一个internal dsl。
Reference
想了解更多关于block
,instance_eval
, internal dsl
可以参考如下两篇文章:
How do I build DSLs with yield and instance_eval?
Creating a ruby dsl