第十章 抽取不相关的子问题
编码在初级阶段的时候,往往把一大堆代码写在一个函数里。慢慢的经验多了,就开始注意写子函数了。要如何写子函数呢?就是这章的重点。这儿主要关注如何积极地发现并抽取出不相关的子逻辑。
1、介绍性的例子
下列函数的高层次目标是“找到距离给定目标最近的位置”:
//return which element of 'array' is closest to the given latitude/longitude. //models the Earth as a perfect sphere. var findClosestLocation = function (lat, lng, array) { var closest; var closest_dist = Number.MAX_VALUE; for(var i = 0; i < array.length; i += 1) { //Convert both points to radians var lat_rad = radians(lat); var lng_rad = radians(lng); var lat2_rad = radians(array[i].latitude); var lng2_rad = radians(array[i].longitude); //use the "apherical law of cosines" formula var dist = Math.acos(Math.sin(lat_rad) * Math.sin(lat2_rad) + Math.cos(lat_rad) * Math.cos(lat2_rad) * Math.cos(lng2_rad - lng_rad)); if(dist < closest_dist) { closest = array[i]; closest_dist = dist; } } return closest; };这段代码中主要工作是“计算两个经纬坐标点之间的球面距离”。如果把这部分代码抽取到一个子函数中呢:
var spherical_distance = function (lat1, lng1, lat2, lng2) { //Convert both points to radians var lat1_rad = radians(lat1); var lng1_rad = radians(lng1); var lat2_rad = radians(lat2); var lng2_rad = radians(lng2); //use the "apherical law of cosines" formula var dist = Math.acos(Math.sin(lat1_rad) * Math.sin(lat2_rad) + Math.cos(lat1_rad) * Math.cos(lat2_rad) * Math.cos(lng2_rad - lng_rad)); };这样一来,主函数就变成了这样:
var findClosestLocation = function (lat, lng, array) { var closest; var closest_dist = Number.MAX_VALUE; for(var i = 0; i < array.length; i += 1) { var dist = spherical_distance(lat, lng, array[i].latitude, array[i].longitude); if(dist < closest_dist) { closest = array[i]; closest_dist = dist; } } return closest; };
2、纯工具代码
有很多操作经常被人用到,比如操作字符串、使用哈希表以及读写文件等等。
这些常见的操作都被以库的形式提供出来了。在编程时要时刻注意这类自相关的操作是不是已经有可使用的库API了。如果没有的话,可以自己写一个类似的库API,以便以后可以随时使用。
3、其他多用途代码
考虑一个结构体中,存在一个链表中,该链表内可以包含许多的元素。结构体中有一个变量记录了元素的个数。
struct element_test { int element_num; list_test *p_head; }如果我们在代码中,需要获得列表内元素的个数,要如何做呢?有许多人,直接使用了element_num;还有许多人,写了这样的一个api:
int get_element_num(element_test *p_test) { return p_test->element_num; }这两种使用方法,我更推崇第二种,因为第二种将获取元素个数的实现细节隐藏了起来,提示外部只需要关注结果,不需要关注过程。
第二种做法,还带来了意外的好处。如果有一天,需要获取的不是所有元素的个数,而是获取大于某一个值的元素的个数的话,只需要修改这个API函数即可,其他地方不用修改。而第一种方式里,需要在每一个直接使用element_num的地方都做修改,这样代码量会大大增加。
第二种做法通常叫做创建通用代码,它将某些代码完全地从项目中解耦出来,它负责的代码可以被独立的测试、使用,并易于理解。
有时候,我们需要像第二种做法一样,从上到下的进行编程,优先考虑高层次模块和函数,先考虑需要解决的问题,然后在每个对与需要解决问题的API进行编码支持。这样编码完成时,你会发现,好多问题都已经被单独处理了。
4、过犹不及
编码过程中,抽取API函数是一个很好地习惯。不过,如果过分沉迷于抽取API,往往会导致代码量复杂,小函数过多,在阅读代码过程中,需要不停地在各个小函数来回跳转,这是不提倡的。
抽取API函数时,应该遵循“保持统一的抽象层”原则。就像目录一样,在一个函数中,函数内部的各部操作应该保持在一个抽象层面。如果都是流程函数,就不要引入细节实现。如果都是细节实现,就不要再过多抽取API,封装成流程。
注意要把一般代码和项目专有的代码分开。一般来讲,一个项目中,大部分的代码都是一般代码。通过建立一大组库和辅助函数来解决一般问题,剩下的就是让你的程序与众不同的核心部分。抽取API函数,拆分不相关的逻辑处理,这使程序员关注小而定义良好的问题,使得这些问题与项目的其他部分脱离,也更有利于以后重用。