活着就为折腾(一)

687 查看

说:

  • 给定一个数组对象:[{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

少敲几下键盘,好歹也算是进步啦~