说:
- 给定一个数组对象:
[{a: 5, b: 5}, {a: 3, b: 4}, {a: 2, b: 0}, {a: 2, b: 1}]
- 编写一个函数,其签名是:
remove_odd_hashes(array, key_1, key_2)
- 要求过滤其中两个
key
相加结果为奇数的散列对象,返回过滤后的数组对象
基准测试:
array_1 = [{a: 5, b: 5}, {a: 3, b: 4}, {a: 2, b: 0}, {a: 2, b: 1}]
response_1 = remove_odd_hashes(array_1, :a, :b)
Test.assert_equals(response_1, [{a: 5, b: 5}, {a: 2, b: 0}])
array_2 = [{a: 5, b: 2}, {a: 3, b: 4}, {a: 2, b: 1}, {a: 2, b: 1}]
response_2 = remove_odd_hashes(array_2, :a, :b)
Test.assert_equals(response_2, [])
array_3 = [{a: 4, b: 2}, {a: 2, b: 4}, {a: 2, b: 0}, {a: 2, b: 2}]
response_3 = remove_odd_hashes(array_3, :a, :b)
Test.assert_equals(response_3, array_3)
开始折腾!
第一步:大致思考
进来是数组,出去是数组,第一反应肯定要 Enumerator
了。关键是选择哪一个方法比较好呢?Ruby 的数组对象没有 #filter
方法,#each
和 #map
比较常用,但直觉告诉我们肯定还有更合适的。看看文档之后发现 #select
和 #delete_if
似乎都不错。
散列对象把两个 key
相加并判断结果的奇偶是很简单的事情,先用最直观的方式写吧:
第二步:初步版本
def remove_odd_hashes(array, key_1, key_2)
array.select do |hash|
(hash[key_1] + hash[key_2]) % 2 == 0
end
end
测试通过
第三步:开始重构
- 语义问题
需求描述是排除相加结果为奇数的散列对象,而现在代码一眼看过去是选择相加结果为偶数的散列对象,尽管逻辑上很容易取反明白过来,但如果是其他人来看需求和代码,难免脑子里要转个弯。不过这个问题相当好解决:
def remove_odd_hashes(array, key_1, key_2)
array.delete_if do |hash|
(hash[key_1] + hash[key_2]) % 2 != 0
end
end
现在代码的表述就相当精准了,仅从数组里删除两个键值相加后为奇数的散列对象,返回剔除过后的数组。这样的代码其实已经具备“产品级别”了,逻辑准确,表达清晰,易读也易维护。
不过既然用 Ruby,不利用一下它优秀的表达性似乎说不过去嘛,以上代码还能在表达上更进一步吗?好吧,我们知道 hash[key_1] + hash[key_2]
的结果是一个数字,取余的算法虽然比较普遍没什么难度,但是如果能直接判断一个数字的奇偶性岂不更好?通过查找文档发现可以这样写:
def remove_odd_hashes(array, key_1, key_2)
array.delete_if { |hash| (hash[key_1] + hash[key_2]).odd? }
end
漂亮,精简过后的代码已经足以一眼看明白,所以合并成一行的语法也不影响可读性啦!(更重要的是语义还有所增强)
- 代码的可靠性
到了这一步,通常我们要开始考虑代码的可靠性了,通过引入边际条件的测试样本,我们可以测试和观察代码的健壮程度。不过这次测试的数据来源是固定了,不需要我们过多考虑边缘案例,所以剩下的问题就是看看现有代码还有没有可靠性的改善余地。
Ruby 的散列对象有一个很常见的代码惯例,那就是使用 #fetch
方法获取键值,它可以在获取失败的时候抛出 KeyError
,要比 nil has no method
友善多了,推荐能用就用。
所以我们再来改进一次吧:
def remove_odd_hashes(array, key_1, key_2)
array.delete_if { |hash| (hash.fetch(key_1) + hash.fetch(key_2)).odd? }
end
- 开始折腾
从数学的角度来考虑,两整数相加怎么可以得到奇数?
数字 1 | 数字 2 | 结果 |
---|---|---|
偶数 f | 偶数 f | 偶数 f |
奇数 t | 偶数 f | 奇数 t |
偶数 f | 奇数 t | 奇数 t |
奇数 t | 奇数 t | 偶数 f |
这是……如果我没搞错的话,应该是异或逻辑,只不过我们要的真是奇数,假是偶数。换言之,如果对两数进行按位异或运算,则两数奇偶不同,则得奇数(真),两数奇偶一样,则得偶数(假)。 #delete_if
把返回真的结果剔除,正好满足我们的需要。
OK,搞起:
def remove_odd_hashes(array, key_1, key_2)
array.delete_if { |hash| hash.fetch(key_1).odd? ^ hash.fetch(key_2).odd? }
end
实际上一点也没简化,反而更难读懂了——也罢,生命在于折腾嘛!
另外 #reject
可以达到 delete_if
一样的效果,所以以下也是等价的实现:
def remove_odd_hashes(array, key_1, key_2)
array.reject { |hash| (hash[key_1] + hash[key_2]).odd? }
end
少敲几下键盘,好歹也算是进步啦~