活着就为折腾(一)

说:

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

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


太极客(Very Geek)
As a designeer, I hope you can prove me wrong.

正在更新 Elixir 语言的系列文章:[链接]

31k 声望
3.1k 粉丝
0 条评论
推荐阅读
为 Koa 框架封装 webpack-dev-middleware 中间件
我见到有很多朋友在 SegmentFault 上面问一些不太好回答的问题,“JavaScript/Node 学好了能做什么?”,“前端架构师每天都做些什么?”等等。这些问题并非不能回答,但是第一、问题本身太过泛泛,很难回答的既针对...

n͛i͛g͛h͛t͛i͛r͛e͛25阅读 12.4k评论 6

gitlab 如何进入控制台
使用下面的命令: {代码...} 然后随便玩吧 {代码...}

ponponon阅读 1.1k

一次SpringBoot版本升级,引发的血案
前言最近项目组升级了SpringBoot版本,由之前的2.0.4升级到最新版本2.7.5,却引出了一个大Bug。到底是怎么回事呢?1.案发现场有一天,项目组的同事反馈给我说,我之前有个接口在新的测试环境报错了,具体异常是:...

一口鸭梨阅读 525

封面图
Spring事务失效场景
}复制代码如果@Transactional 没有特别指定,Spring 只会在遇到运行时异常RuntimeException或者error时进行回滚,而IOException等检查异常不会影响回滚。

数据先声阅读 485

封面图
这几个SQL语法的坑,你踩过吗?
1、LIMIT 语句分页查询是最常用的场景之一,但也通常也是最容易出问题的地方。比如对于下面简单的语句,一般 DBA 想到的办法是在 type, name, create_time 字段上加组合索引。这样条件排序都能有效的利用到索引,...

一口鸭梨阅读 460

7min到40s:SpringBoot启动优化实践
0 背景公司 SpringBoot 项目在日常开发过程中发现服务启动过程异常缓慢,常常需要6-7分钟才能暴露端口,严重降低开发效率。通过 SpringBoot 的 SpringApplicationRunListener 、BeanPostProcessor 原理和源码调试...

小源学算法阅读 440

封面图
你知道微服务架构中的“发件箱模式”吗
前言微服务架构如今非常的流行,这个架构下可能经常会遇到“双写”的场景。双写是指您的应用程序需要在两个不同的系统中更改数据的情况,比如它需要将数据存储在数据库中并向消息队列发送事件。您需要保证这两个操...

小源学算法阅读 430

正在更新 Elixir 语言的系列文章:[链接]

31k 声望
3.1k 粉丝
宣传栏