因为一个BUG, 我在一个摇摇欲坠,几乎碰一下就会散架的项目中某一个角落中发现下面这样一段代码
这段程序与那个BUG有密切的关系。 我来回反复的捉摸这段代码, 发现这段代码实现了两个功能
第一个是在一个从数据库中读取的列表数组中找出某个值是最大的一条记录, 并且把这个最大的值和跟这个值相关的时间给取出来。
第二个比较复杂 ,是将这个列表数组中的值映射到另外一个列表数组中, 可以把这个过程看作是SQL中的JOIN操作, 只是JOIN的条件异常复杂 ,在这里我也不详述了,阅读的同学也不必去深入探究。
就这段代码来说, 很难通过大致观察就理解代码的意思 , 代码之中光循环就套了3层, 而且还有多处复杂的条件判断,代码格式混乱,连编码的底线缩进都没有满足。 可悲的是这种类型的代码广泛存在于全球范围内无数Web服务器之上, 每天运行着。
在很久以前, 那会我还很年轻, 看到项目中哪个地方代码有问题,我就难受, 必须改掉它。 后来我发现, 烂代码就像地沟油, 在我所生活的城市, 到哪里都能碰的到, 除非不吃饭, 否则就只能睁一只眼闭一只眼,只要不是味道有问题, 吃也就吃了。
然而,这次却不一样, 这段代码运行在某个功能项的关键部位, 不透彻的理解清晰这段代码, 以后出现问题还是会被卡在这里。虽然现在我理解了这段代码的意思 ,但过些天回过头来, 我又会忘掉这段代码所表达的意义。这并不是我的记忆力问题的, 而是因为这段代码所表达的意途不够清晰。
于是我把代码重构成了下面这个样子, 代码本身的功能并没有变化
是不是还是看不明白代码所表达的意思? 没关系, 因为这段代码所表示的功能太过于复杂 ,而且还依赖于代码所有的整个函数的上下文, 因此无法理解也无可厚非。 但是从代码结构上来看, 重构后的代码的却清晰了不少。
我将原本拥挤在一起的两个功能进行了拆分, 上面部份是求最大值, 下面部份是对两个数组进行映射。 这里我用到了两个PHP中数组的函数 array_map和array_reduce, 这篇文章想表达的主线思路就是利用此类函数来提高PHP代码的可读性。 这类函数主要包括以下4个函数
array_filter
array_map
array_walk
array_reduce
这4个函数威力巨大, 在处理列表数组方面可以完全替换掉for、foreach、while这些循环控制语句, 这也是函数式编程方式在PHP的一部份体现。
1.array_filter函数
这段代码比较好理解,将数组中性别字段为女的数据项提取出来。 整段代码的逻辑大致如下
1.定义result数组, 用来存放结果
2.循环数组, 对每一个数据项进行条件判断, 查看其中的性别字段是否为女
3.如符合条件则放入result数组中
这是原汁原味的命令式程序代码。
如果data变量中的数据并非存放于php数组中, 而是存在于关系数库的表之中, 那何取得性别为女的数据结果呢? 对于程序员来说这貌似是一个更加简单的问题,一句SQL语句就搞定了
显然, 利用SQL查询数据更加方便,意途也更加清晰,毕间一个SQL表达 式就将所有的程序逻辑都给表达了现来。这句SQL只表达了:“我需要性别为女的数据,至于怎么拿, 我不管 ”, 除了结果 , 其它的它一概不知。
我们不妨把这种思路引入到PHP程序设计之中,不也意味着我们的PHP程序的逻辑表达也更加清晰,代码的可读性也更高的。所幸, 这种利用表达式编程的方法在PHP中也完全可以实现。
利用array_filter函数,可以轻松的完成这个任务, 仔细观察一下, 是不是原来的程序逻辑都不见了,包括定义数组、循环、条件判断这些都不见了,逻辑方面是只剩下了一个性别比较语句,这对于代码所实现的功能一目了然。 和上面的SQL比较一下, 这里的性别判断语句就是SQL中where子句后面的条件判断, 而array_filter函数其实就是SQL中的where子句。 这就是SQL语句面向结果编程的逻辑原封不变的在PHP中的体现,也就是时下最流行的“声明性编程”或者也称为“表达式编程”。
此外, 代码中性别判断语句所在的位置称之为lambda表达式, 更通俗一些的叫法是匿名函数。不难看出, 在SQL的where条件中编写条件判断远不如在匿名函数中写PHP代码来的灵活,在where条件中只能执行or和and逻辑,而在php匿名函数中可以随便怎么写,只要函数的返回值是个布尔值就可以了,这也是php声明性编程优于SQL声明性编程的地方。
2.array_map函数
再来看一个例子
数据中的性别字段是中文的,值也是中文的, 现在想把字段名和字段值都改为英文的, 就可以用上面这段代码实现, 至于实现的逻辑这里不赘述了。
下面是利用SQL的实现方式
SQL中case when语句好像不太好看, 但是不影响整体逻辑的表达。 将这段SQL转换成PHP的方式实现
相比之前的PHP实现, 是不是简洁明了了许多。
在这里使用到了 array_map函数 。 在SQL语句中以select语句最为常用, select的字面意思是“选择”,而select语句也被称之为选择查询, 事实上从关系数据库的角度来说,select被称之为“投影”, 并不是查询什么的。 换言之, select 语句只是将SQL的查询结果以一定的方式(选字段、计算值等等)提取出来了。 php中的array_map表达的也是这层意思, “映射”与“投影”完全是一种意思的不同表达。
3.array_walk函数
array_walk函数没有像 array_map和array_filter这样深刻的意义, 但是它在设计可读性良好的代码时也是不可或缺的。
array_walk是for或foreach语句的替代函数
以上代码分别是 foreach和array_walk对于遍历数组的实现方式。 看起来, 好像array_walk的实现方式更加复杂, 但是在更深层次的语义方面
foreach表达的是循环遍历, 但是在这个循环的过程中,要做什么样的处理,是没有任何约束的, 删除被遍历的数组的某一项 ,或者修改一个十万八千里以外的变量的值,这便是所谓的“代码副作用”,俗话说“白蚁虽小, 危害无穷”, 当这些看似微不足道的副作用发展壮大时, 便会给程序员维护程序代码带来的障碍是致命的。
而array_walk函数缺省情况下所有执行代码的作用域都在匿名函数内,如果要依赖或操作函数之外的数据, 必须通过匿名函数的use关键字导入。通俗一点的请, array_walk函数的权限不如foreach来的大, 因此,使用array_walk函数后,虽然无法让你随心所欲的编程,但是大限度的减少了你代码的副作用,两相权衡array_walk所带来的好处还是有值得使用它的理由的。 首先, 大多数时候写代码根本不需要太大的“权限”,其次, 把代码所影响的范围控制到最小好处不言而喻。微信张小龙讲过,微信做的最好的一点便是“克制”,我们写代码又何尝不是。这一点array_filter和array_map中也有体现, 宽泛的讲,所有使用匿名函数的地方都能享受到这个好处。
array_walk所表达的语义就是“假如你需要用到我, 那么你除了遍历以外,其它的事情最好都别干,否则你还是去用原生的foreach吧”
4.array_reduce函数
array_reduce是上面所讲的三个函数的集大成者,这三个函数的底层完全可以由array_reduce实现。
先看一下下面的php代码
常规的PHP写法,代码分别用于计算数组记录中平均年龄和最大年龄,代码需要循环数组,并把计算结果存入一个标量(单个值,区分于列表变量)。
假如要以表达式编程的方式完成编写这两个功能, 利用array_filter、 array_walk、array_map三个函数是很难一部到位的实现的。
于是, 就到了array_reduce大显身手的时候了
上面的代码是求平均年龄和最大年龄的表达式编程的实现,如果对array_reduce函数的工作机制不了解,看上面两段代码会觉得在看天书。
这是 array_reduce函数的实现代码,函数有3个参数, 3个参数的作用分别是
第一个参数$data, 就要是处理的数据源
第二个参数$callback,循环遍历时会被调用的函数,函数返回的结果在下一次循环调用时会被再次当成参数传入。
第三个参数$initial,作为$callback函数被初次调用时的参数传递
再来一个递归版本的array_reduce实现,帮助更好的理解这个函数的使用意义
善用array_reduce函数几乎可以替换掉绝大多数需要使用foreach、for、while语句的代码。
在标准的函数式编程语言中, 是没有循环控制语句的,假如要进循环计算, 都是使用此类函数来实现的, 如果某些极端的情况下这些函数无法满足需求,那么就以手动写递归来实现循环, 以达到表达式编程的目的。
总结一下, 为什么要在写php代码时使用这4个函数
1.通过函数本身的意义就能表达出代码实现了什么样的功能,而不用去琢磨代码具体细节来理解代码的作用
2.表达式编程相对于命令式编程能极大的简化功能的实现过程, 提升编码效率
3.表达式编程对于代码的可读性、可维护性具有非凡的意义
4.利用匿名函数控制代码的副作用
5.由传统的面向过程式程序设计向现代化的函数式编程靠拢
补充:
通过前面示例的讲解, 利用这4个函数实现的代码相对于传统的实现方式并没有不可思议的变化, 然而, 当需要解决的问题复杂到一定程度时, 合理利用这4个函数会使代码的复杂性大规模下降。